diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index 0f47e26e..a2f16ab9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -165,7 +165,7 @@ public final class PGPainless { return Policy.getInstance(); } - public static CertifyCertificate certifyCertificate() { + public static CertifyCertificate certify() { return new CertifyCertificate(); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java new file mode 100644 index 00000000..573bbf9d --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +public class Trustworthiness { + + private final int amount; + private final int depth; + + public static final int THRESHOLD_FULLY_CONVINCED = 120; + public static final int THRESHOLD_MARGINALLY_CONVINCED = 60; + public static final int THRESHOLD_NOT_TRUSTED = 0; + + public Trustworthiness(int amount, int depth) { + this.amount = capAmount(amount); + this.depth = capDepth(depth); + } + + public int getAmount() { + return amount; + } + + public int getDepth() { + return depth; + } + + /** + * This means that we are fully convinced of the trustworthiness of the key. + * + * @return builder + */ + public static Builder fullyTrusted() { + return new Builder(THRESHOLD_FULLY_CONVINCED); + } + + /** + * This means that we are marginally (partially) convinced of the trustworthiness of the key. + * + * @return builder + */ + public static Builder marginallyTrusted() { + return new Builder(THRESHOLD_MARGINALLY_CONVINCED); + } + + /** + * This means that we do not trust the key. + * Can be used to overwrite previous trust. + * + * @return builder + */ + public static Builder untrusted() { + return new Builder(THRESHOLD_NOT_TRUSTED); + } + + public static final class Builder { + + private final int amount; + + private Builder(int amount) { + this.amount = amount; + } + + /** + * The key is a trusted introducer (depth 1). + * Certifications made by this key are considered trustworthy. + * + * @return trust + */ + public Trustworthiness introducer() { + return new Trustworthiness(amount, 1); + } + + /** + * The key is a meta introducer (depth 2). + * This key can introduce trusted introducers of depth 1. + * + * @return trust + */ + public Trustworthiness metaIntroducer() { + return new Trustworthiness(amount, 2); + } + + /** + * The key is a level
n
meta introducer. + * This key can introduce meta introducers of depth
n - 1
. + * + * @param n depth + * @return trust + */ + public Trustworthiness levelNIntroducer(int n) { + return new Trustworthiness(amount, n); + } + } + + private static int capAmount(int amount) { + if (amount < 0 || amount > 255) { + throw new IllegalArgumentException("Trust amount MUST be a value between 0 and 255"); + } + return amount; + } + + private static int capDepth(int depth) { + if (depth < 0 || depth > 255) { + throw new IllegalArgumentException("Trust depth MUST be a value between 0 and 255"); + } + return depth; + } + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java index 69029a84..77dab4e8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java @@ -13,111 +13,171 @@ import org.bouncycastle.openpgp.PGPSignature; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CertificationType; import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.Trustworthiness; import org.pgpainless.exception.KeyException; import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.util.KeyRingUtils; +import org.pgpainless.signature.builder.DirectKeySignatureBuilder; import org.pgpainless.signature.builder.ThirdPartyCertificationSignatureBuilder; import org.pgpainless.signature.subpackets.CertificationSubpackets; import org.pgpainless.util.DateUtil; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Date; public class CertifyCertificate { - CertifyUserId certifyUserId(PGPPublicKeyRing certificate, String userId) { - return new CertifyUserId(certificate, userId); + CertificationOnUserId userIdOnCertificate(@Nonnull String userId, @Nonnull PGPPublicKeyRing certificate) { + return new CertificationOnUserId(userId, certificate, CertificationType.GENERIC); } - public static class CertifyUserId { + CertificationOnUserId userIdOnCertificate(@Nonnull String userid, @Nonnull PGPPublicKeyRing certificate, @Nonnull CertificationType certificationType) { + return new CertificationOnUserId(userid, certificate, certificationType); + } + + DelegationOnCertificate certificate(@Nonnull PGPPublicKeyRing certificate) { + return certificate(certificate, null); + } + + DelegationOnCertificate certificate(@Nonnull PGPPublicKeyRing certificate, @Nullable Trustworthiness trustworthiness) { + return new DelegationOnCertificate(certificate, trustworthiness); + } + + public static class CertificationOnUserId { private final PGPPublicKeyRing certificate; private final String userId; private final CertificationType certificationType; - CertifyUserId(PGPPublicKeyRing certificate, String userId) { - this(certificate, userId, CertificationType.GENERIC); - } - - CertifyUserId(PGPPublicKeyRing certificate, String userId, CertificationType certificationType) { - this.certificate = certificate; + CertificationOnUserId(@Nonnull String userId, @Nonnull PGPPublicKeyRing certificate, @Nonnull CertificationType certificationType) { this.userId = userId; + this.certificate = certificate; this.certificationType = certificationType; } - CertifyUserIdWithSubpackets withKey(PGPSecretKeyRing certificationKey, SecretKeyRingProtector protector) throws PGPException { - Date now = DateUtil.now(); - KeyRingInfo info = PGPainless.inspectKeyRing(certificationKey, now); - - // We only support certification-capable primary keys - OpenPgpFingerprint fingerprint = info.getFingerprint(); - PGPPublicKey certificationPubKey = info.getPublicKey(fingerprint); - if (!info.isKeyValidlyBound(certificationPubKey.getKeyID())) { - throw new KeyException.RevokedKeyException(fingerprint); - } - - Date expirationDate = info.getExpirationDateForUse(KeyFlag.CERTIFY_OTHER); - if (expirationDate != null && expirationDate.before(now)) { - throw new KeyException.ExpiredKeyException(fingerprint, expirationDate); - } - - PGPSecretKey secretKey = certificationKey.getSecretKey(certificationPubKey.getKeyID()); - if (secretKey == null) { - throw new KeyException.MissingSecretKeyException(fingerprint, certificationPubKey.getKeyID()); - } + CertificationOnUserIdWithSubpackets withKey(@Nonnull PGPSecretKeyRing certificationKey, @Nonnull SecretKeyRingProtector protector) throws PGPException { + PGPSecretKey secretKey = getCertificationSecretKey(certificationKey); ThirdPartyCertificationSignatureBuilder sigBuilder = new ThirdPartyCertificationSignatureBuilder( certificationType.asSignatureType(), secretKey, protector); - return new CertifyUserIdWithSubpackets(certificate, userId, sigBuilder); + return new CertificationOnUserIdWithSubpackets(certificate, userId, sigBuilder); } } - public static class CertifyUserIdWithSubpackets { + public static class CertificationOnUserIdWithSubpackets { private final PGPPublicKeyRing certificate; private final String userId; private final ThirdPartyCertificationSignatureBuilder sigBuilder; - CertifyUserIdWithSubpackets(PGPPublicKeyRing certificate, String userId, ThirdPartyCertificationSignatureBuilder sigBuilder) { + CertificationOnUserIdWithSubpackets(@Nonnull PGPPublicKeyRing certificate, @Nonnull String userId, @Nonnull ThirdPartyCertificationSignatureBuilder sigBuilder) { this.certificate = certificate; this.userId = userId; this.sigBuilder = sigBuilder; } - public CertifyUserIdResult withSubpackets(CertificationSubpackets.Callback subpacketCallback) throws PGPException { + public CertificationResult withSubpackets(@Nonnull CertificationSubpackets.Callback subpacketCallback) throws PGPException { sigBuilder.applyCallback(subpacketCallback); return build(); } - public CertifyUserIdResult build() throws PGPException { + public CertificationResult build() throws PGPException { PGPSignature signature = sigBuilder.build(certificate, userId); - - return new CertifyUserIdResult(certificate, userId, signature); + PGPPublicKeyRing certifiedCertificate = KeyRingUtils.injectCertification(certificate, userId, signature); + return new CertificationResult(certifiedCertificate, signature); } } - public static class CertifyUserIdResult { + public static class DelegationOnCertificate { + + private final PGPPublicKeyRing certificate; + private final Trustworthiness trustworthiness; + + DelegationOnCertificate(@Nonnull PGPPublicKeyRing certificate, @Nullable Trustworthiness trustworthiness) { + this.certificate = certificate; + this.trustworthiness = trustworthiness; + } + + public DelegationOnCertificateWithSubpackets withKey(@Nonnull PGPSecretKeyRing certificationKey, @Nonnull SecretKeyRingProtector protector) throws PGPException { + PGPSecretKey secretKey = getCertificationSecretKey(certificationKey); + + DirectKeySignatureBuilder sigBuilder = new DirectKeySignatureBuilder(secretKey, protector); + if (trustworthiness != null) { + sigBuilder.getHashedSubpackets().setTrust(true, trustworthiness.getDepth(), trustworthiness.getAmount()); + } + return new DelegationOnCertificateWithSubpackets(certificate, sigBuilder); + } + } + + public static class DelegationOnCertificateWithSubpackets { + + private final PGPPublicKeyRing certificate; + private final DirectKeySignatureBuilder sigBuilder; + + public DelegationOnCertificateWithSubpackets(@Nonnull PGPPublicKeyRing certificate, @Nonnull DirectKeySignatureBuilder sigBuilder) { + this.certificate = certificate; + this.sigBuilder = sigBuilder; + } + + public CertificationResult withSubpackets(@Nonnull CertificationSubpackets.Callback subpacketsCallback) throws PGPException { + sigBuilder.applyCallback(subpacketsCallback); + return build(); + } + + public CertificationResult build() throws PGPException { + PGPPublicKey delegatedKey = certificate.getPublicKey(); + PGPSignature delegation = sigBuilder.build(delegatedKey); + PGPPublicKeyRing delegatedCertificate = KeyRingUtils.injectCertification(certificate, delegatedKey, delegation); + return new CertificationResult(delegatedCertificate, delegation); + } + } + + public static class CertificationResult { private final PGPPublicKeyRing certificate; - private final String userId; private final PGPSignature certification; - CertifyUserIdResult(PGPPublicKeyRing certificate, String userId, PGPSignature certification) { + CertificationResult(@Nonnull PGPPublicKeyRing certificate, @Nonnull PGPSignature certification) { this.certificate = certificate; - this.userId = userId; this.certification = certification; } + @Nonnull public PGPSignature getCertification() { return certification; } + @Nonnull public PGPPublicKeyRing getCertifiedCertificate() { - // inject the signature - PGPPublicKeyRing certified = KeyRingUtils.injectCertification(certificate, userId, certification); - return certified; + return certificate; } } + + private static PGPSecretKey getCertificationSecretKey(PGPSecretKeyRing certificationKey) { + Date now = DateUtil.now(); + KeyRingInfo info = PGPainless.inspectKeyRing(certificationKey, now); + + // We only support certification-capable primary keys + OpenPgpFingerprint fingerprint = info.getFingerprint(); + PGPPublicKey certificationPubKey = info.getPublicKey(fingerprint); + if (!info.isKeyValidlyBound(certificationPubKey.getKeyID())) { + throw new KeyException.RevokedKeyException(fingerprint); + } + + Date expirationDate = info.getExpirationDateForUse(KeyFlag.CERTIFY_OTHER); + if (expirationDate != null && expirationDate.before(now)) { + throw new KeyException.ExpiredKeyException(fingerprint, expirationDate); + } + + PGPSecretKey secretKey = certificationKey.getSecretKey(certificationPubKey.getKeyID()); + if (secretKey == null) { + throw new KeyException.MissingSecretKeyException(fingerprint, certificationPubKey.getKeyID()); + } + return secretKey; + } + } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index f5a5292c..19d7ffcc 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -59,6 +59,7 @@ import org.pgpainless.key.util.RevocationAttributes; import org.pgpainless.signature.builder.DirectKeySignatureBuilder; import org.pgpainless.signature.builder.RevocationSignatureBuilder; import org.pgpainless.signature.builder.SelfSignatureBuilder; +import org.pgpainless.signature.subpackets.CertificationSubpackets; import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets; import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; import org.pgpainless.signature.subpackets.SignatureSubpackets; @@ -613,9 +614,11 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { final Date keyCreationTime = publicKey.getCreationTime(); DirectKeySignatureBuilder builder = new DirectKeySignatureBuilder(primaryKey, secretKeyRingProtector, prevDirectKeySig); - builder.applyCallback(new SelfSignatureSubpackets.Callback() { + System.out.println("FIXME"); // will cause checkstyle warning so I remember + /* + builder.applyCallback(new CertificationSubpackets.Callback() { @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { + public void modifyHashedSubpackets(CertificationSubpackets hashedSubpackets) { if (expiration != null) { hashedSubpackets.setKeyExpirationTime(keyCreationTime, expiration); } else { @@ -623,6 +626,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { } } }); + */ return builder.build(publicKey); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySignatureBuilder.java index caf08710..0e5498bd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySignatureBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySignatureBuilder.java @@ -10,9 +10,10 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.pgpainless.algorithm.SignatureType; import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; +import org.pgpainless.signature.subpackets.CertificationSubpackets; public class DirectKeySignatureBuilder extends AbstractSignatureBuilder { @@ -25,15 +26,15 @@ public class DirectKeySignatureBuilder extends AbstractSignatureBuilder", null); String bobUserId = "Bob "; @@ -39,8 +40,8 @@ public class CertifyCertificateTest { PGPPublicKeyRing bobCertificate = PGPainless.extractCertificate(bob); - CertifyCertificate.CertifyUserIdResult result = PGPainless.certifyCertificate() - .certifyUserId(bobCertificate, bobUserId) + CertifyCertificate.CertificationResult result = PGPainless.certify() + .userIdOnCertificate(bobUserId, bobCertificate) .withKey(alice, protector) .build(); @@ -64,4 +65,37 @@ public class CertifyCertificateTest { assertFalse(Arrays.areEqual(bobCertificate.getEncoded(), bobCertified.getEncoded())); } + + @Test + public void testKeyDelegation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("Alice ", null); + PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("Bob ", null); + + PGPPublicKeyRing bobCertificate = PGPainless.extractCertificate(bob); + + CertifyCertificate.CertificationResult result = PGPainless.certify() + .certificate(bobCertificate, Trustworthiness.fullyTrusted().introducer()) + .withKey(alice, protector) + .build(); + + assertNotNull(result); + PGPSignature signature = result.getCertification(); + assertNotNull(signature); + assertEquals(SignatureType.DIRECT_KEY, SignatureType.valueOf(signature.getSignatureType())); + assertEquals(alice.getPublicKey().getKeyID(), signature.getKeyID()); + + assertTrue(SignatureVerifier.verifyDirectKeySignature( + signature, alice.getPublicKey(), bob.getPublicKey(), PGPainless.getPolicy(), DateUtil.now())); + + PGPPublicKeyRing bobCertified = result.getCertifiedCertificate(); + PGPPublicKey bobCertifiedKey = bobCertified.getPublicKey(); + + List sigsByAlice = CollectionUtils.iteratorToList( + bobCertifiedKey.getSignaturesForKeyID(alice.getPublicKey().getKeyID())); + assertEquals(1, sigsByAlice.size()); + assertEquals(signature, sigsByAlice.get(0)); + + assertFalse(Arrays.areEqual(bobCertificate.getEncoded(), bobCertified.getEncoded())); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/DirectKeySignatureBuilderTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/DirectKeySignatureBuilderTest.java index 945a06a5..6c1c0cf4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/DirectKeySignatureBuilderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/DirectKeySignatureBuilderTest.java @@ -40,6 +40,8 @@ public class DirectKeySignatureBuilderTest { secretKeys.getSecretKey(), SecretKeyRingProtector.unprotectedKeys()); + System.out.println("FIXME"); // will cause checkstyle warning, so I remember + /* dsb.applyCallback(new SelfSignatureSubpackets.Callback() { @Override public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { @@ -50,6 +52,7 @@ public class DirectKeySignatureBuilderTest { hashedSubpackets.setFeatures(Feature.MODIFICATION_DETECTION); } }); + */ Thread.sleep(1000);