From 8621ae8a69567418234fbd15b37d460a937985b1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Mar 2025 13:20:17 +0100 Subject: [PATCH] Test v6 third party certification generation --- .../key/certification/CertifyCertificate.kt | 133 ++++++++++++++++++ .../builder/RevocationSignatureBuilder.kt | 15 ++ .../CertifyV6CertificateTest.java | 78 ++++++++++ 3 files changed, 226 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyV6CertificateTest.java diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt index c8c7704f..24c21a0b 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt @@ -15,6 +15,7 @@ import org.bouncycastle.openpgp.api.OpenPGPSignature import org.pgpainless.PGPainless import org.pgpainless.algorithm.CertificationType import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.SignatureType import org.pgpainless.algorithm.Trustworthiness import org.pgpainless.exception.KeyException import org.pgpainless.exception.KeyException.ExpiredKeyException @@ -22,9 +23,11 @@ import org.pgpainless.exception.KeyException.MissingSecretKeyException import org.pgpainless.exception.KeyException.RevokedKeyException import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.signature.builder.RevocationSignatureBuilder import org.pgpainless.signature.builder.ThirdPartyCertificationSignatureBuilder import org.pgpainless.signature.builder.ThirdPartyDirectKeySignatureBuilder import org.pgpainless.signature.subpackets.CertificationSubpackets +import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets /** * API for creating certifications and delegations (Signatures) on keys. This API can be used to @@ -78,6 +81,16 @@ class CertifyCertificate(private val api: PGPainless) { certificationType: CertificationType ) = CertificationOnUserId(userId, certificate, certificationType, api) + /** + * Create a certification revocation signature for the given [userId] on the given + * [certificate]. + * + * @param userId userid to revoke + * @param certificate certificate carrying the userid + */ + fun revokeUserIdOnCertificate(userId: CharSequence, certificate: OpenPGPCertificate) = + RevocationOnUserId(userId, certificate, api) + /** * Create a delegation (direct key signature) over a certificate. This can be used to mark a * certificate as a trusted introducer (see [certificate] method with [Trustworthiness] @@ -115,6 +128,14 @@ class CertifyCertificate(private val api: PGPainless) { fun certificate(certificate: PGPPublicKeyRing, trustworthiness: Trustworthiness?) = DelegationOnCertificate(certificate, trustworthiness, api) + /** + * Create a key revocation signature, revoking a delegation over the given [certificate]. + * + * @param certificate certificate to revoke the delegation to + */ + fun revokeCertificate(certificate: OpenPGPCertificate): RevocationOnCertificate = + RevocationOnCertificate(certificate, api) + class CertificationOnUserId( private val userId: CharSequence, private val certificate: OpenPGPCertificate, @@ -203,6 +224,63 @@ class CertifyCertificate(private val api: PGPainless) { } } + class RevocationOnUserId( + private val userId: CharSequence, + private val certificate: OpenPGPCertificate, + private val api: PGPainless + ) { + + fun withKey( + key: OpenPGPKey, + protector: SecretKeyRingProtector + ): RevocationOnUserIdWithSubpackets { + val secretKey = getCertifyingSecretKey(key, api) + val sigBuilder = + RevocationSignatureBuilder( + SignatureType.CERTIFICATION_REVOCATION, secretKey, protector, api) + + return RevocationOnUserIdWithSubpackets(certificate, userId, sigBuilder, api) + } + } + + class RevocationOnUserIdWithSubpackets( + private val certificate: OpenPGPCertificate, + private val userId: CharSequence, + private val sigBuilder: RevocationSignatureBuilder, + private val api: PGPainless + ) { + + /** + * Apply the given signature subpackets and build the revocation signature. + * + * @param subpacketCallback callback to modify the revocation signatures subpackets + * @return result + * @throws PGPException in case of an OpenPGP related error + */ + fun buildWithSubpackets( + subpacketCallback: RevocationSignatureSubpackets.Callback + ): CertificationResult { + sigBuilder.applyCallback(subpacketCallback) + return build() + } + + /** + * Build the revocation signature. + * + * @return result + * @throws PGPException in case of an OpenPGP related error + */ + fun build(): CertificationResult { + val signature = sigBuilder.build(certificate.primaryKey, userId) + val certifiedCertificate = + api.toCertificate( + KeyRingUtils.injectCertification( + certificate.pgpPublicKeyRing, userId, signature.signature)) + + return CertificationResult(certifiedCertificate, signature) + } + } + class DelegationOnCertificate( private val certificate: OpenPGPCertificate, private val trustworthiness: Trustworthiness?, @@ -290,6 +368,61 @@ class CertifyCertificate(private val api: PGPainless) { } } + class RevocationOnCertificate( + private val certificate: OpenPGPCertificate, + private val api: PGPainless + ) { + + fun withKey( + key: OpenPGPKey, + protector: SecretKeyRingProtector + ): RevocationOnCertificateWithSubpackets { + val secretKey = getCertifyingSecretKey(key, api) + val sigBuilder = + RevocationSignatureBuilder(SignatureType.KEY_REVOCATION, secretKey, protector, api) + return RevocationOnCertificateWithSubpackets(certificate, sigBuilder, api) + } + } + + class RevocationOnCertificateWithSubpackets( + private val certificate: OpenPGPCertificate, + private val sigBuilder: RevocationSignatureBuilder, + private val api: PGPainless + ) { + + /** + * Apply the given signature subpackets and build the delegation revocation signature. + * + * @param subpacketsCallback callback to modify the revocations subpackets + * @return result + * @throws PGPException in case of an OpenPGP related error + */ + fun buildWithSubpackets( + subpacketsCallback: RevocationSignatureSubpackets.Callback + ): CertificationResult { + sigBuilder.applyCallback(subpacketsCallback) + return build() + } + + /** + * Build the delegation revocation signature. + * + * @return result + * @throws PGPException in case of an OpenPGP related error + */ + fun build(): CertificationResult { + val revokedKey = certificate.primaryKey + val revocation = sigBuilder.build(revokedKey) + val revokedCertificate = + api.toCertificate( + KeyRingUtils.injectCertification( + certificate.pgpPublicKeyRing, + revokedKey.pgpPublicKey, + revocation.signature)) + return CertificationResult(revokedCertificate, revocation) + } + } + /** * Result of a certification operation. * diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/RevocationSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/RevocationSignatureBuilder.kt index e41e4308..b1912d8c 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/RevocationSignatureBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/RevocationSignatureBuilder.kt @@ -83,6 +83,21 @@ constructor( build(revokeeUserId.userId), signingKey.publicKey, revokeeUserId) } + fun build( + revokeeKey: OpenPGPComponentKey, + revokeeUserId: CharSequence + ): OpenPGPComponentSignature = + OpenPGPComponentSignature( + buildAndInitSignatureGenerator() + .also { + require(_signatureType == SignatureType.CERTIFICATION_REVOCATION) { + "Signature type is != CERTIFICATION_REVOCATION." + } + } + .generateCertification(revokeeUserId.toString(), revokeeKey.pgpPublicKey), + signingKey.publicKey, + revokeeKey.certificate.getUserId(revokeeUserId.toString())) + init { hashedSubpackets.setRevocable(false) } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyV6CertificateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyV6CertificateTest.java new file mode 100644 index 00000000..8c1a2d9a --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyV6CertificateTest.java @@ -0,0 +1,78 @@ +// 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.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.OpenPGPKeyVersion; +import org.pgpainless.key.protection.SecretKeyRingProtector; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CertifyV6CertificateTest { + + @Test + public void testCertifyV6CertWithV6Key() throws PGPException { + PGPainless api = PGPainless.getInstance(); + + OpenPGPKey aliceKey = api.generateKey(OpenPGPKeyVersion.v6) + .modernKeyRing("Alice "); + + OpenPGPKey bobKey = api.generateKey(OpenPGPKeyVersion.v6) + .modernKeyRing("Bob "); + OpenPGPCertificate bobCert = bobKey.toCertificate(); + + // Create a certification on Bobs certificate + OpenPGPCertificate bobCertified = api.generateCertification() + .userIdOnCertificate("Bob ", bobCert) + .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) + .build().getCertifiedCertificate(); + + // Check that there is a valid certification chain from Alice to Bobs UID + OpenPGPCertificate.OpenPGPSignatureChain signatureChain = + bobCertified.getUserId("Bob ") + .getCertificationBy(aliceKey.toCertificate()); + assertNotNull(signatureChain); + assertTrue(signatureChain.isValid()); + + + + // Revoke Alice' key and... + OpenPGPKey aliceRevoked = api.modify(aliceKey) + .revoke(SecretKeyRingProtector.unprotectedKeys()) + .done(); + + // ...verify we no longer have a valid certification chain + OpenPGPCertificate.OpenPGPSignatureChain missingChain = + bobCertified.getUserId("Bob ") + .getCertificationBy(aliceRevoked.toCertificate()); + assertNull(missingChain); + + OpenPGPCertificate.OpenPGPSignatureChain revokedChain = + bobCertified.getUserId("Bob ") + .getRevocationBy(aliceRevoked); + assertNotNull(revokedChain); + assertTrue(revokedChain.isValid()); + + + // Instead, revoke the certification itself and... + bobCertified = api.generateCertification() + .revokeUserIdOnCertificate("Bob ", bobCertified) + .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) + .build().getCertifiedCertificate(); + + // ...verify we now have a valid certification chain + OpenPGPCertificate.OpenPGPSignatureChain brokenChain = + bobCertified.getUserId("Bob ") + .getRevocationBy(aliceKey.toCertificate()); + assertNotNull(brokenChain); + assertTrue(brokenChain.isValid()); + } +}