commit 39a9c496effde2bd083df0095006ca299f9af2d5
Author: Karsten Loesing <karsten.loesing(a)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);
+ }
}