diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java deleted file mode 100644 index 663ef003..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java +++ /dev/null @@ -1,342 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import javax.annotation.Nonnull; - -import org.bouncycastle.bcpg.sig.IssuerKeyID; -import org.bouncycastle.bcpg.sig.KeyExpirationTime; -import org.bouncycastle.bcpg.sig.RevocationReason; -import org.bouncycastle.bcpg.sig.SignatureExpirationTime; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.util.encoders.Hex; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.util.RevocationAttributes; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.pgpainless.util.ArmorUtils; - -/** - * Utility methods related to signatures. - */ -public final class SignatureUtils { - - public static final int MAX_ITERATIONS = 10000; - - private SignatureUtils() { - - } - - /** - * Extract and return the key expiration date value from the given signature. - * If the signature does not carry a {@link KeyExpirationTime} subpacket, return null. - * - * @param keyCreationDate creation date of the key - * @param signature signature - * @return key expiration date as given by the signature - */ - public static Date getKeyExpirationDate(Date keyCreationDate, PGPSignature signature) { - KeyExpirationTime keyExpirationTime = SignatureSubpacketsUtil.getKeyExpirationTime(signature); - long expiresInSecs = keyExpirationTime == null ? 0 : keyExpirationTime.getTime(); - return datePlusSeconds(keyCreationDate, expiresInSecs); - } - - /** - * Return the expiration date of the signature. - * If the signature has no expiration date, {@link #datePlusSeconds(Date, long)} will return null. - * - * @param signature signature - * @return expiration date of the signature, or null if it does not expire. - */ - public static Date getSignatureExpirationDate(PGPSignature signature) { - Date creationDate = signature.getCreationTime(); - SignatureExpirationTime signatureExpirationTime = SignatureSubpacketsUtil.getSignatureExpirationTime(signature); - long expiresInSecs = signatureExpirationTime == null ? 0 : signatureExpirationTime.getTime(); - return datePlusSeconds(creationDate, expiresInSecs); - } - - /** - * Return a new date which represents the given date plus the given amount of seconds added. - * - * Since '0' is a special date value in the OpenPGP specification - * (e.g. '0' means no expiration for expiration dates), this method will return 'null' if seconds is 0. - * - * @param date date - * @param seconds number of seconds to be added - * @return date plus seconds or null if seconds is '0' - */ - public static Date datePlusSeconds(Date date, long seconds) { - if (seconds == 0) { - return null; - } - return new Date(date.getTime() + 1000 * seconds); - } - - /** - * Return true, if the expiration date of the {@link PGPSignature} lays in the past. - * If no expiration date is present in the signature, it is considered non-expired. - * - * @param signature signature - * @return true if expired, false otherwise - */ - public static boolean isSignatureExpired(PGPSignature signature) { - return isSignatureExpired(signature, new Date()); - } - - /** - * Return true, if the expiration date of the given {@link PGPSignature} is past the given comparison {@link Date}. - * If no expiration date is present in the signature, it is considered non-expiring. - * - * @param signature signature - * @param comparisonDate reference date - * @return true if sig is expired at reference date, false otherwise - */ - public static boolean isSignatureExpired(PGPSignature signature, Date comparisonDate) { - Date expirationDate = getSignatureExpirationDate(signature); - return expirationDate != null && comparisonDate.after(expirationDate); - } - - /** - * Return true if the provided signature is a hard revocation. - * Hard revocations are revocation signatures which either carry a revocation reason of - * {@link RevocationAttributes.Reason#KEY_COMPROMISED} or {@link RevocationAttributes.Reason#NO_REASON}, - * or no reason at all. - * - * @param signature signature - * @return true if signature is a hard revocation - */ - public static boolean isHardRevocation(PGPSignature signature) { - - SignatureType type = SignatureType.valueOf(signature.getSignatureType()); - if (type != SignatureType.KEY_REVOCATION && type != SignatureType.SUBKEY_REVOCATION && type != SignatureType.CERTIFICATION_REVOCATION) { - // Not a revocation - return false; - } - - RevocationReason reasonSubpacket = SignatureSubpacketsUtil.getRevocationReason(signature); - if (reasonSubpacket == null) { - // no reason -> hard revocation - return true; - } - return RevocationAttributes.Reason.isHardRevocation(reasonSubpacket.getRevocationReason()); - } - - /** - * Parse an ASCII encoded list of OpenPGP signatures into a {@link PGPSignatureList} - * and return it as a {@link List}. - * - * @param encodedSignatures ASCII armored signature list - * @return signature list - * - * @throws IOException if the signatures cannot be read - * @throws PGPException in case of a broken signature - */ - public static List readSignatures(String encodedSignatures) throws IOException, PGPException { - @SuppressWarnings("CharsetObjectCanBeUsed") - Charset utf8 = Charset.forName("UTF-8"); - byte[] bytes = encodedSignatures.getBytes(utf8); - return readSignatures(bytes); - } - - /** - * Read a single, or a list of {@link PGPSignature PGPSignatures} and return them as a {@link List}. - * - * @param encodedSignatures ASCII armored or binary signatures - * @return signatures - * @throws IOException if the signatures cannot be read - * @throws PGPException in case of an OpenPGP error - */ - public static List readSignatures(byte[] encodedSignatures) throws IOException, PGPException { - InputStream inputStream = new ByteArrayInputStream(encodedSignatures); - return readSignatures(inputStream); - } - - /** - * Read and return {@link PGPSignature PGPSignatures}. - * This method can deal with signatures that may be armored, compressed and may contain marker packets. - * - * @param inputStream input stream - * @return list of encountered signatures - * @throws IOException in case of a stream error - * @throws PGPException in case of an OpenPGP error - */ - public static List readSignatures(InputStream inputStream) throws IOException, PGPException { - return readSignatures(inputStream, MAX_ITERATIONS); - } - - /** - * Read and return {@link PGPSignature PGPSignatures}. - * This method can deal with signatures that may be binary, armored and may contain marker packets. - * - * @param inputStream input stream - * @param maxIterations number of loop iterations until reading is aborted - * @return list of encountered signatures - * @throws IOException in case of a stream error - */ - public static List readSignatures(InputStream inputStream, int maxIterations) throws IOException { - List signatures = new ArrayList<>(); - InputStream pgpIn = ArmorUtils.getDecoderStream(inputStream); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn); - - int i = 0; - Object nextObject; - while (i++ < maxIterations && (nextObject = objectFactory.nextObject()) != null) { - - // Since signatures are indistinguishable from randomness, there is no point in having them compressed, - // except for an attacker who is trying to exploit flaws in the decompression algorithm. - // Therefore, we ignore compressed data packets without attempting decompression. - if (nextObject instanceof PGPCompressedData) { - PGPCompressedData compressedData = (PGPCompressedData) nextObject; - // getInputStream() does not do decompression, contrary to getDataStream(). - Streams.drain(compressedData.getInputStream()); // Skip packet without decompressing - } - - if (nextObject instanceof PGPSignatureList) { - PGPSignatureList signatureList = (PGPSignatureList) nextObject; - for (PGPSignature s : signatureList) { - signatures.add(s); - } - } - - if (nextObject instanceof PGPSignature) { - signatures.add((PGPSignature) nextObject); - } - } - pgpIn.close(); - - return signatures; - } - - /** - * Determine the issuer key-id of a {@link PGPSignature}. - * This method first inspects the {@link IssuerKeyID} subpacket of the signature and returns the key-id if present. - * If not, it inspects the {@link org.bouncycastle.bcpg.sig.IssuerFingerprint} packet and retrieves the key-id from the fingerprint. - * - * Otherwise, it returns 0. - * @param signature signature - * @return signatures issuing key id - */ - public static long determineIssuerKeyId(PGPSignature signature) { - if (signature.getVersion() == 3) { - // V3 sigs do not contain subpackets - return signature.getKeyID(); - } - - IssuerKeyID issuerKeyId = SignatureSubpacketsUtil.getIssuerKeyId(signature); - OpenPgpFingerprint fingerprint = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature); - - if (issuerKeyId != null && issuerKeyId.getKeyID() != 0) { - return issuerKeyId.getKeyID(); - } - if (issuerKeyId == null && fingerprint != null) { - return fingerprint.getKeyId(); - } - return 0; - } - - /** - * Return the digest prefix of the signature as hex-encoded String. - * - * @param signature signature - * @return digest prefix - */ - public static String getSignatureDigestPrefix(PGPSignature signature) { - return Hex.toHexString(signature.getDigestPrefix()); - } - - public static boolean wasIssuedBy(byte[] fingerprint, PGPSignature signature) { - try { - OpenPgpFingerprint fp = OpenPgpFingerprint.parseFromBinary(fingerprint); - OpenPgpFingerprint issuerFp = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature); - if (issuerFp == null) { - return fp.getKeyId() == signature.getKeyID(); - } - return fp.equals(issuerFp); - } catch (IllegalArgumentException e) { - // Unknown fingerprint length - return false; - } - } - - /** - * Extract all signatures from the given
key
which were issued by
issuerKeyId
- * over
userId
. - * - * @param key public key - * @param userId user-id - * @param issuerKeyId issuer key-id - * @return (potentially empty) list of signatures - */ - public static @Nonnull List getSignaturesOverUserIdBy( - @Nonnull PGPPublicKey key, - @Nonnull String userId, - long issuerKeyId) { - List signaturesByKeyId = new ArrayList<>(); - Iterator userIdSignatures = key.getSignaturesForID(userId); - - // getSignaturesForID() is nullable for some reason -.- - if (userIdSignatures == null) { - return signaturesByKeyId; - } - - // filter for signatures by key-id - while (userIdSignatures.hasNext()) { - PGPSignature signature = userIdSignatures.next(); - if (signature.getKeyID() == issuerKeyId) { - signaturesByKeyId.add(signature); - } - } - - return Collections.unmodifiableList(signaturesByKeyId); - } - - public static @Nonnull List getDelegations(PGPPublicKeyRing key) { - List delegations = new ArrayList<>(); - PGPPublicKey primaryKey = key.getPublicKey(); - Iterator signatures = primaryKey.getKeySignatures(); - outerloop: while (signatures.hasNext()) { - PGPSignature signature = signatures.next(); - Iterator subkeys = key.getPublicKeys(); - while (subkeys.hasNext()) { - if (signature.getKeyID() == subkeys.next().getKeyID()) { - continue outerloop; - } - } - delegations.add(signature); - } - - return delegations; - } - - public static @Nonnull List get3rdPartyCertificationsFor(String userId, PGPPublicKeyRing key) { - PGPPublicKey primaryKey = key.getPublicKey(); - List certifications = new ArrayList<>(); - Iterator it = primaryKey.getSignaturesForID(userId); - while (it.hasNext()) { - PGPSignature sig = it.next(); - if (sig.getKeyID() != primaryKey.getKeyID()) { - certifications.add(sig); - } - } - return certifications; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt new file mode 100644 index 00000000..2f320bf0 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt @@ -0,0 +1,263 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature + +import org.bouncycastle.bcpg.sig.KeyExpirationTime +import org.bouncycastle.openpgp.* +import org.bouncycastle.util.encoders.Hex +import org.bouncycastle.util.io.Streams +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.util.RevocationAttributes.Reason +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil +import org.pgpainless.util.ArmorUtils +import java.io.InputStream +import java.util.* + +const val MAX_ITERATIONS = 10000 + +class SignatureUtils { + companion object { + + /** + * Extract and return the key expiration date value from the given signature. + * If the signature does not carry a {@link KeyExpirationTime} subpacket, return null. + * + * @param keyCreationDate creation date of the key + * @param signature signature + * @return key expiration date as given by the signature + */ + @JvmStatic + fun getKeyExpirationDate(keyCreationDate: Date, signature: PGPSignature): Date? { + val expirationPacket: KeyExpirationTime? = SignatureSubpacketsUtil.getKeyExpirationTime(signature) + val expiresInSeconds = expirationPacket?.time ?: 0 + return datePlusSeconds(keyCreationDate, expiresInSeconds) + } + + /** + * Return the expiration date of the signature. + * If the signature has no expiration date, {@link #datePlusSeconds(Date, long)} will return null. + * + * @param signature signature + * @return expiration date of the signature, or null if it does not expire. + */ + @JvmStatic + fun getSignatureExpirationDate(signature: PGPSignature): Date? { + val creationTime = signature.creationTime + val expirationTime = SignatureSubpacketsUtil.getSignatureExpirationTime(signature) + val expiresInSeconds = expirationTime?.time ?: 0 + return datePlusSeconds(creationTime, expiresInSeconds) + } + + /** + * Return a new date which represents the given date plus the given amount of seconds added. + * + * Since '0' is a special date value in the OpenPGP specification + * (e.g. '0' means no expiration for expiration dates), this method will return 'null' if seconds is 0. + * + * @param date date + * @param seconds number of seconds to be added + * @return date plus seconds or null if seconds is '0' + */ + @JvmStatic + fun datePlusSeconds(date: Date, seconds: Long): Date? { + if (seconds == 0L) { + return null + } + return Date(date.time + 1000 * seconds) + } + + /** + * Return true, if the expiration date of the {@link PGPSignature} lays in the past. + * If no expiration date is present in the signature, it is considered non-expired. + * + * @param signature signature + * @return true if expired, false otherwise + */ + @JvmStatic + fun isSignatureExpired(signature: PGPSignature): Boolean { + return isSignatureExpired(signature, Date()) + } + + /** + * Return true, if the expiration date of the given {@link PGPSignature} is past the given comparison {@link Date}. + * If no expiration date is present in the signature, it is considered non-expiring. + * + * @param signature signature + * @param referenceTime reference date + * @return true if sig is expired at reference date, false otherwise + */ + @JvmStatic + fun isSignatureExpired(signature: PGPSignature, referenceTime: Date): Boolean { + val expirationDate = getSignatureExpirationDate(signature) + return expirationDate != null && referenceTime >= expirationDate + } + + /** + * Return true if the provided signature is a hard revocation. + * Hard revocations are revocation signatures which either carry a revocation reason of + * {@link RevocationAttributes.Reason#KEY_COMPROMISED} or {@link RevocationAttributes.Reason#NO_REASON}, + * or no reason at all. + * + * @param signature signature + * @return true if signature is a hard revocation + */ + @JvmStatic + fun isHardRevocation(signature: PGPSignature): Boolean { + val type = SignatureType.requireFromCode(signature.signatureType) + if (type != SignatureType.KEY_REVOCATION && type != SignatureType.SUBKEY_REVOCATION && type != SignatureType.CERTIFICATION_REVOCATION) { + // Not a revocation + return false + } + + val reason = SignatureSubpacketsUtil.getRevocationReason(signature) ?: return true // no reason -> hard revocation + return Reason.isHardRevocation(reason.revocationReason) + } + + @JvmStatic + fun readSignatures(encodedSignatures: String): List { + return readSignatures(encodedSignatures.toByteArray()) + } + + @JvmStatic + fun readSignatures(encodedSignatures: ByteArray): List { + return readSignatures(encodedSignatures.inputStream()) + } + + @JvmStatic + fun readSignatures(inputStream: InputStream): List { + return readSignatures(inputStream, MAX_ITERATIONS) + } + + /** + * Read and return {@link PGPSignature PGPSignatures}. + * This method can deal with signatures that may be binary, armored and may contain marker packets. + * + * @param inputStream input stream + * @param maxIterations number of loop iterations until reading is aborted + * @return list of encountered signatures + */ + @JvmStatic + fun readSignatures(inputStream: InputStream, maxIterations: Int): List { + val signatures = mutableListOf() + val pgpIn = ArmorUtils.getDecoderStream(inputStream) + val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn) + + var i = 0 + var nextObject: Any? = null + while (i++ < maxIterations && objectFactory.nextObject().also { nextObject = it } != null) { + // Since signatures are indistinguishable from randomness, there is no point in having them compressed, + // except for an attacker who is trying to exploit flaws in the decompression algorithm. + // Therefore, we ignore compressed data packets without attempting decompression. + if (nextObject is PGPCompressedData) { + // getInputStream() does not do decompression, contrary to getDataStream(). + Streams.drain((nextObject as PGPCompressedData).inputStream) // Skip packet without decompressing + } + + if (nextObject is PGPSignatureList) { + signatures.addAll(nextObject as PGPSignatureList) + } + + if (nextObject is PGPSignature) { + signatures.add(nextObject as PGPSignature) + } + } + + pgpIn.close() + return signatures.toList() + } + + /** + * Determine the issuer key-id of a {@link PGPSignature}. + * This method first inspects the {@link IssuerKeyID} subpacket of the signature and returns the key-id if present. + * If not, it inspects the {@link org.bouncycastle.bcpg.sig.IssuerFingerprint} packet and retrieves the key-id from the fingerprint. + * + * Otherwise, it returns 0. + * @param signature signature + * @return signatures issuing key id + */ + @JvmStatic + fun determineIssuerKeyId(signature: PGPSignature): Long { + if (signature.version == 3) { + // V3 sigs do not contain subpackets + return signature.keyID + } + + val issuerKeyId = SignatureSubpacketsUtil.getIssuerKeyId(signature) + val issuerFingerprint = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature) + + if (issuerKeyId != null && issuerKeyId.keyID != 0L) { + return issuerKeyId.keyID + } + if (issuerKeyId == null && issuerFingerprint != null) { + return issuerFingerprint.keyId + } + return 0 + } + + /** + * Return the digest prefix of the signature as hex-encoded String. + * + * @param signature signature + * @return digest prefix + */ + @JvmStatic + fun getSignatureDigestPrefix(signature: PGPSignature): String { + return Hex.toHexString(signature.digestPrefix) + } + + @JvmStatic + fun wasIssuedBy(fingerprint: ByteArray, signature: PGPSignature): Boolean { + return try { + val pgpFingerprint = OpenPgpFingerprint.parseFromBinary(fingerprint) + wasIssuedBy(pgpFingerprint, signature) + } catch (e : IllegalArgumentException) { + // Unknown fingerprint length + false + } + } + + @JvmStatic + fun wasIssuedBy(fingerprint: OpenPgpFingerprint, signature: PGPSignature): Boolean { + val issuerFp = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature) + ?: return fingerprint.keyId == signature.keyID + return fingerprint == issuerFp + } + + /** + * Extract all signatures from the given
key
which were issued by
issuerKeyId
+ * over
userId
. + * + * @param key public key + * @param userId user-id + * @param issuer issuer key-id + * @return (potentially empty) list of signatures + */ + @JvmStatic + fun getSignaturesOverUserIdBy(key: PGPPublicKey, userId: String, issuer: Long): List { + return key.getSignaturesForID(userId) + ?.asSequence() + ?.filter { it.keyID == issuer } + ?.toList() ?: listOf() + } + + @JvmStatic + fun getDelegations(key: PGPPublicKeyRing): List { + return key.publicKey.keySignatures + .asSequence() + .filter { key.getPublicKey(it.keyID) == null } // Filter out back-sigs from subkeys + .toList() + } + + @JvmStatic + fun get3rdPartyCertificationsFor(key: PGPPublicKeyRing, userId: String): List { + return key.publicKey.getSignaturesForID(userId) + .asSequence() + .filter { it.keyID != key.publicKey.keyID } // Filter out self-sigs + .toList() + } + } +} \ No newline at end of file