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}