// SPDX-FileCopyrightText: 2023 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 package org.pgpainless.signature.consumer import java.io.InputStream import java.util.* import openpgp.openPgpKeyId 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.bouncycastle.extensions.issuerKeyId 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 /** * 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 } } // Reject sigs by non-signing keys if (userIdSignatures.none { (_, sigs) -> sigs.any { SignatureSubpacketsUtil.getKeyFlags(it)?.let { f -> KeyFlag.hasKeyFlag(f.flags, KeyFlag.SIGN_DATA) } == true } }) { throw SignatureValidationException( "Signature was generated by non-signing key.") } } 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) } } }