From d163a2b0a6f30890c5544ae96da0277072dfecfd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 25 Jun 2023 12:53:31 +0200 Subject: [PATCH] It clicked for me; Simplified edge/reverseEdge finding --- .../java/org/pgpainless/wot/WebOfTrust.java | 93 +++++++------ .../wot/WebOfTrustCertificateStore.java | 12 +- .../org/pgpainless/wot/WebOfTrustTest.java | 131 +++++++++++++++++- .../testfixtures/TestCertificateStores.java | 44 ++++-- .../wot/dijkstra/sq/CertificationSet.java | 23 +++ .../pgpainless/wot/dijkstra/sq/Network.java | 7 +- 6 files changed, 250 insertions(+), 60 deletions(-) diff --git a/pgpainless-wot/src/main/java/org/pgpainless/wot/WebOfTrust.java b/pgpainless-wot/src/main/java/org/pgpainless/wot/WebOfTrust.java index 69e7a103..e1ea3baa 100644 --- a/pgpainless-wot/src/main/java/org/pgpainless/wot/WebOfTrust.java +++ b/pgpainless-wot/src/main/java/org/pgpainless/wot/WebOfTrust.java @@ -152,9 +152,6 @@ public class WebOfTrust implements CertificateAuthority { // Target -> Issuer, Signatures on the target private final Map> reverseEdges = new HashMap<>(); - // TODO: Get rid of this - Map>> certifications = new HashMap<>(); - private final Iterable validatedCertificates; private final Policy policy; private final ReferenceTime referenceTime; @@ -167,8 +164,7 @@ public class WebOfTrust implements CertificateAuthority { this.referenceTime = referenceTime; synopsizeCertificates(); - processSignaturesOnCertificates(); - identifyEdges(); + findEdges(); } private void synopsizeCertificates() { @@ -204,15 +200,15 @@ public class WebOfTrust implements CertificateAuthority { new HashSet<>(cert.getValidUserIds()))); } - private void processSignaturesOnCertificates() { + private void findEdges() { // Identify certifications and delegations // Target = cert carrying a signature for (KeyRingInfo validatedTarget : validatedCertificates) { - processSigsOnCert(validatedTarget); + findEdgesWithTarget(validatedTarget); } } - private void processSigsOnCert(KeyRingInfo validatedTarget) { + private void findEdgesWithTarget(KeyRingInfo validatedTarget) { PGPPublicKeyRing validatedTargetKeyRing = KeyRingUtils.publicKeys(validatedTarget.getKeys()); OpenPgpFingerprint targetFingerprint = OpenPgpFingerprint.of(validatedTargetKeyRing); PGPPublicKey targetPrimaryKey = validatedTargetKeyRing.getPublicKey(); @@ -221,7 +217,7 @@ public class WebOfTrust implements CertificateAuthority { // Direct-Key Signatures (delegations) by X on Y List delegations = SignatureUtils.getDelegations(validatedTargetKeyRing); for (PGPSignature delegation : delegations) { - indexAndVerifyDelegation(targetPrimaryKey, target, delegation); + processDelegation(targetPrimaryKey, target, delegation); } // Certification Signatures by X on Y over user-ID U @@ -229,38 +225,38 @@ public class WebOfTrust implements CertificateAuthority { while (userIds.hasNext()) { String userId = userIds.next(); List userIdSigs = SignatureUtils.get3rdPartyCertificationsFor(userId, validatedTargetKeyRing); - indexAndVerifyCertifications(targetPrimaryKey, target, userId, userIdSigs); + processCertification(targetPrimaryKey, target, userId, userIdSigs); } } - private void indexAndVerifyDelegation(PGPPublicKey targetPrimaryKey, CertSynopsis target, PGPSignature delegation) { + private void processDelegation(PGPPublicKey targetPrimaryKey, + CertSynopsis target, + PGPSignature delegation) { List issuerCandidates = byKeyId.get(delegation.getKeyID()); if (issuerCandidates == null) { return; } for (KeyRingInfo candidate : issuerCandidates) { - PGPPublicKeyRing issuerKeyRing = KeyRingUtils.publicKeys(candidate.getKeys()); OpenPgpFingerprint issuerFingerprint = OpenPgpFingerprint.of(issuerKeyRing); PGPPublicKey issuerSigningKey = issuerKeyRing.getPublicKey(delegation.getKeyID()); CertSynopsis issuer = certSynopsisMap.get(issuerFingerprint); - boolean valid = false; try { - valid = SignatureVerifier.verifyDirectKeySignature(delegation, issuerSigningKey, targetPrimaryKey, policy, referenceTime.getTimestamp()); + boolean valid = SignatureVerifier.verifyDirectKeySignature(delegation, issuerSigningKey, + targetPrimaryKey, policy, referenceTime.getTimestamp()); + if (valid) { + indexEdge(new Certification(issuer, Optional.empty(), target, delegation)); + } } catch (SignatureValidationException e) { LOGGER.warn("Cannot verify signature by " + issuerFingerprint + " on cert of " + OpenPgpFingerprint.of(targetPrimaryKey), e); } - - if (valid) { - Map> sigsBy = getOrDefault(certifications, issuerFingerprint, HashMap::new); - List targetSigs = getOrDefault(sigsBy, target.getFingerprint(), ArrayList::new); - targetSigs.add(new Certification(issuer, Optional.empty(), target, delegation)); - } } } - private void indexAndVerifyCertifications(PGPPublicKey targetPrimaryKey, CertSynopsis target, String userId, List userIdSigs) { + private void processCertification(PGPPublicKey targetPrimaryKey, + CertSynopsis target, + String userId, List userIdSigs) { for (PGPSignature certification : userIdSigs) { List issuerCandidates = byKeyId.get(certification.getKeyID()); if (issuerCandidates == null) { @@ -274,12 +270,10 @@ public class WebOfTrust implements CertificateAuthority { CertSynopsis issuer = certSynopsisMap.get(issuerFingerprint); try { - boolean valid = SignatureVerifier.verifySignatureOverUserId(userId, certification, issuerSigningKey, targetPrimaryKey, policy, referenceTime.getTimestamp()); - + boolean valid = SignatureVerifier.verifySignatureOverUserId(userId, certification, + issuerSigningKey, targetPrimaryKey, policy, referenceTime.getTimestamp()); if (valid) { - Map> sigsBy = getOrDefault(certifications, issuerFingerprint, HashMap::new); - List targetSigs = getOrDefault(sigsBy, target.getFingerprint(), ArrayList::new); - targetSigs.add(new Certification(issuer, Optional.just(userId), target, certification)); + indexEdge(new Certification(issuer, Optional.just(userId), target, certification)); } } catch (SignatureValidationException e) { LOGGER.warn("Cannot verify signature for '" + userId + "' by " + issuerFingerprint + " on cert of " + target.getFingerprint(), e); @@ -288,28 +282,37 @@ public class WebOfTrust implements CertificateAuthority { } } - private void identifyEdges() { - // Re-order data structure - for (OpenPgpFingerprint issuerFp : certifications.keySet()) { - Map> issuedBy = certifications.get(issuerFp); + private void indexEdge(Certification certification) { + OpenPgpFingerprint issuer = certification.getIssuer().getFingerprint(); + OpenPgpFingerprint target = certification.getTarget().getFingerprint(); - // one issuer can issue many edges - List outEdges = new ArrayList<>(); - for (OpenPgpFingerprint targetFp : issuedBy.keySet()) { + List outEdges = getOrDefault(edges, issuer, ArrayList::new); + indexOutEdge(outEdges, certification); - List onCert = issuedBy.get(targetFp); - CertificationSet edgeSigs = CertificationSet.empty(certSynopsisMap.get(issuerFp), certSynopsisMap.get(targetFp)); - for (Certification certification : onCert) { - edgeSigs.add(certification); - } - outEdges.add(edgeSigs); - - List reverseEdge = getOrDefault(reverseEdges, targetFp, ArrayList::new); - reverseEdge.add(edgeSigs); + List inEdges = getOrDefault(reverseEdges, target, ArrayList::new); + indexInEdge(inEdges, certification); + } + private void indexOutEdge(List outEdges, Certification certification) { + OpenPgpFingerprint target = certification.getTarget().getFingerprint(); + for (CertificationSet outEdge : outEdges) { + if (target.equals(outEdge.getTarget().getFingerprint())) { + outEdge.add(certification); + return; } - edges.put(issuerFp, outEdges); } + outEdges.add(CertificationSet.fromCertification(certification)); + } + + private void indexInEdge(List inEdges, Certification certification) { + OpenPgpFingerprint issuer = certification.getIssuer().getFingerprint(); + for (CertificationSet inEdge : inEdges) { + if (issuer.equals(inEdge.getIssuer().getFingerprint())) { + inEdge.add(certification); + return; + } + } + inEdges.add(CertificationSet.fromCertification(certification)); } /** @@ -322,6 +325,10 @@ public class WebOfTrust implements CertificateAuthority { } } + Network getNetwork() { + return network; + } + // Map signature to its revocation state private static RevocationState revocationStateFromSignature(PGPSignature revocation) { if (revocation == null) { diff --git a/pgpainless-wot/src/main/java/org/pgpainless/wot/WebOfTrustCertificateStore.java b/pgpainless-wot/src/main/java/org/pgpainless/wot/WebOfTrustCertificateStore.java index 914cf95c..e3ffc56b 100644 --- a/pgpainless-wot/src/main/java/org/pgpainless/wot/WebOfTrustCertificateStore.java +++ b/pgpainless-wot/src/main/java/org/pgpainless/wot/WebOfTrustCertificateStore.java @@ -10,6 +10,8 @@ import pgp.certificate_store.exception.BadDataException; import java.io.IOException; import java.util.Iterator; +import java.util.NoSuchElementException; + import pgp.certificate_store.certificate.Certificate; public class WebOfTrustCertificateStore extends PGPCertificateDirectory { @@ -20,7 +22,15 @@ public class WebOfTrustCertificateStore extends PGPCertificateDirectory { public Iterator getAllItems() throws BadDataException, IOException { - Iterator trustRootAndCerts = new PrefixedIterator<>(getTrustRootCertificate(), items()); + Certificate trustRoot; + try { + trustRoot = getTrustRootCertificate(); + } catch (NoSuchElementException e) { + // ignore + trustRoot = null; + } + + Iterator trustRootAndCerts = new PrefixedIterator<>(trustRoot, items()); return trustRootAndCerts; } } diff --git a/pgpainless-wot/src/test/java/org/pgpainless/wot/WebOfTrustTest.java b/pgpainless-wot/src/test/java/org/pgpainless/wot/WebOfTrustTest.java index daf4aec9..15aab5e5 100644 --- a/pgpainless-wot/src/test/java/org/pgpainless/wot/WebOfTrustTest.java +++ b/pgpainless-wot/src/test/java/org/pgpainless/wot/WebOfTrustTest.java @@ -4,23 +4,148 @@ package org.pgpainless.wot; -import org.junit.jupiter.api.Test; -import org.pgpainless.wot.testfixtures.TestCertificateStores; -import pgp.certificate_store.exception.BadDataException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.pgpainless.key.OpenPgpFingerprint; +import org.pgpainless.wot.dijkstra.sq.CertificationSet; +import org.pgpainless.wot.dijkstra.sq.Network; +import org.pgpainless.wot.testfixtures.TestCertificateStores; +import org.pgpainless.wot.testfixtures.WotTestVectors; +import pgp.certificate_store.exception.BadDataException; public class WebOfTrustTest { + OpenPgpFingerprint fooBankCa = OpenPgpFingerprint.of(WotTestVectors.getTestVectors().getFreshFooBankCaCert()); + OpenPgpFingerprint fooBankEmployee = OpenPgpFingerprint.of(WotTestVectors.getTestVectors().getFreshFooBankEmployeeCert()); + OpenPgpFingerprint fooBankAdmin = OpenPgpFingerprint.of(WotTestVectors.getTestVectors().getFreshFooBankAdminCert()); + OpenPgpFingerprint barBankCa = OpenPgpFingerprint.of(WotTestVectors.getTestVectors().getFreshBarBankCaCert()); + OpenPgpFingerprint barBankEmployee = OpenPgpFingerprint.of(WotTestVectors.getTestVectors().getFreshBarBankEmployeeCert()); + + public WebOfTrustTest() throws IOException { + + } + + @Test + public void testWithTwoNodesAndOneDelegation() throws BadDataException, IOException, InterruptedException { + WebOfTrustCertificateStore store = TestCertificateStores.oneDelegationGraph(); + WebOfTrust wot = new WebOfTrust(store); + wot.initialize(); + Network network = wot.getNetwork(); + + assertEquals(2, network.getNodes().size()); + + assertHasEdge(network, fooBankAdmin, barBankCa); + assertHasReverseEdge(network, fooBankAdmin, barBankCa); + + assertHasNoEdge(network, barBankCa, fooBankAdmin); + assertHasNoReverseEdge(network, barBankCa, fooBankAdmin); + } + @Test public void testWithCrossSignedCertificates() throws BadDataException, IOException, InterruptedException { WebOfTrustCertificateStore store = TestCertificateStores.disconnectedGraph(); WebOfTrust wot = new WebOfTrust(store); wot.initialize(); + Network network = wot.getNetwork(); + + assertEquals(5, network.getNodes().size()); + assertTrue(network.getNodes().containsKey(fooBankCa)); + assertTrue(network.getNodes().containsKey(fooBankEmployee)); + assertTrue(network.getNodes().containsKey(fooBankAdmin)); + assertTrue(network.getNodes().containsKey(barBankCa)); + assertTrue(network.getNodes().containsKey(barBankEmployee)); + + // Exemplary edge + List fooBankCaEdges = network.getEdges().get(fooBankCa); + assertEquals(2, fooBankCaEdges.size()); + + CertificationSet fbc2fbe = getEdgeFromTo(network, fooBankCa, fooBankEmployee); + assertNotNull(fbc2fbe); + CertificationSet fbc2fba = getEdgeFromTo(network, fooBankCa, fooBankAdmin); + assertNotNull(fbc2fba); + + assertHasIssuerAndTarget(fbc2fba, fooBankCa, fooBankAdmin); + assertHasIssuerAndTarget(fbc2fbe, fooBankCa, fooBankEmployee); + + assertHasEdge(network, barBankCa, barBankEmployee); + assertHasReverseEdge(network, barBankCa, barBankEmployee); + + assertHasNoEdge(network, fooBankCa, barBankCa); + assertHasNoReverseEdge(network, fooBankCa, barBankCa); // CHECKSTYLE:OFF System.out.println(wot); // CHECKSTYLE:ON } + + private void assertHasIssuerAndTarget(CertificationSet certifications, OpenPgpFingerprint issuer, OpenPgpFingerprint target) { + assertEquals(issuer, certifications.getIssuer().getFingerprint()); + assertEquals(target, certifications.getTarget().getFingerprint()); + } + + private void assertHasEdge(Network network, OpenPgpFingerprint issuer, OpenPgpFingerprint target) { + assertNotNull(getEdgeFromTo(network, issuer, target), "Expected edge from " + issuer + " to " + target + " but got none."); + } + + private void assertHasReverseEdge(Network network, OpenPgpFingerprint issuer, OpenPgpFingerprint target) { + assertNotNull(getReverseEdgeFromTo(network, issuer, target), "Expected reverse edge to " + target + " from " + issuer + " but got none."); + } + + private void assertHasNoEdge(Network network, OpenPgpFingerprint issuer, OpenPgpFingerprint target) { + CertificationSet edge = getEdgeFromTo(network, issuer, target); + assertNull(edge, "Expected no edge from " + issuer + " to " + target + " but got " + edge); + } + + private void assertHasNoReverseEdge(Network network, OpenPgpFingerprint issuer, OpenPgpFingerprint target) { + CertificationSet reverseEdge = getReverseEdgeFromTo(network, issuer, target); + assertNull(reverseEdge, "Expected no reverse edge on " + target + " from " + issuer + " but got " + reverseEdge); + } + + private CertificationSet getEdgeFromTo(Network network, OpenPgpFingerprint issuer, OpenPgpFingerprint target) { + List edges = network.getEdges().get(issuer); + if (edges == null) { + return null; + } + + for (CertificationSet certifications : edges) { + if (target.equals(certifications.getTarget().getFingerprint())) { + return certifications; + } + } + return null; + } + + private CertificationSet getReverseEdgeFromTo(Network network, OpenPgpFingerprint issuer, OpenPgpFingerprint target) { + List revEdges = network.getReverseEdges().get(target); + if (revEdges == null) { + return null; + } + + for (CertificationSet certifications : revEdges) { + if (issuer.equals(certifications.getIssuer().getFingerprint())) { + return certifications; + } + } + return null; + } + + @Test + public void testWotCreationOfEmptyCertificates() throws BadDataException, IOException { + WebOfTrustCertificateStore store = TestCertificateStores.emptyGraph(); + WebOfTrust wot = new WebOfTrust(store); + wot.initialize(); + Network network = wot.getNetwork(); + + assertTrue(network.getNodes().isEmpty()); + assertTrue(network.getEdges().isEmpty()); + assertTrue(network.getReverseEdges().isEmpty()); + } } diff --git a/pgpainless-wot/src/testFixtures/java/org/pgpainless/wot/testfixtures/TestCertificateStores.java b/pgpainless-wot/src/testFixtures/java/org/pgpainless/wot/testfixtures/TestCertificateStores.java index 062c3fee..33320f6a 100644 --- a/pgpainless-wot/src/testFixtures/java/org/pgpainless/wot/testfixtures/TestCertificateStores.java +++ b/pgpainless-wot/src/testFixtures/java/org/pgpainless/wot/testfixtures/TestCertificateStores.java @@ -28,20 +28,9 @@ public class TestCertificateStores { } }; - private static InputStream requireResource(String resourceName) { - InputStream inputStream = TestCertificateStores.class.getClassLoader().getResourceAsStream(resourceName); - if (inputStream == null) { - throw new TestAbortedException("Cannot read resource " + resourceName + ": InputStream is null."); - } - return inputStream; - } - public static WebOfTrustCertificateStore disconnectedGraph() throws BadDataException, IOException, InterruptedException { - SubkeyLookup subkeyLookup = new InMemorySubkeyLookup(); - KeyMaterialReaderBackend readerBackend = new KeyMaterialReader(); - PGPCertificateDirectory.Backend backend = new InMemoryCertificateDirectoryBackend(readerBackend); - WebOfTrustCertificateStore wotStore = new WebOfTrustCertificateStore(backend, subkeyLookup); + WebOfTrustCertificateStore wotStore = createInMemoryStore(); wotStore.insertTrustRoot(getTestVector("cross_signed/foobankCaCert.asc"), merger); wotStore.insert(getTestVector("cross_signed/foobankEmployeeCert.asc"), merger); @@ -52,6 +41,37 @@ public class TestCertificateStores { return wotStore; } + public static WebOfTrustCertificateStore emptyGraph() { + WebOfTrustCertificateStore wotStore = createInMemoryStore(); + + return wotStore; + } + + public static WebOfTrustCertificateStore oneDelegationGraph() throws BadDataException, IOException, InterruptedException { + WebOfTrustCertificateStore wotStore = createInMemoryStore(); + wotStore.insert(getTestVector("cross_signed/foobankAdminCert.asc"), merger); + wotStore.insert(getTestVector("cross_signed/barbankCaCert.asc"), merger); + + return wotStore; + } + + private static WebOfTrustCertificateStore createInMemoryStore() { + SubkeyLookup subkeyLookup = new InMemorySubkeyLookup(); + KeyMaterialReaderBackend readerBackend = new KeyMaterialReader(); + PGPCertificateDirectory.Backend backend = new InMemoryCertificateDirectoryBackend(readerBackend); + WebOfTrustCertificateStore wotStore = new WebOfTrustCertificateStore(backend, subkeyLookup); + + return wotStore; + } + + private static InputStream requireResource(String resourceName) { + InputStream inputStream = TestCertificateStores.class.getClassLoader().getResourceAsStream(resourceName); + if (inputStream == null) { + throw new TestAbortedException("Cannot read resource " + resourceName + ": InputStream is null."); + } + return inputStream; + } + private static InputStream getTestVector(String testVectorName) { return requireResource("test_vectors/" + testVectorName); } diff --git a/wot-dijkstra/src/main/java/org/pgpainless/wot/dijkstra/sq/CertificationSet.java b/wot-dijkstra/src/main/java/org/pgpainless/wot/dijkstra/sq/CertificationSet.java index 7c846ab7..540ff850 100644 --- a/wot-dijkstra/src/main/java/org/pgpainless/wot/dijkstra/sq/CertificationSet.java +++ b/wot-dijkstra/src/main/java/org/pgpainless/wot/dijkstra/sq/CertificationSet.java @@ -36,6 +36,12 @@ public final class CertificationSet { return new CertificationSet(issuer, target, new HashMap<>()); } + public static CertificationSet fromCertification(Certification certification) { + CertificationSet set = CertificationSet.empty(certification.getIssuer(), certification.getTarget()); + set.add(certification); + return set; + } + /** * Create a {@link CertificationSet} from a single certification. * @@ -67,6 +73,23 @@ public final class CertificationSet { this.certifications = new HashMap<>(certifications); } + public CertSynopsis getIssuer() { + return issuer; + } + + public CertSynopsis getTarget() { + return target; + } + + public Map, List> getCertifications() { + // Copy to avoid side effects + Map, List> copy = new HashMap<>(); + for (Optional key : certifications.keySet()) { + copy.put(key, new ArrayList<>(certifications.get(key))); + } + return copy; + } + /** * Merge this {@link CertificationSet} with another instance. * After the operation, this will contain {@link Certification Certifications} from both sets. diff --git a/wot-dijkstra/src/main/java/org/pgpainless/wot/dijkstra/sq/Network.java b/wot-dijkstra/src/main/java/org/pgpainless/wot/dijkstra/sq/Network.java index 0342d3cf..c35e69b1 100644 --- a/wot-dijkstra/src/main/java/org/pgpainless/wot/dijkstra/sq/Network.java +++ b/wot-dijkstra/src/main/java/org/pgpainless/wot/dijkstra/sq/Network.java @@ -102,7 +102,12 @@ public class Network { } public String toString() { - StringBuilder sb = new StringBuilder("Network with " + getNodes().size() + " nodes, " + getEdges().size() + " edges:\n"); + int edgeNum = 0; + for (List edgesFrom : edges.values()) { + edgeNum += edgesFrom.size(); + } + + StringBuilder sb = new StringBuilder("Network with " + getNodes().size() + " nodes, " + edgeNum + " edges:\n"); for (OpenPgpFingerprint issuer : getNodes().keySet()) { for (CertificationSet edge : getReverseEdges().get(issuer)) { sb.append(edge);