1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-09-09 18:29:39 +02:00

Improve API for signatures in results

This commit is contained in:
Paul Schaub 2025-04-07 16:03:01 +02:00
parent 049f7422c0
commit 335cf8d162
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
9 changed files with 91 additions and 50 deletions

View file

@ -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));

View file

@ -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

View file

@ -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())

View file

@ -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}"

View file

@ -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<SubkeyIdentifier, PGPSignature>,
val detachedDocumentSignatures: OpenPGPSignatureSet<OpenPGPDocumentSignature>,
val recipients: Set<SubkeyIdentifier>,
val fileName: String,
val modificationDate: Date,
val fileEncoding: StreamEncoding
) {
@Deprecated(
"Use detachedSignatures instead", replaceWith = ReplaceWith("detachedDocumentSignatures"))
// TODO: Remove in 2.1
val detachedSignatures: MultiMap<SubkeyIdentifier, PGPSignature>
get() {
val map = MultiMap<SubkeyIdentifier, PGPSignature>()
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<SubkeyIdentifier, PGPSignature> = MultiMap()
val detachedSignatures: MutableList<OpenPGPDocumentSignature> = mutableListOf()
val recipients: Set<SubkeyIdentifier> = 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,

View file

@ -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)

View file

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<S : OpenPGPSignature>(val signatures: List<S>) : Iterable<S> {
fun getSignaturesBy(cert: OpenPGPCertificate): List<S> =
signatures.filter { sig -> sig.signature.keyIdentifiers.any { cert.getKey(it) != null } }
fun getSignaturesBy(componentKey: OpenPGPCertificate.OpenPGPComponentKey): List<S> =
signatures.filter { sig ->
sig.signature.keyIdentifiers.any { componentKey.keyIdentifier.matches(it) }
}
override fun iterator(): Iterator<S> {
return signatures.iterator()
}
}

View file

@ -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.
*
* <p>
* 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!!!
);

View file

@ -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()
}
}