001// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
002//
003// SPDX-License-Identifier: Apache-2.0
004
005package org.pgpainless.signature;
006
007import java.io.ByteArrayInputStream;
008import java.io.IOException;
009import java.io.InputStream;
010import java.nio.charset.Charset;
011import java.util.ArrayList;
012import java.util.Date;
013import java.util.List;
014import java.util.Set;
015
016import org.bouncycastle.bcpg.sig.IssuerKeyID;
017import org.bouncycastle.bcpg.sig.KeyExpirationTime;
018import org.bouncycastle.bcpg.sig.RevocationReason;
019import org.bouncycastle.bcpg.sig.SignatureExpirationTime;
020import org.bouncycastle.openpgp.PGPCompressedData;
021import org.bouncycastle.openpgp.PGPException;
022import org.bouncycastle.openpgp.PGPObjectFactory;
023import org.bouncycastle.openpgp.PGPPublicKey;
024import org.bouncycastle.openpgp.PGPSecretKey;
025import org.bouncycastle.openpgp.PGPSignature;
026import org.bouncycastle.openpgp.PGPSignatureGenerator;
027import org.bouncycastle.openpgp.PGPSignatureList;
028import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
029import org.bouncycastle.util.encoders.Hex;
030import org.pgpainless.PGPainless;
031import org.pgpainless.algorithm.HashAlgorithm;
032import org.pgpainless.algorithm.SignatureType;
033import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator;
034import org.pgpainless.implementation.ImplementationFactory;
035import org.pgpainless.key.OpenPgpFingerprint;
036import org.pgpainless.key.util.OpenPgpKeyAttributeUtil;
037import org.pgpainless.key.util.RevocationAttributes;
038import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil;
039import org.pgpainless.util.ArmorUtils;
040
041/**
042 * Utility methods related to signatures.
043 */
044public final class SignatureUtils {
045
046    public static final int MAX_ITERATIONS = 10000;
047
048    private SignatureUtils() {
049
050    }
051
052    /**
053     * Return a signature generator for the provided signing key.
054     * The signature generator will follow the hash algorithm preferences of the signing key and pick the best algorithm.
055     *
056     * @param singingKey signing key
057     * @return signature generator
058     */
059    public static PGPSignatureGenerator getSignatureGeneratorFor(PGPSecretKey singingKey) {
060        return getSignatureGeneratorFor(singingKey.getPublicKey());
061    }
062
063    /**
064     * Return a signature generator for the provided signing key.
065     * The signature generator will follow the hash algorithm preferences of the signing key and pick the best algorithm.
066     *
067     * @param signingPubKey signing key
068     * @return signature generator
069     */
070    public static PGPSignatureGenerator getSignatureGeneratorFor(PGPPublicKey signingPubKey) {
071        PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
072                getPgpContentSignerBuilderForKey(signingPubKey));
073        return signatureGenerator;
074    }
075
076    /**
077     * Return a content signer builder for the passed public key.
078     *
079     * The content signer will use a hash algorithm derived from the keys' algorithm preferences.
080     * If no preferences can be derived, the key will fall back to the default hash algorithm as set in
081     * the {@link org.pgpainless.policy.Policy}.
082     *
083     * @param publicKey public key
084     * @return content signer builder
085     */
086    public static PGPContentSignerBuilder getPgpContentSignerBuilderForKey(PGPPublicKey publicKey) {
087        Set<HashAlgorithm> hashAlgorithmSet = OpenPgpKeyAttributeUtil.getOrGuessPreferredHashAlgorithms(publicKey);
088
089        HashAlgorithm hashAlgorithm = HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(PGPainless.getPolicy())
090                .negotiateHashAlgorithm(hashAlgorithmSet);
091
092        return ImplementationFactory.getInstance().getPGPContentSignerBuilder(publicKey.getAlgorithm(), hashAlgorithm.getAlgorithmId());
093    }
094
095    /**
096     * Extract and return the key expiration date value from the given signature.
097     * If the signature does not carry a {@link KeyExpirationTime} subpacket, return null.
098     *
099     * @param keyCreationDate creation date of the key
100     * @param signature signature
101     * @return key expiration date as given by the signature
102     */
103    public static Date getKeyExpirationDate(Date keyCreationDate, PGPSignature signature) {
104        KeyExpirationTime keyExpirationTime = SignatureSubpacketsUtil.getKeyExpirationTime(signature);
105        long expiresInSecs = keyExpirationTime == null ? 0 : keyExpirationTime.getTime();
106        return datePlusSeconds(keyCreationDate, expiresInSecs);
107    }
108
109    /**
110     * Return the expiration date of the signature.
111     * If the signature has no expiration date, {@link #datePlusSeconds(Date, long)} will return null.
112     *
113     * @param signature signature
114     * @return expiration date of the signature, or null if it does not expire.
115     */
116    public static Date getSignatureExpirationDate(PGPSignature signature) {
117        Date creationDate = signature.getCreationTime();
118        SignatureExpirationTime signatureExpirationTime = SignatureSubpacketsUtil.getSignatureExpirationTime(signature);
119        long expiresInSecs = signatureExpirationTime == null ? 0 : signatureExpirationTime.getTime();
120        return datePlusSeconds(creationDate, expiresInSecs);
121    }
122
123    /**
124     * Return a new date which represents the given date plus the given amount of seconds added.
125     *
126     * Since '0' is a special date value in the OpenPGP specification
127     * (e.g. '0' means no expiration for expiration dates), this method will return 'null' if seconds is 0.
128     *
129     * @param date date
130     * @param seconds number of seconds to be added
131     * @return date plus seconds or null if seconds is '0'
132     */
133    public static Date datePlusSeconds(Date date, long seconds) {
134        if (seconds == 0) {
135            return null;
136        }
137        return new Date(date.getTime() + 1000 * seconds);
138    }
139
140    /**
141     * Return true, if the expiration date of the {@link PGPSignature} lays in the past.
142     * If no expiration date is present in the signature, it is considered non-expired.
143     *
144     * @param signature signature
145     * @return true if expired, false otherwise
146     */
147    public static boolean isSignatureExpired(PGPSignature signature) {
148        return isSignatureExpired(signature, new Date());
149    }
150
151    /**
152     * Return true, if the expiration date of the given {@link PGPSignature} is past the given comparison {@link Date}.
153     * If no expiration date is present in the signature, it is considered non-expiring.
154     *
155     * @param signature signature
156     * @param comparisonDate reference date
157     * @return true if sig is expired at reference date, false otherwise
158     */
159    public static boolean isSignatureExpired(PGPSignature signature, Date comparisonDate) {
160        Date expirationDate = getSignatureExpirationDate(signature);
161        return expirationDate != null && comparisonDate.after(expirationDate);
162    }
163
164    /**
165     * Return true if the provided signature is a hard revocation.
166     * Hard revocations are revocation signatures which either carry a revocation reason of
167     * {@link RevocationAttributes.Reason#KEY_COMPROMISED} or {@link RevocationAttributes.Reason#NO_REASON},
168     * or no reason at all.
169     *
170     * @param signature signature
171     * @return true if signature is a hard revocation
172     */
173    public static boolean isHardRevocation(PGPSignature signature) {
174
175        SignatureType type = SignatureType.valueOf(signature.getSignatureType());
176        if (type != SignatureType.KEY_REVOCATION && type != SignatureType.SUBKEY_REVOCATION && type != SignatureType.CERTIFICATION_REVOCATION) {
177            // Not a revocation
178            return false;
179        }
180
181        RevocationReason reasonSubpacket = SignatureSubpacketsUtil.getRevocationReason(signature);
182        if (reasonSubpacket == null) {
183            // no reason -> hard revocation
184            return true;
185        }
186        return RevocationAttributes.Reason.isHardRevocation(reasonSubpacket.getRevocationReason());
187    }
188
189    /**
190     * Parse an ASCII encoded list of OpenPGP signatures into a {@link PGPSignatureList}
191     * and return it as a {@link List}.
192     *
193     * @param encodedSignatures ASCII armored signature list
194     * @return signature list
195     * @throws IOException if the signatures cannot be read
196     */
197    public static List<PGPSignature> readSignatures(String encodedSignatures) throws IOException, PGPException {
198        @SuppressWarnings("CharsetObjectCanBeUsed")
199        Charset utf8 = Charset.forName("UTF-8");
200        byte[] bytes = encodedSignatures.getBytes(utf8);
201        return readSignatures(bytes);
202    }
203
204    /**
205     * Read a single, or a list of {@link PGPSignature PGPSignatures} and return them as a {@link List}.
206     *
207     * @param encodedSignatures ASCII armored or binary signatures
208     * @return signatures
209     * @throws IOException if the signatures cannot be read
210     * @throws PGPException in case of an OpenPGP error
211     */
212    public static List<PGPSignature> readSignatures(byte[] encodedSignatures) throws IOException, PGPException {
213        InputStream inputStream = new ByteArrayInputStream(encodedSignatures);
214        return readSignatures(inputStream);
215    }
216
217    /**
218     * Read and return {@link PGPSignature PGPSignatures}.
219     * This method can deal with signatures that may be armored, compressed and may contain marker packets.
220     *
221     * @param inputStream input stream
222     * @return list of encountered signatures
223     * @throws IOException in case of a stream error
224     * @throws PGPException in case of an OpenPGP error
225     */
226    public static List<PGPSignature> readSignatures(InputStream inputStream) throws IOException, PGPException {
227        return readSignatures(inputStream, MAX_ITERATIONS);
228    }
229
230    /**
231     * Read and return {@link PGPSignature PGPSignatures}.
232     * This method can deal with signatures that may be armored, compressed and may contain marker packets.
233     *
234     * @param inputStream input stream
235     * @param maxIterations number of loop iterations until reading is aborted
236     * @return list of encountered signatures
237     * @throws IOException in case of a stream error
238     * @throws PGPException in case of an OpenPGP error
239     */
240    public static List<PGPSignature> readSignatures(InputStream inputStream, int maxIterations) throws IOException, PGPException {
241        List<PGPSignature> signatures = new ArrayList<>();
242        InputStream pgpIn = ArmorUtils.getDecoderStream(inputStream);
243        PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn);
244
245        int i = 0;
246        Object nextObject;
247        while (i++ < maxIterations && (nextObject = objectFactory.nextObject()) != null) {
248            if (nextObject instanceof PGPCompressedData) {
249                PGPCompressedData compressedData = (PGPCompressedData) nextObject;
250                objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(compressedData.getDataStream());
251            }
252
253            if (nextObject instanceof PGPSignatureList) {
254                PGPSignatureList signatureList = (PGPSignatureList) nextObject;
255                for (PGPSignature s : signatureList) {
256                    signatures.add(s);
257                }
258            }
259
260            if (nextObject instanceof PGPSignature) {
261                signatures.add((PGPSignature) nextObject);
262            }
263        }
264        pgpIn.close();
265
266        return signatures;
267    }
268
269    /**
270     * Determine the issuer key-id of a {@link PGPSignature}.
271     * This method first inspects the {@link IssuerKeyID} subpacket of the signature and returns the key-id if present.
272     * If not, it inspects the {@link org.bouncycastle.bcpg.sig.IssuerFingerprint} packet and retrieves the key-id from the fingerprint.
273     *
274     * Otherwise, it returns 0.
275     * @param signature signature
276     * @return signatures issuing key id
277     */
278    public static long determineIssuerKeyId(PGPSignature signature) {
279        if (signature.getVersion() == 3) {
280            // V3 sigs do not contain subpackets
281            return signature.getKeyID();
282        }
283
284        IssuerKeyID issuerKeyId = SignatureSubpacketsUtil.getIssuerKeyId(signature);
285        OpenPgpFingerprint fingerprint = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature);
286
287        if (issuerKeyId != null && issuerKeyId.getKeyID() != 0) {
288            return issuerKeyId.getKeyID();
289        }
290        if (issuerKeyId == null && fingerprint != null) {
291            return fingerprint.getKeyId();
292        }
293        return 0;
294    }
295
296    /**
297     * Return the digest prefix of the signature as hex-encoded String.
298     *
299     * @param signature signature
300     * @return digest prefix
301     */
302    public static String getSignatureDigestPrefix(PGPSignature signature) {
303        return Hex.toHexString(signature.getDigestPrefix());
304    }
305
306    public static List<PGPSignature> toList(PGPSignatureList signatures) {
307        List<PGPSignature> list = new ArrayList<>();
308        for (PGPSignature signature : signatures) {
309            list.add(signature);
310        }
311        return list;
312    }
313}