001// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org> 002// 003// SPDX-License-Identifier: Apache-2.0 004 005package org.pgpainless.signature.consumer; 006 007import static org.pgpainless.signature.consumer.SignatureVerifier.verifyOnePassSignature; 008 009import java.io.InputStream; 010import java.util.ArrayList; 011import java.util.Collections; 012import java.util.Date; 013import java.util.Iterator; 014import java.util.List; 015import java.util.Map; 016import java.util.concurrent.ConcurrentHashMap; 017 018import org.bouncycastle.bcpg.sig.KeyFlags; 019import org.bouncycastle.bcpg.sig.SignerUserID; 020import org.bouncycastle.openpgp.PGPPublicKey; 021import org.bouncycastle.openpgp.PGPPublicKeyRing; 022import org.bouncycastle.openpgp.PGPSignature; 023import org.pgpainless.algorithm.KeyFlag; 024import org.pgpainless.algorithm.SignatureType; 025import org.pgpainless.exception.SignatureValidationException; 026import org.pgpainless.policy.Policy; 027import org.pgpainless.signature.SignatureUtils; 028import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; 029import org.slf4j.Logger; 030import org.slf4j.LoggerFactory; 031 032/** 033 * A collection of static methods that validate signing certificates (public keys) and verify signature correctness. 034 */ 035public final class CertificateValidator { 036 037 private CertificateValidator() { 038 039 } 040 041 private static final Logger LOGGER = LoggerFactory.getLogger(CertificateValidator.class); 042 043 /** 044 * Check if the signing key was eligible to create the provided signature. 045 * 046 * That entails: 047 * - Check, if the primary key is being revoked via key-revocation signatures. 048 * - Check, if the keys user-ids are revoked or not bound. 049 * - Check, if the signing subkey is revoked or expired. 050 * - Check, if the signing key is not capable of signing 051 * 052 * @param signature signature 053 * @param signingKeyRing signing key ring 054 * @param policy validation policy 055 * @return true if the signing key was eligible to create the signature 056 * @throws SignatureValidationException in case of a validation constraint violation 057 */ 058 public static boolean validateCertificate(PGPSignature signature, PGPPublicKeyRing signingKeyRing, Policy policy) 059 throws SignatureValidationException { 060 061 Map<PGPSignature, Exception> rejections = new ConcurrentHashMap<>(); 062 long keyId = SignatureUtils.determineIssuerKeyId(signature); 063 PGPPublicKey signingSubkey = signingKeyRing.getPublicKey(keyId); 064 if (signingSubkey == null) { 065 throw new SignatureValidationException("Provided key ring does not contain a subkey with id " + Long.toHexString(keyId)); 066 } 067 068 PGPPublicKey primaryKey = signingKeyRing.getPublicKey(); 069 070 // Key-Revocation Signatures 071 List<PGPSignature> directKeySignatures = new ArrayList<>(); 072 Iterator<PGPSignature> primaryKeyRevocationIterator = primaryKey.getSignaturesOfType(SignatureType.KEY_REVOCATION.getCode()); 073 while (primaryKeyRevocationIterator.hasNext()) { 074 PGPSignature revocation = primaryKeyRevocationIterator.next(); 075 try { 076 if (SignatureVerifier.verifyKeyRevocationSignature(revocation, primaryKey, policy, signature.getCreationTime())) { 077 directKeySignatures.add(revocation); 078 } 079 } catch (SignatureValidationException e) { 080 rejections.put(revocation, e); 081 LOGGER.debug("Rejecting key revocation signature: {}", e.getMessage(), e); 082 } 083 } 084 085 // Direct-Key Signatures 086 Iterator<PGPSignature> keySignatures = primaryKey.getSignaturesOfType(SignatureType.DIRECT_KEY.getCode()); 087 while (keySignatures.hasNext()) { 088 PGPSignature keySignature = keySignatures.next(); 089 try { 090 if (SignatureVerifier.verifyDirectKeySignature(keySignature, primaryKey, policy, signature.getCreationTime())) { 091 directKeySignatures.add(keySignature); 092 } 093 } catch (SignatureValidationException e) { 094 rejections.put(keySignature, e); 095 LOGGER.debug("Rejecting key signature: {}", e.getMessage(), e); 096 } 097 } 098 099 Collections.sort(directKeySignatures, new SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); 100 if (!directKeySignatures.isEmpty()) { 101 if (directKeySignatures.get(0).getSignatureType() == SignatureType.KEY_REVOCATION.getCode()) { 102 throw new SignatureValidationException("Primary key has been revoked."); 103 } 104 } 105 106 // User-ID signatures (certifications, revocations) 107 Iterator<String> userIds = primaryKey.getUserIDs(); 108 Map<String, List<PGPSignature>> userIdSignatures = new ConcurrentHashMap<>(); 109 while (userIds.hasNext()) { 110 List<PGPSignature> signaturesOnUserId = new ArrayList<>(); 111 String userId = userIds.next(); 112 Iterator<PGPSignature> userIdSigs = primaryKey.getSignaturesForID(userId); 113 while (userIdSigs.hasNext()) { 114 PGPSignature userIdSig = userIdSigs.next(); 115 try { 116 if (SignatureVerifier.verifySignatureOverUserId(userId, userIdSig, primaryKey, policy, signature.getCreationTime())) { 117 signaturesOnUserId.add(userIdSig); 118 } 119 } catch (SignatureValidationException e) { 120 rejections.put(userIdSig, e); 121 LOGGER.debug("Rejecting user-id signature: {}", e.getMessage(), e); 122 } 123 } 124 Collections.sort(signaturesOnUserId, new SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); 125 userIdSignatures.put(userId, signaturesOnUserId); 126 } 127 128 boolean anyUserIdValid = false; 129 for (String userId : userIdSignatures.keySet()) { 130 if (!userIdSignatures.get(userId).isEmpty()) { 131 PGPSignature current = userIdSignatures.get(userId).get(0); 132 if (current.getSignatureType() == SignatureType.CERTIFICATION_REVOCATION.getCode()) { 133 LOGGER.debug("User-ID '{}' is revoked.", userId); 134 } else { 135 anyUserIdValid = true; 136 } 137 } 138 } 139 140 if (!anyUserIdValid) { 141 throw new SignatureValidationException("No valid user-id found.", rejections); 142 } 143 144 // Specific signer user-id 145 SignerUserID signerUserID = SignatureSubpacketsUtil.getSignerUserID(signature); 146 if (signerUserID != null) { 147 PGPSignature userIdSig = userIdSignatures.get(signerUserID.getID()).get(0); 148 if (userIdSig.getSignatureType() == SignatureType.CERTIFICATION_REVOCATION.getCode()) { 149 throw new SignatureValidationException("Signature was made with user-id '" + signerUserID.getID() + "' which is revoked."); 150 } 151 } 152 153 if (signingSubkey == primaryKey) { 154 if (!directKeySignatures.isEmpty()) { 155 if (KeyFlag.hasKeyFlag(SignatureSubpacketsUtil.getKeyFlags(directKeySignatures.get(0)).getFlags(), KeyFlag.SIGN_DATA)) { 156 return true; 157 } 158 } 159 } // Subkey Binding Signatures / Subkey Revocation Signatures 160 else { 161 List<PGPSignature> subkeySigs = new ArrayList<>(); 162 Iterator<PGPSignature> bindingRevocations = signingSubkey.getSignaturesOfType(SignatureType.SUBKEY_REVOCATION.getCode()); 163 while (bindingRevocations.hasNext()) { 164 PGPSignature revocation = bindingRevocations.next(); 165 try { 166 if (SignatureVerifier.verifySubkeyBindingRevocation(revocation, primaryKey, signingSubkey, policy, signature.getCreationTime())) { 167 subkeySigs.add(revocation); 168 } 169 } catch (SignatureValidationException e) { 170 rejections.put(revocation, e); 171 LOGGER.debug("Rejecting subkey revocation signature: {}", e.getMessage(), e); 172 } 173 } 174 175 Iterator<PGPSignature> bindingSigs = signingSubkey.getSignaturesOfType(SignatureType.SUBKEY_BINDING.getCode()); 176 while (bindingSigs.hasNext()) { 177 PGPSignature bindingSig = bindingSigs.next(); 178 try { 179 if (SignatureVerifier.verifySubkeyBindingSignature(bindingSig, primaryKey, signingSubkey, policy, signature.getCreationTime())) { 180 subkeySigs.add(bindingSig); 181 } 182 } catch (SignatureValidationException e) { 183 rejections.put(bindingSig, e); 184 LOGGER.debug("Rejecting subkey binding signature: {}", e.getMessage(), e); 185 } 186 } 187 188 Collections.sort(subkeySigs, new SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); 189 if (subkeySigs.isEmpty()) { 190 throw new SignatureValidationException("Subkey is not bound.", rejections); 191 } 192 193 PGPSignature currentSig = subkeySigs.get(0); 194 if (currentSig.getSignatureType() == SignatureType.SUBKEY_REVOCATION.getCode()) { 195 throw new SignatureValidationException("Subkey is revoked."); 196 } 197 198 KeyFlags keyFlags = SignatureSubpacketsUtil.getKeyFlags(currentSig); 199 if (keyFlags == null) { 200 if (directKeySignatures.isEmpty()) { 201 throw new SignatureValidationException("Signature was made by key which is not capable of signing (no keyflags on binding sig, no direct-key sig)."); 202 } 203 PGPSignature directKeySig = directKeySignatures.get(0); 204 KeyFlags directKeyFlags = SignatureSubpacketsUtil.getKeyFlags(directKeySig); 205 if (!KeyFlag.hasKeyFlag(directKeyFlags.getFlags(), KeyFlag.SIGN_DATA)) { 206 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)."); 207 } 208 } else if (!KeyFlag.hasKeyFlag(keyFlags.getFlags(), KeyFlag.SIGN_DATA)) { 209 throw new SignatureValidationException("Signature was made by key which is not capable of signing (no SIGN flag on binding sig)."); 210 } 211 } 212 return true; 213 } 214 215 /** 216 * Validate the given signing key and then verify the given signature while parsing out the signed data. 217 * Uninitialized means that no signed data has been read and the hash generators state has not yet been updated. 218 * 219 * @param signature uninitialized signature 220 * @param signedData input stream containing signed data 221 * @param signingKeyRing key ring containing signing key 222 * @param policy validation policy 223 * @param validationDate date of validation 224 * @return true if the signature is valid, false otherwise 225 * @throws SignatureValidationException for validation constraint violations 226 */ 227 public static boolean validateCertificateAndVerifyUninitializedSignature(PGPSignature signature, 228 InputStream signedData, 229 PGPPublicKeyRing signingKeyRing, 230 Policy policy, 231 Date validationDate) 232 throws SignatureValidationException { 233 validateCertificate(signature, signingKeyRing, policy); 234 long keyId = SignatureUtils.determineIssuerKeyId(signature); 235 return SignatureVerifier.verifyUninitializedSignature(signature, signedData, signingKeyRing.getPublicKey(keyId), policy, validationDate); 236 } 237 238 /** 239 * Validate the signing key and the given initialized signature. 240 * Initialized means that the signatures hash generator has already been updated by reading the signed data completely. 241 * 242 * @param signature initialized signature 243 * @param verificationKeys key ring containing the verification key 244 * @param policy validation policy 245 * @return true if the signature is valid, false otherwise 246 * @throws SignatureValidationException in case of a validation constraint violation 247 */ 248 public static boolean validateCertificateAndVerifyInitializedSignature(PGPSignature signature, PGPPublicKeyRing verificationKeys, Policy policy) 249 throws SignatureValidationException { 250 validateCertificate(signature, verificationKeys, policy); 251 long keyId = SignatureUtils.determineIssuerKeyId(signature); 252 PGPPublicKey signingKey = verificationKeys.getPublicKey(keyId); 253 SignatureVerifier.verifyInitializedSignature(signature, signingKey, policy, signature.getCreationTime()); 254 return true; 255 } 256 257 /** 258 * Validate the signing key certificate and the given {@link OnePassSignatureCheck}. 259 * 260 * @param onePassSignature corresponding one-pass-signature 261 * @param policy policy 262 * @return true if the certificate is valid and the signature is correct, false otherwise. 263 * @throws SignatureValidationException in case of a validation error 264 */ 265 public static boolean validateCertificateAndVerifyOnePassSignature(OnePassSignatureCheck onePassSignature, Policy policy) 266 throws SignatureValidationException { 267 PGPSignature signature = onePassSignature.getSignature(); 268 validateCertificate(signature, onePassSignature.getVerificationKeys(), policy); 269 PGPPublicKey signingKey = onePassSignature.getVerificationKeys().getPublicKey(signature.getKeyID()); 270 verifyOnePassSignature(signature, signingKey, onePassSignature, policy); 271 return true; 272 } 273}