diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java deleted file mode 100644 index f22e057a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java +++ /dev/null @@ -1,299 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer; - -import static org.pgpainless.signature.consumer.SignatureVerifier.verifyOnePassSignature; - -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.bcpg.sig.KeyFlags; -import org.bouncycastle.bcpg.sig.SignerUserID; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A collection of static methods that validate signing certificates (public keys) and verify signature correctness. - */ -public final class CertificateValidator { - - private CertificateValidator() { - - } - - private static final Logger LOGGER = LoggerFactory.getLogger(CertificateValidator.class); - - /** - * 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 validateCertificate(PGPSignature signature, PGPPublicKeyRing signingKeyRing, Policy policy) - throws SignatureValidationException { - - Map rejections = new ConcurrentHashMap<>(); - long keyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKey signingSubkey = signingKeyRing.getPublicKey(keyId); - if (signingSubkey == null) { - throw new SignatureValidationException("Provided key ring does not contain a subkey with id " + Long.toHexString(keyId)); - } - - PGPPublicKey primaryKey = signingKeyRing.getPublicKey(); - - // Key-Revocation Signatures - List directKeySignatures = new ArrayList<>(); - Iterator primaryKeyRevocationIterator = primaryKey.getSignaturesOfType(SignatureType.KEY_REVOCATION.getCode()); - while (primaryKeyRevocationIterator.hasNext()) { - PGPSignature revocation = primaryKeyRevocationIterator.next(); - if (revocation.getKeyID() != primaryKey.getKeyID()) { - // Revocation was not made by primary key, skip - continue; - // TODO: What about external revocation keys? - } - try { - if (SignatureVerifier.verifyKeyRevocationSignature(revocation, primaryKey, policy, signature.getCreationTime())) { - directKeySignatures.add(revocation); - } - } catch (SignatureValidationException e) { - rejections.put(revocation, e); - LOGGER.debug("Rejecting key revocation signature: {}", e.getMessage(), e); - } - } - - // Direct-Key Signatures - Iterator keySignatures = primaryKey.getSignaturesOfType(SignatureType.DIRECT_KEY.getCode()); - while (keySignatures.hasNext()) { - PGPSignature keySignature = keySignatures.next(); - if (keySignature.getKeyID() != primaryKey.getKeyID()) { - // Signature was not made by primary key, skip - continue; - } - try { - if (SignatureVerifier.verifyDirectKeySignature(keySignature, primaryKey, policy, signature.getCreationTime())) { - directKeySignatures.add(keySignature); - } - } catch (SignatureValidationException e) { - rejections.put(keySignature, e); - LOGGER.debug("Rejecting key signature: {}", e.getMessage(), e); - } - } - - Collections.sort(directKeySignatures, new SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); - 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) - List userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(primaryKey); - Map> userIdSignatures = new ConcurrentHashMap<>(); - for (String userId : userIds) { - List signaturesOnUserId = new ArrayList<>(); - Iterator userIdSigs = primaryKey.getSignaturesForID(userId); - while (userIdSigs.hasNext()) { - PGPSignature userIdSig = userIdSigs.next(); - if (userIdSig.getKeyID() != primaryKey.getKeyID()) { - // Sig was made by external key, skip - continue; - } - try { - if (SignatureVerifier.verifySignatureOverUserId(userId, userIdSig, primaryKey, policy, signature.getCreationTime())) { - signaturesOnUserId.add(userIdSig); - } - } catch (SignatureValidationException e) { - rejections.put(userIdSig, e); - LOGGER.debug("Rejecting user-id signature: {}", e.getMessage(), e); - } - } - Collections.sort(signaturesOnUserId, new SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); - userIdSignatures.put(userId, signaturesOnUserId); - } - - boolean anyUserIdValid = false; - boolean hasAnyUserIds = !userIdSignatures.keySet().isEmpty(); - 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.debug("User-ID '{}' is revoked.", userId); - } else { - anyUserIdValid = true; - } - } - } - - if (hasAnyUserIds && !anyUserIdValid) { - throw new SignatureValidationException("No valid user-id found.", rejections); - } - - // Specific signer user-id - SignerUserID signerUserID = SignatureSubpacketsUtil.getSignerUserID(signature); - if (signerUserID != null && policy.getSignerUserIdValidationLevel() == Policy.SignerUserIdValidationLevel.STRICT) { - List signerUserIdSigs = userIdSignatures.get(signerUserID.getID()); - if (signerUserIdSigs == null || signerUserIdSigs.isEmpty()) { - throw new SignatureValidationException("Signature was allegedly made by user-id '" + signerUserID.getID() + - "' but we have no valid signatures for that on the certificate."); - } - - PGPSignature userIdSig = signerUserIdSigs.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()) { - PGPSignature directKeySignature = directKeySignatures.get(0); - KeyFlags keyFlags = SignatureSubpacketsUtil.getKeyFlags(directKeySignature); - if (keyFlags != null && KeyFlag.hasKeyFlag(keyFlags.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()) { - PGPSignature revocation = bindingRevocations.next(); - if (revocation.getKeyID() != primaryKey.getKeyID()) { - // Subkey Revocation was not made by primary key, skip - continue; - } - try { - if (SignatureVerifier.verifySubkeyBindingRevocation(revocation, primaryKey, signingSubkey, policy, signature.getCreationTime())) { - subkeySigs.add(revocation); - } - } catch (SignatureValidationException e) { - rejections.put(revocation, e); - LOGGER.debug("Rejecting subkey revocation signature: {}", e.getMessage(), e); - } - } - - Iterator bindingSigs = signingSubkey.getSignaturesOfType(SignatureType.SUBKEY_BINDING.getCode()); - while (bindingSigs.hasNext()) { - PGPSignature bindingSig = bindingSigs.next(); - try { - if (SignatureVerifier.verifySubkeyBindingSignature(bindingSig, primaryKey, signingSubkey, policy, signature.getCreationTime())) { - subkeySigs.add(bindingSig); - } - } catch (SignatureValidationException e) { - rejections.put(bindingSig, e); - LOGGER.debug("Rejecting subkey binding signature: {}", e.getMessage(), e); - } - } - - Collections.sort(subkeySigs, new SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); - if (subkeySigs.isEmpty()) { - throw new SignatureValidationException("Subkey is not bound.", rejections); - } - - PGPSignature currentSig = subkeySigs.get(0); - if (currentSig.getSignatureType() == SignatureType.SUBKEY_REVOCATION.getCode()) { - throw new SignatureValidationException("Subkey is revoked."); - } - - KeyFlags keyFlags = SignatureSubpacketsUtil.getKeyFlags(currentSig); - if (keyFlags == null) { - if (directKeySignatures.isEmpty()) { - throw new SignatureValidationException("Signature was made by key which is not capable of signing (no keyflags on binding sig, no direct-key sig)."); - } - PGPSignature directKeySig = directKeySignatures.get(0); - KeyFlags directKeyFlags = SignatureSubpacketsUtil.getKeyFlags(directKeySig); - if (directKeyFlags == null || !KeyFlag.hasKeyFlag(directKeyFlags.getFlags(), KeyFlag.SIGN_DATA)) { - throw new SignatureValidationException("Signature was made by key which is not capable of signing (no keyflags on binding sig, no SIGN flag on direct-key sig)."); - } - } else if (!KeyFlag.hasKeyFlag(keyFlags.getFlags(), KeyFlag.SIGN_DATA)) { - throw new SignatureValidationException("Signature was made by key which is not capable of signing (no SIGN flag on binding sig)."); - } - } - return true; - } - - /** - * 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 validateCertificateAndVerifyUninitializedSignature(PGPSignature signature, - InputStream signedData, - PGPPublicKeyRing signingKeyRing, - Policy policy, - Date validationDate) - throws SignatureValidationException { - validateCertificate(signature, signingKeyRing, policy); - long keyId = SignatureUtils.determineIssuerKeyId(signature); - return SignatureVerifier.verifyUninitializedSignature(signature, signedData, signingKeyRing.getPublicKey(keyId), policy, validationDate); - } - - /** - * 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 validateCertificateAndVerifyInitializedSignature(PGPSignature signature, PGPPublicKeyRing verificationKeys, Policy policy) - throws SignatureValidationException { - validateCertificate(signature, verificationKeys, policy); - long keyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKey signingKey = verificationKeys.getPublicKey(keyId); - SignatureVerifier.verifyInitializedSignature(signature, signingKey, policy, signature.getCreationTime()); - return true; - } - - /** - * Validate the signing key certificate and the given {@link OnePassSignatureCheck}. - * - * @param onePassSignature corresponding one-pass-signature - * @param policy policy - * @return true if the certificate is valid and the signature is correct, false otherwise. - * @throws SignatureValidationException in case of a validation error - */ - public static boolean validateCertificateAndVerifyOnePassSignature(OnePassSignatureCheck onePassSignature, Policy policy) - throws SignatureValidationException { - PGPSignature signature = onePassSignature.getSignature(); - validateCertificate(signature, onePassSignature.getVerificationKeys(), policy); - PGPPublicKey signingKey = onePassSignature.getVerificationKeys().getPublicKey(signature.getKeyID()); - verifyOnePassSignature(signature, signingKey, onePassSignature, policy); - return true; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt new file mode 100644 index 00000000..126803d4 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt @@ -0,0 +1,249 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.consumer + +import openpgp.openPgpKeyId +import org.bouncycastle.extensions.issuerKeyId +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.exception.SignatureValidationException +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.policy.Policy +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil +import org.slf4j.LoggerFactory +import java.io.InputStream +import java.util.* + +/** + * A collection of static methods that validate signing certificates (public keys) and verify signature correctness. + */ +class CertificateValidator { + + companion object { + + @JvmStatic + private val LOGGER = LoggerFactory.getLogger(CertificateValidator::class.java) + + /** + * 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 + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun validateCertificate(signature: PGPSignature, + signingKeyRing: PGPPublicKeyRing, + policy: Policy = PGPainless.getPolicy()): Boolean { + val signingSubkey: PGPPublicKey = signingKeyRing.getPublicKey(signature.issuerKeyId) + ?: throw SignatureValidationException("Provided key ring does not contain a subkey with id ${signature.issuerKeyId.openPgpKeyId()}.") + val primaryKey = signingKeyRing.publicKey!! + val directKeyAndRevSigs = mutableListOf() + val rejections = mutableMapOf() + // revocations + primaryKey.getSignaturesOfType(SignatureType.KEY_REVOCATION.code).asSequence() + .filter { it.issuerKeyId == primaryKey.keyID } // We do not support external rev keys + .forEach { + try { + if (SignatureVerifier.verifyKeyRevocationSignature(it, primaryKey, policy, signature.creationTime)) { + directKeyAndRevSigs.add(it) + } + } catch (e: SignatureValidationException) { + rejections[it] = e + LOGGER.debug("Rejecting key revocation signature: ${e.message}", e) + } + } + + // direct-key sigs + primaryKey.getSignaturesOfType(SignatureType.DIRECT_KEY.code).asSequence() + .filter { it.issuerKeyId == primaryKey.keyID } + .forEach { + try { + if (SignatureVerifier.verifyDirectKeySignature(it, primaryKey, policy, signature.creationTime)) { + directKeyAndRevSigs.add(it) + } + } catch (e: SignatureValidationException) { + rejections[it] = e + LOGGER.debug("Rejecting key signature: ${e.message}, e") + } + } + + directKeyAndRevSigs.sortWith(SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)) + if (directKeyAndRevSigs.isNotEmpty()) { + if (directKeyAndRevSigs[0].signatureType == SignatureType.KEY_REVOCATION.code) { + throw SignatureValidationException("Primary key has been revoked.") + } + } + + // UserID signatures + val userIdSignatures = mutableMapOf>() + KeyRingUtils.getUserIdsIgnoringInvalidUTF8(primaryKey).forEach { userId -> + buildList { + primaryKey.getSignaturesForID(userId) + .asSequence() + .filter { it.issuerKeyId == primaryKey.keyID } + .forEach { uidSig -> + try { + if (SignatureVerifier.verifySignatureOverUserId(userId, uidSig, primaryKey, policy, signature.creationTime)) { + add(uidSig) + } + } catch (e: SignatureValidationException) { + rejections[uidSig] = e + LOGGER.debug("Rejecting user-id signature: ${e.message}", e) + } + } + }.sortedWith(SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)) + .let { userIdSignatures[userId] = it } + } + + val hasAnyUserIds = userIdSignatures.isNotEmpty() + val isAnyUserIdValid = userIdSignatures.any { entry -> + entry.value.isNotEmpty() && entry.value[0].signatureType != SignatureType.CERTIFICATION_REVOCATION.code + } + + if (hasAnyUserIds && !isAnyUserIdValid) { + throw SignatureValidationException("No valid user-id found.", rejections) + } + + // Specific signer user-id + if (policy.signerUserIdValidationLevel == Policy.SignerUserIdValidationLevel.STRICT) { + SignatureSubpacketsUtil.getSignerUserID(signature)?.let { + if (userIdSignatures[it.id] == null || userIdSignatures[it.id]!!.isEmpty()) { + throw SignatureValidationException("Signature was allegedly made by user-id '${it.id}'," + + " but we have no valid signatures for that on the certificate.") + } + + if (userIdSignatures[it.id]!![0].signatureType == SignatureType.CERTIFICATION_REVOCATION.code) { + throw SignatureValidationException("Signature was made with user-id '${it.id}' which is revoked.") + } + } + } + + if (signingSubkey.keyID == primaryKey.keyID) { // signing key is primary key + if (directKeyAndRevSigs.isNotEmpty()) { + val directKeySig = directKeyAndRevSigs[0]!! + val flags = SignatureSubpacketsUtil.getKeyFlags(directKeySig) + if (flags != null && KeyFlag.hasKeyFlag(flags.flags, KeyFlag.SIGN_DATA)) { + return true + } + } + } else { // signing key is subkey + val subkeySigs = mutableListOf() + signingSubkey.getSignaturesOfType(SignatureType.SUBKEY_REVOCATION.code).asSequence() + .filter { it.issuerKeyId == primaryKey.keyID } + .forEach { + try { + if (SignatureVerifier.verifySubkeyBindingRevocation(it, primaryKey, signingSubkey, policy, signature.creationTime)) { + subkeySigs.add(it) + } + } catch (e : SignatureValidationException) { + rejections[it] = e + LOGGER.debug("REjecting subkey revocation signature: ${e.message}", e) + } + } + + signingSubkey.getSignaturesOfType(SignatureType.SUBKEY_BINDING.code).asSequence() + .forEach { + try { + if (SignatureVerifier.verifySubkeyBindingSignature(it, primaryKey, signingSubkey, policy, signature.creationTime)) { + subkeySigs.add(it) + } + } catch (e : SignatureValidationException) { + rejections[it] = e + LOGGER.debug("Rejecting subkey binding signature: ${e.message}", e) + } + } + + subkeySigs.sortWith(SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)) + if (subkeySigs.isEmpty()) { + throw SignatureValidationException("Subkey is not bound.", rejections) + } + + if (subkeySigs[0].signatureType == SignatureType.SUBKEY_REVOCATION.code) { + throw SignatureValidationException("Subkey is revoked.") + } + + val keyFlags = SignatureSubpacketsUtil.getKeyFlags(subkeySigs[0]) + if (keyFlags == null || !KeyFlag.hasKeyFlag(keyFlags.flags, KeyFlag.SIGN_DATA)) { + throw SignatureValidationException("Signature was made by key which is not capable of signing (no keyflag).") + } + } + return true + } + + /** + * 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 + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun validateCertificateAndVerifyUninitializedSignature(signature: PGPSignature, + signedData: InputStream, + signingKeyRing: PGPPublicKeyRing, + policy: Policy, + referenceTime: Date = signature.creationTime): Boolean { + return validateCertificate(signature, signingKeyRing, policy) + && SignatureVerifier.verifyUninitializedSignature(signature, signedData, signingKeyRing.getPublicKey(signature.issuerKeyId)!!, policy, referenceTime) + } + + /** + * 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 + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun validateCertificateAndVerifyInitializedSignature(signature: PGPSignature, + verificationKeys: PGPPublicKeyRing, + policy: Policy): Boolean { + return validateCertificate(signature, verificationKeys, policy) && + SignatureVerifier.verifyInitializedSignature(signature, verificationKeys.getPublicKey(signature.issuerKeyId), policy, signature.creationTime) + } + + /** + * Validate the signing key certificate and the given [OnePassSignatureCheck]. + * + * @param onePassSignature corresponding one-pass-signature + * @param policy policy + * @return true if the certificate is valid and the signature is correct, false otherwise. + * @throws SignatureValidationException in case of a validation error + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun validateCertificateAndVerifyOnePassSignature(onePassSignature: OnePassSignatureCheck, + policy: Policy): Boolean { + return validateCertificate(onePassSignature.signature!!, onePassSignature.verificationKeys, policy) && + SignatureVerifier.verifyOnePassSignature(onePassSignature.signature!!, + onePassSignature.verificationKeys.getPublicKey(onePassSignature.signature!!.issuerKeyId), + onePassSignature, policy) + } + } +} \ No newline at end of file