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}