From d1861e51cd06695eaed96b93355ac591a1435ae4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Apr 2025 16:03:01 +0200 Subject: [PATCH] Improve API for signatures in results --- ...oundTripInlineSignInlineVerifyCmdTest.java | 9 ++--- .../main/kotlin/org/pgpainless/PGPainless.kt | 7 ++-- .../OpenPgpMessageInputStream.kt | 33 +++++++++---------- .../SignatureVerification.kt | 13 +++++--- .../encryption_signing/EncryptionResult.kt | 26 +++++++++++---- .../encryption_signing/EncryptionStream.kt | 5 +-- .../encryption_signing/OpenPGPSignatureSet.kt | 23 +++++++++++++ .../java/org/pgpainless/example/Sign.java | 19 +++++------ .../org/pgpainless/sop/DetachedSignImpl.kt | 6 ++-- 9 files changed, 91 insertions(+), 50 deletions(-) create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/OpenPGPSignatureSet.kt diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java index 057cec98..50eeadab 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -15,8 +15,8 @@ import java.nio.charset.StandardCharsets; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; @@ -350,12 +350,13 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { @Test public void createMalformedMessage() throws IOException, PGPException { + PGPainless api = PGPainless.getInstance(); String msg = "Hello, World!\n"; - PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(KEY_2); + OpenPGPKey key = api.readKey().parseKey(KEY_2); ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(ciphertext) - .withOptions(ProducerOptions.sign(SigningOptions.get() + .withOptions(ProducerOptions.sign(SigningOptions.get(api) .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key) ).overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) .setAsciiArmor(false)); diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt index d4892d8a..d8bd8022 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt @@ -152,7 +152,9 @@ class PGPainless( */ @JvmStatic @JvmOverloads - @Deprecated("Call buildKey() on an instance of PGPainless instead.") + @Deprecated( + "Call buildKey() on an instance of PGPainless instead.", + replaceWith = ReplaceWith("buildKey(version)")) fun buildKeyRing(version: OpenPGPKeyVersion = OpenPGPKeyVersion.v4): KeyRingBuilder = getInstance().buildKey(version) @@ -186,7 +188,8 @@ class PGPainless( * @throws PGPException in case of an error */ @JvmStatic - @Deprecated("Use mergeCertificate() instead.") + @Deprecated( + "Use mergeCertificate() instead.", replaceWith = ReplaceWith("mergeCertificate()")) fun mergeCertificate( originalCopy: PGPPublicKeyRing, updatedCopy: PGPPublicKeyRing diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index 68da867f..01456849 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -779,34 +779,37 @@ class OpenPgpMessageInputStream( fun addDetachedSignature(signature: PGPSignature) { val check = initializeSignature(signature) val keyId = signature.issuerKeyId - if (check != null) { + if (check.issuer != null) { detachedSignatures.add(check) } else { LOGGER.debug( "No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") detachedSignaturesWithMissingCert.add( SignatureVerification.Failure( - signature, null, SignatureValidationException("Missing verification key."))) + check, SignatureValidationException("Missing verification key."))) } } fun addPrependedSignature(signature: PGPSignature) { val check = initializeSignature(signature) val keyId = signature.issuerKeyId - if (check != null) { + if (check.issuer != null) { prependedSignatures.add(check) } else { LOGGER.debug( "No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") prependedSignaturesWithMissingCert.add( SignatureVerification.Failure( - signature, null, SignatureValidationException("Missing verification key"))) + check, SignatureValidationException("Missing verification key"))) } } - fun initializeSignature(signature: PGPSignature): OpenPGPDocumentSignature? { - val certificate = findCertificate(signature) ?: return null - val publicKey = certificate.getSigningKeyFor(signature) ?: return null + fun initializeSignature(signature: PGPSignature): OpenPGPDocumentSignature { + val certificate = + findCertificate(signature) ?: return OpenPGPDocumentSignature(signature, null) + val publicKey = + certificate.getSigningKeyFor(signature) + ?: return OpenPGPDocumentSignature(signature, null) initialize(signature, publicKey.pgpPublicKey) return OpenPGPDocumentSignature(signature, publicKey) } @@ -845,12 +848,7 @@ class OpenPgpMessageInputStream( val documentSignature = OpenPGPDocumentSignature( signature, check.verificationKeys.getSigningKeyFor(signature)) - val verification = - SignatureVerification( - signature, - SubkeyIdentifier( - check.verificationKeys.pgpPublicKeyRing, - check.onePassSignature.keyIdentifier)) + val verification = SignatureVerification(documentSignature) try { signature.assertCreatedInBounds( @@ -879,7 +877,8 @@ class OpenPgpMessageInputStream( "No suitable certificate for verification of signature by key ${signature.issuerKeyId.openPgpKeyId()} found.") inbandSignaturesWithMissingCert.add( SignatureVerification.Failure( - signature, null, SignatureValidationException("Missing verification key."))) + OpenPGPDocumentSignature(signature, null), + SignatureValidationException("Missing verification key."))) } } @@ -967,8 +966,7 @@ class OpenPgpMessageInputStream( fun finish(layer: Layer) { for (detached in detachedSignatures) { - val verification = - SignatureVerification(detached.signature, SubkeyIdentifier(detached.issuer)) + val verification = SignatureVerification(detached) try { detached.signature.assertCreatedInBounds( options.getVerifyNotBefore(), options.getVerifyNotAfter()) @@ -988,8 +986,7 @@ class OpenPgpMessageInputStream( } for (prepended in prependedSignatures) { - val verification = - SignatureVerification(prepended.signature, SubkeyIdentifier(prepended.issuer)) + val verification = SignatureVerification(prepended) try { prepended.signature.assertCreatedInBounds( options.getVerifyNotBefore(), options.getVerifyNotAfter()) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt index 3e00fbb2..c32cdc71 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt @@ -5,6 +5,7 @@ package org.pgpainless.decryption_verification import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature import org.pgpainless.decryption_verification.SignatureVerification.Failure import org.pgpainless.exception.SignatureValidationException import org.pgpainless.key.SubkeyIdentifier @@ -20,7 +21,10 @@ import org.pgpainless.signature.SignatureUtils * @param signingKey [SubkeyIdentifier] of the (sub-) key that is used for signature verification. * Note, that this might be null, e.g. in case of a [Failure] due to missing verification key. */ -data class SignatureVerification(val signature: PGPSignature, val signingKey: SubkeyIdentifier) { +data class SignatureVerification(val documentSignature: OpenPGPDocumentSignature) { + + val signature: PGPSignature = documentSignature.signature + val signingKey: SubkeyIdentifier = SubkeyIdentifier(documentSignature.issuer) override fun toString(): String { return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)};" + @@ -36,15 +40,16 @@ data class SignatureVerification(val signature: PGPSignature, val signingKey: Su * @param validationException exception that caused the verification to fail */ data class Failure( - val signature: PGPSignature, - val signingKey: SubkeyIdentifier?, + val documentSignature: OpenPGPDocumentSignature, val validationException: SignatureValidationException ) { + val signature: PGPSignature = documentSignature.signature + val signingKey: SubkeyIdentifier? = documentSignature.issuer?.let { SubkeyIdentifier(it) } constructor( verification: SignatureVerification, validationException: SignatureValidationException - ) : this(verification.signature, verification.signingKey, validationException) + ) : this(verification.documentSignature, validationException) override fun toString(): String { return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)}; Key: ${signingKey?.toString() ?: "null"}; Failure: ${validationException.message}" diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt index a969d8c3..626c1de4 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt @@ -9,6 +9,7 @@ import org.bouncycastle.openpgp.PGPLiteralData import org.bouncycastle.openpgp.PGPPublicKeyRing import org.bouncycastle.openpgp.PGPSignature import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.StreamEncoding import org.pgpainless.algorithm.SymmetricKeyAlgorithm @@ -19,13 +20,25 @@ import org.pgpainless.util.MultiMap data class EncryptionResult( val encryptionAlgorithm: SymmetricKeyAlgorithm, val compressionAlgorithm: CompressionAlgorithm, - val detachedSignatures: MultiMap, + val detachedDocumentSignatures: OpenPGPSignatureSet, val recipients: Set, val fileName: String, val modificationDate: Date, val fileEncoding: StreamEncoding ) { + @Deprecated( + "Use detachedSignatures instead", replaceWith = ReplaceWith("detachedDocumentSignatures")) + // TODO: Remove in 2.1 + val detachedSignatures: MultiMap + get() { + val map = MultiMap() + detachedDocumentSignatures.signatures + .map { SubkeyIdentifier(it.issuer) to it.signature } + .forEach { map.put(it.first, it.second) } + return map + } + /** * Return true, if the message is marked as for-your-eyes-only. This is typically done by * setting the filename "_CONSOLE". @@ -59,7 +72,7 @@ data class EncryptionResult( var _encryptionAlgorithm: SymmetricKeyAlgorithm? = null var _compressionAlgorithm: CompressionAlgorithm? = null - val detachedSignatures: MultiMap = MultiMap() + val detachedSignatures: MutableList = mutableListOf() val recipients: Set = mutableSetOf() private var _fileName = "" private var _modificationDate = Date(0) @@ -85,10 +98,9 @@ data class EncryptionResult( (recipients as MutableSet).add(recipient) } - fun addDetachedSignature( - signingSubkeyIdentifier: SubkeyIdentifier, - detachedSignature: PGPSignature - ) = apply { detachedSignatures.put(signingSubkeyIdentifier, detachedSignature) } + fun addDetachedSignature(signature: OpenPGPDocumentSignature): Builder = apply { + detachedSignatures.add(signature) + } fun build(): EncryptionResult { checkNotNull(_encryptionAlgorithm) { "Encryption algorithm not set." } @@ -97,7 +109,7 @@ data class EncryptionResult( return EncryptionResult( _encryptionAlgorithm!!, _compressionAlgorithm!!, - detachedSignatures, + OpenPGPSignatureSet(detachedSignatures), recipients, _fileName, _modificationDate, diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt index 67a83093..4c913a62 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt @@ -13,11 +13,11 @@ import org.bouncycastle.openpgp.PGPCompressedDataGenerator import org.bouncycastle.openpgp.PGPEncryptedDataGenerator import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPLiteralDataGenerator +import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature import org.pgpainless.PGPainless import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.StreamEncoding import org.pgpainless.algorithm.SymmetricKeyAlgorithm -import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.util.ArmoredOutputStreamFactory // 1 << 8 causes wrong partial body length encoding @@ -246,8 +246,9 @@ class EncryptionStream( options.signingOptions.signingMethods.entries.reversed().forEach { (key, method) -> method.signatureGenerator.generate().let { sig -> + val documentSignature = OpenPGPDocumentSignature(sig, key.publicKey) if (method.isDetached) { - resultBuilder.addDetachedSignature(SubkeyIdentifier(key), sig) + resultBuilder.addDetachedSignature(documentSignature) } if (!method.isDetached || options.isCleartextSigned) { sig.encode(signatureLayerStream) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/OpenPGPSignatureSet.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/OpenPGPSignatureSet.kt new file mode 100644 index 00000000..8770b7e3 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/OpenPGPSignatureSet.kt @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPSignature + +class OpenPGPSignatureSet(val signatures: List) : Iterable { + + fun getSignaturesBy(cert: OpenPGPCertificate): List = + signatures.filter { sig -> sig.signature.keyIdentifiers.any { cert.getKey(it) != null } } + + fun getSignaturesBy(componentKey: OpenPGPCertificate.OpenPGPComponentKey): List = + signatures.filter { sig -> + sig.signature.keyIdentifiers.any { componentKey.keyIdentifier.matches(it) } + } + + override fun iterator(): Iterator { + return signatures.iterator() + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/Sign.java b/pgpainless-core/src/test/java/org/pgpainless/example/Sign.java index 8d1320f5..fbef5b40 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/Sign.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/Sign.java @@ -14,9 +14,9 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.api.OpenPGPCertificate; import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.bouncycastle.openpgp.api.OpenPGPSignature; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -26,7 +26,6 @@ import org.pgpainless.encryption_signing.EncryptionResult; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; -import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.ArmorUtils; @@ -70,7 +69,7 @@ public class Sign { /** * Demonstration of how to create a detached signature for a message. * A detached signature can be distributed alongside the message/file itself. - * + *

* The message/file doesn't need to be altered for detached signature creation. */ @Test @@ -82,9 +81,9 @@ public class Sign { // After signing, you want to distribute the original value of 'message' along with the 'detachedSignature' // from below. ByteArrayOutputStream ignoreMe = new ByteArrayOutputStream(); - EncryptionStream signingStream = PGPainless.encryptAndOrSign() + EncryptionStream signingStream = api.generateMessage() .onOutputStream(ignoreMe) - .withOptions(ProducerOptions.sign(SigningOptions.get() + .withOptions(ProducerOptions.sign(SigningOptions.get(api) .addDetachedSignature(protector, key, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)) .setAsciiArmor(false) ); @@ -94,9 +93,9 @@ public class Sign { EncryptionResult result = signingStream.getResult(); - OpenPGPCertificate.OpenPGPComponentKey signingKey = PGPainless.inspectKeyRing(key).getSigningSubkeys().get(0); - PGPSignature signature = result.getDetachedSignatures().get(new SubkeyIdentifier(signingKey)).iterator().next(); - String detachedSignature = ArmorUtils.toAsciiArmoredString(signature.getEncoded()); + OpenPGPCertificate.OpenPGPComponentKey signingKey = api.inspect(key).getSigningSubkeys().get(0); + OpenPGPSignature.OpenPGPDocumentSignature signature = result.getDetachedDocumentSignatures().getSignaturesBy(signingKey).get(0); + String detachedSignature = ArmorUtils.toAsciiArmoredString(signature.getSignature().getEncoded()); assertTrue(detachedSignature.startsWith("-----BEGIN PGP SIGNATURE-----")); @@ -126,9 +125,9 @@ public class Sign { "limitations under the License."; InputStream messageIn = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream signedOut = new ByteArrayOutputStream(); - EncryptionStream signingStream = PGPainless.encryptAndOrSign() + EncryptionStream signingStream = api.generateMessage() .onOutputStream(signedOut) - .withOptions(ProducerOptions.sign(SigningOptions.get() + .withOptions(ProducerOptions.sign(SigningOptions.get(api) .addDetachedSignature(protector, key, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)) // Human-readable text document .setCleartextSigned() // <- Explicitly use Cleartext Signature Framework!!! ); diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt index 54c61aae..e23ca1c3 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt @@ -73,16 +73,16 @@ class DetachedSignImpl(private val api: PGPainless) : DetachedSign { // forget passphrases protector.clear() - val signatures = result.detachedSignatures.map { it.value }.flatten() + val signatures = result.detachedDocumentSignatures val out = if (armor) ArmoredOutputStreamFactory.get(outputStream) else outputStream - signatures.forEach { it.encode(out) } + signatures.forEach { it.signature.encode(out) } out.close() outputStream.close() return SigningResult.builder() - .setMicAlg(micAlgFromSignatures(signatures)) + .setMicAlg(micAlgFromSignatures(signatures.map { it.signature })) .build() } }