From c1170773bc46b98d7dd9bb6c4dfad60b19fc8f85 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 7 May 2022 14:17:44 +0200 Subject: [PATCH] Implement certification of third party keys --- .../main/java/org/pgpainless/PGPainless.java | 5 + .../algorithm/CertificationType.java | 46 +++++++ .../key/certification/CertifyCertificate.java | 123 ++++++++++++++++++ .../key/certification/package-info.java | 8 ++ .../certification/CertifyCertificateTest.java | 67 ++++++++++ 5 files changed, 249 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/certification/package-info.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index 827085ce..0f47e26e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -16,6 +16,7 @@ import org.pgpainless.decryption_verification.DecryptionBuilder; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.encryption_signing.EncryptionBuilder; import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.key.certification.CertifyCertificate; import org.pgpainless.key.generation.KeyRingBuilder; import org.pgpainless.key.generation.KeyRingTemplates; import org.pgpainless.key.info.KeyRingInfo; @@ -163,4 +164,8 @@ public final class PGPainless { public static Policy getPolicy() { return Policy.getInstance(); } + + public static CertifyCertificate certifyCertificate() { + return new CertifyCertificate(); + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.java new file mode 100644 index 00000000..f5c8ec7e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import javax.annotation.Nonnull; + +/** + * Subset of {@link SignatureType}, reduced to certification types. + */ +public enum CertificationType { + + /** + * The issuer of this certification does not make any particular assertion as to how well the certifier has + * checked that the owner of the key is in fact the person described by the User ID. + */ + GENERIC(SignatureType.GENERIC_CERTIFICATION), + + /** + * The issuer of this certification has not done any verification of the claim that the owner of this key is + * the User ID specified. + */ + NONE(SignatureType.NO_CERTIFICATION), + + /** + * The issuer of this certification has done some casual verification of the claim of identity. + */ + CASUAL(SignatureType.CASUAL_CERTIFICATION), + + /** + * The issuer of this certification has done some casual verification of the claim of identity. + */ + POSITIVE(SignatureType.POSITIVE_CERTIFICATION), + ; + + private final SignatureType signatureType; + + CertificationType(@Nonnull SignatureType signatureType) { + this.signatureType = signatureType; + } + + public @Nonnull SignatureType asSignatureType() { + return signatureType; + } +} 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 new file mode 100644 index 00000000..69029a84 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.certification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CertificationType; +import org.pgpainless.algorithm.KeyFlag; +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.ThirdPartyCertificationSignatureBuilder; +import org.pgpainless.signature.subpackets.CertificationSubpackets; +import org.pgpainless.util.DateUtil; + +import java.util.Date; + +public class CertifyCertificate { + + CertifyUserId certifyUserId(PGPPublicKeyRing certificate, String userId) { + return new CertifyUserId(certificate, userId); + } + + public static class CertifyUserId { + + 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; + this.userId = userId; + 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()); + } + + ThirdPartyCertificationSignatureBuilder sigBuilder = new ThirdPartyCertificationSignatureBuilder( + certificationType.asSignatureType(), secretKey, protector); + + return new CertifyUserIdWithSubpackets(certificate, userId, sigBuilder); + } + } + + public static class CertifyUserIdWithSubpackets { + + private final PGPPublicKeyRing certificate; + private final String userId; + private final ThirdPartyCertificationSignatureBuilder sigBuilder; + + CertifyUserIdWithSubpackets(PGPPublicKeyRing certificate, String userId, ThirdPartyCertificationSignatureBuilder sigBuilder) { + this.certificate = certificate; + this.userId = userId; + this.sigBuilder = sigBuilder; + } + + public CertifyUserIdResult withSubpackets(CertificationSubpackets.Callback subpacketCallback) throws PGPException { + sigBuilder.applyCallback(subpacketCallback); + return build(); + } + + public CertifyUserIdResult build() throws PGPException { + PGPSignature signature = sigBuilder.build(certificate, userId); + + return new CertifyUserIdResult(certificate, userId, signature); + } + } + + public static class CertifyUserIdResult { + + private final PGPPublicKeyRing certificate; + private final String userId; + private final PGPSignature certification; + + CertifyUserIdResult(PGPPublicKeyRing certificate, String userId, PGPSignature certification) { + this.certificate = certificate; + this.userId = userId; + this.certification = certification; + } + + public PGPSignature getCertification() { + return certification; + } + + public PGPPublicKeyRing getCertifiedCertificate() { + // inject the signature + PGPPublicKeyRing certified = KeyRingUtils.injectCertification(certificate, userId, certification); + return certified; + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/certification/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/certification/package-info.java new file mode 100644 index 00000000..db2f4857 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/certification/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * API for key certifications. + */ +package org.pgpainless.key.certification; diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java new file mode 100644 index 00000000..7093865d --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.certification; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.List; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.Arrays; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.signature.consumer.SignatureVerifier; +import org.pgpainless.util.CollectionUtils; +import org.pgpainless.util.DateUtil; + +public class CertifyCertificateTest { + + @Test + public void testSuccessfulCertificationOfUserId() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("Alice ", null); + String bobUserId = "Bob "; + PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing(bobUserId, null); + + PGPPublicKeyRing bobCertificate = PGPainless.extractCertificate(bob); + + CertifyCertificate.CertifyUserIdResult result = PGPainless.certifyCertificate() + .certifyUserId(bobCertificate, bobUserId) + .withKey(alice, protector) + .build(); + + assertNotNull(result); + PGPSignature signature = result.getCertification(); + assertNotNull(signature); + assertEquals(SignatureType.GENERIC_CERTIFICATION, SignatureType.valueOf(signature.getSignatureType())); + assertEquals(alice.getPublicKey().getKeyID(), signature.getKeyID()); + + assertTrue(SignatureVerifier.verifyUserIdCertification( + bobUserId, signature, alice.getPublicKey(), bob.getPublicKey(), PGPainless.getPolicy(), DateUtil.now())); + + PGPPublicKeyRing bobCertified = result.getCertifiedCertificate(); + PGPPublicKey bobCertifiedKey = bobCertified.getPublicKey(); + // There are 2 sigs now, bobs own and alice' + assertEquals(2, CollectionUtils.iteratorToList(bobCertifiedKey.getSignaturesForID(bobUserId)).size()); + 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())); + } +}