Kotlin conversion: CertificateValidator

This commit is contained in:
Paul Schaub 2023-10-13 13:38:45 +02:00
parent 853e3de472
commit efae652a66
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
2 changed files with 249 additions and 299 deletions

View File

@ -1,299 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<PGPSignature, Exception> 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<PGPSignature> directKeySignatures = new ArrayList<>();
Iterator<PGPSignature> 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<PGPSignature> 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<String> userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(primaryKey);
Map<String, List<PGPSignature>> userIdSignatures = new ConcurrentHashMap<>();
for (String userId : userIds) {
List<PGPSignature> signaturesOnUserId = new ArrayList<>();
Iterator<PGPSignature> 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<PGPSignature> 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<PGPSignature> subkeySigs = new ArrayList<>();
Iterator<PGPSignature> 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<PGPSignature> 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;
}
}

View File

@ -0,0 +1,249 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<PGPSignature>()
val rejections = mutableMapOf<PGPSignature, Exception>()
// 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<String, List<PGPSignature>>()
KeyRingUtils.getUserIdsIgnoringInvalidUTF8(primaryKey).forEach { userId ->
buildList<PGPSignature> {
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<PGPSignature>()
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)
}
}
}