From 431a65517e9baa2d7e10c8017c3cf0e17b10627a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 3 May 2021 13:37:47 +0200 Subject: [PATCH] Add documentation to signature related classes --- .../SignatureVerifyingInputStream.java | 2 +- .../signature/DetachedSignature.java | 41 +++ .../signature/OnePassSignature.java | 46 ++++ .../signature/SelectSignatureFromKey.java | 236 ++++++++++++++++-- .../signature/SignatureChainValidator.java | 92 ++++++- .../SignatureCreationDateComparator.java | 17 ++ .../pgpainless/signature/SignatureUtils.java | 84 ++++++- .../SignatureValidityComparator.java | 15 ++ .../SignatureSubpacketGeneratorUtil.java | 36 ++- .../subpackets/SignatureSubpacketsUtil.java | 61 ++++- .../signature/SelectSignatureFromKeyTest.java | 1 - 11 files changed, 587 insertions(+), 44 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerifyingInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerifyingInputStream.java index 5e8eae44..d5f4c891 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerifyingInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerifyingInputStream.java @@ -118,7 +118,7 @@ public class SignatureVerifyingInputStream extends FilterInputStream { signatureStructureIsAcceptable(signingKey, policy).verify(signature); signatureIsEffective(new Date()).verify(signature); - SignatureChainValidator.validateSigningKey(signature, onePassSignature.getVerificationKeys(), PGPainless.getPolicy(), signature.getCreationTime()); + SignatureChainValidator.validateSigningKey(signature, onePassSignature.getVerificationKeys(), PGPainless.getPolicy()); } catch (SignatureValidationException e) { throw new SignatureException("Signature key is not valid.", e); diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/DetachedSignature.java b/pgpainless-core/src/main/java/org/pgpainless/signature/DetachedSignature.java index 81118aed..862a833d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/DetachedSignature.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/DetachedSignature.java @@ -20,38 +20,79 @@ import org.bouncycastle.openpgp.PGPSignature; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.SubkeyIdentifier; +/** + * Tuple-class which bundles together a signature, the signing key that created the signature, + * an identifier of the signing key and a record of whether or not the signature was verified. + */ public class DetachedSignature { private final PGPSignature signature; private final PGPKeyRing signingKeyRing; private final SubkeyIdentifier signingKeyIdentifier; private boolean verified; + /** + * Create a new {@link DetachedSignature} object. + * + * @param signature signature + * @param signingKeyRing signing key that created the signature + * @param signingKeyIdentifier identifier of the used signing key + */ public DetachedSignature(PGPSignature signature, PGPKeyRing signingKeyRing, SubkeyIdentifier signingKeyIdentifier) { this.signature = signature; this.signingKeyRing = signingKeyRing; this.signingKeyIdentifier = signingKeyIdentifier; } + /** + * Mark this {@link DetachedSignature} as verified. + * + * @param verified verified + */ public void setVerified(boolean verified) { this.verified = verified; } + /** + * Return true iff the signature is verified. + * + * @return verified + */ public boolean isVerified() { return verified; } + /** + * Return the OpenPGP signature. + * + * @return signature + */ public PGPSignature getSignature() { return signature; } + /** + * Return an identifier pointing to the exact signing key which was used to create this signature. + * + * @return signing key identifier + */ public SubkeyIdentifier getSigningKeyIdentifier() { return signingKeyIdentifier; } + /** + * Return the key ring that contains the signing key that created this signature. + * + * @return key ring + */ public PGPKeyRing getSigningKeyRing() { return signingKeyRing; } + /** + * Return the {@link OpenPgpV4Fingerprint} of the key that created the signature. + * + * @return fingerprint of the signing key + */ @Deprecated public OpenPgpV4Fingerprint getFingerprint() { return signingKeyIdentifier.getSubkeyFingerprint(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/OnePassSignature.java b/pgpainless-core/src/main/java/org/pgpainless/signature/OnePassSignature.java index d3ef07f2..693311d8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/OnePassSignature.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/OnePassSignature.java @@ -21,29 +21,65 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.pgpainless.key.OpenPgpV4Fingerprint; +/** + * Tuple-class that bundles together a {@link PGPOnePassSignature} object, a {@link PGPPublicKeyRing} + * destined to verify the signature, the {@link PGPSignature} itself and a record of whether or not the signature + * was verified. + */ public class OnePassSignature { private final PGPOnePassSignature onePassSignature; private final PGPPublicKeyRing verificationKeys; private PGPSignature signature; private boolean verified; + /** + * Create a new {@link OnePassSignature}. + * + * @param onePassSignature one-pass signature packet used to initialize the signature verifier. + * @param verificationKeys verification keys + */ public OnePassSignature(PGPOnePassSignature onePassSignature, PGPPublicKeyRing verificationKeys) { this.onePassSignature = onePassSignature; this.verificationKeys = verificationKeys; } + /** + * Return true if the signature is verified. + * + * @return verified + */ public boolean isVerified() { return verified; } + /** + * Return the {@link PGPOnePassSignature} object. + * + * @return onePassSignature + */ public PGPOnePassSignature getOnePassSignature() { return onePassSignature; } + /** + * Return the {@link OpenPgpV4Fingerprint} of the signing key. + * + * @return signing key fingerprint + */ public OpenPgpV4Fingerprint getFingerprint() { return new OpenPgpV4Fingerprint(verificationKeys.getPublicKey(onePassSignature.getKeyID())); } + /** + * Verify the one-pass signature. + * Note: This method only checks if the signature itself is correct. + * It does not check if the signing key was eligible to create the signature, or if the signature is expired etc. + * Those checks are being done by {@link org.pgpainless.decryption_verification.SignatureVerifyingInputStream}. + * + * @param signature parsed-out signature + * @return true if the signature was verified, false otherwise + * @throws PGPException if signature verification fails with an exception. + */ public boolean verify(PGPSignature signature) throws PGPException { this.verified = getOnePassSignature().verify(signature); if (verified) { @@ -52,10 +88,20 @@ public class OnePassSignature { return verified; } + /** + * Return the signature. + * + * @return signature + */ public PGPSignature getSignature() { return signature; } + /** + * Return the key ring used to verify the signature. + * + * @return verification keys + */ public PGPPublicKeyRing getVerificationKeys() { return verificationKeys; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SelectSignatureFromKey.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SelectSignatureFromKey.java index 4d90b8cf..b1dca645 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SelectSignatureFromKey.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/SelectSignatureFromKey.java @@ -31,20 +31,18 @@ import org.pgpainless.algorithm.SignatureType; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; +/** + * Utility class to select signatures from keys based on certain criteria. + * This abstract class provides a method {@link #accept(PGPSignature, PGPPublicKey, PGPKeyRing)} which shall only + * return true if the provided signature is acceptable regarding the implementations selection criteria. + * + * The idea is to create an implementation of the class for each criterion, so that those criteria can be + * composed to create complex validity checks. + */ public abstract class SelectSignatureFromKey { private static final Logger LOGGER = Logger.getLogger(SelectSignatureFromKey.class.getName()); - public static SelectSignatureFromKey isValidAt(Date validationDate) { - return new SelectSignatureFromKey() { - @Override - public boolean accept(PGPSignature signature, PGPPublicKey key, PGPKeyRing keyRing) { - Date expirationDate = SignatureUtils.getSignatureExpirationDate(signature); - return !signature.getCreationTime().after(validationDate) && (expirationDate == null || expirationDate.after(validationDate)); - } - }; - } - public abstract boolean accept(PGPSignature signature, PGPPublicKey key, PGPKeyRing keyRing); public List select(List signatures, PGPPublicKey key, PGPKeyRing keyRing) { @@ -57,6 +55,36 @@ public abstract class SelectSignatureFromKey { return selected; } + /** + * Criterion that checks if the signature is valid at the validation date. + * A signature is not valid if it was created after the validation date, or if it is expired at the validation date. + * + * creationTime <= validationDate < expirationDate. + * + * @param validationDate validation date + * @return criterion implementation + */ + public static SelectSignatureFromKey isValidAt(Date validationDate) { + return new SelectSignatureFromKey() { + @Override + public boolean accept(PGPSignature signature, PGPPublicKey key, PGPKeyRing keyRing) { + Date expirationDate = SignatureUtils.getSignatureExpirationDate(signature); + return !signature.getCreationTime().after(validationDate) && (expirationDate == null || expirationDate.after(validationDate)); + } + }; + } + + /** + * Criterion that checks if the provided signature is a valid subkey binding signature. + * + * A signature is only a valid subkey binding signature if it is of type {@link SignatureType#SUBKEY_BINDING}, + * if it was created by the primary key, and - if the subkey is capable of signing - it contains a valid + * primary key binding signature. + * + * @param primaryKey primary key + * @param subkey subkey + * @return criterion to validate binding signatures + */ public static SelectSignatureFromKey isValidSubkeyBindingSignature(PGPPublicKey primaryKey, PGPPublicKey subkey) { return new SelectSignatureFromKey() { @Override @@ -70,16 +98,20 @@ public abstract class SelectSignatureFromKey { return false; } - boolean subkeyBindingSigValid; - try { - signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), primaryKey); - subkeyBindingSigValid = signature.verifyCertification(primaryKey, subkey); - } catch (PGPException e) { - LOGGER.log(Level.INFO, "Verification of subkey binding signature failed.", e); + if (!isSigNotExpired().accept(signature, subkey, keyRing)) { + LOGGER.log(Level.INFO, "Subkey binding signature expired."); return false; } - if (!subkeyBindingSigValid) { + // Check signature correctness + try { + signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), primaryKey); + boolean subkeyBindingSigValid = signature.verifyCertification(primaryKey, subkey); + if (!subkeyBindingSigValid) { + return false; + } + } catch (PGPException e) { + LOGGER.log(Level.INFO, "Verification of subkey binding signature failed.", e); return false; } @@ -96,6 +128,13 @@ public abstract class SelectSignatureFromKey { }; } + /** + * Criterion that checks if a primary key binding signature is valid. + * + * @param subkey subkey + * @param primaryKey primary key + * @return criterion to validate primary key binding signatures + */ public static SelectSignatureFromKey isValidPrimaryKeyBindingSignature(PGPPublicKey subkey, PGPPublicKey primaryKey) { return new SelectSignatureFromKey() { @Override @@ -118,6 +157,7 @@ public abstract class SelectSignatureFromKey { return false; } + // Check signature correctness try { signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), subkey); return signature.verifyCertification(primaryKey, subkey); @@ -128,6 +168,12 @@ public abstract class SelectSignatureFromKey { }; } + /** + * Criterion that checks if a signature has an embedded valid primary key binding signature. + * @param subkey subkey + * @param primaryKey primary key + * @return criterion + */ public static SelectSignatureFromKey hasValidPrimaryKeyBindingSignatureSubpacket(PGPPublicKey subkey, PGPPublicKey primaryKey) { return new SelectSignatureFromKey() { @Override @@ -149,6 +195,14 @@ public abstract class SelectSignatureFromKey { }; } + /** + * Criterion that checks if a signature is a valid v4 direct-key signature. + * Note: This method does not check expiration. + * + * @param signer signing key + * @param signee signed key + * @return criterion + */ public static SelectSignatureFromKey isValidDirectKeySignature(PGPPublicKey signer, PGPPublicKey signee) { return new SelectSignatureFromKey() { @Override @@ -161,6 +215,7 @@ public abstract class SelectSignatureFromKey { return false; } + // Check signature correctness try { signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), signer); return signature.verifyCertification(signee); @@ -171,6 +226,12 @@ public abstract class SelectSignatureFromKey { }; } + /** + * Criterion that checks if a signature is a valid key revocation signature. + * + * @param key primary key + * @return criterion + */ public static SelectSignatureFromKey isValidKeyRevocationSignature(PGPPublicKey key) { return and( isVersion4Signature(), @@ -182,6 +243,11 @@ public abstract class SelectSignatureFromKey { ); } + /** + * Criterion that only accepts valid subkey revocation signatures. + * + * @return criterion + */ public static SelectSignatureFromKey isValidSubkeyRevocationSignature() { return new SelectSignatureFromKey() { @Override @@ -192,6 +258,13 @@ public abstract class SelectSignatureFromKey { }; } + /** + * Criterion that only accepts valid subkey revocation signatures. + * + * @param subkey subkey + * @param primaryKey primary key + * @return criterion + */ public static SelectSignatureFromKey isValidSubkeyRevocationSignature(PGPPublicKey subkey, PGPPublicKey primaryKey) { return SelectSignatureFromKey.and( isVersion4Signature(), @@ -201,6 +274,13 @@ public abstract class SelectSignatureFromKey { ); } + /** + * Criterion that only accepts signatures which are valid user-id revocations. + * + * @param revoker signing key + * @param userId user id + * @return criterion + */ public static SelectSignatureFromKey isValidCertificationRevocationSignature(PGPPublicKey revoker, String userId) { return and( isVersion4Signature(), @@ -210,6 +290,14 @@ public abstract class SelectSignatureFromKey { ); } + /** + * Criterion that only accepts signatures which are valid signatures over a user-id. + * This method only checks signature correctness, not expiry etc. + * + * @param userId user-id + * @param signingKey signing key + * @return criterion + */ public static SelectSignatureFromKey isValidSignatureOnUserId(String userId, PGPPublicKey signingKey) { return new SelectSignatureFromKey() { @Override @@ -225,6 +313,14 @@ public abstract class SelectSignatureFromKey { }; } + /** + * Criterion that only accepts signatures which are valid signatures over a key. + * This method only checks signature correctness, not expiry etc. + * + * @param target signed key + * @param signer signing key + * @return criterion + */ public static SelectSignatureFromKey isVerifyingSignatureOnKey(PGPPublicKey target, PGPPublicKey signer) { return new SelectSignatureFromKey() { @Override @@ -241,6 +337,15 @@ public abstract class SelectSignatureFromKey { }; } + /** + * Criterion that only accepts signatures which are correct binding signatures. + * This method only checks signature correctness, not expiry etc. + * + * @param primaryKey primary key + * @param subkey subkey + * @param signingKey signing key (either primary, or subkey) + * @return criterion + */ public static SelectSignatureFromKey isVerifyingSignatureOnKeys(PGPPublicKey primaryKey, PGPPublicKey subkey, PGPPublicKey signingKey) { if (signingKey.getKeyID() != primaryKey.getKeyID() && signingKey.getKeyID() != subkey.getKeyID()) { throw new IllegalArgumentException("Signing key MUST be either the primary or subkey."); @@ -259,6 +364,17 @@ public abstract class SelectSignatureFromKey { }; } + /** + * Criterion that only accepts certification signatures. + * + * Those are signature of the following types: + * - {@link SignatureType#NO_CERTIFICATION}, + * - {@link SignatureType#CASUAL_CERTIFICATION}, + * - {@link SignatureType#GENERIC_CERTIFICATION}, + * - {@link SignatureType#POSITIVE_CERTIFICATION}. + * + * @return criterion + */ public static SelectSignatureFromKey isCertification() { return new SelectSignatureFromKey() { @Override @@ -268,6 +384,13 @@ public abstract class SelectSignatureFromKey { }; } + /** + * Criterion that only accepts "well formed" signatures. + * A signature is "well formed", iff it has a creation time subpacket and if it does not predate + * its creating keys creation time. + * + * @return criterion + */ public static SelectSignatureFromKey isWellFormed() { return and( hasCreationTimeSubpacket(), @@ -275,10 +398,21 @@ public abstract class SelectSignatureFromKey { ); } + /** + * Criterion that only accepts v4 signatures. + * + * @return criterion + */ public static SelectSignatureFromKey isVersion4Signature() { return isVersion(4); } + /** + * Criterion that only accepts signatures which carry a creation time subpacket. + * According to the RFC, all signatures are required to have such a subpacket. + * + * @return criterion + */ public static SelectSignatureFromKey hasCreationTimeSubpacket() { return new SelectSignatureFromKey() { @Override @@ -288,10 +422,22 @@ public abstract class SelectSignatureFromKey { }; } + /** + * Criterion that only accepts signatures that were created by the provided key. + * + * @param publicKey public key of the creation key pair + * @return criterion + */ public static SelectSignatureFromKey isCreatedBy(PGPPublicKey publicKey) { return isCreatedBy(publicKey.getKeyID()); } + /** + * Criterion that only accepts signatures which were created by the public key with the provided key id. + * + * @param keyId key id + * @return criterion + */ public static SelectSignatureFromKey isCreatedBy(long keyId) { return new SelectSignatureFromKey() { @Override @@ -301,10 +447,21 @@ public abstract class SelectSignatureFromKey { }; } + /** + * Criterion that only accepts signatures which are not expired RIGHT NOW. + * + * @return criterion + */ public static SelectSignatureFromKey isSigNotExpired() { return isSigNotExpired(new Date()); } + /** + * Criterion that only accepts signatures which are not expired at comparisonDate. + * + * @param comparisonDate comparison date + * @return criterion + */ public static SelectSignatureFromKey isSigNotExpired(Date comparisonDate) { return new SelectSignatureFromKey() { @Override @@ -314,6 +471,11 @@ public abstract class SelectSignatureFromKey { }; } + /** + * Criterion that only accepts signatures which do not predate their signing key's creation date. + * + * @return criterion + */ public static SelectSignatureFromKey doesNotPredateKeyCreationDate() { return new SelectSignatureFromKey() { @Override @@ -327,6 +489,12 @@ public abstract class SelectSignatureFromKey { }; } + /** + * Criterion that only accepts signatures which do not predate the creation date of the provided key. + * + * @param creator key + * @return criterion + */ public static SelectSignatureFromKey doesNotPredateKeyCreationDate(PGPPublicKey creator) { return new SelectSignatureFromKey() { @Override @@ -336,6 +504,12 @@ public abstract class SelectSignatureFromKey { }; } + /** + * Criterion that only accepts signatures of the provided signature version. + * + * @param version signature version + * @return criterion + */ public static SelectSignatureFromKey isVersion(int version) { return new SelectSignatureFromKey() { @Override @@ -345,6 +519,12 @@ public abstract class SelectSignatureFromKey { }; } + /** + * Criterion that only accepts signatures that are of the provided {@link SignatureType}. + * + * @param signatureType signature type that shall be accepted + * @return criterion + */ public static SelectSignatureFromKey isOfType(SignatureType signatureType) { return new SelectSignatureFromKey() { @Override @@ -354,6 +534,13 @@ public abstract class SelectSignatureFromKey { }; } + /** + * Compose different {@link SelectSignatureFromKey} by combining them with a logic AND. + * A signature will only be accepted, iff it satisfies every selector from selectors. + * + * @param selectors one or more selectors + * @return combined selector using AND operator + */ public static SelectSignatureFromKey and(SelectSignatureFromKey... selectors) { return new SelectSignatureFromKey() { @Override @@ -368,6 +555,13 @@ public abstract class SelectSignatureFromKey { }; } + /** + * Compose different {@link SelectSignatureFromKey} by combining them with a logic OR. + * A signature will only be accepted, iff it satisfies at least one selector from selectors. + * + * @param selectors one or more selectors + * @return combined selector using OR operator + */ public static SelectSignatureFromKey or(SelectSignatureFromKey... selectors) { return new SelectSignatureFromKey() { @Override @@ -381,6 +575,14 @@ public abstract class SelectSignatureFromKey { }; } + /** + * Negate the result of a {@link SelectSignatureFromKey} implementations {@link #accept(PGPSignature, PGPPublicKey, PGPKeyRing)}. + * The resulting {@link SelectSignatureFromKey} will only accept signatures that are rejected by the provided selector + * and reject those that are accepted by it. + * + * @param selector selector whose logic operation will be negated + * @return negated selector + */ public static SelectSignatureFromKey not(SelectSignatureFromKey selector) { return new SelectSignatureFromKey() { @Override diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureChainValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureChainValidator.java index 52ed9a3f..3751e3fd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureChainValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureChainValidator.java @@ -26,6 +26,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; +import org.bouncycastle.bcpg.sig.SignerUserID; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSignature; @@ -35,11 +36,32 @@ import org.pgpainless.exception.SignatureValidationException; import org.pgpainless.policy.Policy; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; +/** + * This class implements validity checks on OpenPGP signatures. + * Its responsibilities are checking if a signing key was eligible to create a certain signature + * and if the signature is valid at the time of validation. + */ public class SignatureChainValidator { private static final Logger LOGGER = Logger.getLogger(SignatureChainValidator.class.getName()); - public static boolean validateSigningKey(PGPSignature signature, PGPPublicKeyRing signingKeyRing, Policy policy, Date validationDate) throws SignatureValidationException { + /** + * Check if the signing key was eligible to create the provided signature. + * + * That entails: + * - Check, if the primary key is being revoked via key-revocation signatures. + * - Check, if the keys user-ids are revoked or not bound. + * - Check, if the signing subkey is revoked or expired. + * - Check, if the signing key is not capable of signing + * + * @param signature signature + * @param signingKeyRing signing key ring + * @param policy validation policy + * @return true if the signing key was eligible to create the signature + * @throws SignatureValidationException in case of a validation constraint violation + */ + public static boolean validateSigningKey(PGPSignature signature, PGPPublicKeyRing signingKeyRing, Policy policy) + throws SignatureValidationException { Map rejections = new ConcurrentHashMap<>(); @@ -50,6 +72,7 @@ public class SignatureChainValidator { PGPPublicKey primaryKey = signingKeyRing.getPublicKey(); + // Key-Revocation Signatures List directKeySignatures = new ArrayList<>(); Iterator primaryKeyRevocationIterator = primaryKey.getSignaturesOfType(SignatureType.KEY_REVOCATION.getCode()); while (primaryKeyRevocationIterator.hasNext()) { @@ -64,6 +87,7 @@ public class SignatureChainValidator { } } + // Direct-Key Signatures Iterator keySignatures = primaryKey.getSignaturesOfType(SignatureType.DIRECT_KEY.getCode()); while (keySignatures.hasNext()) { PGPSignature keySignature = keySignatures.next(); @@ -78,14 +102,13 @@ public class SignatureChainValidator { } Collections.sort(directKeySignatures, new SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); - if (directKeySignatures.isEmpty()) { - - } else { + if (!directKeySignatures.isEmpty()) { if (directKeySignatures.get(0).getSignatureType() == SignatureType.KEY_REVOCATION.getCode()) { throw new SignatureValidationException("Primary key has been revoked."); } } + // User-ID signatures (certifications, revocations) Iterator userIds = primaryKey.getUserIDs(); Map> userIdSignatures = new ConcurrentHashMap<>(); while (userIds.hasNext()) { @@ -107,23 +130,39 @@ public class SignatureChainValidator { userIdSignatures.put(userId, signaturesOnUserId); } - boolean userIdValid = false; + boolean anyUserIdValid = false; for (String userId : userIdSignatures.keySet()) { if (!userIdSignatures.get(userId).isEmpty()) { PGPSignature current = userIdSignatures.get(userId).get(0); if (current.getSignatureType() == SignatureType.CERTIFICATION_REVOCATION.getCode()) { LOGGER.log(Level.FINE, "User-ID '" + userId + "' is revoked."); } else { - userIdValid = true; + anyUserIdValid = true; } } } - if (!userIdValid) { - throw new SignatureValidationException("Key is not valid at this point.", rejections); + if (!anyUserIdValid) { + throw new SignatureValidationException("No valid user-id found.", rejections); } - if (signingSubkey != primaryKey) { + // Specific signer user-id + SignerUserID signerUserID = SignatureSubpacketsUtil.getSignerUserID(signature); + if (signerUserID != null) { + PGPSignature userIdSig = userIdSignatures.get(signerUserID.getID()).get(0); + if (userIdSig.getSignatureType() == SignatureType.CERTIFICATION_REVOCATION.getCode()) { + throw new SignatureValidationException("Signature was made with user-id '" + signerUserID.getID() + "' which is revoked."); + } + } + + if (signingSubkey == primaryKey) { + if (!directKeySignatures.isEmpty()) { + if (KeyFlag.hasKeyFlag(SignatureSubpacketsUtil.getKeyFlags(directKeySignatures.get(0)).getFlags(), KeyFlag.SIGN_DATA)) { + return true; + } + } + } // Subkey Binding Signatures / Subkey Revocation Signatures + else { List subkeySigs = new ArrayList<>(); Iterator bindingRevocations = signingSubkey.getSignaturesOfType(SignatureType.SUBKEY_REVOCATION.getCode()); while (bindingRevocations.hasNext()) { @@ -168,14 +207,41 @@ public class SignatureChainValidator { return true; } - public static boolean validateSignatureChain(PGPSignature signature, InputStream signedData, PGPPublicKeyRing signingKeyRing, Policy policy, Date validationDate) + /** + * Validate the given signing key and then verify the given signature while parsing out the signed data. + * Uninitialized means that no signed data has been read and the hash generators state has not yet been updated. + * + * @param signature uninitialized signature + * @param signedData input stream containing signed data + * @param signingKeyRing key ring containing signing key + * @param policy validation policy + * @param validationDate date of validation + * @return true if the signature is valid, false otherwise + * @throws SignatureValidationException for validation constraint violations + */ + public static boolean validateSignatureChain(PGPSignature signature, + InputStream signedData, + PGPPublicKeyRing signingKeyRing, + Policy policy, + Date validationDate) throws SignatureValidationException { - validateSigningKey(signature, signingKeyRing, policy, validationDate); + validateSigningKey(signature, signingKeyRing, policy); return SignatureValidator.verifyUninitializedSignature(signature, signedData, signingKeyRing.getPublicKey(signature.getKeyID()), policy, validationDate); } - public static boolean validateSignature(PGPSignature signature, PGPPublicKeyRing verificationKeys, Policy policy) throws SignatureValidationException { - validateSigningKey(signature, verificationKeys, policy, signature.getCreationTime()); + /** + * Validate the signing key and the given initialized signature. + * Initialized means that the signatures hash generator has already been updated by reading the signed data completely. + * + * @param signature initialized signature + * @param verificationKeys key ring containing the verification key + * @param policy validation policy + * @return true if the signature is valid, false otherwise + * @throws SignatureValidationException in case of a validation constraint violation + */ + public static boolean validateSignature(PGPSignature signature, PGPPublicKeyRing verificationKeys, Policy policy) + throws SignatureValidationException { + validateSigningKey(signature, verificationKeys, policy); PGPPublicKey signingKey = verificationKeys.getPublicKey(signature.getKeyID()); SignatureValidator.verifyInitializedSignature(signature, signingKey, policy, signature.getCreationTime()); return true; diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureCreationDateComparator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureCreationDateComparator.java index 2f53e1a3..7cd70d38 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureCreationDateComparator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureCreationDateComparator.java @@ -19,21 +19,38 @@ import java.util.Comparator; import org.bouncycastle.openpgp.PGPSignature; +/** + * Comparator which can be used to sort signatures with regard to their creation time. + */ public class SignatureCreationDateComparator implements Comparator { public static final Order DEFAULT_ORDER = Order.OLD_TO_NEW; public enum Order { + /** + * Oldest signatures first. + */ OLD_TO_NEW, + + /** + * Newest signatures first. + */ NEW_TO_OLD } private final Order order; + /** + * Create a new comparator which sorts signatures old to new. + */ public SignatureCreationDateComparator() { this(DEFAULT_ORDER); } + /** + * Create a new comparator which sorts signatures according to the passed ordering. + * @param order ordering + */ public SignatureCreationDateComparator(Order order) { this.order = order; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java index 5df4d15a..019727b1 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java @@ -31,7 +31,7 @@ import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.SignatureType; @@ -39,38 +39,89 @@ import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.key.util.OpenPgpKeyAttributeUtil; import org.pgpainless.key.util.RevocationAttributes; +import org.pgpainless.policy.Policy; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; +/** + * Utility methods related to signatures. + */ public class SignatureUtils { + /** + * Return a signature generator for the provided signing key. + * The signature generator will follow the hash algorithm preferences of the signing key and pick the best algorithm. + * + * @param singingKey signing key + * @return signature generator + */ public static PGPSignatureGenerator getSignatureGeneratorFor(PGPSecretKey singingKey) { return getSignatureGeneratorFor(singingKey.getPublicKey()); } + /** + * Return a signature generator for the provided signing key. + * The signature generator will follow the hash algorithm preferences of the signing key and pick the best algorithm. + * + * @param signingPubKey signing key + * @return signature generator + */ public static PGPSignatureGenerator getSignatureGeneratorFor(PGPPublicKey signingPubKey) { PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator( getPgpContentSignerBuilderForKey(signingPubKey)); return signatureGenerator; } - private static BcPGPContentSignerBuilder getPgpContentSignerBuilderForKey(PGPPublicKey publicKey) { + /** + * Return a content signer builder fot the passed public key. + * + * The content signer will use a hash algorithm derived from the keys algorithm preferences. + * If no preferences can be derived, the key will fall back to the default hash algorithm as set in + * the {@link org.pgpainless.policy.Policy}. + * + * @param publicKey public key + * @return content signer builder + */ + private static PGPContentSignerBuilder getPgpContentSignerBuilderForKey(PGPPublicKey publicKey) { List preferredHashAlgorithms = OpenPgpKeyAttributeUtil.getPreferredHashAlgorithms(publicKey); if (preferredHashAlgorithms.isEmpty()) { preferredHashAlgorithms = OpenPgpKeyAttributeUtil.guessPreferredHashAlgorithms(publicKey); } HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(preferredHashAlgorithms); - return new BcPGPContentSignerBuilder(publicKey.getAlgorithm(), hashAlgorithm.getAlgorithmId()); + return ImplementationFactory.getInstance().getPGPContentSignerBuilder(publicKey.getAlgorithm(), hashAlgorithm.getAlgorithmId()); } + /** + * Negotiate an acceptable hash algorithm from the provided list of options. + * Acceptance of hash algorithms can be changed by setting a custom {@link Policy}. + * + * @param preferredHashAlgorithms list of preferred hash algorithms of a key + * @return first acceptable algorithm, or policies default hash algorithm + */ private static HashAlgorithm negotiateHashAlgorithm(List preferredHashAlgorithms) { - if (preferredHashAlgorithms.isEmpty()) { - return PGPainless.getPolicy().getSignatureHashAlgorithmPolicy().defaultHashAlgorithm(); + Policy policy = PGPainless.getPolicy(); + for (HashAlgorithm option : preferredHashAlgorithms) { + if (policy.getSignatureHashAlgorithmPolicy().isAcceptable(option)) { + return option; + } } - return preferredHashAlgorithms.get(0); + + return PGPainless.getPolicy().getSignatureHashAlgorithmPolicy().defaultHashAlgorithm(); } - public static PGPSignature getLatestValidSignature(PGPPublicKey publicKey, List signatures, PGPKeyRing keyRing) throws PGPException { + /** + * Return the latest valid signature on the provided public key. + * + * @param publicKey signed key + * @param signatures signatures + * @param keyRing key ring containing signature creator key + * @return latest valid signature + * @throws PGPException in case of a validation error + */ + public static PGPSignature getLatestValidSignature(PGPPublicKey publicKey, + List signatures, + PGPKeyRing keyRing) + throws PGPException { List valid = new ArrayList<>(); for (PGPSignature signature : signatures) { long issuerID = signature.getKeyID(); @@ -93,6 +144,16 @@ public class SignatureUtils { return valid.isEmpty() ? null : valid.get(valid.size() - 1); } + /** + * Return true, iff a signature is valid. + * + * TODO: There is code duplication here ({@link SelectSignatureFromKey}, {@link SignatureChainValidator}, {@link SignatureValidator}). + * @param signature signature to validate + * @param issuer signing key + * @param target signed key + * @return true if signature is valid + * @throws PGPException if a validation error occurs. + */ public static boolean isSignatureValid(PGPSignature signature, PGPPublicKey issuer, PGPPublicKey target) throws PGPException { SignatureType signatureType = SignatureType.valueOf(signature.getSignatureType()); switch (signatureType) { @@ -276,6 +337,15 @@ public class SignatureUtils { return latestSelfSig.getSignatureType() != SignatureType.CERTIFICATION_REVOCATION.getCode(); } + /** + * Return true if the provided signature is a hard revocation. + * Hard revocations are revocation signatures which either carry a revocation reason of + * {@link RevocationAttributes.Reason#KEY_COMPROMISED} or {@link RevocationAttributes.Reason#NO_REASON}, + * or no reason at all. + * + * @param signature signature + * @return true if signature is a hard revocation + */ public static boolean isHardRevocation(PGPSignature signature) { SignatureType type = SignatureType.valueOf(signature.getSignatureType()); diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureValidityComparator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureValidityComparator.java index d60fb39e..8612c98d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureValidityComparator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureValidityComparator.java @@ -19,15 +19,30 @@ import java.util.Comparator; import org.bouncycastle.openpgp.PGPSignature; +/** + * Comparator which sorts signatures based on an ordering and on revocation hardness. + * + * If a list of signatures gets ordered using this comparator, hard revocations will always + * come first. + * Further, signatures are ordered by date according to the {@link org.pgpainless.signature.SignatureCreationDateComparator.Order}. + */ public class SignatureValidityComparator implements Comparator { private final SignatureCreationDateComparator.Order order; private final SignatureCreationDateComparator creationDateComparator; + /** + * Create a new {@link SignatureValidityComparator} which orders signatures oldest first. + * Still, hard revocations will come first. + */ public SignatureValidityComparator() { this(SignatureCreationDateComparator.DEFAULT_ORDER); } + /** + * Create a new {@link SignatureValidityComparator} which orders signatures following the passed ordering. + * Still, hard revocations will come first. + */ public SignatureValidityComparator(SignatureCreationDateComparator.Order order) { this.order = order; this.creationDateComparator = new SignatureCreationDateComparator(order); diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorUtil.java index aadfe0e3..a2a07ffe 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorUtil.java @@ -31,6 +31,15 @@ import org.pgpainless.algorithm.KeyFlag; */ public class SignatureSubpacketGeneratorUtil { + /** + * Return a list of {@link SignatureSubpacket SignatureSubpackets} from the subpacket generator, which correspond + * to the given {@link org.pgpainless.algorithm.SignatureSubpacket} type. + * + * @param type subpacket type + * @param generator subpacket generator + * @param

generic subpacket type + * @return possibly empty list of subpackets + */ public static

List

getSubpacketsOfType(org.pgpainless.algorithm.SignatureSubpacket type, PGPSignatureSubpacketGenerator generator) { SignatureSubpacket[] subpackets = generator.getSubpackets(type.getCode()); @@ -41,11 +50,25 @@ public class SignatureSubpacketGeneratorUtil { return list; } + /** + * Remove all packets of the given type from the {@link PGPSignatureSubpacketGenerator PGPSignatureSubpacketGenerators} + * internal set. + * + * @param subpacketType type of subpacket to remove + * @param subpacketGenerator subpacket generator + */ public static void removeAllPacketsOfType(org.pgpainless.algorithm.SignatureSubpacket subpacketType, PGPSignatureSubpacketGenerator subpacketGenerator) { removeAllPacketsOfType(subpacketType.getCode(), subpacketGenerator); } + /** + * Remove all packets of the given type from the {@link PGPSignatureSubpacketGenerator PGPSignatureSubpacketGenerators} + * internal set. + * + * @param type type of subpacket to remove + * @param subpacketGenerator subpacket generator + */ public static void removeAllPacketsOfType(int type, PGPSignatureSubpacketGenerator subpacketGenerator) { for (SignatureSubpacket subpacket : subpacketGenerator.getSubpackets(type)) { subpacketGenerator.removePacket(subpacket); @@ -98,11 +121,22 @@ public class SignatureSubpacketGeneratorUtil { return secondsToExpire; } + /** + * Return true, if the subpacket generator has a {@link KeyFlags} subpacket which carries the given key flag. + * Returns false, if no {@link KeyFlags} subpacket is present. + * If there are more than one instance of a {@link KeyFlags} packet present, only the last occurrence will + * be tested. + * + * @param keyFlag flag to test for + * @param generator subpackets generator + * @return true if the generator has the given key flag set + */ public static boolean hasKeyFlag(KeyFlag keyFlag, PGPSignatureSubpacketGenerator generator) { List keyFlagPackets = getSubpacketsOfType(org.pgpainless.algorithm.SignatureSubpacket.keyFlags, generator); if (keyFlagPackets.isEmpty()) { return false; } - return KeyFlag.hasKeyFlag(keyFlagPackets.get(0).getFlags(), keyFlag); + KeyFlags last = keyFlagPackets.get(keyFlagPackets.size() - 1); + return KeyFlag.hasKeyFlag(last.getFlags(), keyFlag); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java index d27ea65d..8eb5393c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java @@ -46,6 +46,7 @@ import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; import org.bouncycastle.util.encoders.Hex; import org.pgpainless.algorithm.SignatureSubpacket; import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.signature.SignatureUtils; /** * Utility class to access signature subpackets from signatures. @@ -67,6 +68,13 @@ public class SignatureSubpacketsUtil { return hashedOrUnhashed(signature, SignatureSubpacket.issuerFingerprint); } + /** + * Return the {@link IssuerFingerprint} subpacket of the signature into a {@link OpenPgpV4Fingerprint}. + * If no issuer fingerprint is present in the signature, return null. + * + * @param signature signature + * @return v4 fingerprint of the issuer, or null + */ public static OpenPgpV4Fingerprint getIssuerFingerprintAsOpenPgpV4Fingerprint(PGPSignature signature) { IssuerFingerprint subpacket = getIssuerFingerprint(signature); if (subpacket == null) { @@ -123,12 +131,20 @@ public class SignatureSubpacketsUtil { return hashed(signature, SignatureSubpacket.signatureExpirationTime); } + /** + * Return the signatures expiration time as a date. + * The expiration date is computed by adding the expiration time to the signature creation date. + * If the signature has no expiration time subpacket, or the expiration time is set to '0', this message returns null. + * + * @param signature signature + * @return expiration time as date + */ public static Date getSignatureExpirationTimeAsDate(PGPSignature signature) { SignatureExpirationTime subpacket = getSignatureExpirationTime(signature); - if (subpacket == null || subpacket.getTime() == 0) { + if (subpacket == null) { return null; } - return new Date(signature.getCreationTime().getTime() + 1000 * subpacket.getTime()); + return SignatureUtils.datePlusSeconds(signature.getCreationTime(), subpacket.getTime()); } /** @@ -142,15 +158,25 @@ public class SignatureSubpacketsUtil { return hashed(signature, SignatureSubpacket.keyExpirationTime); } + /** + * Return the signatures key-expiration time as a date. + * The expiration date is computed by adding the signatures' key-expiration time to the signing keys + * creation date. + * If the signature does not have a key-expiration time subpacket, or its value is '0', this method returns null. + * + * @param signature self-signature carrying the key-expiration time subpacket + * @param signingKey signature creation key + * @return key expiration time as date + */ public static Date getKeyExpirationTimeAsDate(PGPSignature signature, PGPPublicKey signingKey) { KeyExpirationTime subpacket = getKeyExpirationTime(signature); - if (subpacket == null || subpacket.getTime() == 0) { + if (subpacket == null) { return null; } if (signature.getKeyID() != signingKey.getKeyID()) { throw new IllegalArgumentException("Provided key (" + Long.toHexString(signingKey.getKeyID()) + ") did not create the signature (" + Long.toHexString(signature.getKeyID()) + ")"); } - return new Date(signingKey.getCreationTime().getTime() + 1000 * subpacket.getTime()); + return SignatureUtils.datePlusSeconds(signingKey.getCreationTime(), subpacket.getTime()); } /** @@ -328,14 +354,41 @@ public class SignatureSubpacketsUtil { return hashed(signature, SignatureSubpacket.trustSignature); } + /** + * Select a list of all signature subpackets of the given type, which are present in the hashed area of + * the given signature. + * + * @param signature signature + * @param type subpacket type + * @param

generic subpacket type + * @return list of subpackets from the hashed area + */ private static

P hashed(PGPSignature signature, SignatureSubpacket type) { return getSignatureSubpacket(signature.getHashedSubPackets(), type); } + /** + * Select a list of all signature subpackets of the given type, which are present in the unhashed area of + * the given signature. + * + * @param signature signature + * @param type subpacket type + * @param

generic subpacket type + * @return list of subpackets from the unhashed area + */ private static

P unhashed(PGPSignature signature, SignatureSubpacket type) { return getSignatureSubpacket(signature.getUnhashedSubPackets(), type); } + /** + * Select a list of all signature subpackets of the given type, which are present in either the hashed + * or the unhashed area of the given signature. + * + * @param signature signature + * @param type subpacket type + * @param

generic subpacket type + * @return list of subpackets from the hashed/unhashed area + */ private static

P hashedOrUnhashed(PGPSignature signature, SignatureSubpacket type) { P hashedSubpacket = hashed(signature, type); return hashedSubpacket != null ? hashedSubpacket : unhashed(signature, type); diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/selection/signature/SelectSignatureFromKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/selection/signature/SelectSignatureFromKeyTest.java index 11dc3bb1..2ece0158 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/selection/signature/SelectSignatureFromKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/selection/signature/SelectSignatureFromKeyTest.java @@ -15,7 +15,6 @@ */ package org.pgpainless.util.selection.signature; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail;