From 1ae6b2d74e9e8fe8c833038b5cb8262261c5aa18 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 24 Feb 2026 13:21:03 +0100 Subject: [PATCH 1/5] AbstractOpenPGPDocumentSignatureGenerator: Properly apply signature creation time from SignatureParameters This fixes OpenPGPMessageGenerator not applying custom signature creation times for message signatures --- .../openpgp/api/AbstractOpenPGPDocumentSignatureGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/AbstractOpenPGPDocumentSignatureGenerator.java b/pg/src/main/java/org/bouncycastle/openpgp/api/AbstractOpenPGPDocumentSignatureGenerator.java index 4111697472..241c8e1b2b 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/api/AbstractOpenPGPDocumentSignatureGenerator.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/AbstractOpenPGPDocumentSignatureGenerator.java @@ -251,7 +251,7 @@ protected PGPSignatureGenerator initSignatureGenerator( } return Utils.getPgpSignatureGenerator(implementation, signingKey.getPGPPublicKey(), - unlockedKey.getPrivateKey(), parameters, null, null); + unlockedKey.getPrivateKey(), parameters, parameters.getSignatureCreationTime(), null); } private int getPreferredHashAlgorithm(OpenPGPCertificate.OpenPGPComponentKey key) From d1ba4bb6c6c9dab8bf3a8cd2de05d176179af314 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 24 Feb 2026 13:22:57 +0100 Subject: [PATCH 2/5] Swap and simplify if statement in subkey signature chain creation --- .../bouncycastle/openpgp/api/OpenPGPCertificate.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java index 19b954afa2..ac68a7113f 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java @@ -804,17 +804,17 @@ private void processSubkey(OpenPGPSubkey subkey) } OpenPGPSignatureChains issuerChains = getAllSignatureChainsFor(issuer); - if (!issuerChains.chains.isEmpty()) + if (issuerChains.chains.isEmpty()) + { + subkeyChains.add(OpenPGPSignatureChain.direct(sig)); + } + else { for (Iterator it2 = issuerChains.chains.iterator(); it2.hasNext(); ) { subkeyChains.add(it2.next().plus(sig)); } } - else - { - subkeyChains.add(new OpenPGPSignatureChain(OpenPGPSignatureChain.Link.create(sig))); - } } this.componentSignatureChains.put(subkey, subkeyChains); } From 96d5b32700fec7cd326db38c3a559e7c63e7df6a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 24 Feb 2026 13:33:50 +0100 Subject: [PATCH 3/5] Add support for simplified component binding signature history evaluation Historically, OpenPGP implementations check the full signature history of a component key, when evaluating its validity. E.g. if a message signature was created at t1, signing key binding signatures at t2 are ignored, since they are not yet effective. Instead, binding signatures from t1 or earlier (t0) are checked. Now, there are multiple reasons why a certificate holder might want to clean older key signatures from their certificate. Most notably, PQC signatures are rather large in size, so cleaning historic signatures from time to time in order to maintain a reasonable certificate size is sensible. Unfortunately, with the current binding signature evluation model, this might lead to historic document signatures or third-party certifications to break, as the binding from t1 or t0 might no longer be there. Therefore, it is desirable to implement a different component binding signature evaluation model which prevents such 'temporal holes'. Such a model is described and proposed in https://mailarchive.ietf.org/arch/msg/openpgp/kA4YtiP3j8LJUift1_D0mWIHVV0/ This PR compartmentalizes the signature validity period evaluation into a delegate (ComponentSignatureEvaluator) and allows the user to swap out the classic implementation with a custom implementation that implements the simplified validity check logic. This can be done via the OpenPGPPolicy class. --- .../openpgp/api/OpenPGPCertificate.java | 99 +++++++++- .../openpgp/api/OpenPGPDefaultPolicy.java | 30 ++++ .../openpgp/api/OpenPGPPolicy.java | 7 + .../api/test/OpenPGPCertificateTest.java | 170 ++++++++++++++++++ 4 files changed, 302 insertions(+), 4 deletions(-) diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java index ac68a7113f..82475c87a8 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java @@ -2870,21 +2870,24 @@ public static class OpenPGPSignatureChain implements Comparable, Iterable { private final List chainLinks = new ArrayList(); + private final ComponentSignatureEvaluator componentSignatureEvaluator; - private OpenPGPSignatureChain(Link rootLink) + private OpenPGPSignatureChain(Link rootLink, ComponentSignatureEvaluator componentSignatureEvaluator) { this.chainLinks.add(rootLink); + this.componentSignatureEvaluator = componentSignatureEvaluator; } - private OpenPGPSignatureChain(List links) + private OpenPGPSignatureChain(List links, ComponentSignatureEvaluator componentSignatureEvaluator) { this.chainLinks.addAll(links); + this.componentSignatureEvaluator = componentSignatureEvaluator; } // copy constructor private OpenPGPSignatureChain(OpenPGPSignatureChain copy) { - this(copy.chainLinks); + this(copy.chainLinks, copy.componentSignatureEvaluator); } /** @@ -2958,7 +2961,7 @@ public OpenPGPSignatureChain plus(OpenPGPComponentSignature sig) */ public static OpenPGPSignatureChain direct(OpenPGPComponentSignature sig) { - return new OpenPGPSignatureChain(Link.create(sig)); + return new OpenPGPSignatureChain(Link.create(sig), sig.target.certificate.policy.getComponentSignatureEvaluator()); } /** @@ -3062,6 +3065,11 @@ public boolean isHardRevocation() * @return most recent signature creation time */ public Date getSince() + { + return componentSignatureEvaluator.getSignatureChainValidityPeriodBeginning(this); + } + + public Date getMostRecentLinkCreationTime() { Date latestDate = null; for (Iterator it = chainLinks.iterator(); it.hasNext(); ) @@ -3076,6 +3084,12 @@ public Date getSince() } return latestDate; } + + public Date getIssuerKeyCreationTime() + { + return getLeafLinkTargetKey().getCertificate().getPrimaryKey().getCreationTime(); + } + // public Date getSince() // { // // Find most recent chain link @@ -3646,4 +3660,81 @@ private void addSignaturesToChains(List signatures, O chains.add(OpenPGPSignatureChain.direct(it.next())); } } + + /** + * Delegate for component signature evaluation. + * The introduction of PQC in OpenPGP makes it desirable to allow for cleanup of historic binding signatures, + * since PQC signatures are rather large, so accumulating them can lead to very large certificates. + * The classic model of component signature evaluation evaluates the complete history of component binding + * signatures when evaluating the validity of a certificate component + * (see {@link #completeComponentSignatureHistoryEvaluator()}). + * Removing old signatures with the classic evaluation model can lead to historic document- or certification + * signatures to suddenly become invalid. + * Therefore, we need a way to swap out the evaluation method by introducing this delegate, which can have + * different concrete implementations. + */ + public interface ComponentSignatureEvaluator { + Date getSignatureChainValidityPeriodBeginning(OpenPGPSignatureChain chain); + } + + /** + * This {@link ComponentSignatureEvaluator} performs an evaluation of the complete history of the components + * signatures. + * This behavior is consistent with most OpenPGP implementations, but might lead to "temporal holes". + * When evaluating the validity of a component at evaluation time N, we ignore all binding signatures + * made after N and check if the latest binding before N is not yet expired at N. + * Hard revocations at any time invalidate the component. + * Soft revocations only invalidate the component if they are made before N, not yet expired at N and not yet + * overwritten by a valid binding newer than the revocation. + *

+ * The problem with this method of evaluation is, that it can lead to temporal holes when historic self signatures + * are removed from the certificate (e.g. in order to reduce its size). + * Removing all but the latest bindings will render the key invalid for document signatures made before the latest + * bindings. + * @return component signature evaluator consistent with legacy implementations + * + * @see + * OpenPGP Interoperability Test Suite - Temporary validity + + */ + public static ComponentSignatureEvaluator completeComponentSignatureHistoryEvaluator() { + return new ComponentSignatureEvaluator() { + @Override + public Date getSignatureChainValidityPeriodBeginning(OpenPGPSignatureChain chain) { + return chain.getMostRecentLinkCreationTime(); + } + }; + } + + /** + * This {@link ComponentSignatureEvaluator} performs a simplified evaluation of the components binding signatures. + * Compared to the implementation in {@link #completeComponentSignatureHistoryEvaluator()}, this implementation prevents the + * issue of "temporal holes" and is therefore better suited for modern OpenPGP implementations where signatures + * are frequently cleaned up (e.g. PQC keys with large signatures). + *

+ * This evaluator considers a component valid at time N iff + *

    + *
  • the latest binding signature exists and does not predate the component key itself
  • + *
  • the latest binding signature is not yet expired at N
  • + *
  • the component key was created before or at N
  • + *
  • if there is a soft-revocation created after the latest binding; the revocation is expired at N
  • + *
  • the component is not hard-revoked
  • + *
+ * This implementation ensures that when superseded binding signatures are removed from a certificate, + * historic document signatures remain valid. + * Note though, that this method may render the certificate valid for historic periods where the certificate + * was purposefully temporarily invalidated by expiring self-signatures. + * + * @return component signature history evaluator which performs a simplified evaluation, fixing temporal holes + * @see + * OpenPGP Mailing List - PQC requires urgent semantic cleanup + */ + public static ComponentSignatureEvaluator simplifiedComponentSignatureHistoryEvaluator() { + return new ComponentSignatureEvaluator() { + @Override + public Date getSignatureChainValidityPeriodBeginning(OpenPGPSignatureChain chain) { + return chain.getIssuerKeyCreationTime(); + } + }; + } } diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPDefaultPolicy.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPDefaultPolicy.java index b751ce27ee..923e27067d 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPDefaultPolicy.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPDefaultPolicy.java @@ -19,6 +19,7 @@ public class OpenPGPDefaultPolicy private int defaultDocumentSignatureHashAlgorithm = HashAlgorithmTags.SHA512; private int defaultCertificationSignatureHashAlgorithm = HashAlgorithmTags.SHA512; private int defaultSymmetricKeyAlgorithm = SymmetricKeyAlgorithmTags.AES_128; + private OpenPGPCertificate.ComponentSignatureEvaluator componentSignatureEvaluator; public OpenPGPDefaultPolicy() { @@ -80,6 +81,13 @@ public OpenPGPDefaultPolicy() acceptPublicKeyAlgorithm(PublicKeyAlgorithmTags.X448); acceptPublicKeyAlgorithm(PublicKeyAlgorithmTags.Ed25519); acceptPublicKeyAlgorithm(PublicKeyAlgorithmTags.Ed448); + + /* + * Certificate component signature evaluation + */ + // Evaluate the complete temporal history of certificate components. + // This is consistent with legacy OpenPGP implementations. + evaluateCompleteComponentSignatureHistory(); } public OpenPGPDefaultPolicy rejectHashAlgorithm(int hashAlgorithmId) @@ -212,6 +220,28 @@ public boolean isAcceptablePublicKeyStrength(int publicKeyAlgorithmId, int bitSt return isAcceptable(publicKeyAlgorithmId, bitStrength, publicKeyMinimalBitStrengths); } + @Override + public OpenPGPCertificate.ComponentSignatureEvaluator getComponentSignatureEvaluator() { + return componentSignatureEvaluator; + } + + public OpenPGPDefaultPolicy setComponentSignatureEvaluator( + OpenPGPCertificate.ComponentSignatureEvaluator componentSignatureEvaluator) + { + this.componentSignatureEvaluator = componentSignatureEvaluator; + return this; + } + + public OpenPGPDefaultPolicy evaluateCompleteComponentSignatureHistory() + { + return setComponentSignatureEvaluator(OpenPGPCertificate.completeComponentSignatureHistoryEvaluator()); + } + + public OpenPGPDefaultPolicy evaluateSimplifiedSignatureHistory() + { + return setComponentSignatureEvaluator(OpenPGPCertificate.simplifiedComponentSignatureHistoryEvaluator()); + } + @Override public OpenPGPNotationRegistry getNotationRegistry() { diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPPolicy.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPPolicy.java index e8f870a479..dd22b206fc 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPPolicy.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPPolicy.java @@ -321,4 +321,11 @@ public void addKnownNotation(String notationName) this.knownNotations.add(notationName); } } + + /** + * The {@link OpenPGPCertificate.ComponentSignatureEvaluator} delegate defines, how component signatures on + * {@link OpenPGPCertificate OpenPGPCertificates} are being evaluated. + * @return delegate for component signature evaluation + */ + OpenPGPCertificate.ComponentSignatureEvaluator getComponentSignatureEvaluator(); } diff --git a/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPCertificateTest.java b/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPCertificateTest.java index 7fbd93f5a2..2b1e4c2a9c 100644 --- a/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPCertificateTest.java +++ b/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPCertificateTest.java @@ -1,26 +1,40 @@ package org.bouncycastle.openpgp.api.test; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Date; +import java.util.Iterator; import java.util.List; import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.HashAlgorithmTags; import org.bouncycastle.bcpg.KeyIdentifier; import org.bouncycastle.bcpg.SignatureSubpacketTags; import org.bouncycastle.bcpg.sig.Features; import org.bouncycastle.bcpg.sig.KeyFlags; import org.bouncycastle.openpgp.OpenPGPTestKeys; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; import org.bouncycastle.openpgp.api.OpenPGPApi; import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPDefaultPolicy; import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.openpgp.api.OpenPGPKeyGenerator; +import org.bouncycastle.openpgp.api.OpenPGPMessageGenerator; +import org.bouncycastle.openpgp.api.OpenPGPMessageInputStream; +import org.bouncycastle.openpgp.api.OpenPGPMessageOutputStream; import org.bouncycastle.openpgp.api.SignatureParameters; import org.bouncycastle.openpgp.api.SignatureSubpacketsFunction; import org.bouncycastle.openpgp.api.util.UTCUtil; @@ -49,6 +63,8 @@ protected void performTestWith(OpenPGPApi api) testSKSignsPKRevokedNoSubpacket(api); testPKSignsPKRevocationSuperseded(api); testGetPrimaryUserId(api); + + testHistoricSignatureEvaluationWithCleanedCertificate(api); } private void testOpenPGPv6Key(OpenPGPApi api) @@ -826,6 +842,160 @@ public PGPSignatureSubpacketGenerator apply(PGPSignatureSubpacketGenerator subpa key.getPrimaryUserId(oneHourAgo)); } + private void testHistoricSignatureEvaluationWithCleanedCertificate(OpenPGPApi api) + throws PGPException, IOException { + Date t0 = UTCUtil.parse("2024-01-01 00:00:00 UTC"); + Date t1 = UTCUtil.parse("2024-01-02 00:00:00 UTC"); + Date t2 = UTCUtil.parse("2024-01-03 00:00:00 UTC"); + + String userId = "Alice "; + // Create key at t0 + OpenPGPKey initialKey = api.generateKey(4, t0) + .withPrimaryKey() + .addSigningSubkey() + .addUserId(userId) + .build(); + OpenPGPCertificate initialCert = initialKey.toCertificate(); + + // Generate message at t1 + OpenPGPMessageGenerator mGen = api.signAndOrEncryptMessage() + .addSigningKey(initialKey, new SignatureParameters.Callback() { + @Override + public SignatureParameters apply(SignatureParameters parameters) { + return parameters.setSignatureCreationTime(t1); + } + }); + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPMessageOutputStream mOut = mGen.open(bOut); + mOut.write("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + mOut.close(); + byte[] historicMessage = bOut.toByteArray(); + System.out.println(bOut.toString()); + + // Message is valid with initial certificate + ByteArrayInputStream bIn = new ByteArrayInputStream(historicMessage); + OpenPGPMessageInputStream mIn = api.decryptAndOrVerifyMessage() + .addVerificationCertificate(initialCert) + .process(bIn); + org.bouncycastle.util.io.Streams.drain(mIn); + mIn.close(); + isTrue(mIn.getResult().getSignatures().get(0).isValid()); + + // Create new key signatures at t2 and strip t0 signatures + List strippedKeys = new ArrayList<>(); + for (PGPPublicKey k : initialCert.getPGPPublicKeyRing()) + { + PGPPublicKey cleaned = PGPPublicKey.removeCertification(k, userId); + if (cleaned == null) + { + cleaned = k; + } + Iterator sigs = cleaned.getSignatures(); + while (sigs.hasNext()) { + PGPSignature sig = sigs.next(); + cleaned = PGPPublicKey.removeCertification(cleaned, sig); + } + strippedKeys.add(cleaned); + } + System.out.println(new OpenPGPCertificate(new PGPPublicKeyRing(strippedKeys)).toAsciiArmoredString()); + + List updateKeys = new ArrayList<>(); + PGPKeyPair primaryKey = initialKey.getPrimarySecretKey().unlock().getKeyPair(); + Iterator strippedIterator = strippedKeys.iterator(); + PGPPublicKey updatedPrimaryKey = strippedIterator.next(); + + // Reissue direct-key sig at t2 + PGPSignatureGenerator sGen = new PGPSignatureGenerator( + api.getImplementation().pgpContentSignerBuilder( + primaryKey.getPublicKey().getAlgorithm(), + HashAlgorithmTags.SHA3_512), + primaryKey.getPublicKey()); + PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator(); + spGen.setIssuerFingerprint(true, primaryKey.getPublicKey()); + spGen.setSignatureCreationTime(true, t2); + spGen.setKeyFlags(KeyFlags.CERTIFY_OTHER); + sGen.setHashedSubpackets(spGen.generate()); + sGen.init(PGPSignature.DIRECT_KEY, primaryKey.getPrivateKey()); + updatedPrimaryKey = PGPPublicKey.addCertification( + updatedPrimaryKey, + sGen.generateCertification(updatedPrimaryKey)); + + // reissue userid sig at t2 + sGen.init(PGPSignature.POSITIVE_CERTIFICATION, primaryKey.getPrivateKey()); + updatedPrimaryKey = PGPPublicKey.addCertification( + updatedPrimaryKey, + userId, + sGen.generateCertification(userId, updatedPrimaryKey)); + + updateKeys.add(updatedPrimaryKey); + + // reissue signature subkey binding at t2 + PGPPublicKey signingKey = strippedIterator.next(); + PGPPrivateKey privateSigningKey = initialKey.getSecretKey(signingKey.getKeyIdentifier()) + .unlock().getKeyPair().getPrivateKey(); + + PGPSignatureGenerator backSigGen = new PGPSignatureGenerator( + api.getImplementation().pgpContentSignerBuilder(signingKey.getAlgorithm(), HashAlgorithmTags.SHA3_512), + signingKey); + PGPSignatureSubpacketGenerator backSigSubPacketGen = new PGPSignatureSubpacketGenerator(); + backSigSubPacketGen.setSignatureCreationTime(t2); + backSigSubPacketGen.setIssuerFingerprint(true, signingKey); + backSigGen.setHashedSubpackets(backSigSubPacketGen.generate()); + backSigGen.init(PGPSignature.PRIMARYKEY_BINDING, privateSigningKey); + PGPSignature backSig = backSigGen.generateCertification(updatedPrimaryKey, signingKey); + + spGen = new PGPSignatureSubpacketGenerator(); + spGen.setIssuerFingerprint(true, updatedPrimaryKey); + spGen.setSignatureCreationTime(true, t2); + spGen.setKeyFlags(KeyFlags.SIGN_DATA); + spGen.addEmbeddedSignature(true, backSig); + sGen.setHashedSubpackets(spGen.generate()); + sGen.init(PGPSignature.SUBKEY_BINDING, primaryKey.getPrivateKey()); + + signingKey = PGPPublicKey.addCertification(signingKey, sGen.generateCertification(updatedPrimaryKey, signingKey)); + updateKeys.add(signingKey); + + // Reassemble update key + PGPPublicKeyRing updatedKeyRing = new PGPPublicKeyRing(updateKeys); + + // Check that with complete history evaluation, historic signature is now no longer valid + OpenPGPDefaultPolicy fullHistoryEvaluation = new OpenPGPDefaultPolicy() + .evaluateCompleteComponentSignatureHistory(); + OpenPGPCertificate updatedCert = new OpenPGPCertificate( + updatedKeyRing, api.getImplementation(), fullHistoryEvaluation); + isFalse("With full history eval, primary key MUST NOT be bound at t1", + updatedCert.getPrimaryKey().isBoundAt(t1)); + isFalse("With full history eval, signing key MUST NOT be bound at t1", + updatedCert.getSigningKeys().get(0).isBoundAt(t1)); + + mIn = api.decryptAndOrVerifyMessage() + .addVerificationCertificate(updatedCert) + .process(new ByteArrayInputStream(historicMessage)); + org.bouncycastle.util.io.Streams.drain(mIn); + mIn.close(); + isFalse("With full history eval, historic message MUST NOT be validly signed by updated key", + mIn.getResult().getSignatures().get(0).isValid()); + + // Check that with simplified history evaluation, historic signatures remain valid + OpenPGPDefaultPolicy simplifiedHistoryEvaluation = new OpenPGPDefaultPolicy() + .evaluateSimplifiedSignatureHistory(); + updatedCert = new OpenPGPCertificate( + updatedKeyRing, api.getImplementation(), simplifiedHistoryEvaluation); + isTrue("With simplified history eval, primary key MUST be bound at t1", + updatedCert.getPrimaryKey() + .isBoundAt(t1)); + isTrue("With simplified history eval, signing key MUST be bound at t1", + updatedCert.getSigningKeys().get(0).isBoundAt(t1)); + + mIn = api.decryptAndOrVerifyMessage() + .addVerificationCertificate(updatedCert) + .process(new ByteArrayInputStream(historicMessage)); + org.bouncycastle.util.io.Streams.drain(mIn); + mIn.close(); + isTrue("With simplified history eval, historic message MUST be validly signed by updated key", + mIn.getResult().getSignatures().get(0).isValid()); + } + public static class TestSignature { private final PGPSignature signature; From 6ab16af1767643e879db803d42cc2ece411b21e7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 25 Feb 2026 12:18:31 +0100 Subject: [PATCH 4/5] Better naming of methods and members, improve documentation --- .../openpgp/api/OpenPGPCertificate.java | 67 +++++++++++++------ .../openpgp/api/OpenPGPDefaultPolicy.java | 45 ++++++++++--- .../openpgp/api/OpenPGPPolicy.java | 2 +- 3 files changed, 84 insertions(+), 30 deletions(-) diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java index 82475c87a8..aeb8d4d434 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java @@ -2870,24 +2870,24 @@ public static class OpenPGPSignatureChain implements Comparable, Iterable { private final List chainLinks = new ArrayList(); - private final ComponentSignatureEvaluator componentSignatureEvaluator; + private final OpenPGPPolicy policy; - private OpenPGPSignatureChain(Link rootLink, ComponentSignatureEvaluator componentSignatureEvaluator) + private OpenPGPSignatureChain(Link rootLink, OpenPGPPolicy policy) { this.chainLinks.add(rootLink); - this.componentSignatureEvaluator = componentSignatureEvaluator; + this.policy = policy; } - private OpenPGPSignatureChain(List links, ComponentSignatureEvaluator componentSignatureEvaluator) + private OpenPGPSignatureChain(List links, OpenPGPPolicy policy) { this.chainLinks.addAll(links); - this.componentSignatureEvaluator = componentSignatureEvaluator; + this.policy = policy; } // copy constructor private OpenPGPSignatureChain(OpenPGPSignatureChain copy) { - this(copy.chainLinks, copy.componentSignatureEvaluator); + this(copy.chainLinks, copy.policy); } /** @@ -2961,7 +2961,7 @@ public OpenPGPSignatureChain plus(OpenPGPComponentSignature sig) */ public static OpenPGPSignatureChain direct(OpenPGPComponentSignature sig) { - return new OpenPGPSignatureChain(Link.create(sig), sig.target.certificate.policy.getComponentSignatureEvaluator()); + return new OpenPGPSignatureChain(Link.create(sig), sig.target.certificate.policy); } /** @@ -3066,9 +3066,15 @@ public boolean isHardRevocation() */ public Date getSince() { - return componentSignatureEvaluator.getSignatureChainValidityPeriodBeginning(this); + return policy.getComponentSignatureEffectivenessEvaluator() + .getSignatureChainValidityPeriodBeginning(this); } + /** + * Return the signature creation time of the most recent signature in the chain. + * + * @return most recent signature creation time in chain + */ public Date getMostRecentLinkCreationTime() { Date latestDate = null; @@ -3085,9 +3091,16 @@ public Date getMostRecentLinkCreationTime() return latestDate; } - public Date getIssuerKeyCreationTime() + /** + * Return the key creation time of the component signatures target key. + * In certificates composed of a primary key and subkeys, this is sufficient, + * since subkeys MUST NOT predate the primary key. + * + * @return most recent + */ + public Date getTargetKeyCreationTime() { - return getLeafLinkTargetKey().getCertificate().getPrimaryKey().getCreationTime(); + return getLeafLinkTargetKey().getCreationTime(); } // public Date getSince() @@ -3414,6 +3427,16 @@ public OpenPGPComponentSignature getSignature() { return signature; } + + /** + * Return the issuer key of this link. + * + * @return issuer + */ + public OpenPGPComponentKey getIssuer() + { + return getSignature().getIssuer(); + } } /** @@ -3667,7 +3690,7 @@ private void addSignaturesToChains(List signatures, O * since PQC signatures are rather large, so accumulating them can lead to very large certificates. * The classic model of component signature evaluation evaluates the complete history of component binding * signatures when evaluating the validity of a certificate component - * (see {@link #completeComponentSignatureHistoryEvaluator()}). + * (see {@link #strictlyTemporallyConstrainedSignatureEvaluator()}). * Removing old signatures with the classic evaluation model can lead to historic document- or certification * signatures to suddenly become invalid. * Therefore, we need a way to swap out the evaluation method by introducing this delegate, which can have @@ -3678,8 +3701,7 @@ public interface ComponentSignatureEvaluator { } /** - * This {@link ComponentSignatureEvaluator} performs an evaluation of the complete history of the components - * signatures. + * This {@link ComponentSignatureEvaluator} strictly constraints the temporal validity of component signatures. * This behavior is consistent with most OpenPGP implementations, but might lead to "temporal holes". * When evaluating the validity of a component at evaluation time N, we ignore all binding signatures * made after N and check if the latest binding before N is not yet expired at N. @@ -3697,7 +3719,7 @@ public interface ComponentSignatureEvaluator { * OpenPGP Interoperability Test Suite - Temporary validity */ - public static ComponentSignatureEvaluator completeComponentSignatureHistoryEvaluator() { + public static ComponentSignatureEvaluator strictlyTemporallyConstrainedSignatureEvaluator() { return new ComponentSignatureEvaluator() { @Override public Date getSignatureChainValidityPeriodBeginning(OpenPGPSignatureChain chain) { @@ -3707,17 +3729,20 @@ public Date getSignatureChainValidityPeriodBeginning(OpenPGPSignatureChain chain } /** - * This {@link ComponentSignatureEvaluator} performs a simplified evaluation of the components binding signatures. - * Compared to the implementation in {@link #completeComponentSignatureHistoryEvaluator()}, this implementation prevents the - * issue of "temporal holes" and is therefore better suited for modern OpenPGP implementations where signatures - * are frequently cleaned up (e.g. PQC keys with large signatures). + * This {@link ComponentSignatureEvaluator} allows for retroactive validation of historic document- or + * third-party signatures via new component binding signatures. + * Compared to the implementation in {@link #strictlyTemporallyConstrainedSignatureEvaluator()}, + * this implementation prevents the issue of "temporal holes" and is therefore better suited for + * modern OpenPGP implementations where signatures are frequently cleaned up (e.g. PQC keys with + * large signatures). *

* This evaluator considers a component valid at time N iff *

    *
  • the latest binding signature exists and does not predate the component key itself
  • *
  • the latest binding signature is not yet expired at N
  • *
  • the component key was created before or at N
  • - *
  • if there is a soft-revocation created after the latest binding; the revocation is expired at N
  • + *
  • if there is a soft-revocation created after the latest binding; the revocation is + * expired at N
  • *
  • the component is not hard-revoked
  • *
* This implementation ensures that when superseded binding signatures are removed from a certificate, @@ -3729,11 +3754,11 @@ public Date getSignatureChainValidityPeriodBeginning(OpenPGPSignatureChain chain * @see * OpenPGP Mailing List - PQC requires urgent semantic cleanup */ - public static ComponentSignatureEvaluator simplifiedComponentSignatureHistoryEvaluator() { + public static ComponentSignatureEvaluator retroactivelyTemporallyRevalidatingSignatureEvaluator() { return new ComponentSignatureEvaluator() { @Override public Date getSignatureChainValidityPeriodBeginning(OpenPGPSignatureChain chain) { - return chain.getIssuerKeyCreationTime(); + return chain.getTargetKeyCreationTime(); } }; } diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPDefaultPolicy.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPDefaultPolicy.java index 923e27067d..9821861cf5 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPDefaultPolicy.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPDefaultPolicy.java @@ -85,9 +85,9 @@ public OpenPGPDefaultPolicy() /* * Certificate component signature evaluation */ - // Evaluate the complete temporal history of certificate components. + // Strictly constrain the temporal validity of component signatures during signature evaluation. // This is consistent with legacy OpenPGP implementations. - evaluateCompleteComponentSignatureHistory(); + applyStrictTemporalComponentSignatureValidityConstraints(); } public OpenPGPDefaultPolicy rejectHashAlgorithm(int hashAlgorithmId) @@ -221,25 +221,54 @@ public boolean isAcceptablePublicKeyStrength(int publicKeyAlgorithmId, int bitSt } @Override - public OpenPGPCertificate.ComponentSignatureEvaluator getComponentSignatureEvaluator() { + public OpenPGPCertificate.ComponentSignatureEvaluator getComponentSignatureEffectivenessEvaluator() { return componentSignatureEvaluator; } - public OpenPGPDefaultPolicy setComponentSignatureEvaluator( + public OpenPGPDefaultPolicy setComponentSignatureEffectivenessEvaluator( OpenPGPCertificate.ComponentSignatureEvaluator componentSignatureEvaluator) { this.componentSignatureEvaluator = componentSignatureEvaluator; return this; } - public OpenPGPDefaultPolicy evaluateCompleteComponentSignatureHistory() + /** + * When evaluating a document signature or third-party certification issued at time t1, + * only consider component binding signatures made at t1 or prior and reject component + * binding signatures made at t2+. + * This behavior is consistent with OpenPGP implementations, but might break historical + * document signatures and third-party certifications if old component signatures are + * cleaned from the certificate (temporal holes). + * You can prevent temporal holes with {@link #allowRetroactiveComponentSignatureValidation()}. + *

+ * This behavior is currently the default. + * + * @return policy + */ + public OpenPGPDefaultPolicy applyStrictTemporalComponentSignatureValidityConstraints() { - return setComponentSignatureEvaluator(OpenPGPCertificate.completeComponentSignatureHistoryEvaluator()); + return setComponentSignatureEffectivenessEvaluator( + OpenPGPCertificate.strictlyTemporallyConstrainedSignatureEvaluator()); } - public OpenPGPDefaultPolicy evaluateSimplifiedSignatureHistory() + /** + * When evaluating a document signature or third-party certification issued at time t1, + * also consider component binding signatures created at t2+. + * This behavior prevents historical document or certification signatures from breaking + * if older binding signatures are cleaned from the issuer certificate. + * Since PQC signatures may quickly blow up the certificate in size, it is desirable to + * clean up old signatures every once in a while and allowing retroactive validation of + * historic signatures via new component signatures prevents temporal holes. + *

+ * Calling this will overwrite the - currently default - behavior from + * {@link #applyStrictTemporalComponentSignatureValidityConstraints()}. + * + * @return policy + */ + public OpenPGPDefaultPolicy allowRetroactiveComponentSignatureValidation() { - return setComponentSignatureEvaluator(OpenPGPCertificate.simplifiedComponentSignatureHistoryEvaluator()); + return setComponentSignatureEffectivenessEvaluator( + OpenPGPCertificate.retroactivelyTemporallyRevalidatingSignatureEvaluator()); } @Override diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPPolicy.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPPolicy.java index dd22b206fc..d5015ea3d3 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPPolicy.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPPolicy.java @@ -327,5 +327,5 @@ public void addKnownNotation(String notationName) * {@link OpenPGPCertificate OpenPGPCertificates} are being evaluated. * @return delegate for component signature evaluation */ - OpenPGPCertificate.ComponentSignatureEvaluator getComponentSignatureEvaluator(); + OpenPGPCertificate.ComponentSignatureEvaluator getComponentSignatureEffectivenessEvaluator(); } From e7918fadf976e4a1564a2087b3f758e159255a54 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 25 Feb 2026 13:18:08 +0100 Subject: [PATCH 5/5] Fix method names --- .../bouncycastle/openpgp/api/test/OpenPGPCertificateTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPCertificateTest.java b/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPCertificateTest.java index 2b1e4c2a9c..66c399a611 100644 --- a/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPCertificateTest.java +++ b/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPCertificateTest.java @@ -960,7 +960,7 @@ public SignatureParameters apply(SignatureParameters parameters) { // Check that with complete history evaluation, historic signature is now no longer valid OpenPGPDefaultPolicy fullHistoryEvaluation = new OpenPGPDefaultPolicy() - .evaluateCompleteComponentSignatureHistory(); + .applyStrictTemporalComponentSignatureValidityConstraints(); OpenPGPCertificate updatedCert = new OpenPGPCertificate( updatedKeyRing, api.getImplementation(), fullHistoryEvaluation); isFalse("With full history eval, primary key MUST NOT be bound at t1", @@ -978,7 +978,7 @@ public SignatureParameters apply(SignatureParameters parameters) { // Check that with simplified history evaluation, historic signatures remain valid OpenPGPDefaultPolicy simplifiedHistoryEvaluation = new OpenPGPDefaultPolicy() - .evaluateSimplifiedSignatureHistory(); + .allowRetroactiveComponentSignatureValidation(); updatedCert = new OpenPGPCertificate( updatedKeyRing, api.getImplementation(), simplifiedHistoryEvaluation); isTrue("With simplified history eval, primary key MUST be bound at t1",