[tor-commits] [metrics-lib/master] Parse Ed25519 and SHA-256 elements in descriptors.

karsten at torproject.org karsten at torproject.org
Mon Dec 21 19:45:59 UTC 2015


commit 39a9c496effde2bd083df0095006ca299f9af2d5
Author: Karsten Loesing <karsten.loesing at gmx.net>
Date:   Wed Dec 16 10:41:28 2015 +0100

    Parse Ed25519 and SHA-256 elements in descriptors.
    
    More precisely,
     - support Ed25519 certificates and Ed25519 master keys as well as
       SHA-256 digests and Ed25519 signatures thereof in server
       descriptors and extra-info descriptors,
     - parse RSA-1024 signatures of SHA-1 digests of extra-info
       descriptors,
     - parse Ed25519 master keys in votes, and
     - parse Ed25519 and RSA-1024 identity digests in microdescriptors.
    
    This patch is based on metrics-db's bridge descriptor sanitizer.
---
 CHANGELOG.md                                       |    5 +
 .../torproject/descriptor/ExtraInfoDescriptor.java |   26 +++
 src/org/torproject/descriptor/Microdescriptor.java |   10 ++
 .../torproject/descriptor/NetworkStatusEntry.java  |    5 +
 .../torproject/descriptor/ServerDescriptor.java    |   27 +++
 .../descriptor/impl/ExtraInfoDescriptorImpl.java   |  157 ++++++++++++++++-
 .../descriptor/impl/MicrodescriptorImpl.java       |   29 +++-
 .../descriptor/impl/NetworkStatusEntryImpl.java    |   19 +++
 .../torproject/descriptor/impl/ParseHelper.java    |   73 ++++++++
 .../descriptor/impl/ServerDescriptorImpl.java      |  137 ++++++++++++++-
 .../impl/ExtraInfoDescriptorImplTest.java          |  176 +++++++++++++++++++-
 .../descriptor/impl/MicrodescriptorImplTest.java   |   82 +++++++++
 .../impl/RelayNetworkStatusVoteImplTest.java       |   54 +++++-
 .../descriptor/impl/ServerDescriptorImplTest.java  |  117 +++++++++++++
 14 files changed, 902 insertions(+), 15 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5456795..1f2bc0d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,11 @@
      existing types, ServerDescriptor and ExtraInfoDescriptor, are
      still usable and will not be deprecated, because applications may
      not care whether a relay or a bridge published a descriptor.
+   - Support Ed25519 certificates, Ed25519 master keys, SHA-256
+     digests, and Ed25519 signatures thereof in server descriptors and
+     extra-info descriptors, and support Ed25519 master keys in votes.
+   - Include RSA-1024 signatures of SHA-1 digests of extra-info
+     descriptors, which were parsed and discarded before.
 
 
 # Changes in version 1.0.0 - 2015-12-05
diff --git a/src/org/torproject/descriptor/ExtraInfoDescriptor.java b/src/org/torproject/descriptor/ExtraInfoDescriptor.java
index 380be00..1b978a4 100644
--- a/src/org/torproject/descriptor/ExtraInfoDescriptor.java
+++ b/src/org/torproject/descriptor/ExtraInfoDescriptor.java
@@ -12,6 +12,10 @@ public interface ExtraInfoDescriptor extends Descriptor {
    * extra-info descriptor in a server descriptor. */
   public String getExtraInfoDigest();
 
+  /* Return the base64-encoded SHA-256 descriptor digest that may be used
+   * to reference this extra-info descriptor in a server descriptor. */
+  public String getExtraInfoDigestSha256();
+
   /* Return the relay's nickname. */
   public String getNickname();
 
@@ -260,5 +264,27 @@ public interface ExtraInfoDescriptor extends Descriptor {
   /* Return the (possibly empty) list of transports supported by this
    * bridge. */
   public List<String> getTransports();
+
+  /* Return the signature of the PKCS1-padded extra-info descriptor
+   * digest, or null if the descriptor doesn't contain a signature (which
+   * is the case in sanitized bridge descriptors). */
+  public String getRouterSignature();
+
+  /* Return the base64-encoded Ed25519 certificate, or null if the
+   * descriptor doesn't contain one. */
+  public String getIdentityEd25519();
+
+  /* Return the base64-encoded Ed25519 master key, which may either be
+   * parsed from the optional "master-key-ed25519" line or derived from
+   * the (likewise optional) Ed25519 certificate following the
+   * "identity-ed25519" line, or null if the descriptor contains neither
+   * Ed25519 master key nor Ed25519 certificate. */
+  public String getMasterKeyEd25519();
+
+  /* Return the base64-encoded Ed25519 signature of a SHA-256 digest of
+   * the entire descriptor, from the first character up to and including
+   * the first space after the "router-sig-ed25519" string, prefixed with
+   * the string "Tor router descriptor signature v1". */
+  public String getRouterSignatureEd25519();
 }
 
diff --git a/src/org/torproject/descriptor/Microdescriptor.java b/src/org/torproject/descriptor/Microdescriptor.java
index 7715d35..3cebeb6 100644
--- a/src/org/torproject/descriptor/Microdescriptor.java
+++ b/src/org/torproject/descriptor/Microdescriptor.java
@@ -43,5 +43,15 @@ public interface Microdescriptor extends Descriptor {
   /* Return the port list of the IPv6 port summary or null if the
    * microdescriptor didn't contain an IPv6 port summary line. */
   public String getIpv6PortList();
+
+  /* Return the optional, base64-encoded RSA-1024 identity that is only
+   * included to prevent collisions between microdescriptors, or null if
+   * no such identity is included. */
+  public String getRsa1024Identity();
+
+  /* Return the optional, base64-encoded Ed25519 identity that is only
+   * included to prevent collisions between microdescriptors, or null if
+   * no such identity is included. */
+  public String getEd25519Identity();
 }
 
diff --git a/src/org/torproject/descriptor/NetworkStatusEntry.java b/src/org/torproject/descriptor/NetworkStatusEntry.java
index fb9163e..584f07b 100644
--- a/src/org/torproject/descriptor/NetworkStatusEntry.java
+++ b/src/org/torproject/descriptor/NetworkStatusEntry.java
@@ -75,5 +75,10 @@ public interface NetworkStatusEntry {
   /* Return the port list of the port summary or null if the status entry
    * didn't contain a port summary line. */
   public String getPortList();
+
+  /* Return the relay's base64-encoded Ed25519 master key, "none" if the
+   * relay doesn't have an Ed25519 identity, or null if the status entry
+   * didn't contain this information.  Only included in votes. */
+  public String getMasterKeyEd25519();
 }
 
diff --git a/src/org/torproject/descriptor/ServerDescriptor.java b/src/org/torproject/descriptor/ServerDescriptor.java
index 598b9b5..3266e9d 100644
--- a/src/org/torproject/descriptor/ServerDescriptor.java
+++ b/src/org/torproject/descriptor/ServerDescriptor.java
@@ -11,6 +11,10 @@ public interface ServerDescriptor extends Descriptor {
    * descriptor in a network status. */
   public String getServerDescriptorDigest();
 
+  /* Return the base64-encoded SHA-256 descriptor digest that may be used
+   * to reference this server descriptor in a network status. */
+  public String getServerDescriptorDigestSha256();
+
   /* Return the relay's nickname. */
   public String getNickname();
 
@@ -119,6 +123,12 @@ public interface ServerDescriptor extends Descriptor {
    * the relay did not upload a corresponding extra-info descriptor. */
   public String getExtraInfoDigest();
 
+  /* Return the base64-encoded SHA-256 digest of the extra-info descriptor
+   * referenced from this server descriptor, or null if the relay either
+   * did not upload a corresponding extra-info descriptor or did not refer
+   * to it using a SHA-256 digest. */
+  public String getExtraInfoDigestSha256();
+
   /* Return the hidden service descriptor version(s) that this relay
    * stores and serves, or null if it doesn't store and serve any hidden
    * service descriptors. */
@@ -147,5 +157,22 @@ public interface ServerDescriptor extends Descriptor {
   /* Return the ntor onion key base64 string with padding omitted, or null
    * if the server descriptors didn't contain an ntor onion key line. */
   public String getNtorOnionKey();
+
+  /* Return the base64-encoded Ed25519 certificate, or null if the
+   * descriptor doesn't contain one. */
+  public String getIdentityEd25519();
+
+  /* Return the base64-encoded Ed25519 master key, which may either be
+   * parsed from the optional "master-key-ed25519" line or derived from
+   * the (likewise optional) Ed25519 certificate following the
+   * "identity-ed25519" line, or null if the descriptor contains neither
+   * Ed25519 master key nor Ed25519 certificate. */
+  public String getMasterKeyEd25519();
+
+  /* Return the base64-encoded Ed25519 signature of a SHA-256 digest of
+   * the entire descriptor, from the first character up to and including
+   * the first space after the "router-sig-ed25519" string, prefixed with
+   * the string "Tor router descriptor signature v1". */
+  public String getRouterSignatureEd25519();
 }
 
diff --git a/src/org/torproject/descriptor/impl/ExtraInfoDescriptorImpl.java b/src/org/torproject/descriptor/impl/ExtraInfoDescriptorImpl.java
index 61b8d13..4abace6 100644
--- a/src/org/torproject/descriptor/impl/ExtraInfoDescriptorImpl.java
+++ b/src/org/torproject/descriptor/impl/ExtraInfoDescriptorImpl.java
@@ -31,6 +31,7 @@ public abstract class ExtraInfoDescriptorImpl extends DescriptorImpl
     super(descriptorBytes, failUnrecognizedDescriptorLines, false);
     this.parseDescriptorBytes();
     this.calculateDigest();
+    this.calculateDigestSha256();
     Set<String> exactlyOnceKeywords = new HashSet<String>(Arrays.asList((
         "extra-info,published").split(",")));
     this.checkExactlyOnceKeywords(exactlyOnceKeywords);
@@ -52,8 +53,9 @@ public abstract class ExtraInfoDescriptorImpl extends DescriptorImpl
     Set<String> bridgeStatsKeywords = new HashSet<String>(Arrays.asList(
         "bridge-stats-end,bridge-stats-ips".split(",")));
     Set<String> atMostOnceKeywords = new HashSet<String>(Arrays.asList((
-        "read-history,write-history,dirreq-read-history,"
-        + "dirreq-write-history,geoip-db-digest,router-signature,"
+        "identity-ed25519,master-key-ed25519,read-history,write-history,"
+        + "dirreq-read-history,dirreq-write-history,geoip-db-digest,"
+        + "router-sig-ed25519,router-signature,router-digest-sha256,"
         + "router-digest").split(",")));
     atMostOnceKeywords.addAll(dirreqStatsKeywords);
     atMostOnceKeywords.addAll(entryStatsKeywords);
@@ -75,7 +77,8 @@ public abstract class ExtraInfoDescriptorImpl extends DescriptorImpl
   private void parseDescriptorBytes() throws DescriptorParseException {
     Scanner s = new Scanner(new String(this.rawDescriptorBytes)).
         useDelimiter("\n");
-    boolean skipCrypto = false;
+    String nextCrypto = null;
+    List<String> cryptoLines = null;
     while (s.hasNext()) {
       String line = s.next();
       String lineNoOpt = line.startsWith("opt ") ?
@@ -162,15 +165,49 @@ public abstract class ExtraInfoDescriptorImpl extends DescriptorImpl
         this.parseBridgeIpTransportsLine(line, lineNoOpt, partsNoOpt);
       } else if (keyword.equals("transport")) {
         this.parseTransportLine(line, lineNoOpt, partsNoOpt);
+      } else if (keyword.equals("identity-ed25519")) {
+        this.parseIdentityEd25519Line(line, lineNoOpt, partsNoOpt);
+        nextCrypto = "identity-ed25519";
+      } else if (keyword.equals("master-key-ed25519")) {
+        this.parseMasterKeyEd25519Line(line, lineNoOpt, partsNoOpt);
+      } else if (keyword.equals("router-sig-ed25519")) {
+        this.parseRouterSigEd25519Line(line, lineNoOpt, partsNoOpt);
       } else if (keyword.equals("router-signature")) {
         this.parseRouterSignatureLine(line, lineNoOpt, partsNoOpt);
+        nextCrypto = "router-signature";
       } else if (keyword.equals("router-digest")) {
         this.parseRouterDigestLine(line, lineNoOpt, partsNoOpt);
+      } else if (keyword.equals("router-digest-sha256")) {
+        this.parseRouterDigestSha256Line(line, lineNoOpt, partsNoOpt);
       } else if (line.startsWith("-----BEGIN")) {
-        skipCrypto = true;
+        cryptoLines = new ArrayList<String>();
+        cryptoLines.add(line);
       } else if (line.startsWith("-----END")) {
-        skipCrypto = false;
-      } else if (!skipCrypto) {
+        cryptoLines.add(line);
+        StringBuilder sb = new StringBuilder();
+        for (String cryptoLine : cryptoLines) {
+          sb.append("\n" + cryptoLine);
+        }
+        String cryptoString = sb.toString().substring(1);
+        if ("router-signature".equals(nextCrypto)) {
+          this.routerSignature = cryptoString;
+        } else if ("identity-ed25519".equals(nextCrypto)) {
+          this.identityEd25519 = cryptoString;
+          this.parseIdentityEd25519CryptoBlock(cryptoString);
+        } else if (this.failUnrecognizedDescriptorLines) {
+          throw new DescriptorParseException("Unrecognized crypto "
+              + "block '" + cryptoString + "' in extra-info descriptor.");
+        } else {
+          if (this.unrecognizedLines == null) {
+            this.unrecognizedLines = new ArrayList<String>();
+          }
+          this.unrecognizedLines.addAll(cryptoLines);
+        }
+        cryptoLines = null;
+        nextCrypto = null;
+      } else if (cryptoLines != null) {
+        cryptoLines.add(line);
+      } else {
         ParseHelper.parseKeyword(line, partsNoOpt[0]);
         if (this.failUnrecognizedDescriptorLines) {
           throw new DescriptorParseException("Unrecognized line '"
@@ -610,7 +647,6 @@ public abstract class ExtraInfoDescriptorImpl extends DescriptorImpl
     if (!lineNoOpt.equals("router-signature")) {
       throw new DescriptorParseException("Illegal line '" + line + "'.");
     }
-    /* Not parsing crypto parts (yet). */
   }
 
   private void parseRouterDigestLine(String line, String lineNoOpt,
@@ -622,6 +658,57 @@ public abstract class ExtraInfoDescriptorImpl extends DescriptorImpl
         partsNoOpt[1]);
   }
 
+  private void parseIdentityEd25519Line(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (partsNoOpt.length != 1) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+  }
+
+  private void parseIdentityEd25519CryptoBlock(String cryptoString)
+      throws DescriptorParseException {
+    String masterKeyEd25519FromIdentityEd25519 =
+        ParseHelper.parseMasterKeyEd25519FromIdentityEd25519CryptoBlock(
+        cryptoString);
+    if (this.masterKeyEd25519 != null && !this.masterKeyEd25519.equals(
+        masterKeyEd25519FromIdentityEd25519)) {
+      throw new DescriptorParseException("Mismatch between "
+          + "identity-ed25519 and master-key-ed25519.");
+    }
+    this.masterKeyEd25519 = masterKeyEd25519FromIdentityEd25519;
+  }
+
+  private void parseMasterKeyEd25519Line(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (partsNoOpt.length != 2) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+    String masterKeyEd25519FromMasterKeyEd25519Line = partsNoOpt[1];
+    if (this.masterKeyEd25519 != null && !masterKeyEd25519.equals(
+        masterKeyEd25519FromMasterKeyEd25519Line)) {
+      throw new DescriptorParseException("Mismatch between "
+          + "identity-ed25519 and master-key-ed25519.");
+    }
+    this.masterKeyEd25519 = masterKeyEd25519FromMasterKeyEd25519Line;
+  }
+
+  private void parseRouterSigEd25519Line(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (partsNoOpt.length != 2) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+    this.routerSignatureEd25519 = partsNoOpt[1];
+  }
+
+  private void parseRouterDigestSha256Line(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (partsNoOpt.length != 2) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+    ParseHelper.parseThirtyTwoByteBase64String(line, partsNoOpt[1]);
+    this.extraInfoDigestSha256 = partsNoOpt[1];
+  }
+
   private void calculateDigest() throws DescriptorParseException {
     if (this.extraInfoDigest != null) {
       /* We already learned the descriptor digest of this bridge
@@ -653,11 +740,47 @@ public abstract class ExtraInfoDescriptorImpl extends DescriptorImpl
     }
   }
 
+  private void calculateDigestSha256() throws DescriptorParseException {
+    if (this.extraInfoDigestSha256 != null) {
+      /* We already learned the descriptor digest of this bridge
+       * descriptor from a "router-digest-sha256" line. */
+      return;
+    }
+    try {
+      String ascii = new String(this.getRawDescriptorBytes(), "US-ASCII");
+      String startToken = "extra-info ";
+      String sigToken = "\n-----END SIGNATURE-----\n";
+      int start = ascii.indexOf(startToken);
+      int sig = ascii.indexOf(sigToken) + sigToken.length();
+      if (start >= 0 && sig >= 0 && sig > start) {
+        byte[] forDigest = new byte[sig - start];
+        System.arraycopy(this.getRawDescriptorBytes(), start, forDigest,
+            0, sig - start);
+        this.extraInfoDigestSha256 = DatatypeConverter.printBase64Binary(
+            MessageDigest.getInstance("SHA-256").digest(forDigest)).
+            replaceAll("=", "");
+      }
+    } catch (UnsupportedEncodingException e) {
+      /* Handle below. */
+    } catch (NoSuchAlgorithmException e) {
+      /* Handle below. */
+    }
+    if (this.extraInfoDigestSha256 == null) {
+      throw new DescriptorParseException("Could not calculate extra-info "
+          + "descriptor SHA-256 digest.");
+    }
+  }
+
   private String extraInfoDigest;
   public String getExtraInfoDigest() {
     return this.extraInfoDigest;
   }
 
+  private String extraInfoDigestSha256;
+  public String getExtraInfoDigestSha256() {
+    return this.extraInfoDigestSha256;
+  }
+
   private String nickname;
   public String getNickname() {
     return this.nickname;
@@ -933,5 +1056,25 @@ public abstract class ExtraInfoDescriptorImpl extends DescriptorImpl
   public List<String> getTransports() {
     return new ArrayList<String>(this.transports);
   }
+
+  private String routerSignature;
+  public String getRouterSignature() {
+    return this.routerSignature;
+  }
+
+  private String identityEd25519;
+  public String getIdentityEd25519() {
+    return this.identityEd25519;
+  }
+
+  private String masterKeyEd25519;
+  public String getMasterKeyEd25519() {
+    return this.masterKeyEd25519;
+  }
+
+  private String routerSignatureEd25519;
+  public String getRouterSignatureEd25519() {
+    return this.routerSignatureEd25519;
+  }
 }
 
diff --git a/src/org/torproject/descriptor/impl/MicrodescriptorImpl.java b/src/org/torproject/descriptor/impl/MicrodescriptorImpl.java
index ffa5a0f..1987659 100644
--- a/src/org/torproject/descriptor/impl/MicrodescriptorImpl.java
+++ b/src/org/torproject/descriptor/impl/MicrodescriptorImpl.java
@@ -48,7 +48,7 @@ public class MicrodescriptorImpl extends DescriptorImpl
         "onion-key".split(",")));
     this.checkExactlyOnceKeywords(exactlyOnceKeywords);
     Set<String> atMostOnceKeywords = new HashSet<String>(Arrays.asList((
-        "ntor-onion-key,family,p,p6").split(",")));
+        "ntor-onion-key,family,p,p6,id").split(",")));
     this.checkAtMostOnceKeywords(atMostOnceKeywords);
     this.checkFirstKeyword("onion-key");
     this.clearParsedKeywords();
@@ -80,6 +80,8 @@ public class MicrodescriptorImpl extends DescriptorImpl
         this.parsePLine(line, parts);
       } else if (keyword.equals("p6")) {
         this.parseP6Line(line, parts);
+      } else if (keyword.equals("id")) {
+        this.parseIdLine(line, parts);
       } else if (line.startsWith("-----BEGIN")) {
         crypto = new StringBuilder();
         crypto.append(line + "\n");
@@ -196,6 +198,21 @@ public class MicrodescriptorImpl extends DescriptorImpl
     }
   }
 
+  private void parseIdLine(String line, String[] parts)
+      throws DescriptorParseException {
+    if (parts.length != 3) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    } else if ("ed25519".equals(parts[1])) {
+      ParseHelper.parseThirtyTwoByteBase64String(line, parts[2]);
+      this.ed25519Identity = parts[2];
+    } else if ("rsa1024".equals(parts[1])) {
+      ParseHelper.parseTwentyByteBase64String(line, parts[2]);
+      this.rsa1024Identity = parts[2];
+    } else {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+  }
+
   private void calculateDigest() throws DescriptorParseException {
     try {
       String ascii = new String(this.getRawDescriptorBytes(), "US-ASCII");
@@ -265,5 +282,15 @@ public class MicrodescriptorImpl extends DescriptorImpl
   public String getIpv6PortList() {
     return this.ipv6PortList;
   }
+
+  private String rsa1024Identity;
+  public String getRsa1024Identity() {
+    return this.rsa1024Identity;
+  }
+
+  private String ed25519Identity;
+  public String getEd25519Identity() {
+    return this.ed25519Identity;
+  }
 }
 
diff --git a/src/org/torproject/descriptor/impl/NetworkStatusEntryImpl.java b/src/org/torproject/descriptor/impl/NetworkStatusEntryImpl.java
index dac6052..94575c6 100644
--- a/src/org/torproject/descriptor/impl/NetworkStatusEntryImpl.java
+++ b/src/org/torproject/descriptor/impl/NetworkStatusEntryImpl.java
@@ -91,6 +91,8 @@ public class NetworkStatusEntryImpl implements NetworkStatusEntry {
         this.parsePLine(line, parts);
       } else if (keyword.equals("m")) {
         this.parseMLine(line, parts);
+      } else if (keyword.equals("id")) {
+        this.parseIdLine(line, parts);
       } else if (this.failUnrecognizedDescriptorLines) {
         throw new DescriptorParseException("Unrecognized line '" + line
             + "' in status entry.");
@@ -237,6 +239,18 @@ public class NetworkStatusEntryImpl implements NetworkStatusEntry {
     }
   }
 
+  private void parseIdLine(String line, String[] parts)
+      throws DescriptorParseException {
+    if (parts.length != 3 || !"ed25519".equals(parts[1])) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    } else if ("none".equals(parts[2])) {
+      this.masterKeyEd25519 = "none";
+    } else {
+      ParseHelper.parseThirtyTwoByteBase64String(line, parts[2]);
+      this.masterKeyEd25519 = parts[2];
+    }
+  }
+
   private void clearAtMostOnceKeywords() {
     this.atMostOnceKeywords = null;
   }
@@ -328,5 +342,10 @@ public class NetworkStatusEntryImpl implements NetworkStatusEntry {
   public String getPortList() {
     return this.portList;
   }
+
+  private String masterKeyEd25519;
+  public String getMasterKeyEd25519() {
+    return this.masterKeyEd25519;
+  }
 }
 
diff --git a/src/org/torproject/descriptor/impl/ParseHelper.java b/src/org/torproject/descriptor/impl/ParseHelper.java
index 4e91e92..a354831 100644
--- a/src/org/torproject/descriptor/impl/ParseHelper.java
+++ b/src/org/torproject/descriptor/impl/ParseHelper.java
@@ -434,5 +434,78 @@ public class ParseHelper {
     }
     return result;
   }
+
+  public static String
+      parseMasterKeyEd25519FromIdentityEd25519CryptoBlock(
+      String identityEd25519CryptoBlock) throws DescriptorParseException {
+    String identityEd25519CryptoBlockNoNewlines =
+        identityEd25519CryptoBlock.replaceAll("\n", "");
+    String beginEd25519CertLine = "-----BEGIN ED25519 CERT-----",
+        endEd25519CertLine = "-----END ED25519 CERT-----";
+    if (!identityEd25519CryptoBlockNoNewlines.startsWith(
+        beginEd25519CertLine)) {
+      throw new DescriptorParseException("Illegal start of "
+          + "identity-ed25519 crypto block '" + identityEd25519CryptoBlock
+          + "'.");
+    }
+    if (!identityEd25519CryptoBlockNoNewlines.endsWith(
+        endEd25519CertLine)) {
+      throw new DescriptorParseException("Illegal end of "
+          + "identity-ed25519 crypto block '" + identityEd25519CryptoBlock
+          + "'.");
+    }
+    String identityEd25519Base64 = identityEd25519CryptoBlockNoNewlines.
+        substring(beginEd25519CertLine.length(),
+        identityEd25519CryptoBlock.length()
+        - endEd25519CertLine.length()).replaceAll("=", "");
+    byte[] identityEd25519 = DatatypeConverter.parseBase64Binary(
+        identityEd25519Base64);
+    if (identityEd25519.length < 40) {
+      throw new DescriptorParseException("Invalid length of "
+          + "identity-ed25519 (in bytes): " + identityEd25519.length);
+    } else if (identityEd25519[0] != 0x01) {
+      throw new DescriptorParseException("Unknown version in "
+          + "identity-ed25519: " + identityEd25519[0]);
+    } else if (identityEd25519[1] != 0x04) {
+      throw new DescriptorParseException("Unknown cert type in "
+          + "identity-ed25519: " + identityEd25519[1]);
+    } else if (identityEd25519[6] != 0x01) {
+      throw new DescriptorParseException("Unknown certified key type in "
+          + "identity-ed25519: " + identityEd25519[1]);
+    } else if (identityEd25519[39] == 0x00) {
+      throw new DescriptorParseException("No extensions in "
+          + "identity-ed25519 (which would contain the encoded "
+          + "master-key-ed25519): " + identityEd25519[39]);
+    } else {
+      int extensionStart = 40;
+      for (int i = 0; i < (int) identityEd25519[39]; i++) {
+        if (identityEd25519.length < extensionStart + 4) {
+          throw new DescriptorParseException("Invalid extension with id "
+              + i + " in identity-ed25519.");
+        }
+        int extensionLength = identityEd25519[extensionStart];
+        extensionLength <<= 8;
+        extensionLength += identityEd25519[extensionStart + 1];
+        int extensionType = identityEd25519[extensionStart + 2];
+        if (extensionLength == 32 && extensionType == 4) {
+          if (identityEd25519.length < extensionStart + 4 + 32) {
+            throw new DescriptorParseException("Invalid extension with "
+                + "id " + i + " in identity-ed25519.");
+          }
+          byte[] masterKeyEd25519 = new byte[32];
+          System.arraycopy(identityEd25519, extensionStart + 4,
+              masterKeyEd25519, 0, masterKeyEd25519.length);
+          String masterKeyEd25519Base64 = DatatypeConverter.
+              printBase64Binary(masterKeyEd25519).replaceAll("=", "");
+          String masterKeyEd25519Base64NoTrailingEqualSigns =
+              masterKeyEd25519Base64.replaceAll("=", "");
+          return masterKeyEd25519Base64NoTrailingEqualSigns;
+        }
+        extensionStart += 4 + extensionLength;
+      }
+    }
+    throw new DescriptorParseException("Unable to locate "
+        + "master-key-ed25519 in identity-ed25519.");
+  }
 }
 
diff --git a/src/org/torproject/descriptor/impl/ServerDescriptorImpl.java b/src/org/torproject/descriptor/impl/ServerDescriptorImpl.java
index 426f4d0..3dd6c40 100644
--- a/src/org/torproject/descriptor/impl/ServerDescriptorImpl.java
+++ b/src/org/torproject/descriptor/impl/ServerDescriptorImpl.java
@@ -28,15 +28,18 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
     super(descriptorBytes, failUnrecognizedDescriptorLines, false);
     this.parseDescriptorBytes();
     this.calculateDigest();
+    this.calculateDigestSha256();
     Set<String> exactlyOnceKeywords = new HashSet<String>(Arrays.asList(
         "router,bandwidth,published".split(",")));
     this.checkExactlyOnceKeywords(exactlyOnceKeywords);
     Set<String> atMostOnceKeywords = new HashSet<String>(Arrays.asList((
-        "platform,fingerprint,hibernating,uptime,contact,family,"
-        + "read-history,write-history,eventdns,caches-extra-info,"
-        + "extra-info-digest,hidden-service-dir,protocols,"
-        + "allow-single-hop-exits,onion-key,signing-key,ipv6-policy,"
-        + "ntor-onion-key,router-signature,router-digest").split(",")));
+        "identity-ed25519,master-key-ed25519,platform,fingerprint,"
+        + "hibernating,uptime,contact,family,read-history,write-history,"
+        + "eventdns,caches-extra-info,extra-info-digest,"
+        + "hidden-service-dir,protocols,allow-single-hop-exits,onion-key,"
+        + "signing-key,ipv6-policy,ntor-onion-key,router-sig-ed25519,"
+        + "router-signature,router-digest-sha256,router-digest").
+        split(",")));
     this.checkAtMostOnceKeywords(atMostOnceKeywords);
     this.checkFirstKeyword("router");
     if (this.getKeywordCount("accept") == 0 &&
@@ -115,10 +118,19 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
         this.parseDircacheportLine(line, lineNoOpt, partsNoOpt);
       } else if (keyword.equals("router-digest")) {
         this.parseRouterDigestLine(line, lineNoOpt, partsNoOpt);
+      } else if (keyword.equals("router-digest-sha256")) {
+        this.parseRouterDigestSha256Line(line, lineNoOpt, partsNoOpt);
       } else if (keyword.equals("ipv6-policy")) {
         this.parseIpv6PolicyLine(line, lineNoOpt, partsNoOpt);
       } else if (keyword.equals("ntor-onion-key")) {
         this.parseNtorOnionKeyLine(line, lineNoOpt, partsNoOpt);
+      } else if (keyword.equals("identity-ed25519")) {
+        this.parseIdentityEd25519Line(line, lineNoOpt, partsNoOpt);
+        nextCrypto = "identity-ed25519";
+      } else if (keyword.equals("master-key-ed25519")) {
+        this.parseMasterKeyEd25519Line(line, lineNoOpt, partsNoOpt);
+      } else if (keyword.equals("router-sig-ed25519")) {
+        this.parseRouterSigEd25519Line(line, lineNoOpt, partsNoOpt);
       } else if (line.startsWith("-----BEGIN")) {
         cryptoLines = new ArrayList<String>();
         cryptoLines.add(line);
@@ -135,6 +147,9 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
           this.signingKey = cryptoString;
         } else if ("router-signature".equals(nextCrypto)) {
           this.routerSignature = cryptoString;
+        } else if ("identity-ed25519".equals(nextCrypto)) {
+          this.identityEd25519 = cryptoString;
+          this.parseIdentityEd25519CryptoBlock(cryptoString);
         } else if (this.failUnrecognizedDescriptorLines) {
           throw new DescriptorParseException("Unrecognized crypto "
               + "block '" + cryptoString + "' in server descriptor.");
@@ -392,6 +407,10 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
     }
     this.extraInfoDigest = ParseHelper.parseTwentyByteHexString(line,
         partsNoOpt[1]);
+    if (partsNoOpt.length >= 3) {
+      ParseHelper.parseThirtyTwoByteBase64String(line, partsNoOpt[2]);
+      this.extraInfoDigestSha256 = partsNoOpt[2];
+    }
   }
 
   private void parseHiddenServiceDirLine(String line, String lineNoOpt,
@@ -508,6 +527,57 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
     this.ntorOnionKey = partsNoOpt[1].replaceAll("=", "");
   }
 
+  private void parseIdentityEd25519Line(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (partsNoOpt.length != 1) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+  }
+
+  private void parseIdentityEd25519CryptoBlock(String cryptoString)
+      throws DescriptorParseException {
+    String masterKeyEd25519FromIdentityEd25519 =
+        ParseHelper.parseMasterKeyEd25519FromIdentityEd25519CryptoBlock(
+        cryptoString);
+    if (this.masterKeyEd25519 != null && !this.masterKeyEd25519.equals(
+        masterKeyEd25519FromIdentityEd25519)) {
+      throw new DescriptorParseException("Mismatch between "
+          + "identity-ed25519 and master-key-ed25519.");
+    }
+    this.masterKeyEd25519 = masterKeyEd25519FromIdentityEd25519;
+  }
+
+  private void parseMasterKeyEd25519Line(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (partsNoOpt.length != 2) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+    String masterKeyEd25519FromMasterKeyEd25519Line = partsNoOpt[1];
+    if (this.masterKeyEd25519 != null && !masterKeyEd25519.equals(
+        masterKeyEd25519FromMasterKeyEd25519Line)) {
+      throw new DescriptorParseException("Mismatch between "
+          + "identity-ed25519 and master-key-ed25519.");
+    }
+    this.masterKeyEd25519 = masterKeyEd25519FromMasterKeyEd25519Line;
+  }
+
+  private void parseRouterSigEd25519Line(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (partsNoOpt.length != 2) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+    this.routerSignatureEd25519 = partsNoOpt[1];
+  }
+
+  private void parseRouterDigestSha256Line(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (partsNoOpt.length != 2) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+    ParseHelper.parseThirtyTwoByteBase64String(line, partsNoOpt[1]);
+    this.serverDescriptorDigestSha256 = partsNoOpt[1];
+  }
+
   private void calculateDigest() throws DescriptorParseException {
     if (this.serverDescriptorDigest != null) {
       /* We already learned the descriptor digest of this bridge
@@ -539,11 +609,48 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
     }
   }
 
+  private void calculateDigestSha256() throws DescriptorParseException {
+    if (this.serverDescriptorDigestSha256 != null) {
+      /* We already learned the descriptor digest of this bridge
+       * descriptor from a "router-digest-sha256" line. */
+      return;
+    }
+    try {
+      String ascii = new String(this.getRawDescriptorBytes(), "US-ASCII");
+      String startToken = "router ";
+      String sigToken = "\n-----END SIGNATURE-----\n";
+      int start = ascii.indexOf(startToken);
+      int sig = ascii.indexOf(sigToken) + sigToken.length();
+      if (start >= 0 && sig >= 0 && sig > start) {
+        byte[] forDigest = new byte[sig - start];
+        System.arraycopy(this.getRawDescriptorBytes(), start, forDigest,
+            0, sig - start);
+        this.serverDescriptorDigestSha256 =
+            DatatypeConverter.printBase64Binary(
+            MessageDigest.getInstance("SHA-256").digest(forDigest)).
+            replaceAll("=", "");
+      }
+    } catch (UnsupportedEncodingException e) {
+      /* Handle below. */
+    } catch (NoSuchAlgorithmException e) {
+      /* Handle below. */
+    }
+    if (this.serverDescriptorDigestSha256 == null) {
+      throw new DescriptorParseException("Could not calculate server "
+          + "descriptor SHA-256 digest.");
+    }
+  }
+
   private String serverDescriptorDigest;
   public String getServerDescriptorDigest() {
     return this.serverDescriptorDigest;
   }
 
+  private String serverDescriptorDigestSha256;
+  public String getServerDescriptorDigestSha256() {
+    return this.serverDescriptorDigestSha256;
+  }
+
   private String nickname;
   public String getNickname() {
     return this.nickname;
@@ -670,6 +777,11 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
     return this.extraInfoDigest;
   }
 
+  private String extraInfoDigestSha256;
+  public String getExtraInfoDigestSha256() {
+    return this.extraInfoDigestSha256;
+  }
+
   private Integer[] hiddenServiceDirVersions;
   public List<Integer> getHiddenServiceDirVersions() {
     return this.hiddenServiceDirVersions == null ? null :
@@ -707,5 +819,20 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
   public String getNtorOnionKey() {
     return this.ntorOnionKey;
   }
+
+  private String identityEd25519;
+  public String getIdentityEd25519() {
+    return this.identityEd25519;
+  }
+
+  private String masterKeyEd25519;
+  public String getMasterKeyEd25519() {
+    return this.masterKeyEd25519;
+  }
+
+  private String routerSignatureEd25519;
+  public String getRouterSignatureEd25519() {
+    return this.routerSignatureEd25519;
+  }
 }
 
diff --git a/test/org/torproject/descriptor/impl/ExtraInfoDescriptorImplTest.java b/test/org/torproject/descriptor/impl/ExtraInfoDescriptorImplTest.java
index 4483af2..d70ac39 100644
--- a/test/org/torproject/descriptor/impl/ExtraInfoDescriptorImplTest.java
+++ b/test/org/torproject/descriptor/impl/ExtraInfoDescriptorImplTest.java
@@ -2,7 +2,6 @@
  * See LICENSE for licensing information */
 package org.torproject.descriptor.impl;
 
-import org.torproject.descriptor.DescriptorParseException;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -16,7 +15,10 @@ import java.util.Map;
 import java.util.SortedMap;
 
 import org.junit.Test;
+import org.torproject.descriptor.BridgeExtraInfoDescriptor;
+import org.torproject.descriptor.DescriptorParseException;
 import org.torproject.descriptor.ExtraInfoDescriptor;
+import org.torproject.descriptor.RelayExtraInfoDescriptor;
 
 /* Test parsing of extra-info descriptors. */
 public class ExtraInfoDescriptorImplTest {
@@ -166,11 +168,28 @@ public class ExtraInfoDescriptorImplTest {
       db.routerSignatureLines = line;
       return new RelayExtraInfoDescriptorImpl(db.buildDescriptor(), true);
     }
+    private String identityEd25519Lines = null,
+        masterKeyEd25519Line = null, routerSigEd25519Line = null;
+    private static ExtraInfoDescriptor createWithEd25519Lines(
+        String identityEd25519Lines, String masterKeyEd25519Line,
+        String routerSigEd25519Line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.identityEd25519Lines = identityEd25519Lines;
+      db.masterKeyEd25519Line = masterKeyEd25519Line;
+      db.routerSigEd25519Line = routerSigEd25519Line;
+      return new RelayExtraInfoDescriptorImpl(db.buildDescriptor(), true);
+    }
     private byte[] buildDescriptor() {
       StringBuilder sb = new StringBuilder();
       if (this.extraInfoLine != null) {
         sb.append(this.extraInfoLine + "\n");
       }
+      if (this.identityEd25519Lines != null) {
+        sb.append(this.identityEd25519Lines + "\n");
+      }
+      if (this.masterKeyEd25519Line != null) {
+        sb.append(this.masterKeyEd25519Line + "\n");
+      }
       if (this.publishedLine != null) {
         sb.append(this.publishedLine + "\n");
       }
@@ -230,6 +249,9 @@ public class ExtraInfoDescriptorImplTest {
           return null;
         }
       }
+      if (this.routerSigEd25519Line != null) {
+        sb.append(this.routerSigEd25519Line + "\n");
+      }
       if (this.routerSignatureLines != null) {
         sb.append(this.routerSignatureLines + "\n");
       }
@@ -1430,5 +1452,157 @@ public class ExtraInfoDescriptorImplTest {
     unrecognizedLines.add(unrecognizedLine);
     assertEquals(unrecognizedLines, descriptor.getUnrecognizedLines());
   }
+
+  private static final String IDENTITY_ED25519_LINES =
+      "identity-ed25519\n"
+      + "-----BEGIN ED25519 CERT-----\n"
+      + "AQQABiX1AVGv5BuzJroQXbOh6vv1nbwc5rh2S13PyRFuLhTiifK4AQAgBACBCMwr"
+      + "\n4qgIlFDIzoC9ieJOtSkwrK+yXJPKlP8ojvgkx8cGKvhokOwA1eYDombzfwHcJ1"
+      + "EV\nbhEn/6g8i7wzO3LoqefIUrSAeEExOAOmm5mNmUIzL8EtnT6JHCr/sqUTUgA="
+      + "\n"
+      + "-----END ED25519 CERT-----";
+
+  private static final String MASTER_KEY_ED25519_LINE =
+      "master-key-ed25519 gQjMK+KoCJRQyM6AvYniTrUpMKyvslyTypT/KI74JMc";
+
+  private static final String ROUTER_SIG_ED25519_LINE =
+      "router-sig-ed25519 y7WF9T2GFwkSDPZEhB55HgquIFOl5uXUFMYJPq3CXXUTKeJ"
+      + "kSrtaZUB5s34fWdHQNtl84mH4dVaFMunHnwgYAw";
+
+  @Test()
+  public void testEd25519() throws DescriptorParseException {
+    ExtraInfoDescriptor descriptor =
+        DescriptorBuilder.createWithEd25519Lines(IDENTITY_ED25519_LINES,
+        MASTER_KEY_ED25519_LINE, ROUTER_SIG_ED25519_LINE);
+    assertEquals(IDENTITY_ED25519_LINES.substring(
+        IDENTITY_ED25519_LINES.indexOf("\n") + 1),
+        descriptor.getIdentityEd25519());
+    assertEquals(MASTER_KEY_ED25519_LINE.substring(
+        MASTER_KEY_ED25519_LINE.indexOf(" ") + 1),
+        descriptor.getMasterKeyEd25519());
+    assertEquals(ROUTER_SIG_ED25519_LINE.substring(
+        ROUTER_SIG_ED25519_LINE.indexOf(" ") + 1),
+        descriptor.getRouterSignatureEd25519());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testEd25519IdentityMasterKeyMismatch()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithEd25519Lines(IDENTITY_ED25519_LINES,
+        "master-key-ed25519 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+        ROUTER_SIG_ED25519_LINE);
+  }
+
+  @Test()
+  public void testEd25519IdentityMissing()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithEd25519Lines(null,
+        MASTER_KEY_ED25519_LINE, ROUTER_SIG_ED25519_LINE);
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testEd25519IdentityDuplicate()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithEd25519Lines(IDENTITY_ED25519_LINES + "\n"
+        + IDENTITY_ED25519_LINES, MASTER_KEY_ED25519_LINE,
+        ROUTER_SIG_ED25519_LINE);
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testEd25519IdentityEmptyCrypto()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithEd25519Lines("identity-ed25519\n"
+        + "-----BEGIN ED25519 CERT-----\n-----END ED25519 CERT-----",
+        MASTER_KEY_ED25519_LINE, ROUTER_SIG_ED25519_LINE);
+  }
+
+  @Test()
+  public void testEd25519MasterKeyMissing()
+      throws DescriptorParseException {
+    ExtraInfoDescriptor descriptor =
+        DescriptorBuilder.createWithEd25519Lines(IDENTITY_ED25519_LINES,
+        null, ROUTER_SIG_ED25519_LINE);
+    assertEquals(MASTER_KEY_ED25519_LINE.substring(
+        MASTER_KEY_ED25519_LINE.indexOf(" ") + 1),
+        descriptor.getMasterKeyEd25519());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testEd25519MasterKeyDuplicate()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithEd25519Lines(IDENTITY_ED25519_LINES,
+        MASTER_KEY_ED25519_LINE + "\n" + MASTER_KEY_ED25519_LINE,
+        ROUTER_SIG_ED25519_LINE);
+  }
+
+  @Test()
+  public void testEd25519RouterSigMissing()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithEd25519Lines(IDENTITY_ED25519_LINES,
+        MASTER_KEY_ED25519_LINE, null);
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testEd25519RouterSigDuplicate()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithEd25519Lines(IDENTITY_ED25519_LINES,
+        MASTER_KEY_ED25519_LINE, ROUTER_SIG_ED25519_LINE + "\n"
+        + ROUTER_SIG_ED25519_LINE);
+  }
+
+  @Test()
+  public void testExtraInfoDigestSha256Relay()
+      throws DescriptorParseException {
+    byte[] descriptorBytes = ("extra-info Unnamed "
+        + "EA5B335055D2F03013FF030381F02B1C631EC723\n"
+        + "identity-ed25519\n"
+        + "-----BEGIN ED25519 CERT-----\n"
+        + "AQQABiZRAenzZorGtx6xapoEeaqcLLOk3uWwJXTvOVLluSXXbRSZAQAgBADLN5"
+        + "wp\nCEOrRbshSbj1NDAUgc6cxU65M/Vx1x+b5+EXbkQZ5uiyB4pphVF5kPPT1P"
+        + "SleYqM\n8j+tlKh2i6+Xr0xScSPpmtG00/D0MoRlT7ZdaaaT5iw1DWDQCZ8BHG"
+        + "lAZwU=\n"
+        + "-----END ED25519 CERT-----\n"
+        + "published 2015-12-01 04:38:12\n"
+        + "write-history 2015-12-01 01:40:37 (14400 s) 88704000,60825600,"
+        + "61747200,76953600,61516800,59443200\n"
+        + "read-history 2015-12-01 01:40:37 (14400 s) 87321600,59443200,"
+        + "59904000,74880000,60364800,58060800\n"
+        + "router-sig-ed25519 c6eUeJs/SVjun3JhmjByEeWdRDyunSMAnGVhx71JiRj"
+        + "YzR8x5IcPebylG7m10wiolFxinvw78UhrrGo9Sq5ZBw\n"
+        + "router-signature\n"
+        + "-----BEGIN SIGNATURE-----\n"
+        + "oC2qFHCDOKSRoIPR86jdRxEYia390Z4d8fT0yr/1mg4RQ7lHmxlzFT6QxCswdX"
+        + "Ry\nvGNGR0wARySgyE+YKKWYn/Hp547JhhWd9Oc7BuFMY0XMvl/HOo+B9VjW+l"
+        + "nv6UBE\niqxx3C3Iw0ymohvOenyCUa/7TmsT7eVotDO57uIoGEc=\n"
+        + "-----END SIGNATURE-----\n"
+        + "").getBytes();
+    RelayExtraInfoDescriptor descriptor =
+        new RelayExtraInfoDescriptorImpl(descriptorBytes, true);
+    assertEquals("Pt1BtzfRwhYqGCDo8jjchS8nJP3ovrDyHGn+dqPbMgw",
+        descriptor.getExtraInfoDigestSha256());
+  }
+
+  @Test()
+  public void testExtraInfoDigestSha256Bridge()
+      throws DescriptorParseException {
+    byte[] descriptorBytes = ("extra-info idideditheconfig "
+        + "DC28749EC9E26E61DE492E46CD830379E9931B09\n"
+        + "master-key-ed25519 "
+        + "38FzmOIE6Mm85Ytx0MhFM6X9EuxWRUgb6HjyMGuO2AU\n"
+        + "published 2015-12-03 13:23:19\n"
+        + "write-history 2015-12-03 09:59:32 (14400 s) 53913600,52992000,"
+        + "53222400,53222400,53452800,53222400\n"
+        + "read-history 2015-12-03 09:59:32 (14400 s) 61056000,60364800,"
+        + "60364800,60134400,60595200,60364800\n"
+        + "geoip-db-digest 5BF366AD4A0572D82A1A0F6628AF8EF7725E3AB9\n"
+        + "geoip6-db-digest 212DE17D5A368DCAFA19B95F168BFFA101145A93\n"
+        + "router-digest-sha256 "
+        + "TvrqpjI7OmCtwGwair/NHUxg5ROVVQYz6/EDyXsDHR4\n"
+        + "router-digest 00B98F076B586272C3172B7F3DA29ADEE75F2ED8\n").getBytes();
+    BridgeExtraInfoDescriptor descriptor =
+        new BridgeExtraInfoDescriptorImpl(descriptorBytes, true);
+    assertEquals("TvrqpjI7OmCtwGwair/NHUxg5ROVVQYz6/EDyXsDHR4",
+        descriptor.getExtraInfoDigestSha256());
+  }
 }
 
diff --git a/test/org/torproject/descriptor/impl/MicrodescriptorImplTest.java b/test/org/torproject/descriptor/impl/MicrodescriptorImplTest.java
new file mode 100644
index 0000000..ab4b8c8
--- /dev/null
+++ b/test/org/torproject/descriptor/impl/MicrodescriptorImplTest.java
@@ -0,0 +1,82 @@
+package org.torproject.descriptor.impl;
+
+import org.junit.Test;
+import org.torproject.descriptor.DescriptorParseException;
+import org.torproject.descriptor.Microdescriptor;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class MicrodescriptorImplTest {
+
+  /* Helper class to build a microdescriptor based on default data and
+   * modifications requested by test methods. */
+  private static class DescriptorBuilder {
+    private String onionKeyLines = "onion-key\n"
+        + "-----BEGIN RSA PUBLIC KEY-----\n"
+        + "MIGJAoGBALNZ4pNsHHkl7a+kFWbBmPHNAepjjvuhjTr1TaMB3UKuCRaXJmS2Qr"
+        + "CW\nkTmINqdQUccwb3ghb7EBZfDtCUvjcwMSEsRRTVIZqVQsYj6m3n1CegOc4o"
+        + "UutXaZ\nfkyty5XOgV4Qucx9wokzTMCHlO0V0x9y0FwFsK5Nb6ugqfQLLQ6XAg"
+        + "MBAAE=\n"
+        + "-----END RSA PUBLIC KEY-----";
+    private static Microdescriptor createWithDefaultLines()
+        throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      return new MicrodescriptorImpl(db.buildDescriptor(), true);
+    }
+    private String ntorOnionKeyLine =
+        "ntor-onion-key PXLa7IGE+TzPDMsM5j9rFnDa37rd6kfZa5QuzqqJukw=";
+    private String idLine = "id rsa1024 bvegfGxp8k7T9QFpjPTrPaJTa/8";
+    private static Microdescriptor createWithIdLine(String line)
+        throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.idLine = line;
+      return new MicrodescriptorImpl(db.buildDescriptor(), true);
+    }
+    private byte[] buildDescriptor() {
+      StringBuilder sb = new StringBuilder();
+      if (this.onionKeyLines != null) {
+        sb.append(this.onionKeyLines + "\n");
+      }
+      if (this.ntorOnionKeyLine != null) {
+        sb.append(this.ntorOnionKeyLine + "\n");
+      }
+      if (this.idLine != null) {
+        sb.append(this.idLine + "\n");
+      }
+      return sb.toString().getBytes();
+    }
+  }
+
+  @Test()
+  public void testDefaults() throws DescriptorParseException {
+    DescriptorBuilder.createWithDefaultLines();
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testIdRsa1024TooShort() throws DescriptorParseException {
+    DescriptorBuilder.createWithIdLine("id rsa1024 AAAA");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testIdRsa1024TooLong() throws DescriptorParseException {
+    DescriptorBuilder.createWithIdLine("id ed25519 AAAAAAAAAAAAAAAAAAAAAA"
+        + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testIdRsa512() throws DescriptorParseException {
+    DescriptorBuilder.createWithIdLine("id rsa512 "
+        + "bvegfGxp8k7T9QFpjPTrPaJTa/8");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testIdEd25519Duplicate() throws DescriptorParseException {
+    DescriptorBuilder.createWithIdLine(
+        "id rsa1024 bvegfGxp8k7T9QFpjPTrPaJTa/8\n"
+        + "id rsa1024 bvegfGxp8k7T9QFpjPTrPaJTa/8");
+  }
+}
diff --git a/test/org/torproject/descriptor/impl/RelayNetworkStatusVoteImplTest.java b/test/org/torproject/descriptor/impl/RelayNetworkStatusVoteImplTest.java
index a200dc4..46688d6 100644
--- a/test/org/torproject/descriptor/impl/RelayNetworkStatusVoteImplTest.java
+++ b/test/org/torproject/descriptor/impl/RelayNetworkStatusVoteImplTest.java
@@ -267,7 +267,13 @@ public class RelayNetworkStatusVoteImplTest {
       vb.dirKeyCertificationLines = lines;
       return new RelayNetworkStatusVoteImpl(vb.buildVote(), true);
     }
-    private List<String> statusEntries = new ArrayList<String>();
+    private List<String> statusEntries = null;
+    private static RelayNetworkStatusVote createWithStatusEntries(
+        List<String> statusEntries) throws DescriptorParseException {
+      VoteBuilder vb = new VoteBuilder();
+      vb.statusEntries = statusEntries;
+      return new RelayNetworkStatusVoteImpl(vb.buildVote(), true);
+    }
     private String directoryFooterLine = "directory-footer";
     private static RelayNetworkStatusVote
         createWithDirectoryFooterLine(String line)
@@ -343,6 +349,10 @@ public class RelayNetworkStatusVoteImplTest {
     }
 
     private VoteBuilder() {
+      if (this.statusEntries != null) {
+        return;
+      }
+      this.statusEntries = new ArrayList<>();
       this.statusEntries.add("r right2privassy3 "
           + "ADQ6gCT3DiFHKPDFr3rODBUI8HM lJY5Vf7kXec+VdkGW2flEsfkFC8 "
           + "2011-11-12 00:03:40 50.63.8.215 9023 0\n"
@@ -1181,5 +1191,47 @@ public class RelayNetworkStatusVoteImplTest {
     unrecognizedLines.add(unrecognizedLine);
     assertEquals(unrecognizedLines, vote.getUnrecognizedLines());
   }
+
+  @Test()
+  public void testIdEd25519MasterKey()
+      throws DescriptorParseException {
+    String masterKey25519 = "8RH34kO07Pp+XYwzdoATVyCibIvmbslUjRkAm7J4IA8";
+    List<String> statusEntries = new ArrayList<>();
+    statusEntries.add("r PDrelay1 AAFJ5u9xAqrKlpDW6N0pMhJLlKs "
+        + "bgJiI/la3e9u0K7cQ5pMSXhigHI 2015-12-01 04:54:30 95.215.44.189 "
+        + "8080 0\n"
+        + "id ed25519 " + masterKey25519);
+    RelayNetworkStatusVote vote =
+        VoteBuilder.createWithStatusEntries(statusEntries);
+    String fingerprint = vote.getStatusEntries().firstKey();
+    assertEquals(masterKey25519,
+        vote.getStatusEntry(fingerprint).getMasterKeyEd25519());
+  }
+
+  @Test()
+  public void testIdEd25519None()
+      throws DescriptorParseException {
+    List<String> statusEntries = new ArrayList<>();
+    statusEntries.add("r MathematicalApology AAPJIrV9nhfgTYQs0vsTghFaP2A "
+        + "uA7p0m68O8ILXsf3aLZUj0EvDNE 2015-12-01 18:01:49 172.99.69.177 "
+        + "443 9030\n"
+        + "id ed25519 none");
+    RelayNetworkStatusVote vote =
+        VoteBuilder.createWithStatusEntries(statusEntries);
+    String fingerprint = vote.getStatusEntries().firstKey();
+    assertEquals("none",
+        vote.getStatusEntry(fingerprint).getMasterKeyEd25519());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testIdRsa1024None()
+      throws DescriptorParseException {
+    List<String> statusEntries = new ArrayList<>();
+    statusEntries.add("r MathematicalApology AAPJIrV9nhfgTYQs0vsTghFaP2A "
+        + "uA7p0m68O8ILXsf3aLZUj0EvDNE 2015-12-01 18:01:49 172.99.69.177 "
+        + "443 9030\n"
+        + "id rsa1024 none");
+    VoteBuilder.createWithStatusEntries(statusEntries);
+  }
 }
 
diff --git a/test/org/torproject/descriptor/impl/ServerDescriptorImplTest.java b/test/org/torproject/descriptor/impl/ServerDescriptorImplTest.java
index d2d03f3..a98df9f 100644
--- a/test/org/torproject/descriptor/impl/ServerDescriptorImplTest.java
+++ b/test/org/torproject/descriptor/impl/ServerDescriptorImplTest.java
@@ -228,11 +228,28 @@ public class ServerDescriptorImplTest {
       return new RelayServerDescriptorImpl(db.buildDescriptor(),
           failUnrecognizedDescriptorLines);
     }
+    private String identityEd25519Lines = null,
+        masterKeyEd25519Line = null, routerSigEd25519Line = null;
+    private static ServerDescriptor createWithEd25519Lines(
+        String identityEd25519Lines, String masterKeyEd25519Line,
+        String routerSigEd25519Line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.identityEd25519Lines = identityEd25519Lines;
+      db.masterKeyEd25519Line = masterKeyEd25519Line;
+      db.routerSigEd25519Line = routerSigEd25519Line;
+      return new RelayServerDescriptorImpl(db.buildDescriptor(), true);
+    }
     private byte[] buildDescriptor() {
       StringBuilder sb = new StringBuilder();
       if (this.routerLine != null) {
         sb.append(this.routerLine + "\n");
       }
+      if (this.identityEd25519Lines != null) {
+        sb.append(this.identityEd25519Lines + "\n");
+      }
+      if (this.masterKeyEd25519Line != null) {
+        sb.append(this.masterKeyEd25519Line + "\n");
+      }
       if (this.bandwidthLine != null) {
         sb.append(this.bandwidthLine + "\n");
       }
@@ -313,6 +330,9 @@ public class ServerDescriptorImplTest {
           return null;
         }
       }
+      if (this.routerSigEd25519Line != null) {
+        sb.append(this.routerSigEd25519Line + "\n");
+      }
       if (this.routerSignatureLines != null) {
         sb.append(this.routerSignatureLines + "\n");
       }
@@ -1364,5 +1384,102 @@ public class ServerDescriptorImplTest {
         createWithUnrecognizedLine(sb.toString().substring(1), false);
     assertEquals(unrecognizedLines, descriptor.getUnrecognizedLines());
   }
+
+  private static final String IDENTITY_ED25519_LINES =
+      "identity-ed25519\n"
+      + "-----BEGIN ED25519 CERT-----\n"
+      + "AQQABiX1AVGv5BuzJroQXbOh6vv1nbwc5rh2S13PyRFuLhTiifK4AQAgBACBCMwr"
+      + "\n4qgIlFDIzoC9ieJOtSkwrK+yXJPKlP8ojvgkx8cGKvhokOwA1eYDombzfwHcJ1"
+      + "EV\nbhEn/6g8i7wzO3LoqefIUrSAeEExOAOmm5mNmUIzL8EtnT6JHCr/sqUTUgA="
+      + "\n"
+      + "-----END ED25519 CERT-----";
+
+  private static final String MASTER_KEY_ED25519_LINE =
+      "master-key-ed25519 gQjMK+KoCJRQyM6AvYniTrUpMKyvslyTypT/KI74JMc";
+
+  private static final String ROUTER_SIG_ED25519_LINE =
+      "router-sig-ed25519 y7WF9T2GFwkSDPZEhB55HgquIFOl5uXUFMYJPq3CXXUTKeJ"
+      + "kSrtaZUB5s34fWdHQNtl84mH4dVaFMunHnwgYAw";
+
+  @Test()
+  public void testEd25519() throws DescriptorParseException {
+    ServerDescriptor descriptor =
+        DescriptorBuilder.createWithEd25519Lines(IDENTITY_ED25519_LINES,
+        MASTER_KEY_ED25519_LINE, ROUTER_SIG_ED25519_LINE);
+    assertEquals(IDENTITY_ED25519_LINES.substring(
+        IDENTITY_ED25519_LINES.indexOf("\n") + 1),
+        descriptor.getIdentityEd25519());
+    assertEquals(MASTER_KEY_ED25519_LINE.substring(
+        MASTER_KEY_ED25519_LINE.indexOf(" ") + 1),
+        descriptor.getMasterKeyEd25519());
+    assertEquals(ROUTER_SIG_ED25519_LINE.substring(
+        ROUTER_SIG_ED25519_LINE.indexOf(" ") + 1),
+        descriptor.getRouterSignatureEd25519());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testEd25519IdentityMasterKeyMismatch()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithEd25519Lines(IDENTITY_ED25519_LINES,
+        "master-key-ed25519 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+        ROUTER_SIG_ED25519_LINE);
+  }
+
+  @Test()
+  public void testEd25519IdentityMissing()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithEd25519Lines(null,
+        MASTER_KEY_ED25519_LINE, ROUTER_SIG_ED25519_LINE);
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testEd25519IdentityDuplicate()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithEd25519Lines(IDENTITY_ED25519_LINES + "\n"
+        + IDENTITY_ED25519_LINES, MASTER_KEY_ED25519_LINE,
+        ROUTER_SIG_ED25519_LINE);
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testEd25519IdentityEmptyCrypto()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithEd25519Lines("identity-ed25519\n"
+        + "-----BEGIN ED25519 CERT-----\n-----END ED25519 CERT-----",
+        MASTER_KEY_ED25519_LINE, ROUTER_SIG_ED25519_LINE);
+  }
+
+  @Test()
+  public void testEd25519MasterKeyMissing()
+      throws DescriptorParseException {
+    ServerDescriptor descriptor =
+        DescriptorBuilder.createWithEd25519Lines(IDENTITY_ED25519_LINES,
+        null, ROUTER_SIG_ED25519_LINE);
+    assertEquals(MASTER_KEY_ED25519_LINE.substring(
+        MASTER_KEY_ED25519_LINE.indexOf(" ") + 1),
+        descriptor.getMasterKeyEd25519());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testEd25519MasterKeyDuplicate()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithEd25519Lines(IDENTITY_ED25519_LINES,
+        MASTER_KEY_ED25519_LINE + "\n" + MASTER_KEY_ED25519_LINE,
+        ROUTER_SIG_ED25519_LINE);
+  }
+
+  @Test()
+  public void testEd25519RouterSigMissing()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithEd25519Lines(IDENTITY_ED25519_LINES,
+        MASTER_KEY_ED25519_LINE, null);
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testEd25519RouterSigDuplicate()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithEd25519Lines(IDENTITY_ED25519_LINES,
+        MASTER_KEY_ED25519_LINE, ROUTER_SIG_ED25519_LINE + "\n"
+        + ROUTER_SIG_ED25519_LINE);
+  }
 }
 





More information about the tor-commits mailing list