From e9d8ddc57b5a4f5f5fd48b88c4d9fbec31dc5d2e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 25 Oct 2023 19:07:52 +0200 Subject: [PATCH] Kotlin conversion: SignatureValidator --- .../consumer/SignatureValidator.java | 681 ----------------- .../extensions/PGPSignatureExtensions.kt | 11 + .../signature/consumer/SignatureValidator.kt | 693 ++++++++++++++++++ 3 files changed, 704 insertions(+), 681 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java deleted file mode 100644 index 18bf5883..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java +++ /dev/null @@ -1,681 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer; - -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.bcpg.sig.KeyFlags; -import org.bouncycastle.bcpg.sig.NotationData; -import org.bouncycastle.bcpg.sig.SignatureCreationTime; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SignatureSubpacket; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.pgpainless.util.DateUtil; -import org.pgpainless.util.NotationRegistry; - -/** - * A collection of validators that perform validation steps over signatures. - */ -public abstract class SignatureValidator { - - public abstract void verify(PGPSignature signature) throws SignatureValidationException; - - /** - * Check, whether there is the possibility that the given signature was created by the given key. - * {@link #verify(PGPSignature)} throws a {@link SignatureValidationException} if we can say with certainty that - * the signature was not created by the given key (e.g. if the sig carries another issuer, issuer fingerprint packet). - * - * If there is no information found in the signature about who created it (no issuer, no fingerprint), - * {@link #verify(PGPSignature)} will simply return since it is plausible that the given key created the sig. - * - * @param signingKey signing key - * @return validator that throws a {@link SignatureValidationException} if the signature was not possibly made by - * the given key. - */ - public static SignatureValidator wasPossiblyMadeByKey(PGPPublicKey signingKey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - OpenPgpFingerprint signingKeyFingerprint = OpenPgpFingerprint.of(signingKey); - - Long issuer = SignatureSubpacketsUtil.getIssuerKeyIdAsLong(signature); - if (issuer != null) { - if (issuer != signingKey.getKeyID()) { - throw new SignatureValidationException("Signature was not created by " + - signingKeyFingerprint + " (signature issuer: " + Long.toHexString(issuer) + ")"); - } - } - - OpenPgpFingerprint fingerprint = - SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature); - if (fingerprint != null) { - if (!fingerprint.equals(signingKeyFingerprint)) { - throw new SignatureValidationException("Signature was not created by " + - signingKeyFingerprint + " (signature fingerprint: " + fingerprint + ")"); - } - } - - // No issuer information found, so we cannot rule out that we did not create the sig - } - }; - - } - - /** - * Verify that a subkey binding signature - if the subkey is signing-capable - contains a valid primary key - * binding signature. - * - * @param primaryKey primary key - * @param subkey subkey - * @param policy policy - * @param referenceDate reference date for signature verification - * @return validator - */ - public static SignatureValidator hasValidPrimaryKeyBindingSignatureIfRequired(PGPPublicKey primaryKey, - PGPPublicKey subkey, Policy policy, - Date referenceDate) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - if (!PublicKeyAlgorithm.requireFromId(signature.getKeyAlgorithm()).isSigningCapable()) { - // subkey is not signing capable -> No need to process embedded sigs - return; - } - - KeyFlags keyFlags = SignatureSubpacketsUtil.getKeyFlags(signature); - if (keyFlags == null) { - return; - } - if (!KeyFlag.hasKeyFlag(keyFlags.getFlags(), KeyFlag.SIGN_DATA) - && !KeyFlag.hasKeyFlag(keyFlags.getFlags(), KeyFlag.CERTIFY_OTHER)) { - return; - } - - try { - PGPSignatureList embeddedSignatures = SignatureSubpacketsUtil.getEmbeddedSignature(signature); - if (embeddedSignatures == null) { - throw new SignatureValidationException( - "Missing primary key binding signature on signing capable subkey " + - Long.toHexString(subkey.getKeyID()), Collections.emptyMap()); - } - - boolean hasValidPrimaryKeyBinding = false; - Map rejectedEmbeddedSigs = new ConcurrentHashMap<>(); - for (PGPSignature embedded : embeddedSignatures) { - - if (SignatureType.valueOf(embedded.getSignatureType()) == SignatureType.PRIMARYKEY_BINDING) { - - try { - signatureStructureIsAcceptable(subkey, policy).verify(embedded); - signatureIsEffective(referenceDate).verify(embedded); - correctPrimaryKeyBindingSignature(primaryKey, subkey).verify(embedded); - - hasValidPrimaryKeyBinding = true; - break; - } catch (SignatureValidationException e) { - rejectedEmbeddedSigs.put(embedded, e); - } - } - } - - if (!hasValidPrimaryKeyBinding) { - throw new SignatureValidationException( - "Missing primary key binding signature on signing capable subkey " + - Long.toHexString(subkey.getKeyID()), rejectedEmbeddedSigs); - } - } catch (PGPException e) { - throw new SignatureValidationException("Cannot process list of embedded signatures.", e); - } - } - }; - } - - /** - * Verify that a signature has an acceptable structure. - * - * @param signingKey signing key - * @param policy policy - * @return validator - */ - public static SignatureValidator signatureStructureIsAcceptable(PGPPublicKey signingKey, Policy policy) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - signatureIsNotMalformed(signingKey).verify(signature); - if (signature.getVersion() >= 4) { - signatureDoesNotHaveCriticalUnknownNotations(policy.getNotationRegistry()).verify(signature); - signatureDoesNotHaveCriticalUnknownSubpackets().verify(signature); - } - signatureUsesAcceptableHashAlgorithm(policy).verify(signature); - signatureUsesAcceptablePublicKeyAlgorithm(policy, signingKey).verify(signature); - } - }; - } - - /** - * Verify that a signature was made using an acceptable {@link PublicKeyAlgorithm}. - * - * @param policy policy - * @param signingKey signing key - * @return validator - */ - public static SignatureValidator signatureUsesAcceptablePublicKeyAlgorithm(Policy policy, - PGPPublicKey signingKey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - PublicKeyAlgorithm algorithm = PublicKeyAlgorithm.requireFromId(signingKey.getAlgorithm()); - int bitStrength = signingKey.getBitStrength(); - if (bitStrength == -1) { - throw new SignatureValidationException("Cannot determine bit strength of signing key."); - } - if (!policy.getPublicKeyAlgorithmPolicy().isAcceptable(algorithm, bitStrength)) { - throw new SignatureValidationException("Signature was made using unacceptable key. " + - algorithm + " (" + bitStrength + - " bits) is not acceptable according to the public key algorithm policy."); - } - } - }; - } - - /** - * Verify that a signature uses an acceptable {@link HashAlgorithm}. - * - * @param policy policy - * @return validator - */ - public static SignatureValidator signatureUsesAcceptableHashAlgorithm(Policy policy) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - try { - HashAlgorithm hashAlgorithm = HashAlgorithm.requireFromId(signature.getHashAlgorithm()); - Policy.HashAlgorithmPolicy hashAlgorithmPolicy = - getHashAlgorithmPolicyForSignature(signature, policy); - if (!hashAlgorithmPolicy.isAcceptable(signature.getHashAlgorithm(), signature.getCreationTime())) { - throw new SignatureValidationException("Signature uses unacceptable hash algorithm " + - hashAlgorithm + " (Signature creation time: " + - DateUtil.formatUTCDate(signature.getCreationTime()) + ")"); - } - } catch (NoSuchElementException e) { - throw new SignatureValidationException("Signature uses unknown hash algorithm " + - signature.getHashAlgorithm()); - } - } - }; - } - - /** - * Return the applicable {@link Policy.HashAlgorithmPolicy} for the given {@link PGPSignature}. - * Revocation signatures are being policed using a different policy than non-revocation signatures. - * - * @param signature signature - * @param policy revocation policy for revocation sigs, normal policy for non-rev sigs - * @return policy - */ - private static Policy.HashAlgorithmPolicy getHashAlgorithmPolicyForSignature(PGPSignature signature, - Policy policy) { - SignatureType type = SignatureType.valueOf(signature.getSignatureType()); - Policy.HashAlgorithmPolicy hashAlgorithmPolicy; - if (type == SignatureType.CERTIFICATION_REVOCATION || type == SignatureType.KEY_REVOCATION || - type == SignatureType.SUBKEY_REVOCATION) { - hashAlgorithmPolicy = policy.getRevocationSignatureHashAlgorithmPolicy(); - } else { - hashAlgorithmPolicy = policy.getSignatureHashAlgorithmPolicy(); - } - return hashAlgorithmPolicy; - } - - /** - * Verify that a signature does not carry critical unknown notations. - * - * @param registry notation registry of known notations - * @return validator - */ - public static SignatureValidator signatureDoesNotHaveCriticalUnknownNotations(NotationRegistry registry) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - List hashedNotations = SignatureSubpacketsUtil.getHashedNotationData(signature); - for (NotationData notation : hashedNotations) { - if (!notation.isCritical()) { - continue; - } - if (!registry.isKnownNotation(notation.getNotationName())) { - throw new SignatureValidationException("Signature contains unknown critical notation '" + - notation.getNotationName() + "' in its hashed area."); - } - } - } - }; - } - - /** - * Verify that a signature does not contain critical unknown subpackets. - * - * @return validator - */ - public static SignatureValidator signatureDoesNotHaveCriticalUnknownSubpackets() { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - PGPSignatureSubpacketVector hashedSubpackets = signature.getHashedSubPackets(); - for (int criticalTag : hashedSubpackets.getCriticalTags()) { - try { - SignatureSubpacket.requireFromCode(criticalTag); - } catch (NoSuchElementException e) { - throw new SignatureValidationException( - "Signature contains unknown critical subpacket of type " + - Long.toHexString(criticalTag)); - } - } - } - }; - } - - /** - * Verify that a signature is effective right now. - * - * @return validator - */ - public static SignatureValidator signatureIsEffective() { - return signatureIsEffective(new Date()); - } - - /** - * Verify that a signature is effective at the given reference date. - * - * @param referenceDate reference date for signature verification - * @return validator - */ - public static SignatureValidator signatureIsEffective(Date referenceDate) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - signatureIsAlreadyEffective(referenceDate).verify(signature); - signatureIsNotYetExpired(referenceDate).verify(signature); - } - }; - } - - /** - * Verify that a signature was created prior to the given reference date. - * - * @param referenceDate reference date for signature verification - * @return validator - */ - public static SignatureValidator signatureIsAlreadyEffective(Date referenceDate) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - Date signatureCreationTime = SignatureSubpacketsUtil.getSignatureCreationTime(signature).getTime(); - // Hard revocations are always effective - if (SignatureUtils.isHardRevocation(signature)) { - return; - } - - if (signatureCreationTime.after(referenceDate)) { - throw new SignatureValidationException("Signature was created at " + signatureCreationTime + - " and is therefore not yet valid at " + referenceDate); - } - } - }; - } - - /** - * Verify that a signature is not yet expired. - * - * @param referenceDate reference date for signature verification - * @return validator - */ - public static SignatureValidator signatureIsNotYetExpired(Date referenceDate) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - // Hard revocations do not expire - if (SignatureUtils.isHardRevocation(signature)) { - return; - } - - Date signatureExpirationTime = SignatureSubpacketsUtil.getSignatureExpirationTimeAsDate(signature); - if (signatureExpirationTime != null && signatureExpirationTime.before(referenceDate)) { - throw new SignatureValidationException("Signature is already expired (expiration: " + - signatureExpirationTime + ", validation: " + referenceDate + ")"); - } - } - }; - } - - /** - * Verify that a signature is not malformed. - * A signature is malformed if it has no hashed creation time subpacket, - * it predates the creation time of the signing key, or it predates the creation date - * of the signing key binding signature. - * - * @param creator signing key - * @return validator - */ - public static SignatureValidator signatureIsNotMalformed(PGPPublicKey creator) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - if (signature.getVersion() >= 4) { - signatureHasHashedCreationTime().verify(signature); - } - signatureDoesNotPredateSigningKey(creator).verify(signature); - if (signature.getSignatureType() != SignatureType.PRIMARYKEY_BINDING.getCode()) { - signatureDoesNotPredateSigningKeyBindingDate(creator).verify(signature); - } - } - }; - } - - public static SignatureValidator signatureDoesNotPredateSignee(PGPPublicKey signee) { - return signatureDoesNotPredateKeyCreation(signee); - } - - /** - * Verify that a signature has a hashed creation time subpacket. - * - * @return validator - */ - public static SignatureValidator signatureHasHashedCreationTime() { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - SignatureCreationTime creationTime = SignatureSubpacketsUtil.getSignatureCreationTime(signature); - if (creationTime == null) { - throw new SignatureValidationException( - "Malformed signature. Signature has no signature creation time subpacket in its hashed area."); - } - } - }; - } - - /** - * Verify that a signature does not predate the creation time of the signing key. - * - * @param key signing key - * @return validator - */ - public static SignatureValidator signatureDoesNotPredateSigningKey(PGPPublicKey key) { - return signatureDoesNotPredateKeyCreation(key); - } - - /** - * Verify that a signature does not predate the creation time of the given key. - * - * @param key key - * @return validator - */ - public static SignatureValidator signatureDoesNotPredateKeyCreation(PGPPublicKey key) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - Date keyCreationTime = key.getCreationTime(); - Date signatureCreationTime = signature.getCreationTime(); - - if (keyCreationTime.after(signatureCreationTime)) { - throw new SignatureValidationException("Signature predates key (key creation: " + - keyCreationTime + ", signature creation: " + signatureCreationTime + ")"); - } - } - }; - } - - /** - * Verify that a signature does not predate the binding date of the signing key. - * - * @param signingKey signing key - * @return validator - */ - public static SignatureValidator signatureDoesNotPredateSigningKeyBindingDate(PGPPublicKey signingKey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - if (signingKey.isMasterKey()) { - return; - } - boolean predatesBindingSig = true; - Iterator bindingSignatures = - signingKey.getSignaturesOfType(SignatureType.SUBKEY_BINDING.getCode()); - if (!bindingSignatures.hasNext()) { - throw new SignatureValidationException("Signing subkey does not have a subkey binding signature."); - } - while (bindingSignatures.hasNext()) { - PGPSignature bindingSig = bindingSignatures.next(); - if (!bindingSig.getCreationTime().after(signature.getCreationTime())) { - predatesBindingSig = false; - } - } - if (predatesBindingSig) { - throw new SignatureValidationException( - "Signature was created before the signing key was bound to the key ring."); - } - } - }; - } - - /** - * Verify that a subkey binding signature is correct. - * - * @param primaryKey primary key - * @param subkey subkey - * @return validator - */ - public static SignatureValidator correctSubkeyBindingSignature(PGPPublicKey primaryKey, PGPPublicKey subkey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - if (primaryKey.getKeyID() == subkey.getKeyID()) { - throw new SignatureValidationException("Primary key cannot be its own subkey."); - } - try { - signature.init(ImplementationFactory.getInstance() - .getPgpContentVerifierBuilderProvider(), primaryKey); - boolean valid = signature.verifyCertification(primaryKey, subkey); - if (!valid) { - throw new SignatureValidationException("Signature is not correct."); - } - } catch (PGPException | ClassCastException e) { - throw new SignatureValidationException("Cannot verify subkey binding signature correctness", e); - } - } - }; - } - - /** - * Verify that a primary key binding signature is correct. - * - * @param primaryKey primary key - * @param subkey subkey - * @return validator - */ - public static SignatureValidator correctPrimaryKeyBindingSignature(PGPPublicKey primaryKey, PGPPublicKey subkey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - try { - signature.init(ImplementationFactory.getInstance().getPgpContentVerifierBuilderProvider(), subkey); - boolean valid = signature.verifyCertification(primaryKey, subkey); - if (!valid) { - throw new SignatureValidationException("Primary Key Binding Signature is not correct."); - } - } catch (PGPException | ClassCastException e) { - throw new SignatureValidationException( - "Cannot verify primary key binding signature correctness", e); - } - } - }; - } - - /** - * Verify that a direct-key signature is correct. - * - * @param signer signing key - * @param signee signed key - * @return validator - */ - public static SignatureValidator correctSignatureOverKey(PGPPublicKey signer, PGPPublicKey signee) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - try { - signature.init(ImplementationFactory.getInstance().getPgpContentVerifierBuilderProvider(), signer); - boolean valid; - if (signer.getKeyID() == signee.getKeyID() || signature.getSignatureType() == PGPSignature.DIRECT_KEY) { - valid = signature.verifyCertification(signee); - } else { - valid = signature.verifyCertification(signer, signee); - } - if (!valid) { - throw new SignatureValidationException("Signature is not correct."); - } - } catch (PGPException | ClassCastException e) { - throw new SignatureValidationException("Cannot verify direct-key signature correctness", e); - } - } - }; - } - - /** - * Verify that a signature is a certification signature. - * - * @return validator - */ - public static SignatureValidator signatureIsCertification() { - return signatureIsOfType( - SignatureType.POSITIVE_CERTIFICATION, - SignatureType.CASUAL_CERTIFICATION, - SignatureType.GENERIC_CERTIFICATION, - SignatureType.NO_CERTIFICATION); - } - - /** - * Verify that a signature type equals one of the given {@link SignatureType SignatureTypes}. - * - * @param signatureTypes one or more signature types - * @return validator - */ - public static SignatureValidator signatureIsOfType(SignatureType... signatureTypes) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - SignatureType type = SignatureType.valueOf(signature.getSignatureType()); - boolean valid = false; - for (SignatureType allowed : signatureTypes) { - if (type == allowed) { - valid = true; - break; - } - } - if (!valid) { - throw new SignatureValidationException("Signature is of type " + type + " while only " + - Arrays.toString(signatureTypes) + " are allowed here."); - } - } - }; - } - - /** - * Verify that a signature over a user-id is correct. - * - * @param userId user-id - * @param certifiedKey key carrying the user-id - * @param certifyingKey key that created the signature. - * @return validator - */ - public static SignatureValidator correctSignatureOverUserId(String userId, PGPPublicKey certifiedKey, - PGPPublicKey certifyingKey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - try { - signature.init(ImplementationFactory.getInstance() - .getPgpContentVerifierBuilderProvider(), certifyingKey); - boolean valid = signature.verifyCertification(userId, certifiedKey); - if (!valid) { - throw new SignatureValidationException("Signature over user-id '" + userId + - "' is not correct."); - } - } catch (PGPException | ClassCastException e) { - throw new SignatureValidationException("Cannot verify signature over user-id '" + - userId + "'.", e); - } - } - }; - } - - /** - * Verify that a signature over a user-attribute packet is correct. - * - * @param userAttributes user attributes - * @param certifiedKey key carrying the user-attributes - * @param certifyingKey key that created the certification signature - * @return validator - */ - public static SignatureValidator correctSignatureOverUserAttributes(PGPUserAttributeSubpacketVector userAttributes, - PGPPublicKey certifiedKey, - PGPPublicKey certifyingKey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - try { - signature.init(ImplementationFactory.getInstance() - .getPgpContentVerifierBuilderProvider(), certifyingKey); - boolean valid = signature.verifyCertification(userAttributes, certifiedKey); - if (!valid) { - throw new SignatureValidationException("Signature over user-attribute vector is not correct."); - } - } catch (PGPException | ClassCastException e) { - throw new SignatureValidationException("Cannot verify signature over user-attribute vector.", e); - } - } - }; - } - - public static SignatureValidator signatureWasCreatedInBounds(Date notBefore, Date notAfter) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - Date timestamp = signature.getCreationTime(); - if (notBefore != null && timestamp.before(notBefore)) { - throw new SignatureValidationException( - "Signature was made before the earliest allowed signature creation time. Created: " + - DateUtil.formatUTCDate(timestamp) + " Earliest allowed: " + - DateUtil.formatUTCDate(notBefore)); - } - if (notAfter != null && timestamp.after(notAfter)) { - throw new SignatureValidationException( - "Signature was made after the latest allowed signature creation time. Created: " + - DateUtil.formatUTCDate(timestamp) + " Latest allowed: " + - DateUtil.formatUTCDate(notAfter)); - } - } - }; - } - -} diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt index 2be011bd..5547e48b 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt @@ -8,6 +8,8 @@ import java.util.* import openpgp.plusSeconds import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.algorithm.RevocationState import org.pgpainless.algorithm.SignatureType import org.pgpainless.key.OpenPgpFingerprint @@ -94,3 +96,12 @@ fun PGPSignature?.toRevocationState() = val PGPSignature.fingerprint: OpenPgpFingerprint? get() = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(this) + +val PGPSignature.publicKeyAlgorithm: PublicKeyAlgorithm + get() = PublicKeyAlgorithm.requireFromId(keyAlgorithm) + +val PGPSignature.signatureHashAlgorithm: HashAlgorithm + get() = HashAlgorithm.requireFromId(hashAlgorithm) + +fun PGPSignature.isOfType(type: SignatureType): Boolean = + SignatureType.requireFromCode(signatureType) == type diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt new file mode 100644 index 00000000..c7cbd6fd --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt @@ -0,0 +1,693 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.consumer + +import java.lang.Exception +import java.util.Date +import openpgp.formatUTC +import openpgp.openPgpKeyId +import org.bouncycastle.extensions.fingerprint +import org.bouncycastle.extensions.isHardRevocation +import org.bouncycastle.extensions.isOfType +import org.bouncycastle.extensions.publicKeyAlgorithm +import org.bouncycastle.extensions.signatureExpirationDate +import org.bouncycastle.extensions.signatureHashAlgorithm +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.SignatureSubpacket +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.exception.SignatureValidationException +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.policy.Policy +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil +import org.pgpainless.util.NotationRegistry + +abstract class SignatureValidator { + + @Throws(SignatureValidationException::class) abstract fun verify(signature: PGPSignature) + + companion object { + + /** + * Check, whether there is the possibility that the given signature was created by the given + * key. [verify] throws a [SignatureValidationException] if we can say with certainty that + * the signature was not created by the given key (e.g. if the sig carries another issuer, + * issuer fingerprint packet). + * + * If there is no information found in the signature about who created it (no issuer, no + * fingerprint), [verify] will simply return since it is plausible that the given key + * created the sig. + * + * @param signingKey signing key + * @return validator that throws a [SignatureValidationException] if the signature was not + * possibly made by the given key. + */ + @JvmStatic + fun wasPossiblyMadeByKey(signingKey: PGPPublicKey): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + val signingKeyFingerprint = OpenPgpFingerprint.of(signingKey) + val issuer = SignatureSubpacketsUtil.getIssuerKeyIdAsLong(signature) + + if (issuer != null) { + if (issuer != signingKey.keyID) { + throw SignatureValidationException( + "Signature was not created by" + + " $signingKeyFingerprint (signature issuer: ${issuer.openPgpKeyId()})") + } + } + + if (signature.fingerprint != null && + signature.fingerprint != signingKeyFingerprint) { + throw SignatureValidationException( + "Signature was not created by" + + " $signingKeyFingerprint (signature fingerprint: ${signature.fingerprint})") + } + } + + // No issuer information found, so we cannot rule out that we did not create the sig + } + } + + /** + * Verify that a subkey binding signature - if the subkey is signing-capable - contains a + * valid primary key binding signature. + * + * @param primaryKey primary key + * @param subkey subkey + * @param policy policy + * @param referenceDate reference date for signature verification + * @return validator + */ + @JvmStatic + fun hasValidPrimaryKeyBindingSignatureIfRequired( + primaryKey: PGPPublicKey, + subkey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (!signature.publicKeyAlgorithm.isSigningCapable()) { + // subkey is not signing capable -> No need to process embedded signatures + return + } + + // Make sure we have key flags + SignatureSubpacketsUtil.getKeyFlags(signature)?.let { + if (!KeyFlag.hasKeyFlag(it.flags, KeyFlag.SIGN_DATA) && + !KeyFlag.hasKeyFlag(it.flags, KeyFlag.CERTIFY_OTHER)) { + return + } + } + ?: return + + try { + val embeddedSignatures = + SignatureSubpacketsUtil.getEmbeddedSignature(signature) + if (embeddedSignatures.isEmpty) { + throw SignatureValidationException( + "Missing primary key binding" + + " signature on signing capable subkey ${subkey.keyID.openPgpKeyId()}", + mapOf()) + } + + val rejectedEmbeddedSignatures = mutableMapOf() + if (!embeddedSignatures.any { embedded -> + if (embedded.isOfType(SignatureType.PRIMARYKEY_BINDING)) { + try { + signatureStructureIsAcceptable(subkey, policy).verify(embedded) + signatureIsEffective(referenceTime).verify(embedded) + correctPrimaryKeyBindingSignature(primaryKey, subkey) + .verify(embedded) + return@any true + } catch (e: SignatureValidationException) { + rejectedEmbeddedSignatures[embedded] = e + } + } + false + }) { + throw SignatureValidationException( + "Missing primary key binding signature on signing capable subkey ${subkey.keyID.openPgpKeyId()}", + rejectedEmbeddedSignatures) + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Cannot process list of embedded signatures.", e) + } + } + } + } + + /** + * Verify that a signature has an acceptable structure. + * + * @param signingKey signing key + * @param policy policy + * @return validator + */ + @JvmStatic + fun signatureStructureIsAcceptable( + signingKey: PGPPublicKey, + policy: Policy + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + signatureIsNotMalformed(signingKey).verify(signature) + if (signature.version >= 4) { + signatureDoesNotHaveCriticalUnknownNotations(policy.notationRegistry) + .verify(signature) + signatureDoesNotHaveCriticalUnknownSubpackets().verify(signature) + } + signatureUsesAcceptableHashAlgorithm(policy).verify(signature) + signatureUsesAcceptablePublicKeyAlgorithm(policy, signingKey).verify(signature) + } + } + } + + /** + * Verify that a signature was made using an acceptable [PublicKeyAlgorithm]. + * + * @param policy policy + * @param signingKey signing key + * @return validator + */ + @JvmStatic + fun signatureUsesAcceptablePublicKeyAlgorithm( + policy: Policy, + signingKey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (signingKey.bitStrength == -1) { + throw SignatureValidationException( + "Cannot determine bit strength of signing key.") + } + if (!policy.publicKeyAlgorithmPolicy.isAcceptable( + signingKey.publicKeyAlgorithm, signingKey.bitStrength)) { + throw SignatureValidationException( + "Signature was made using unacceptable key. " + + "${signingKey.publicKeyAlgorithm} (${signingKey.bitStrength} bits) is " + + "not acceptable according to the public key algorithm policy.") + } + } + } + } + + @JvmStatic + fun signatureUsesAcceptableHashAlgorithm(policy: Policy): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + try { + val algorithmPolicy = getHashAlgorithmPolicyForSignature(signature, policy) + if (!algorithmPolicy.isAcceptable( + signature.signatureHashAlgorithm, signature.creationTime)) { + throw SignatureValidationException( + "Signature uses unacceptable" + + " hash algorithm ${signature.signatureHashAlgorithm}" + + " (Signature creation time: ${signature.creationTime.formatUTC()})") + } + } catch (e: NoSuchElementException) { + throw SignatureValidationException( + "Signature uses unknown hash" + " algorithm ${signature.hashAlgorithm}") + } + } + } + } + + /** + * Return the applicable [Policy.HashAlgorithmPolicy] for the given [PGPSignature]. + * Revocation signatures are being policed using a different policy than non-revocation + * signatures. + * + * @param signature signature + * @param policy revocation policy for revocation sigs, normal policy for non-rev sigs + * @return policy + */ + @JvmStatic + private fun getHashAlgorithmPolicyForSignature( + signature: PGPSignature, + policy: Policy + ): Policy.HashAlgorithmPolicy { + val type = SignatureType.requireFromCode(signature.signatureType) + return when (type) { + SignatureType.CERTIFICATION_REVOCATION, + SignatureType.KEY_REVOCATION, + SignatureType.SUBKEY_REVOCATION -> policy.revocationSignatureHashAlgorithmPolicy + else -> policy.signatureHashAlgorithmPolicy + } + } + + /** + * Verify that a signature does not carry critical unknown notations. + * + * @param registry notation registry of known notations + * @return validator + */ + @JvmStatic + fun signatureDoesNotHaveCriticalUnknownNotations( + registry: NotationRegistry + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + SignatureSubpacketsUtil.getHashedNotationData(signature) + .filter { it.isCritical && !registry.isKnownNotation(it.notationName) } + .forEach { + throw SignatureValidationException( + "Signature contains unknown critical notation '${it.notationName}' in its hashed area.") + } + } + } + } + + /** + * Verify that a signature does not contain critical unknown subpackets. + * + * @return validator + */ + @JvmStatic + fun signatureDoesNotHaveCriticalUnknownSubpackets(): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + signature.hashedSubPackets.criticalTags.forEach { + try { + SignatureSubpacket.requireFromCode(it) + } catch (e: NoSuchElementException) { + throw SignatureValidationException( + "Signature contains unknown critical subpacket of type 0x${Integer.toHexString(it)}") + } + } + } + } + } + + /** + * Verify that a signature is effective at the given reference date. + * + * @param referenceTime reference date for signature verification + * @return validator + */ + @JvmStatic + @JvmOverloads + fun signatureIsEffective(referenceTime: Date = Date()): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + signatureIsAlreadyEffective(referenceTime).verify(signature) + signatureIsNotYetExpired(referenceTime).verify(signature) + } + } + } + + /** + * Verify that a signature was created prior to the given reference date. + * + * @param referenceTime reference date for signature verification + * @return validator + */ + @JvmStatic + fun signatureIsAlreadyEffective(referenceTime: Date): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (signature.isHardRevocation) { + return + } + if (signature.creationTime > referenceTime) { + throw SignatureValidationException( + "Signature was created at ${signature.creationTime.formatUTC()} and" + + " is therefore not yet valid at ${referenceTime.formatUTC()}") + } + } + } + } + + /** + * Verify that a signature is not yet expired. + * + * @param referenceTime reference date for signature verification + * @return validator + */ + @JvmStatic + fun signatureIsNotYetExpired(referenceTime: Date): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (signature.isHardRevocation) { + return + } + val expirationDate = signature.signatureExpirationDate + if (expirationDate != null && expirationDate < referenceTime) { + throw SignatureValidationException( + "Signature is already expired " + + "(expiration: ${expirationDate.formatUTC()}," + + " validation: ${referenceTime.formatUTC()})") + } + } + } + } + + /** + * Verify that a signature is not malformed. A signature is malformed if it has no hashed + * creation time subpacket, it predates the creation time of the signing key, or it predates + * the creation date of the signing key binding signature. + * + * @param signingKey signing key + * @return validator + */ + @JvmStatic + fun signatureIsNotMalformed(signingKey: PGPPublicKey): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (signature.version >= 4) { + signatureHasHashedCreationTime().verify(signature) + } + signatureDoesNotPredateSigningKey(signingKey).verify(signature) + if (!signature.isOfType(SignatureType.PRIMARYKEY_BINDING)) { + signatureDoesNotPredateSigningKeyBindingDate(signingKey).verify(signature) + } + } + } + } + + @JvmStatic + fun signatureDoesNotPredateSignee(signee: PGPPublicKey): SignatureValidator { + return signatureDoesNotPredateKeyCreation(signee) + } + + /** + * Verify that a signature does not predate the creation time of the signing key. + * + * @param key signing key + * @return validator + */ + @JvmStatic + fun signatureDoesNotPredateSigningKey(signingKey: PGPPublicKey): SignatureValidator { + return signatureDoesNotPredateKeyCreation(signingKey) + } + + /** + * Verify that a signature does not predate the creation time of the given key. + * + * @param key key + * @return validator + */ + @JvmStatic + fun signatureDoesNotPredateKeyCreation(key: PGPPublicKey): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (key.creationTime > signature.creationTime) { + throw SignatureValidationException( + "Signature predates key" + + " (key creation: ${key.creationTime.formatUTC()}," + + " signature creation: ${signature.creationTime.formatUTC()})") + } + } + } + } + + /** + * Verify that a signature has a hashed creation time subpacket. + * + * @return validator + */ + @JvmStatic + fun signatureHasHashedCreationTime(): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (SignatureSubpacketsUtil.getSignatureCreationTime(signature) == null) { + throw SignatureValidationException( + "Malformed signature." + + "Signature has no signature creation time subpacket in its hashed area.") + } + } + } + } + + /** + * Verify that a signature does not predate the binding date of the signing key. + * + * @param signingKey signing key + * @return validator + */ + @JvmStatic + fun signatureDoesNotPredateSigningKeyBindingDate( + signingKey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (signingKey.isMasterKey) { + return + } + if (signingKey + .getSignaturesOfType(SignatureType.SUBKEY_BINDING.code) + .asSequence() + .map { + if (signature.creationTime < it.creationTime) { + throw SignatureValidationException( + "Signature was created " + + "before the signing key was bound to the certificate.") + } + } + .none()) { + throw SignatureValidationException( + "Signing subkey does not have a subkey binding signature.") + } + } + } + } + + /** + * Verify that a subkey binding signature is correct. + * + * @param primaryKey primary key + * @param subkey subkey + * @return validator + */ + @JvmStatic + fun correctSubkeyBindingSignature( + primaryKey: PGPPublicKey, + subkey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (primaryKey.keyID == subkey.keyID) { + throw SignatureValidationException("Primary key cannot be its own subkey.") + } + try { + signature.init( + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, + primaryKey) + if (!signature.verifyCertification(primaryKey, subkey)) { + throw SignatureValidationException("Signature is not correct.") + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Cannot verify subkey binding signature correctness", e) + } catch (e: ClassCastException) { + throw SignatureValidationException( + "Cannot verify subkey binding signature correctness", e) + } + } + } + } + + /** + * Verify that a primary key binding signature is correct. + * + * @param primaryKey primary key + * @param subkey subkey + * @return validator + */ + @JvmStatic + fun correctPrimaryKeyBindingSignature( + primaryKey: PGPPublicKey, + subkey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (primaryKey.keyID == subkey.keyID) { + throw SignatureValidationException("Primary key cannot be its own subkey.") + } + try { + signature.init( + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, + subkey) + if (!signature.verifyCertification(primaryKey, subkey)) { + throw SignatureValidationException( + "Primary Key Binding Signature is not correct.") + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Cannot verify primary key binding signature correctness", e) + } catch (e: ClassCastException) { + throw SignatureValidationException( + "Cannot verify primary key binding signature correctness", e) + } + } + } + } + + /** + * Verify that a direct-key signature is correct. + * + * @param signingKey signing key + * @param signedKey signed key + * @return validator + */ + @JvmStatic + fun correctSignatureOverKey( + signingKey: PGPPublicKey, + signedKey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + try { + signature.init( + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, + signingKey) + val valid = + if (signingKey.keyID == signedKey.keyID || + signature.isOfType(SignatureType.DIRECT_KEY)) { + signature.verifyCertification(signedKey) + } else { + signature.verifyCertification(signingKey, signedKey) + } + if (!valid) { + throw SignatureValidationException("Signature is not correct.") + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Cannot verify direct-key signature correctness", e) + } catch (e: ClassCastException) { + throw SignatureValidationException( + "Cannot verify direct-key signature correctness", e) + } + } + } + } + + @JvmStatic + fun signatureIsCertification(): SignatureValidator { + return signatureIsOfType( + SignatureType.POSITIVE_CERTIFICATION, + SignatureType.CASUAL_CERTIFICATION, + SignatureType.GENERIC_CERTIFICATION, + SignatureType.NO_CERTIFICATION) + } + + /** + * Verify that a signature type equals one of the given [SignatureType]. + * + * @param signatureType one or more signature types + * @return validator + */ + @JvmStatic + fun signatureIsOfType(vararg signatureType: SignatureType): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (signatureType.none { signature.isOfType(it) }) { + throw SignatureValidationException( + "Signature is of type" + + " ${SignatureType.requireFromCode(signature.signatureType)}, " + + "while only ${signatureType.contentToString()} are allowed here.") + } + } + } + } + + /** + * Verify that a signature over a user-id is correct. + * + * @param userId user-id + * @param certifiedKey key carrying the user-id + * @param certifyingKey key that created the signature. + * @return validator + */ + @JvmStatic + fun correctSignatureOverUserId( + userId: CharSequence, + certifiedKey: PGPPublicKey, + certifyingKey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + try { + signature.init( + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, + certifyingKey) + if (!signature.verifyCertification(userId.toString(), certifiedKey)) { + throw SignatureValidationException( + "Signature over user-id '$userId' is not valid.") + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Cannot verify signature over user-id '$userId'.", e) + } catch (e: ClassCastException) { + throw SignatureValidationException( + "Cannot verify signature over user-id '$userId'.", e) + } + } + } + } + + /** + * Verify that a signature over a user-attribute packet is correct. + * + * @param userAttributes user attributes + * @param certifiedKey key carrying the user-attributes + * @param certifyingKey key that created the certification signature + * @return validator + */ + @JvmStatic + fun correctSignatureOverUserAttributes( + userAttributes: PGPUserAttributeSubpacketVector, + certifiedKey: PGPPublicKey, + certifyingKey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + try { + signature.init( + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, + certifyingKey) + if (!signature.verifyCertification(userAttributes, certifiedKey)) { + throw SignatureValidationException( + "Signature over user-attributes is not correct.") + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Cannot verify signature over user-attribute vector.", e) + } catch (e: ClassCastException) { + throw SignatureValidationException( + "Cannot verify signature over user-attribute vector.", e) + } + } + } + } + + @JvmStatic + fun signatureWasCreatedInBounds(notBefore: Date?, notAfter: Date?): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + val timestamp = signature.creationTime + if (notBefore != null && timestamp < notBefore) { + throw SignatureValidationException( + "Signature was made before the earliest allowed signature creation time." + + " Created: ${timestamp.formatUTC()}," + + " earliest allowed: ${notBefore.formatUTC()}") + } + if (notAfter != null && timestamp > notAfter) { + throw SignatureValidationException( + "Signature was made before the latest allowed signature creation time." + + " Created: ${timestamp.formatUTC()}," + + " latest allowed: ${notAfter.formatUTC()}") + } + } + } + } + } +}