commit 90d61b700454df4f418ec497191cfb435e55ee10 Author: Karsten Loesing karsten.loesing@gmx.net Date: Tue Jun 14 10:10:24 2016 +0200
Support other algorithms for directory signatures than sha1.
- Support more than one "directory-signature" line in a vote, which may become relevant when authorities start signing votes using more than one algorithm. - Provide directory signatures in consensuses and votes in a list rather than a map to support multiple signatures made using the same identity key digest but different algorithms. - Be more lenient about digest lengths in directory signatures which may be longer or shorter than 20 bytes.
Implements #18875.
While implementing this, make "sha1" constant and deprecate RelayNetworkStatusVote.getSigningKeyDigest(), because it's remissible and ambiguous. Suggested or based on discussions with iwakeh. --- CHANGELOG.md | 8 +++ .../descriptor/RelayNetworkStatusConsensus.java | 11 ++++ .../descriptor/RelayNetworkStatusVote.java | 19 ++++++ .../descriptor/impl/DirectorySignatureImpl.java | 10 ++-- .../descriptor/impl/NetworkStatusImpl.java | 22 +++++-- .../torproject/descriptor/impl/ParseHelper.java | 17 +++++- .../impl/RelayNetworkStatusVoteImpl.java | 17 ++++-- .../impl/RelayNetworkStatusConsensusImplTest.java | 35 ++++++++--- .../impl/RelayNetworkStatusVoteImplTest.java | 69 ++++++++++++++++++++++ 9 files changed, 184 insertions(+), 24 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c243d8..160d405 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@
* Medium changes - Parse "package" lines in consensuses and votes. + - Support more than one "directory-signature" line in a vote, which + may become relevant when authorities start signing votes using + more than one algorithm. + - Provide directory signatures in consensuses and votes in a list + rather than a map to support multiple signatures made using the + same identity key digest but different algorithms. + - Be more lenient about digest lengths in directory signatures + which may be longer or shorter than 20 bytes.
# Changes in version 1.2.0 - 2016-05-31 diff --git a/src/org/torproject/descriptor/RelayNetworkStatusConsensus.java b/src/org/torproject/descriptor/RelayNetworkStatusConsensus.java index 90f96d7..15fdaca 100644 --- a/src/org/torproject/descriptor/RelayNetworkStatusConsensus.java +++ b/src/org/torproject/descriptor/RelayNetworkStatusConsensus.java @@ -186,11 +186,22 @@ public interface RelayNetworkStatusConsensus extends Descriptor { * SHA-1 digests of the authorities' identity keys in the version 3 * directory protocol, encoded as 40 upper-case hexadecimal characters. * + * @deprecated Replaced by {@link #getSignatures()} which permits an + * arbitrary number of signatures made by an authority using the same + * identity key digest and different algorithms. + * * @since 1.0.0 */ public SortedMap<String, DirectorySignature> getDirectorySignatures();
/** + * Return the list of signatures contained in this consensus. + * + * @since 1.3.0 + */ + public List<DirectorySignature> getSignatures(); + + /** * Return optional weights to be applied to router bandwidths during * path selection with map keys being case-sensitive weight identifiers * and map values being weight values, or null if the consensus doesn't diff --git a/src/org/torproject/descriptor/RelayNetworkStatusVote.java b/src/org/torproject/descriptor/RelayNetworkStatusVote.java index 6e7e1b7..1f77db6 100644 --- a/src/org/torproject/descriptor/RelayNetworkStatusVote.java +++ b/src/org/torproject/descriptor/RelayNetworkStatusVote.java @@ -325,6 +325,12 @@ public interface RelayNetworkStatusVote extends Descriptor { * 40 upper-case hexadecimal characters, or null if this digest cannot * be obtained from the directory signature. * + * @deprecated Removed in order to be more explicit that authorities may + * use different digest algorithms than "sha1"; see + * {@link #getSignatures()} and + * {@link DirectorySignature#getSigningKeyDigest()} for + * alternatives. + * * @since 1.0.0 */ public String getSigningKeyDigest(); @@ -382,8 +388,21 @@ public interface RelayNetworkStatusVote extends Descriptor { * 3 directory protocol, encoded as 40 upper-case hexadecimal * characters. * + * @deprecated Replaced by {@link #getSignatures()} which permits an + * arbitrary number of signatures made by the authority using the same + * identity key digest and different algorithms. + * * @since 1.0.0 */ public SortedMap<String, DirectorySignature> getDirectorySignatures(); + + /** + * Return a list of signatures contained in this vote, which is + * typically a single signature made by the authority but which may also + * be more than one signature made with different keys or algorithms. + * + * @since 1.3.0 + */ + public List<DirectorySignature> getSignatures(); }
diff --git a/src/org/torproject/descriptor/impl/DirectorySignatureImpl.java b/src/org/torproject/descriptor/impl/DirectorySignatureImpl.java index 597ccce..a955f62 100644 --- a/src/org/torproject/descriptor/impl/DirectorySignatureImpl.java +++ b/src/org/torproject/descriptor/impl/DirectorySignatureImpl.java @@ -53,9 +53,9 @@ public class DirectorySignatureImpl implements DirectorySignature { throw new DescriptorParseException("Illegal line '" + line + "'."); } - this.identity = ParseHelper.parseTwentyByteHexString(line, + this.identity = ParseHelper.parseHexString(line, parts[1 + algorithmOffset]); - this.signingKeyDigest = ParseHelper.parseTwentyByteHexString( + this.signingKeyDigest = ParseHelper.parseHexString( line, parts[2 + algorithmOffset]); break; case "-----BEGIN": @@ -86,10 +86,12 @@ public class DirectorySignatureImpl implements DirectorySignature { } }
- private String algorithm = "sha1"; + static final String DEFAULT_ALGORITHM = "sha1"; + + private String algorithm; @Override public String getAlgorithm() { - return this.algorithm; + return this.algorithm == null ? DEFAULT_ALGORITHM : this.algorithm; }
private String identity; diff --git a/src/org/torproject/descriptor/impl/NetworkStatusImpl.java b/src/org/torproject/descriptor/impl/NetworkStatusImpl.java index 9a950cf..5fa22c7 100644 --- a/src/org/torproject/descriptor/impl/NetworkStatusImpl.java +++ b/src/org/torproject/descriptor/impl/NetworkStatusImpl.java @@ -217,12 +217,12 @@ public abstract class NetworkStatusImpl extends DescriptorImpl {
protected void parseDirectorySignature(byte[] directorySignatureBytes) throws DescriptorParseException { - if (this.directorySignatures == null) { - this.directorySignatures = new TreeMap<>(); + if (this.signatures == null) { + this.signatures = new ArrayList<>(); } DirectorySignatureImpl signature = new DirectorySignatureImpl( directorySignatureBytes, failUnrecognizedDescriptorLines); - this.directorySignatures.put(signature.getIdentity(), signature); + this.signatures.add(signature); List<String> unrecognizedStatusEntryLines = signature. getAndClearUnrecognizedLines(); if (unrecognizedStatusEntryLines != null) { @@ -251,10 +251,20 @@ public abstract class NetworkStatusImpl extends DescriptorImpl { return this.statusEntries.get(fingerprint); }
- protected SortedMap<String, DirectorySignature> directorySignatures; + protected List<DirectorySignature> signatures; + public List<DirectorySignature> getSignatures() { + return this.signatures == null ? null + : new ArrayList<>(this.signatures); + } public SortedMap<String, DirectorySignature> getDirectorySignatures() { - return this.directorySignatures == null ? null : - new TreeMap<>(this.directorySignatures); + SortedMap<String, DirectorySignature> directorySignatures = null; + if (this.signatures != null) { + directorySignatures = new TreeMap<>(); + for (DirectorySignature signature : this.signatures) { + directorySignatures.put(signature.getIdentity(), signature); + } + } + return directorySignatures; } }
diff --git a/src/org/torproject/descriptor/impl/ParseHelper.java b/src/org/torproject/descriptor/impl/ParseHelper.java index 1d349b4..82c0813 100644 --- a/src/org/torproject/descriptor/impl/ParseHelper.java +++ b/src/org/torproject/descriptor/impl/ParseHelper.java @@ -207,11 +207,22 @@ public class ParseHelper { return result; }
- private static Pattern twentyByteHexPattern = - Pattern.compile("^[0-9a-fA-F]{40}$"); protected static String parseTwentyByteHexString(String line, String hexString) throws DescriptorParseException { - if (!twentyByteHexPattern.matcher(hexString).matches()) { + return parseHexString(line, hexString, 40); + } + + protected static String parseHexString(String line, String hexString) + throws DescriptorParseException { + return parseHexString(line, hexString, -1); + } + + private static Pattern hexPattern = Pattern.compile("^[0-9a-fA-F]*$"); + private static String parseHexString(String line, String hexString, + int expectedLength) throws DescriptorParseException { + if (!hexPattern.matcher(hexString).matches() || + hexString.length() % 2 != 0 || + (expectedLength >= 0 && hexString.length() != expectedLength)) { throw new DescriptorParseException("Illegal hex string in line '" + line + "'."); } diff --git a/src/org/torproject/descriptor/impl/RelayNetworkStatusVoteImpl.java b/src/org/torproject/descriptor/impl/RelayNetworkStatusVoteImpl.java index d9bdd3c..384ad1f 100644 --- a/src/org/torproject/descriptor/impl/RelayNetworkStatusVoteImpl.java +++ b/src/org/torproject/descriptor/impl/RelayNetworkStatusVoteImpl.java @@ -3,6 +3,7 @@ package org.torproject.descriptor.impl;
import org.torproject.descriptor.DescriptorParseException; +import org.torproject.descriptor.DirectorySignature;
import java.util.ArrayList; import java.util.Arrays; @@ -47,7 +48,7 @@ public class RelayNetworkStatusVoteImpl extends NetworkStatusImpl + "valid-until,voting-delay,known-flags,dir-source," + "dir-key-certificate-version,fingerprint,dir-key-published," + "dir-key-expires,dir-identity-key,dir-signing-key," - + "dir-key-certification,directory-signature").split(","))); + + "dir-key-certification").split(","))); this.checkExactlyOnceKeywords(exactlyOnceKeywords); Set<String> atMostOnceKeywords = new HashSet<>(Arrays.asList(( "consensus-methods,client-versions,server-versions," @@ -55,6 +56,9 @@ public class RelayNetworkStatusVoteImpl extends NetworkStatusImpl + "legacy-key,dir-key-crosscert,dir-address,directory-footer"). split(","))); this.checkAtMostOnceKeywords(atMostOnceKeywords); + Set<String> atLeastOnceKeywords = new HashSet<>(Arrays.asList( + "directory-signature")); + this.checkAtLeastOnceKeywords(atLeastOnceKeywords); this.checkFirstKeyword("network-status-version"); this.clearParsedKeywords(); } @@ -605,9 +609,14 @@ public class RelayNetworkStatusVoteImpl extends NetworkStatusImpl @Override public String getSigningKeyDigest() { String signingKeyDigest = null; - if (!this.directorySignatures.isEmpty()) { - signingKeyDigest = this.directorySignatures.get( - this.directorySignatures.firstKey()).getSigningKeyDigest(); + if (this.signatures != null && !this.signatures.isEmpty()) { + for (DirectorySignature signature : this.signatures) { + if (DirectorySignatureImpl.DEFAULT_ALGORITHM.equals( + signature.getAlgorithm())) { + signingKeyDigest = signature.getSigningKeyDigest(); + break; + } + } } return signingKeyDigest; } diff --git a/test/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImplTest.java b/test/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImplTest.java index 86e4e78..d864337 100644 --- a/test/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImplTest.java +++ b/test/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImplTest.java @@ -3,6 +3,8 @@ package org.torproject.descriptor.impl;
import org.torproject.descriptor.DescriptorParseException; +import org.torproject.descriptor.DirectorySignature; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -308,10 +310,13 @@ public class RelayNetworkStatusConsensusImplTest { "00795A6E8D91C270FC23B30F388A495553E01894")); assertEquals("188.177.149.216", consensus.getStatusEntry( "00795A6E8D91C270FC23B30F388A495553E01894").getAddress()); - assertEquals("3509BA5A624403A905C74DA5C8A0CEC9E0D3AF86", - consensus.getDirectorySignatures().get( - "14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4"). - getSigningKeyDigest()); + for (DirectorySignature signature : consensus.getSignatures()) { + if ("14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4".equals( + signature.getIdentity())) { + assertEquals("3509BA5A624403A905C74DA5C8A0CEC9E0D3AF86", + signature.getSigningKeyDigest()); + } + } assertEquals(285, (int) consensus.getBandwidthWeights().get("Wbd")); assertTrue(consensus.getUnrecognizedLines().isEmpty()); } @@ -1114,22 +1119,38 @@ public class RelayNetworkStatusConsensusImplTest { DirectorySignatureBuilder.createWithIdentity("ED03BB616EB2F60"); }
- @Test(expected = DescriptorParseException.class) + @Test() public void testDirectorySignatureIdentityTooLong() throws DescriptorParseException { + /* This hex string has an unusual length of 58 hex characters, but + * dir-spec.txt only requires a hex string, and we can't know all hex + * string lengths for all future digest algorithms, so let's just + * accept this. */ DirectorySignatureBuilder.createWithIdentity( "ED03BB616EB2F60BEC80151114BB25CEF515B226ED03BB616EB2F60BEC"); }
- @Test(expected = DescriptorParseException.class) + @Test() public void testDirectorySignatureSigningKeyTooShort() throws DescriptorParseException { - DirectorySignatureBuilder.createWithSigningKey("845CF1D0B370CA4"); + /* See above, we accept this hex string even though it's unusually + * short. */ + DirectorySignatureBuilder.createWithSigningKey("845CF1D0B370CA"); }
@Test(expected = DescriptorParseException.class) + public void testDirectorySignatureSigningKeyTooShortOddNumber() + throws DescriptorParseException { + /* We don't accept this hex string, because it contains an odd number + * of hex characters. */ + DirectorySignatureBuilder.createWithSigningKey("845"); + } + + @Test() public void testDirectorySignatureSigningKeyTooLong() throws DescriptorParseException { + /* See above, we accept this hex string even though it's unusually + * long. */ DirectorySignatureBuilder.createWithSigningKey( "845CF1D0B370CA443A8579D18E7987E7E532F639845CF1D0B370CA443A"); } diff --git a/test/org/torproject/descriptor/impl/RelayNetworkStatusVoteImplTest.java b/test/org/torproject/descriptor/impl/RelayNetworkStatusVoteImplTest.java index f7ddd8c..1c840f5 100644 --- a/test/org/torproject/descriptor/impl/RelayNetworkStatusVoteImplTest.java +++ b/test/org/torproject/descriptor/impl/RelayNetworkStatusVoteImplTest.java @@ -3,6 +3,8 @@ package org.torproject.descriptor.impl;
import org.torproject.descriptor.DescriptorParseException; +import org.torproject.descriptor.DirectorySignature; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -532,6 +534,18 @@ public class RelayNetworkStatusVoteImplTest { vote.getDirKeyCrosscert().split("\n")[0]); assertEquals("-----BEGIN SIGNATURE-----", vote.getDirKeyCertification().split("\n")[0]); + assertEquals(1, vote.getSignatures().size()); + DirectorySignature signature = vote.getSignatures().get(0); + assertEquals("sha1", signature.getAlgorithm()); + assertEquals("80550987E1D626E3EBA5E5E75A458DE0626D088C", + signature.getIdentity()); + assertEquals("EEB9299D295C1C815E289FBF2F2BBEA5F52FDD19", + signature.getSigningKeyDigest()); + assertEquals("-----BEGIN SIGNATURE-----\n" + + "iHEU3Iidya5RIrjyYgv8tlU0R+rF56/3/MmaaZi0a67e7ZkISfQ4dghScHxn" + + "F3Yh\nrXVaaoP07r6Ta+s0g1Zijm3lms50Nk/4tV2p8Y63c3F4Q3DAnK40Oi" + + "kfOIwEj+Ny\n+zBRQssP3hPhTPOj/A7o3mZZwtL6x1sxpeu/nME1l5E=\n" + + "-----END SIGNATURE-----\n", signature.getSignature()); assertTrue(vote.getUnrecognizedLines().isEmpty()); }
@@ -1204,6 +1218,61 @@ public class RelayNetworkStatusVoteImplTest { VoteBuilder.createWithDirectorySignatureLines(null); }
+ @Test() + public void testDirectorySignaturesLinesTwoAlgorithms() + throws DescriptorParseException { + String identitySha256 = "32519E5CB7254AB5A94CC9925EC7676E53D5D52EEAB7" + + "914BD3ED751E537CAFCC"; + String signingKeyDigestSha256 = "5A59D99C17831B9254422B6C5AA10CC59381" + + "6CAA5241E22ECAE8BBB4E8E9D1FC"; + String signatureSha256 = "-----BEGIN SIGNATURE-----\n" + + "x57Alc424/zHS73SHokghGtNBVrBjtUz+gSL5w9AHGKUQcMyfw4Z9aDlKpTbFc" + + "5W\nnyIvFmM9C2OAH0S1+a647HHIxhE0zKf4+yKSwzqSyL6sbKQygVlJsRHNRr" + + "cFg8lp\nqBxEwvxQoA4xEDqnerR92pbK9l42nNLiKOcoReUqbbQ=\n" + + "-----END SIGNATURE-----"; + String identitySha1 = "80550987E1D626E3EBA5E5E75A458DE0626D088C"; + String signingKeyDigestSha1 = + "EEB9299D295C1C815E289FBF2F2BBEA5F52FDD19"; + String signatureSha1 = "-----BEGIN SIGNATURE-----\n" + + "iHEU3Iidya5RIrjyYgv8tlU0R+rF56/3/MmaaZi0a67e7ZkISfQ4dghScHxnF3" + + "Yh\nrXVaaoP07r6Ta+s0g1Zijm3lms50Nk/4tV2p8Y63c3F4Q3DAnK40OikfOI" + + "wEj+Ny\n+zBRQssP3hPhTPOj/A7o3mZZwtL6x1sxpeu/nME1l5E=\n" + + "-----END SIGNATURE-----"; + String signaturesLines = String.format( + "directory-signature sha256 %s %s\n%s\n" + + "directory-signature %s %s\n%s", identitySha256, + signingKeyDigestSha256, signatureSha256, identitySha1, + signingKeyDigestSha1, signatureSha1); + RelayNetworkStatusVote vote = + VoteBuilder.createWithDirectorySignatureLines(signaturesLines); + assertEquals(2, vote.getSignatures().size()); + DirectorySignature firstSignature = vote.getSignatures().get(0); + assertEquals("sha256", firstSignature.getAlgorithm()); + assertEquals(identitySha256, firstSignature.getIdentity()); + assertEquals(signingKeyDigestSha256, + firstSignature.getSigningKeyDigest()); + assertEquals(signatureSha256 + "\n", firstSignature.getSignature()); + DirectorySignature secondSignature = vote.getSignatures().get(1); + assertEquals("sha1", secondSignature.getAlgorithm()); + assertEquals(identitySha1, secondSignature.getIdentity()); + assertEquals(signingKeyDigestSha1, + secondSignature.getSigningKeyDigest()); + assertEquals(signatureSha1 + "\n", secondSignature.getSignature()); + assertEquals(signingKeyDigestSha1, vote.getSigningKeyDigest()); + } + + @Test() + public void testDirectorySignaturesLinesTwoAlgorithmsSameDigests() + throws DescriptorParseException { + String signaturesLines = "directory-signature 00 00\n" + + "-----BEGIN SIGNATURE-----\n00\n-----END SIGNATURE-----\n" + + "directory-signature sha256 00 00\n" + + "-----BEGIN SIGNATURE-----\n00\n-----END SIGNATURE-----"; + RelayNetworkStatusVote vote = + VoteBuilder.createWithDirectorySignatureLines(signaturesLines); + assertEquals(2, vote.getSignatures().size()); + } + @Test(expected = DescriptorParseException.class) public void testUnrecognizedHeaderLineFail() throws DescriptorParseException {
tor-commits@lists.torproject.org