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