From 4f924c519c40cde3957243b8c21b02f9102ceec3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Jun 2023 19:03:05 +0200 Subject: [PATCH] Port WebOfTrust to Kotlin --- .../java/org/pgpainless/wot/WebOfTrust.java | 390 ------------------ .../java/org/pgpainless/wot/package-info.java | 8 - .../wot/sugar/IterableIterator.java | 25 -- .../wot/sugar/PrefixedIterator.java | 37 -- .../org/pgpainless/wot/sugar/Supplier.java | 10 - .../pgpainless/wot/sugar/package-info.java | 8 - .../kotlin/org/pgpainless/wot/WebOfTrust.kt | 301 ++++++++++++++ .../org/pgpainless/wot/WebOfTrustTest.java | 2 +- 8 files changed, 302 insertions(+), 479 deletions(-) delete mode 100644 pgpainless-wot/src/main/java/org/pgpainless/wot/WebOfTrust.java delete mode 100644 pgpainless-wot/src/main/java/org/pgpainless/wot/package-info.java delete mode 100644 pgpainless-wot/src/main/java/org/pgpainless/wot/sugar/IterableIterator.java delete mode 100644 pgpainless-wot/src/main/java/org/pgpainless/wot/sugar/PrefixedIterator.java delete mode 100644 pgpainless-wot/src/main/java/org/pgpainless/wot/sugar/Supplier.java delete mode 100644 pgpainless-wot/src/main/java/org/pgpainless/wot/sugar/package-info.java create mode 100644 pgpainless-wot/src/main/kotlin/org/pgpainless/wot/WebOfTrust.kt diff --git a/pgpainless-wot/src/main/java/org/pgpainless/wot/WebOfTrust.java b/pgpainless-wot/src/main/java/org/pgpainless/wot/WebOfTrust.java deleted file mode 100644 index 3ffcf1c5..00000000 --- a/pgpainless-wot/src/main/java/org/pgpainless/wot/WebOfTrust.java +++ /dev/null @@ -1,390 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.wot; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; - -import org.bouncycastle.bcpg.sig.RevocationReason; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.RevocationState; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.key.util.RevocationAttributes; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.consumer.SignatureVerifier; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.pgpainless.wot.dijkstra.sq.CertSynopsis; -import org.pgpainless.wot.dijkstra.sq.Certification; -import org.pgpainless.wot.dijkstra.sq.CertificationSet; -import org.pgpainless.wot.dijkstra.sq.Network; -import org.pgpainless.wot.dijkstra.sq.ReferenceTime; -import org.pgpainless.wot.sugar.IterableIterator; -import org.pgpainless.wot.sugar.PrefixedIterator; -import org.pgpainless.wot.sugar.Supplier; -import org.pgpainless.wot.util.CertificationFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import pgp.cert_d.PGPCertificateDirectory; -import pgp.certificate_store.certificate.Certificate; -import pgp.certificate_store.exception.BadDataException; - -/** - * Build a Web of Trust from a set of certificates. - *

- * The process of building a WoT is as follows: - *

- * - * @see OpenPGP Web of Trust - */ -public class WebOfTrust { - - private static final Logger LOGGER = LoggerFactory.getLogger(WebOfTrust.class); - - private final PGPCertificateDirectory certificateStore; - private Network network; - - public WebOfTrust(PGPCertificateDirectory certificateStore) { - this.certificateStore = certificateStore; - } - - /** - * Do the heavy lifting of calculating the web of trust. - */ - public void initialize() throws BadDataException, IOException { - Certificate trustRoot = null; - try { - trustRoot = certificateStore.getTrustRootCertificate(); - } catch (NoSuchElementException e) { - // ignore - } - Iterator certificates = certificateStore.items(); - Iterator withTrustRoot = new PrefixedIterator<>(trustRoot, certificates); - IterableIterator iterable = new IterableIterator<>(withTrustRoot); - network = fromCertificates(iterable, PGPainless.getPolicy(), ReferenceTime.now()); - } - - - /** - * Create a {@link Network} from a set of certificates. - * - * @param certificates set of certificates - * @param policy evaluation policy - * @param optReferenceTime reference time for evaluation - * @return network - */ - public static Network fromCertificates( - Iterable certificates, - Policy policy, - ReferenceTime optReferenceTime) { - - ReferenceTime referenceTime = optReferenceTime == null ? ReferenceTime.now() : optReferenceTime; - List validCerts = parseValidCertificates(certificates, policy, referenceTime.getTimestamp()); - - LOGGER.debug("Successfully parsed " + validCerts.size() + " certificates."); - return fromValidCertificates( - validCerts, - policy, - referenceTime - ); - } - - private static List parseValidCertificates(Iterable certificates, Policy policy, Date referenceTime) { - // Parse all certificates - List validCerts = new ArrayList<>(); - for (Certificate cert : certificates) { - try { - PGPPublicKeyRing publicKey = PGPainless.readKeyRing().publicKeyRing(cert.getInputStream()); - // No Certificate data - if (publicKey == null) { - throw new IOException("Certificate " + cert.getFingerprint() + " was null. No certificate data?"); - } - - KeyRingInfo info = new KeyRingInfo(publicKey, policy, referenceTime); - if (info.getValidUserIds().isEmpty()) { - LOGGER.warn("Certificate " + cert.getFingerprint() + " has no valid user-ids. Ignore."); - // Ignore invalid cert - // TODO: Allow user-id-less certificates? - } else { - validCerts.add(info); - } - } catch (IOException e) { - LOGGER.warn("Could not parse certificate " + cert.getFingerprint(), e); - } - } - return validCerts; - } - - /** - * Create a {@link Network} from a set of validated certificates. - * - * @param validatedCertificates set of validated certificates - * @param referenceTime reference time - * @return network - */ - public static Network fromValidCertificates( - List validatedCertificates, - Policy policy, - ReferenceTime referenceTime) { - - // TODO: Move heavy lifting from NetworkBuilder constructor to buildNetwork()? - NetworkBuilder nb = new NetworkBuilder(validatedCertificates, policy, referenceTime); - return nb.buildNetwork(); - } - - /** - * Class for building the {@link Network Flow network} from the given set of OpenPGP keys. - * - */ - private static final class NetworkBuilder { - - // certificates keyed by fingerprint - private final Map byFingerprint = new HashMap<>(); - // certificates keyed by (sub-) key-id - private final Map> byKeyId = new HashMap<>(); - // certificate synopses keyed by fingerprint - private final Map certSynopsisMap = new HashMap<>(); - - // Issuer -> Targets, edges keyed by issuer - private final Map> edges = new HashMap<>(); - // Target -> Issuers, edges keyed by target - private final Map> reverseEdges = new HashMap<>(); - - private final Policy policy; - private final ReferenceTime referenceTime; - - private NetworkBuilder(List validatedCertificates, - Policy policy, - ReferenceTime referenceTime) { - this.policy = policy; - this.referenceTime = referenceTime; - - synopsizeCertificates(validatedCertificates); - findEdges(validatedCertificates); - } - - private void synopsizeCertificates(List validatedCertificates) { - for (KeyRingInfo cert : validatedCertificates) { - synopsize(cert); - } - } - - private void synopsize(KeyRingInfo cert) { - - // index by fingerprint - if (!byFingerprint.containsKey(cert.getFingerprint())) { - byFingerprint.put(cert.getFingerprint(), cert); - } - - // index by key-ID - List certsWithKey = byKeyId.get(cert.getKeyId()); - // noinspection Java8MapApi - if (certsWithKey == null) { - certsWithKey = new ArrayList<>(); - // TODO: Something is fishy here... - for (PGPPublicKey key : cert.getValidSubkeys()) { - byKeyId.put(key.getKeyID(), certsWithKey); - } - } - certsWithKey.add(cert); - - Map userIds = new HashMap<>(); - for (String userId : cert.getUserIds()) { - RevocationState state = revocationStateFromSignature(cert.getUserIdRevocation(userId)); - userIds.put(userId, state); - } - - // index synopses - Date expirationDate; - try { - expirationDate = cert.getExpirationDateForUse(KeyFlag.CERTIFY_OTHER); - } catch (NoSuchElementException e) { - // Some keys are malformed and have no KeyFlags - return; - } - certSynopsisMap.put(cert.getFingerprint(), - new CertSynopsis(cert.getFingerprint(), - expirationDate, - revocationStateFromSignature(cert.getRevocationSelfSignature()), - userIds)); - - } - - private void findEdges(List validatedCertificates) { - // Identify certifications and delegations - // Target = cert carrying a signature - for (KeyRingInfo validatedTarget : validatedCertificates) { - findEdgesWithTarget(validatedTarget); - } - } - - private void findEdgesWithTarget(KeyRingInfo validatedTarget) { - PGPPublicKeyRing validatedTargetKeyRing = KeyRingUtils.publicKeys(validatedTarget.getKeys()); - OpenPgpFingerprint targetFingerprint = OpenPgpFingerprint.of(validatedTargetKeyRing); - PGPPublicKey targetPrimaryKey = validatedTargetKeyRing.getPublicKey(); - CertSynopsis target = certSynopsisMap.get(targetFingerprint); - - // Direct-Key Signatures (delegations) by X on Y - List delegations = SignatureUtils.getDelegations(validatedTargetKeyRing); - for (PGPSignature delegation : delegations) { - processDelegation(targetPrimaryKey, target, delegation); - } - - // Certification Signatures by X on Y over user-ID U - Iterator userIds = targetPrimaryKey.getUserIDs(); - while (userIds.hasNext()) { - String userId = userIds.next(); - List userIdSigs = SignatureUtils.get3rdPartyCertificationsFor(userId, validatedTargetKeyRing); - processCertification(targetPrimaryKey, target, userId, userIdSigs); - } - } - - 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); - try { - boolean valid = SignatureVerifier.verifyDirectKeySignature(delegation, issuerSigningKey, - targetPrimaryKey, policy, referenceTime.getTimestamp()); - if (valid) { - indexEdge(CertificationFactory.fromDelegation(issuer, target, delegation)); - } - } catch (SignatureValidationException e) { - LOGGER.warn("Cannot verify signature by " + issuerFingerprint + " on cert of " + OpenPgpFingerprint.of(targetPrimaryKey), e); - } - } - } - - private void processCertification(PGPPublicKey targetPrimaryKey, - CertSynopsis target, - String userId, List userIdSigs) { - for (PGPSignature certification : userIdSigs) { - List issuerCandidates = byKeyId.get(certification.getKeyID()); - if (issuerCandidates == null) { - continue; - } - - for (KeyRingInfo candidate : issuerCandidates) { - PGPPublicKeyRing issuerKeyRing = KeyRingUtils.publicKeys(candidate.getKeys()); - OpenPgpFingerprint issuerFingerprint = OpenPgpFingerprint.of(issuerKeyRing); - PGPPublicKey issuerSigningKey = issuerKeyRing.getPublicKey(certification.getKeyID()); - CertSynopsis issuer = certSynopsisMap.get(issuerFingerprint); - - try { - boolean valid = SignatureVerifier.verifySignatureOverUserId(userId, certification, - issuerSigningKey, targetPrimaryKey, policy, referenceTime.getTimestamp()); - if (valid) { - indexEdge(CertificationFactory.fromCertification(issuer, target, userId, certification)); - } - } catch (SignatureValidationException e) { - LOGGER.warn("Cannot verify signature for '" + userId + "' by " + issuerFingerprint + " on cert of " + target.getFingerprint(), e); - } - } - } - } - - private void indexEdge(Certification certification) { - OpenPgpFingerprint issuer = certification.getIssuer().getFingerprint(); - OpenPgpFingerprint target = certification.getTarget().getFingerprint(); - - List outEdges = getOrDefault(edges, issuer, ArrayList::new); - indexOutEdge(outEdges, certification); - - 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; - } - } - 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)); - } - - /** - * Return the constructed, initialized {@link Network}. - * - * @return finished network - */ - public Network buildNetwork() { - return new Network(certSynopsisMap, edges, reverseEdges, referenceTime); - } - } - - Network getNetwork() { - return network; - } - - // Map signature to its revocation state - private static RevocationState revocationStateFromSignature(PGPSignature revocation) { - if (revocation == null) { - return RevocationState.notRevoked(); - } - - RevocationReason revocationReason = SignatureSubpacketsUtil.getRevocationReason(revocation); - if (revocationReason == null) { - return RevocationState.hardRevoked(); - } - - return RevocationAttributes.Reason.isHardRevocation(revocationReason.getRevocationReason()) ? - RevocationState.hardRevoked() : RevocationState.softRevoked(revocation.getCreationTime()); - } - - // Java 8 is not supported on old Android - private static V getOrDefault(Map map, K key, Supplier defaultValue) { - V value = map.get(key); - if (value == null) { - value = defaultValue.get(); - map.put(key, value); - } - return value; - } - - @Override - public String toString() { - return network.toString(); - } -} diff --git a/pgpainless-wot/src/main/java/org/pgpainless/wot/package-info.java b/pgpainless-wot/src/main/java/org/pgpainless/wot/package-info.java deleted file mode 100644 index c35fe768..00000000 --- a/pgpainless-wot/src/main/java/org/pgpainless/wot/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * PGPainless API for Web of Trust. - */ -package org.pgpainless.wot; diff --git a/pgpainless-wot/src/main/java/org/pgpainless/wot/sugar/IterableIterator.java b/pgpainless-wot/src/main/java/org/pgpainless/wot/sugar/IterableIterator.java deleted file mode 100644 index a2de4256..00000000 --- a/pgpainless-wot/src/main/java/org/pgpainless/wot/sugar/IterableIterator.java +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.wot.sugar; - -import java.util.Iterator; - -/** - * Because an {@link Iterator} is not {@link Iterable} ¯\_(ツ)_/¯. - * @param item - */ -public final class IterableIterator implements Iterable { - - private final Iterator iterator; - - public IterableIterator(Iterator iterator) { - this.iterator = iterator; - } - - @Override - public Iterator iterator() { - return iterator; - } -} diff --git a/pgpainless-wot/src/main/java/org/pgpainless/wot/sugar/PrefixedIterator.java b/pgpainless-wot/src/main/java/org/pgpainless/wot/sugar/PrefixedIterator.java deleted file mode 100644 index 1c31085f..00000000 --- a/pgpainless-wot/src/main/java/org/pgpainless/wot/sugar/PrefixedIterator.java +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.wot.sugar; - -import java.util.Iterator; - -/** - * Returns a new {@link Iterator} with a prepended item. - * @param item type - */ -public class PrefixedIterator implements Iterator { - - private T prefix; - private Iterator iterator; - - public PrefixedIterator(T prefix, Iterator iterator) { - this.prefix = prefix; - this.iterator = iterator; - } - - @Override - public boolean hasNext() { - return prefix != null || iterator.hasNext(); - } - - @Override - public T next() { - if (prefix != null) { - T t = prefix; - prefix = null; - return t; - } - return iterator.next(); - } -} diff --git a/pgpainless-wot/src/main/java/org/pgpainless/wot/sugar/Supplier.java b/pgpainless-wot/src/main/java/org/pgpainless/wot/sugar/Supplier.java deleted file mode 100644 index c0a95f50..00000000 --- a/pgpainless-wot/src/main/java/org/pgpainless/wot/sugar/Supplier.java +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.wot.sugar; - -public interface Supplier { - - T get(); -} diff --git a/pgpainless-wot/src/main/java/org/pgpainless/wot/sugar/package-info.java b/pgpainless-wot/src/main/java/org/pgpainless/wot/sugar/package-info.java deleted file mode 100644 index 54a524f8..00000000 --- a/pgpainless-wot/src/main/java/org/pgpainless/wot/sugar/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Syntactic sugar, backported from Java 8. - */ -package org.pgpainless.wot.sugar; diff --git a/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/WebOfTrust.kt b/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/WebOfTrust.kt new file mode 100644 index 00000000..44a9acb7 --- /dev/null +++ b/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/WebOfTrust.kt @@ -0,0 +1,301 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.wot + +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.RevocationState +import org.pgpainless.exception.SignatureValidationException +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.info.KeyRingInfo +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.key.util.RevocationAttributes +import org.pgpainless.policy.Policy +import org.pgpainless.signature.SignatureUtils +import org.pgpainless.signature.consumer.SignatureVerifier +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil +import org.pgpainless.wot.dijkstra.sq.* +import org.pgpainless.wot.dijkstra.sq.CertificationSet.Companion.fromCertification +import org.pgpainless.wot.dijkstra.sq.ReferenceTime.Companion.now +import org.pgpainless.wot.util.CertificationFactory.Companion.fromCertification +import org.pgpainless.wot.util.CertificationFactory.Companion.fromDelegation +import org.slf4j.LoggerFactory +import pgp.cert_d.PGPCertificateDirectory +import pgp.certificate_store.certificate.Certificate +import java.io.IOException +import java.util.* + +class WebOfTrust(private val certificateStore: PGPCertificateDirectory) { + + lateinit var network: Network + + fun initialize() { + var trustRoot: Certificate? = null + try { + trustRoot = certificateStore.trustRootCertificate + } catch (e: NoSuchElementException) { + // ignore + } + + val certificates = if (trustRoot == null) { + certificateStore.items().asSequence() + } else { + sequenceOf(trustRoot) + certificateStore.items().asSequence() + } + + network = fromCertificates(certificates, PGPainless.getPolicy(), now()) + } + + companion object { + @JvmStatic + fun fromCertificates(certificates: Sequence, + policy: Policy, + referenceTime: ReferenceTime): Network { + return fromValidCertificates( + parseValidCertificates(certificates, policy, referenceTime), + policy, + referenceTime + ) + } + + @JvmStatic + fun fromValidCertificates(certificates: List, + policy: Policy, + referenceTime: ReferenceTime): Network { + val nb = NetworkBuilder(certificates, policy, referenceTime) + return nb.buildNetwork() + } + + @JvmStatic + private fun parseValidCertificates(certificates: Sequence, + policy: Policy, + referenceTime: ReferenceTime): List { + return certificates + .mapNotNull { cert -> + try { + PGPainless.readKeyRing().publicKeyRing(cert.inputStream) + } catch (e: IOException) { + null + } + } + .map { cert -> + KeyRingInfo(cert, policy, referenceTime.timestamp) + } + .toList() + } + + // Map signature to its revocation state + @JvmStatic + private fun revocationStateFromSignature(revocation: PGPSignature?): RevocationState { + if (revocation == null) { + return RevocationState.notRevoked() + } + val revocationReason = SignatureSubpacketsUtil.getRevocationReason(revocation) + ?: return RevocationState.hardRevoked() + return if (RevocationAttributes.Reason.isHardRevocation(revocationReason.revocationReason)) + RevocationState.hardRevoked() + else + RevocationState.softRevoked(revocation.creationTime) + } + + /** + * Class for building the [Flow network][Network] from the given set of OpenPGP keys. + * + */ + private class NetworkBuilder constructor(validatedCertificates: List, + private val policy: Policy, + private val referenceTime: ReferenceTime) { + + private val LOGGER = LoggerFactory.getLogger(NetworkBuilder::class.java) + + // certificates keyed by fingerprint + private val byFingerprint: MutableMap = HashMap() + + // certificates keyed by (sub-) key-id + private val byKeyId: MutableMap> = HashMap() + + // certificate synopses keyed by fingerprint + private val certSynopsisMap: MutableMap = HashMap() + + // Issuer -> Targets, edges keyed by issuer + private val edges: MutableMap> = HashMap() + + // Target -> Issuers, edges keyed by target + private val reverseEdges: MutableMap> = HashMap() + + init { + synopsizeCertificates(validatedCertificates) + findEdges(validatedCertificates) + } + + private fun synopsizeCertificates(validatedCertificates: List) { + for (cert in validatedCertificates) { + synopsize(cert) + } + } + + private fun synopsize(cert: KeyRingInfo) { + + // index by fingerprint + if (!byFingerprint.containsKey(cert.fingerprint)) { + byFingerprint[cert.fingerprint] = cert + } + + // index by key-ID + var certsWithKey = byKeyId[cert.keyId] + // noinspection Java8MapApi + if (certsWithKey == null) { + certsWithKey = mutableListOf() + // TODO: Something is fishy here... + for (key in cert.validSubkeys) { + byKeyId[key.keyID] = certsWithKey + } + } + certsWithKey.add(cert) + val userIds: MutableMap = HashMap() + for (userId in cert.userIds) { + val state: RevocationState = revocationStateFromSignature(cert.getUserIdRevocation(userId)) + userIds[userId] = state + } + + // index synopses + val expirationDate: Date? = try { + cert.getExpirationDateForUse(KeyFlag.CERTIFY_OTHER) + } catch (e: NoSuchElementException) { + // Some keys are malformed and have no KeyFlags + return + } + certSynopsisMap[cert.fingerprint] = CertSynopsis(cert.fingerprint, + expirationDate, + revocationStateFromSignature(cert.revocationSelfSignature), + userIds) + } + + private fun findEdges(validatedCertificates: List) { + // Identify certifications and delegations + // Target = cert carrying a signature + for (validatedTarget in validatedCertificates) { + findEdgesWithTarget(validatedTarget) + } + } + + private fun findEdgesWithTarget(validatedTarget: KeyRingInfo) { + val validatedTargetKeyRing = KeyRingUtils.publicKeys(validatedTarget.keys) + val targetFingerprint = OpenPgpFingerprint.of(validatedTargetKeyRing) + val targetPrimaryKey = validatedTargetKeyRing.publicKey!! + val target = certSynopsisMap[targetFingerprint]!! + + // Direct-Key Signatures (delegations) by X on Y + val delegations = SignatureUtils.getDelegations(validatedTargetKeyRing) + for (delegation in delegations) { + processDelegation(targetPrimaryKey, target, delegation) + } + + // Certification Signatures by X on Y over user-ID U + val userIds = targetPrimaryKey.userIDs + while (userIds.hasNext()) { + val userId = userIds.next() + val userIdSigs = SignatureUtils.get3rdPartyCertificationsFor(userId, validatedTargetKeyRing) + processCertification(targetPrimaryKey, target, userId, userIdSigs) + } + } + + private fun processDelegation(targetPrimaryKey: PGPPublicKey, + target: CertSynopsis, + delegation: PGPSignature) { + val issuerCandidates = byKeyId[delegation.keyID] + ?: return + for (candidate in issuerCandidates) { + val issuerKeyRing = KeyRingUtils.publicKeys(candidate.keys) + val issuerFingerprint = OpenPgpFingerprint.of(issuerKeyRing) + val issuerSigningKey = issuerKeyRing.getPublicKey(delegation.keyID) + val issuer = certSynopsisMap[issuerFingerprint] + ?: continue + try { + val valid = SignatureVerifier.verifyDirectKeySignature(delegation, issuerSigningKey, + targetPrimaryKey, policy, referenceTime.timestamp) + if (valid) { + indexEdge(fromDelegation(issuer, target, delegation)) + } + } catch (e: SignatureValidationException) { + val targetFingerprint = OpenPgpFingerprint.of(targetPrimaryKey) + LOGGER.warn("Cannot verify signature by $issuerFingerprint on cert of $targetFingerprint", e) + } + } + } + + private fun processCertification(targetPrimaryKey: PGPPublicKey, + target: CertSynopsis, + userId: String, + userIdSigs: List) { + for (certification in userIdSigs) { + val issuerCandidates = byKeyId[certification.keyID] + ?: continue + for (candidate in issuerCandidates) { + val issuerKeyRing = KeyRingUtils.publicKeys(candidate.keys) + val issuerFingerprint = OpenPgpFingerprint.of(issuerKeyRing) + val issuerSigningKey = issuerKeyRing.getPublicKey(certification.keyID) + ?: continue + val issuer = certSynopsisMap[issuerFingerprint] + ?: continue + try { + val valid = SignatureVerifier.verifySignatureOverUserId(userId, certification, + issuerSigningKey, targetPrimaryKey, policy, referenceTime.timestamp) + if (valid) { + indexEdge(fromCertification(issuer, target, userId, certification)) + } + } catch (e: SignatureValidationException) { + LOGGER.warn("Cannot verify signature for '$userId' by $issuerFingerprint" + + " on cert of ${target.fingerprint}", e) + } + } + } + } + + private fun indexEdge(certification: Certification) { + // Index edge as outgoing edge for issuer + val issuer = certification.issuer.fingerprint + edges.getOrPut(issuer) { mutableListOf() }.also { indexOutEdge(it, certification) } + + // Index edge as incoming edge for target + val target = certification.target.fingerprint + reverseEdges.getOrPut(target) { mutableListOf() }.also { indexInEdge(it, certification) } + } + + private fun indexOutEdge(outEdges: MutableList, certification: Certification) { + val target = certification.target.fingerprint + for (outEdge in outEdges) { + if (target == outEdge.target.fingerprint) { + outEdge.add(certification) + return + } + } + outEdges.add(fromCertification(certification)) + } + + private fun indexInEdge(inEdges: MutableList, certification: Certification) { + val issuer = certification.issuer.fingerprint + for (inEdge in inEdges) { + if (issuer == inEdge.issuer.fingerprint) { + inEdge.add(certification) + return + } + } + inEdges.add(fromCertification(certification)) + } + + /** + * Return the constructed, initialized [Network]. + * + * @return finished network + */ + fun buildNetwork(): Network { + return Network(certSynopsisMap, edges, reverseEdges, referenceTime) + } + } + } +} 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 7ea75509..55cdf81f 100644 --- a/pgpainless-wot/src/test/java/org/pgpainless/wot/WebOfTrustTest.java +++ b/pgpainless-wot/src/test/java/org/pgpainless/wot/WebOfTrustTest.java @@ -139,7 +139,7 @@ public class WebOfTrustTest { } @Test - public void testWotCreationOfEmptyCertificates() throws BadDataException, IOException { + public void testWotCreationOfEmptyCertificates() { PGPCertificateDirectory store = TestCertificateStores.emptyGraph(); WebOfTrust wot = new WebOfTrust(store); wot.initialize();