diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java index bf7f904c..589e8d92 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java @@ -242,6 +242,69 @@ public final class SigningOptions { return this; } + /** + * Create a binary inline signature using the signing key with the given keyId. + * + * @param secretKeyDecryptor decryptor to unlock the secret key + * @param secretKey secret key ring + * @param keyId keyId of the signing (sub-)key + * @return builder + * @throws PGPException if the secret key cannot be unlocked or if no signing method can be created. + * @throws KeyException.UnacceptableSigningKeyException if the key ring does not carry any signing-capable subkeys + * @throws KeyException.MissingSecretKeyException if the key ring does not contain the identified secret key + */ + @Nonnull + public SigningOptions addInlineSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing secretKey, + long keyId) throws PGPException { + return addInlineSignature(secretKeyDecryptor, secretKey, keyId, DocumentSignatureType.BINARY_DOCUMENT, null); + } + + + /** + * Create an inline signature using the signing key with the given keyId. + * + * @param secretKeyDecryptor decryptor to unlock the secret key + * @param secretKey secret key ring + * @param keyId keyId of the signing (sub-)key + * @param signatureType signature type + * @param subpacketsCallback callback to modify the signatures subpackets + * @return builder + * @throws PGPException if the secret key cannot be unlocked or if no signing method can be created. + * @throws KeyException.UnacceptableSigningKeyException if the key ring does not carry any signing-capable subkeys + * @throws KeyException.MissingSecretKeyException if the key ring does not contain the identified secret key + */ + @Nonnull + public SigningOptions addInlineSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing secretKey, + long keyId, + @Nonnull DocumentSignatureType signatureType, + @Nullable BaseSignatureSubpackets.Callback subpacketsCallback) throws PGPException { + KeyRingInfo keyRingInfo = PGPainless.inspectKeyRing(secretKey); + + List signingPubKeys = keyRingInfo.getSigningSubkeys(); + if (signingPubKeys.isEmpty()) { + throw new KeyException.UnacceptableSigningKeyException(OpenPgpFingerprint.of(secretKey)); + } + + for (PGPPublicKey signingPubKey : signingPubKeys) { + if (signingPubKey.getKeyID() == keyId) { + + PGPSecretKey signingSecKey = secretKey.getSecretKey(signingPubKey.getKeyID()); + if (signingSecKey == null) { + throw new KeyException.MissingSecretKeyException(OpenPgpFingerprint.of(secretKey), signingPubKey.getKeyID()); + } + PGPPrivateKey signingSubkey = UnlockSecretKey.unlockSecretKey(signingSecKey, secretKeyDecryptor); + Set hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(signingPubKey.getKeyID()); + HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, PGPainless.getPolicy()); + addSigningMethod(secretKey, signingSubkey, subpacketsCallback, hashAlgorithm, signatureType, false); + return this; + } + } + + throw new KeyException.MissingSecretKeyException(OpenPgpFingerprint.of(secretKey), keyId); + } + /** * Add detached signatures with all key rings from the provided secret key ring collection. * @@ -385,6 +448,68 @@ public final class SigningOptions { return this; } + /** + * Create a detached binary signature using the signing key with the given keyId. + * + * @param secretKeyDecryptor decryptor to unlock the secret key + * @param secretKey secret key ring + * @param keyId keyId of the signing (sub-)key + * @return builder + * @throws PGPException if the secret key cannot be unlocked or if no signing method can be created. + * @throws KeyException.UnacceptableSigningKeyException if the key ring does not carry any signing-capable subkeys + * @throws KeyException.MissingSecretKeyException if the key ring does not contain the identified secret key + */ + @Nonnull + public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing secretKey, + long keyId) throws PGPException { + return addDetachedSignature(secretKeyDecryptor, secretKey, keyId, DocumentSignatureType.BINARY_DOCUMENT, null); + } + + /** + * Create a detached signature using the signing key with the given keyId. + * + * @param secretKeyDecryptor decryptor to unlock the secret key + * @param secretKey secret key ring + * @param keyId keyId of the signing (sub-)key + * @param signatureType signature type + * @param subpacketsCallback callback to modify the signatures subpackets + * @return builder + * @throws PGPException if the secret key cannot be unlocked or if no signing method can be created. + * @throws KeyException.UnacceptableSigningKeyException if the key ring does not carry any signing-capable subkeys + * @throws KeyException.MissingSecretKeyException if the key ring does not contain the identified secret key + */ + @Nonnull + public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing secretKey, + long keyId, + @Nonnull DocumentSignatureType signatureType, + @Nullable BaseSignatureSubpackets.Callback subpacketsCallback) throws PGPException { + KeyRingInfo keyRingInfo = PGPainless.inspectKeyRing(secretKey); + + List signingPubKeys = keyRingInfo.getSigningSubkeys(); + if (signingPubKeys.isEmpty()) { + throw new KeyException.UnacceptableSigningKeyException(OpenPgpFingerprint.of(secretKey)); + } + + for (PGPPublicKey signingPubKey : signingPubKeys) { + if (signingPubKey.getKeyID() == keyId) { + + PGPSecretKey signingSecKey = secretKey.getSecretKey(signingPubKey.getKeyID()); + if (signingSecKey == null) { + throw new KeyException.MissingSecretKeyException(OpenPgpFingerprint.of(secretKey), signingPubKey.getKeyID()); + } + PGPPrivateKey signingSubkey = UnlockSecretKey.unlockSecretKey(signingSecKey, secretKeyDecryptor); + Set hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(signingPubKey.getKeyID()); + HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, PGPainless.getPolicy()); + addSigningMethod(secretKey, signingSubkey, subpacketsCallback, hashAlgorithm, signatureType, true); + return this; + } + } + + throw new KeyException.MissingSecretKeyException(OpenPgpFingerprint.of(secretKey), keyId); + } + private void addSigningMethod(@Nonnull PGPSecretKeyRing secretKey, @Nonnull PGPPrivateKey signingSubkey, @Nullable BaseSignatureSubpackets.Callback subpacketCallback, diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java new file mode 100644 index 00000000..5304c32c --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java @@ -0,0 +1,145 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.DocumentSignatureType; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.SignatureVerification; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.rsa.RsaLength; +import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.MultiMap; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MultiSigningSubkeyTest { + + private static PGPSecretKeyRing signingKey; + private static PGPPublicKeyRing signingCert; + private static SubkeyIdentifier primaryKey; + private static SubkeyIdentifier signingKey1; + private static SubkeyIdentifier signingKey2; + private static SecretKeyRingProtector protector; + + @BeforeAll + public static void generateKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + signingKey = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.RSA(RsaLength._3072), KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .addUserId("Alice ") + .build(); + signingCert = PGPainless.extractCertificate(signingKey); + Iterator signingSubkeys = PGPainless.inspectKeyRing(signingKey).getSigningSubkeys().listIterator(); + primaryKey = new SubkeyIdentifier(signingKey, signingSubkeys.next().getKeyID()); + signingKey1 = new SubkeyIdentifier(signingKey, signingSubkeys.next().getKeyID()); + signingKey2 = new SubkeyIdentifier(signingKey, signingSubkeys.next().getKeyID()); + protector = SecretKeyRingProtector.unprotectedKeys(); + } + + @Test + public void detachedSignWithAllSubkeys() throws PGPException, IOException { + ByteArrayInputStream dataIn = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream signingStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.sign(SigningOptions.get().addDetachedSignature(protector, signingKey, DocumentSignatureType.BINARY_DOCUMENT))); + Streams.pipeAll(dataIn, signingStream); + signingStream.close(); + + MultiMap sigs = signingStream.getResult().getDetachedSignatures(); + assertTrue(sigs.containsKey(primaryKey)); + assertTrue(sigs.containsKey(signingKey1)); + assertTrue(sigs.containsKey(signingKey2)); + } + + @Test + public void detachedSignWithSingleSubkey() throws PGPException, IOException { + ByteArrayInputStream dataIn = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream signingStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.sign(SigningOptions.get().addDetachedSignature(protector, signingKey, signingKey1.getKeyId()))); + Streams.pipeAll(dataIn, signingStream); + signingStream.close(); + + MultiMap sigs = signingStream.getResult().getDetachedSignatures(); + assertEquals(1, sigs.flatten().size()); + assertTrue(sigs.containsKey(signingKey1)); + } + + @Test + public void inlineSignWithAllSubkeys() throws PGPException, IOException { + ByteArrayInputStream dataIn = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream signingStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.sign(SigningOptions.get().addInlineSignature(protector, signingKey, DocumentSignatureType.BINARY_DOCUMENT))); + Streams.pipeAll(dataIn, signingStream); + signingStream.close(); + + ByteArrayInputStream signedIn = new ByteArrayInputStream(out.toByteArray()); + DecryptionStream verificationStream = PGPainless.decryptAndOrVerify().onInputStream(signedIn) + .withOptions(ConsumerOptions.get().addVerificationCert(signingCert)); + Streams.drain(verificationStream); + verificationStream.close(); + + List sigs = verificationStream.getMetadata().getVerifiedSignatures(); + List sigKeys = sigs.stream().map(SignatureVerification::getSigningKey) + .collect(Collectors.toList()); + assertTrue(sigKeys.contains(primaryKey)); + assertTrue(sigKeys.contains(signingKey1)); + assertTrue(sigKeys.contains(signingKey2)); + } + + @Test + public void inlineSignWithSingleSubkey() throws PGPException, IOException { + ByteArrayInputStream dataIn = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream signingStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.sign(SigningOptions.get().addInlineSignature(protector, signingKey, signingKey1.getKeyId()))); + Streams.pipeAll(dataIn, signingStream); + signingStream.close(); + + ByteArrayInputStream signedIn = new ByteArrayInputStream(out.toByteArray()); + DecryptionStream verificationStream = PGPainless.decryptAndOrVerify().onInputStream(signedIn) + .withOptions(ConsumerOptions.get().addVerificationCert(signingCert)); + Streams.drain(verificationStream); + verificationStream.close(); + + List sigs = verificationStream.getMetadata().getVerifiedSignatures(); + assertEquals(1, sigs.size()); + assertEquals(signingKey1, sigs.get(0).getSigningKey()); + } + +}