Skip to content
Draft
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
134 changes: 125 additions & 9 deletions pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<OpenPGPSignatureChain> 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);
}
Expand Down Expand Up @@ -2870,21 +2870,24 @@ public static class OpenPGPSignatureChain
implements Comparable<OpenPGPSignatureChain>, Iterable<OpenPGPSignatureChain.Link>
{
private final List<Link> chainLinks = new ArrayList<Link>();
private final OpenPGPPolicy policy;

private OpenPGPSignatureChain(Link rootLink)
private OpenPGPSignatureChain(Link rootLink, OpenPGPPolicy policy)
{
this.chainLinks.add(rootLink);
this.policy = policy;
}

private OpenPGPSignatureChain(List<Link> links)
private OpenPGPSignatureChain(List<Link> links, OpenPGPPolicy policy)
{
this.chainLinks.addAll(links);
this.policy = policy;
}

// copy constructor
private OpenPGPSignatureChain(OpenPGPSignatureChain copy)
{
this(copy.chainLinks);
this(copy.chainLinks, copy.policy);
}

/**
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -3062,6 +3065,17 @@ public boolean isHardRevocation()
* @return most recent signature creation time
*/
public Date getSince()
{
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;
for (Iterator it = chainLinks.iterator(); it.hasNext(); )
Expand All @@ -3076,6 +3090,19 @@ public Date getSince()
}
return latestDate;
}

/**
* 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().getCreationTime();
}

// public Date getSince()
// {
// // Find most recent chain link
Expand Down Expand Up @@ -3400,6 +3427,16 @@ public OpenPGPComponentSignature getSignature()
{
return signature;
}

/**
* Return the issuer key of this link.
*
* @return issuer
*/
public OpenPGPComponentKey getIssuer()
{
return getSignature().getIssuer();
}
}

/**
Expand Down Expand Up @@ -3646,4 +3683,83 @@ private void addSignaturesToChains(List<OpenPGPComponentSignature> 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 #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
* different concrete implementations.
*/
public interface ComponentSignatureEvaluator {
Date getSignatureChainValidityPeriodBeginning(OpenPGPSignatureChain chain);
}

/**
* 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.
* 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.
* <p>
* 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 <a href="https://sequoia-pgp.gitlab.io/openpgp-interoperability-test-suite/results.html#Temporary_validity">
* OpenPGP Interoperability Test Suite - Temporary validity</a>

*/
public static ComponentSignatureEvaluator strictlyTemporallyConstrainedSignatureEvaluator() {
return new ComponentSignatureEvaluator() {
@Override
public Date getSignatureChainValidityPeriodBeginning(OpenPGPSignatureChain chain) {
return chain.getMostRecentLinkCreationTime();
}
};
}

/**
* 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).
* <p>
* This evaluator considers a component valid at time N iff
* <ul>
* <li>the latest binding signature exists and does not predate the component key itself</li>
* <li>the latest binding signature is not yet expired at N</li>
* <li>the component key was created before or at N</li>
* <li>if there is a soft-revocation created after the latest binding; the revocation is
* expired at N</li>
* <li>the component is not hard-revoked</li>
* </ul>
* 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 <a href="https://mailarchive.ietf.org/arch/msg/openpgp/kA4YtiP3j8LJUift1_D0mWIHVV0/">
* OpenPGP Mailing List - PQC requires urgent semantic cleanup</a>
*/
public static ComponentSignatureEvaluator retroactivelyTemporallyRevalidatingSignatureEvaluator() {
return new ComponentSignatureEvaluator() {
@Override
public Date getSignatureChainValidityPeriodBeginning(OpenPGPSignatureChain chain) {
return chain.getTargetKeyCreationTime();
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -80,6 +81,13 @@ public OpenPGPDefaultPolicy()
acceptPublicKeyAlgorithm(PublicKeyAlgorithmTags.X448);
acceptPublicKeyAlgorithm(PublicKeyAlgorithmTags.Ed25519);
acceptPublicKeyAlgorithm(PublicKeyAlgorithmTags.Ed448);

/*
* Certificate component signature evaluation
*/
// Strictly constrain the temporal validity of component signatures during signature evaluation.
// This is consistent with legacy OpenPGP implementations.
applyStrictTemporalComponentSignatureValidityConstraints();
}

public OpenPGPDefaultPolicy rejectHashAlgorithm(int hashAlgorithmId)
Expand Down Expand Up @@ -212,6 +220,57 @@ public boolean isAcceptablePublicKeyStrength(int publicKeyAlgorithmId, int bitSt
return isAcceptable(publicKeyAlgorithmId, bitStrength, publicKeyMinimalBitStrengths);
}

@Override
public OpenPGPCertificate.ComponentSignatureEvaluator getComponentSignatureEffectivenessEvaluator() {
return componentSignatureEvaluator;
}

public OpenPGPDefaultPolicy setComponentSignatureEffectivenessEvaluator(
OpenPGPCertificate.ComponentSignatureEvaluator componentSignatureEvaluator)
{
this.componentSignatureEvaluator = componentSignatureEvaluator;
return this;
}

/**
* 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()}.
* <p>
* This behavior is currently the default.
*
* @return policy
*/
public OpenPGPDefaultPolicy applyStrictTemporalComponentSignatureValidityConstraints()
{
return setComponentSignatureEffectivenessEvaluator(
OpenPGPCertificate.strictlyTemporallyConstrainedSignatureEvaluator());
}

/**
* 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.
* <p>
* Calling this will overwrite the - currently default - behavior from
* {@link #applyStrictTemporalComponentSignatureValidityConstraints()}.
*
* @return policy
*/
public OpenPGPDefaultPolicy allowRetroactiveComponentSignatureValidation()
{
return setComponentSignatureEffectivenessEvaluator(
OpenPGPCertificate.retroactivelyTemporallyRevalidatingSignatureEvaluator());
}

@Override
public OpenPGPNotationRegistry getNotationRegistry()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ public OpenPGPKeyEditor addSigningSubkey(PGPKeyPair signingSubkey,

PGPPublicKey publicPrimaryKey = key.getPrimaryKey().getPGPPublicKey();

final PGPSignature backSig = Utils.getBackSignature(signingSubkey, backSigParameters, publicPrimaryKey, implementation, null);
final PGPSignature backSig = Utils.getBackSignature(signingSubkey, backSigParameters, publicPrimaryKey, implementation, backSigParameters.getSignatureCreationTime());

updateKey(signingSubkey, bindingSigCallback, publicPrimaryKey, new Utils.HashedSubpacketsOperation()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 getComponentSignatureEffectivenessEvaluator();
}
Loading