mirror of
https://github.com/pgpainless/pgpainless.git
synced 2024-11-17 18:02:05 +01:00
SigningOptions: Add methods to sign with a single, chosen signing subkey
This commit is contained in:
parent
5aabd1ced4
commit
1fca51d771
2 changed files with 270 additions and 0 deletions
|
@ -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<PGPPublicKey> 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<HashAlgorithm> 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<PGPPublicKey> 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<HashAlgorithm> 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,
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <alice@pgpainless.org>")
|
||||
.build();
|
||||
signingCert = PGPainless.extractCertificate(signingKey);
|
||||
Iterator<PGPPublicKey> 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<SubkeyIdentifier, PGPSignature> 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<SubkeyIdentifier, PGPSignature> 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<SignatureVerification> sigs = verificationStream.getMetadata().getVerifiedSignatures();
|
||||
List<SubkeyIdentifier> 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<SignatureVerification> sigs = verificationStream.getMetadata().getVerifiedSignatures();
|
||||
assertEquals(1, sigs.size());
|
||||
assertEquals(signingKey1, sigs.get(0).getSigningKey());
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue