Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e0222af
OpenPGP Smart Card Support: Add support for message signing
vanitasvitae Sep 8, 2026
5e613d2
Reuse parseEphemeralKeyAndKeyEnc methods
vanitasvitae Sep 8, 2026
42bbca6
Direct conversion of JCE ECDH key without PGPPublicKey workaround
vanitasvitae Sep 8, 2026
d55fcc9
B1 - move PGPContentSignerBuilderProviderFactory into openpgp/api/ope…
vanitasvitae Sep 10, 2026
f339de7
B2 - Move build(sigType) into PGPExternalContentSignerBuilder to avoi…
vanitasvitae Sep 10, 2026
3b8cf61
Conventions: Curly Brackets
vanitasvitae Sep 10, 2026
2a87e19
Conventions: Wildcard imports
vanitasvitae Sep 10, 2026
dd2af32
Conventions: Uncommented code
vanitasvitae Sep 10, 2026
50eb8cb
Conventions: Readme with newline
vanitasvitae Sep 10, 2026
a8eb7c5
Conventions: Suppress println statement
vanitasvitae Sep 10, 2026
8e3fe9c
Conventions C6: Bring exceptions of decrypt/sign in line
vanitasvitae Sep 10, 2026
e89c278
ExternalContentSignerBuilder: Explain why we need two PGPDigestCalcul…
vanitasvitae Sep 10, 2026
6eff13a
ExternalContentSignerBuilder: Replace use of pqc DigestUtils with Mes…
vanitasvitae Sep 10, 2026
bef5f29
Reduce visibility of BcPublicKeyDataDecryptorFactory.decodePeerKey()
vanitasvitae Sep 10, 2026
3732edc
B3 - Fix NPE when no card holds the signing key
vanitasvitae Sep 10, 2026
dafad9b
B4 - BcPublicKeyDataDecryptorFactory: Throw PGPException for unsuppor…
vanitasvitae Sep 10, 2026
aba2fc8
B5 - fix off-by-one in shortened legacy identifier check
vanitasvitae Sep 10, 2026
bf6cedb
B5 - Add test for off-by-one error
vanitasvitae Sep 10, 2026
139199d
B6 - SupportedAlgorithms: Fix IllegalArgumentException for EC.matches…
vanitasvitae Sep 10, 2026
5df3c4d
B7 - Fix StringIndexOutOfBoundsException on toString() if SupportedAl…
vanitasvitae Sep 10, 2026
01bdbc6
B8 - OpenPGPSmartCardBackend: Do not shadow the converter in subclasses
vanitasvitae Sep 10, 2026
049daae
B9 - Shortened legacy fingerprints: Make identifier writer and matche…
vanitasvitae Sep 10, 2026
a2f1ba1
D1 - Replace BCECPublicKeyHelper, EDECPublicKeyConverter with KeyFact…
vanitasvitae Sep 10, 2026
057aa82
D2 - JceExternalPublicKeyDataDecryptorFactoryBuilder: Use helper.crea…
vanitasvitae Sep 10, 2026
cb86392
F1: openpgp.operator: Remove dependency on openpgp.api
vanitasvitae Sep 17, 2026
513bb96
F2: Do not throw unchecked PGPRuntimeOperationException during decrypt
vanitasvitae Sep 17, 2026
8e10966
F3: Implement proper OpenPGP digest OID lookup
vanitasvitae Sep 17, 2026
fb34495
F4: Fix signing with auth key PIN verification
vanitasvitae Sep 17, 2026
9af38e5
OpenPGPMessageProcessor: Add .setExceptionCallback()
vanitasvitae Sep 17, 2026
4e5a6fc
DecryptionTest: printStacktrace() exceptions
vanitasvitae Sep 17, 2026
ed06faa
F9: OpenPGPSmartCardBackend.convertPublicKey(): Detect algorithm by OID
vanitasvitae Sep 17, 2026
5973461
Add branches for Ed448, X448
vanitasvitae Sep 17, 2026
2b1b53f
Generalize convertPublicKey() methods
vanitasvitae Sep 17, 2026
d210c3c
SimulatorOpenPGPSmartCardBackend: Initialize with non-null JcaPGPKeyC…
vanitasvitae Sep 17, 2026
e9c0f90
Transform YubikeySmartCardBackendTest into general OpenPGPSmartCardBa…
vanitasvitae Sep 17, 2026
8a7be67
Reorganize tests
vanitasvitae Sep 17, 2026
660c41c
F10: SimulatorOpenPGPSmartCard: Use PublicKeyFactory.createKey(byte[]…
vanitasvitae Sep 17, 2026
fa2461c
Add missing package-info.java files
vanitasvitae Sep 17, 2026
3dc8e52
ExternalOpenPGPKeyUtils.fromCertificate(): Pass in implementation
vanitasvitae Sep 17, 2026
8c6d4ad
F6: Properly handle failing backends, add test for failing signature,…
vanitasvitae Sep 17, 2026
ef66b06
F7: BcPublicKeyDataDecryptorFactory: Add deprecated shim for getCrypt…
vanitasvitae Sep 17, 2026
9d5dba2
Remove notice about ElGamal, X448 from BcSmartCardPublicKeyDataDecryp…
vanitasvitae Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public PGPSignatureGenerator(
}

/**
* Initialise the generator for signing.
* Initialize the generator for signing.
*
* @param signatureType type of signature
* @param key private signing key
Expand All @@ -105,7 +105,16 @@ public void init(
{
throw new PGPException("Illegal signature type 0xFF provided.");
}
contentSigner = contentSignerBuilder.build(signatureType, key);

if (key == null)
{
contentSigner = contentSignerBuilder.build(signatureType);
}
else
{
contentSigner = contentSignerBuilder.build(signatureType, key);
}

sigOut = contentSigner.getOutputStream();
sigType = contentSigner.getType();
lastb = 0;
Expand All @@ -115,7 +124,7 @@ public void init(
throw new PGPException("key algorithm mismatch");
}

if (key.getPublicKeyPacket().getVersion() != version)
if (key != null && key.getPublicKeyPacket().getVersion() != version)
{
throw new PGPException("Key version mismatch.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.bouncycastle.bcpg.sig.PreferredAlgorithms;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPKeyPair;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.api.exception.InvalidSigningKeyException;
import org.bouncycastle.openpgp.api.exception.KeyPassphraseException;
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProvider;
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProviderFactory;

public class AbstractOpenPGPDocumentSignatureGenerator<T extends AbstractOpenPGPDocumentSignatureGenerator<T>>
{
Expand All @@ -23,6 +29,7 @@ public class AbstractOpenPGPDocumentSignatureGenerator<T extends AbstractOpenPGP
protected final List<OpenPGPKey.OpenPGPSecretKey> signingKeys = new ArrayList<OpenPGPKey.OpenPGPSecretKey>();
protected final List<SignatureParameters.Callback> signatureCallbacks = new ArrayList<SignatureParameters.Callback>();
protected final List<KeyPassphraseProvider> signingKeyPassphraseProviders = new ArrayList<KeyPassphraseProvider>();
protected final Set<PGPContentSignerBuilderProviderFactory> customContentSignerBuilderProviderFactories = new LinkedHashSet<PGPContentSignerBuilderProviderFactory>();

protected final KeyPassphraseProvider.DefaultKeyPassphraseProvider defaultKeyPassphraseProvider =
new KeyPassphraseProvider.DefaultKeyPassphraseProvider();
Expand Down Expand Up @@ -81,6 +88,18 @@ public T addKeyPassphrase(char[] passphrase)
return (T)this;
}

/**
* Add a custom {@link PGPContentSignerBuilderProviderFactory} for external key signing.
* This is useful to allow e.g. signing with keys stored on hardware tokens or smart cards.
* @param factory custom factory
* @return this
*/
public T addCustomPGPContentSignerBuilderProviderFactory(PGPContentSignerBuilderProviderFactory factory)
{
customContentSignerBuilderProviderFactories.add(factory);
return (T)this;
}

/**
* Add an {@link OpenPGPKey} for message signing.
* The {@link #signingKeySelector} is responsible for selecting one or more subkeys of the key to sign with.
Expand Down Expand Up @@ -243,15 +262,44 @@ protected PGPSignatureGenerator initSignatureGenerator(
throw new InvalidSigningKeyException(signingKey);
}

char[] passphrase = passphraseProvider.getKeyPassword(signingKey);
PGPKeyPair unlockedKey = signingKey.unlock(passphrase).getKeyPair();
if (unlockedKey == null)
if (signingKey.getPGPSecretKey().isExternalKey())
{
throw new KeyPassphraseException(signingKey, new PGPException("Cannot unlock secret key."));
PGPPublicKey publicKey = signingKey.getPGPPublicKey();
for (PGPContentSignerBuilderProviderFactory sigFac : customContentSignerBuilderProviderFactories)
{
PGPContentSignerBuilderProvider sigProv = sigFac.getPGPContentSignerBuilderProvider(
signingKey, passphraseProvider, parameters.getSignatureHashAlgorithmId());
PGPContentSignerBuilder contentSignerBuilder;
try
{
contentSignerBuilder = sigProv.get(signingKey);
}
catch (IllegalArgumentException e)
{
// Mismatched key
continue;
}

PGPSignatureGenerator sigGen = new PGPSignatureGenerator(contentSignerBuilder, publicKey);
sigGen.init(parameters.getSignatureType(), null);

return Utils.applyDefaultSubpackets(publicKey, parameters, parameters.getSignatureCreationTime(), null, sigGen);
}

throw new IllegalStateException("Cannot initialize signature generator for external key " + signingKey.getKeyIdentifier());
}
else
{
char[] passphrase = passphraseProvider.getKeyPassword(signingKey);
PGPKeyPair unlockedKey = signingKey.unlock(passphrase).getKeyPair();
if (unlockedKey == null)
{
throw new KeyPassphraseException(signingKey, new PGPException("Cannot unlock secret key."));
}

return Utils.getPgpSignatureGenerator(implementation, signingKey.getPGPPublicKey(),
unlockedKey.getPrivateKey(), parameters, parameters.getSignatureCreationTime(), null);
return Utils.getPgpSignatureGenerator(implementation, signingKey.getPGPPublicKey(),
unlockedKey.getPrivateKey(), parameters, parameters.getSignatureCreationTime(), null);
}
}

private int getPreferredHashAlgorithm(OpenPGPCertificate.OpenPGPComponentKey key)
Expand Down
4 changes: 4 additions & 0 deletions pg/src/main/java/org/bouncycastle/openpgp/api/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ static PGPSignatureGenerator getPgpSignatureGenerator(OpenPGPImplementation impl
publicKey);
sigGen.init(parameters.getSignatureType(), privateKey);

return applyDefaultSubpackets(publicKey, parameters, date, operation, sigGen);
}

static PGPSignatureGenerator applyDefaultSubpackets(PGPPublicKey publicKey, SignatureParameters parameters, Date date, HashedSubpacketsOperation operation, PGPSignatureGenerator sigGen) throws PGPException {
final PGPSignatureSubpacketGenerator hashedSubpackets = new PGPSignatureSubpacketGenerator();
hashedSubpackets.setIssuerFingerprint(true, publicKey);
if (date != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ public class KeyPassphraseException
{
private final Exception cause;

public KeyPassphraseException(OpenPGPCertificate.OpenPGPComponentKey key, String message, Exception cause)
{
super(key, message + "\n" + componentKeyErrorMessage(key, cause));
this.cause = cause;
}

public KeyPassphraseException(OpenPGPCertificate.OpenPGPComponentKey key, Exception cause)
{
super(key, componentKeyErrorMessage(key, cause));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ public JcaOpenPGPImplementation(Provider provider, SecureRandom secureRandom)
this.secureRandom = secureRandom;
}

public Provider getProvider()
{
return provider;
}

@Override
public PGPObjectFactory pgpObjectFactory(InputStream packetInputStream)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.bouncycastle.openpgp.api.operator.bc;

import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPKeyPair;
import org.bouncycastle.openpgp.api.OpenPGPKey;
Expand All @@ -17,7 +18,7 @@
* private-key operation by implementing {@link #getExternalKeyCryptoCallback()}.
* <p>
* Note that a secret key handled through this class need not actually be external: if the supplied key
* does carry usable software key material, {@link #getCryptoCallback()} returns the inherited software
* does carry usable software key material, {@link #getCryptoCallback(AsymmetricKeyParameter)} returns the inherited software
* callback so the (much cheaper) in-process path is used instead.
*/
public abstract class BcExternalPublicKeyDataDecryptorFactory
Expand Down Expand Up @@ -61,12 +62,12 @@ private static PGPKeyPair unlock(OpenPGPKey.OpenPGPSecretKey secretKey)
}

@Override
protected BcPublicKeyCryptoCallback getCryptoCallback()
protected BcPublicKeyCryptoCallback getCryptoCallback(AsymmetricKeyParameter privKey)
{
// if software key material is available we can skip the costly hardware round trip
if (!secretKey.getPGPSecretKey().isExternalKey())
{
return super.getCryptoCallback();
return super.getCryptoCallback(privKey);
}
return getExternalKeyCryptoCallback();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,35 +180,35 @@ private class JceExternalFactory
protected byte[] decryptRSA(int keyAlgorithm, byte[] sessionKey)
throws PGPException
{
return cryptoCallback.decryptRSA(keyAlgorithm, sessionKey);
return cryptoCallback.decrypt(keyAlgorithm, new byte[][]{sessionKey});
}

@Override
protected byte[] decryptElGamal(int keyAlgorithm, byte[][] secKeyData)
throws PGPException
{
return cryptoCallback.decryptElGamal(keyAlgorithm, secKeyData);
return cryptoCallback.decrypt(keyAlgorithm, secKeyData);
}

@Override
protected byte[] agreeECDH(ECDHPublicBCPGKey ecKey, byte[] ephemeralKeyBytes)
throws PGPException
{
return cryptoCallback.decryptECDH(ecKey, toECPublicKey(ecKey, ephemeralKeyBytes));
return cryptoCallback.decrypt(PublicKeyAlgorithmTags.ECDH, toECPublicKey(ecKey, ephemeralKeyBytes));
}

@Override
protected byte[] agreeX25519(byte[] ephemeralKey)
throws PGPException
{
return cryptoCallback.decryptX25519(toXDHPublicKey(EdECObjectIdentifiers.id_X25519, ephemeralKey));
return cryptoCallback.decrypt(PublicKeyAlgorithmTags.X25519, toXDHPublicKey(EdECObjectIdentifiers.id_X25519, ephemeralKey));
}

@Override
protected byte[] agreeX448(byte[] ephemeralKey)
throws PGPException
{
return cryptoCallback.decryptX448(toXDHPublicKey(EdECObjectIdentifiers.id_X448, ephemeralKey));
return cryptoCallback.decrypt(PublicKeyAlgorithmTags.X448, toXDHPublicKey(EdECObjectIdentifiers.id_X448, ephemeralKey));
}

@Override
Expand Down Expand Up @@ -385,59 +385,26 @@ private PublicKey toECPublicKey(ECDHPublicBCPGKey ecKey, byte[] pEnc)
public static abstract class PublicKeyCryptoCallback
{
/**
* Perform RSA decryption of an encrypted session key.
* Perform RSA/ElGamal decryption of an encrypted session key.
*
* @param keyAlgorithm public key algorithm
* @param pEnc encrypted session key
* @return decrypted session key
* @throws PGPException if the message cannot be decrypted
*/
public abstract byte[] decryptRSA(int keyAlgorithm,
byte[] pEnc)
public abstract byte[] decrypt(int keyAlgorithm,
byte[][] pEnc)
throws PGPException;

/**
* Perform ElGamal decryption of an encrypted session key.
* Perform an ECDH / X25519 / X448 agreement to calculate a shared secret.
*
* @param keyAlgorithm public key algorithm
* @param secKeyData encrypted session key data
* @return decrypted session key
* @throws PGPException if the message cannot be decrypted
*/
public abstract byte[] decryptElGamal(int keyAlgorithm,
byte[][] secKeyData)
throws PGPException;

/**
* Perform an ECDH agreement to calculate a shared secret.
*
* @param pubKey our ECDH public key
* @param ephemeralKey the sender's ephemeral public key
* @param ephemeralKey the message's ephemeral public key
* @return shared secret
* @throws PGPException if the message cannot be decrypted
*/
public abstract byte[] decryptECDH(ECDHPublicBCPGKey pubKey,
PublicKey ephemeralKey)
public abstract byte[] decrypt(int keyAlgorithm, PublicKey ephemeralKey)
throws PGPException;

/**
* Perform an X25519 agreement to calculate a shared secret.
*
* @param ephemeralKey the sender's ephemeral X25519 public key
* @return shared secret
* @throws PGPException if the message cannot be decrypted
*/
public abstract byte[] decryptX25519(PublicKey ephemeralKey)
throws PGPException;

/**
* Perform an X448 agreement to calculate a shared secret.
*
* @param ephemeralKey the sender's ephemeral X448 public key
* @return shared secret
* @throws PGPException if the message cannot be decrypted
*/
public abstract byte[] decryptX448(PublicKey ephemeralKey)
throws PGPException;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ public interface PGPContentSignerBuilder
{
PGPContentSigner build(final int signatureType, final PGPPrivateKey privateKey)
throws PGPException;

PGPContentSigner build(final int signatureType)
throws PGPException;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.bouncycastle.openpgp.operator;

import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.api.OpenPGPKey;

/**
* Provider class for {@link PGPContentSignerBuilder} instances.
Expand All @@ -27,4 +28,9 @@ public PGPContentSignerBuilderProvider(int hashAlgorithmId)
* @return content signer builder
*/
public abstract PGPContentSignerBuilder get(PGPPublicKey signingKey);

public PGPContentSignerBuilder get(OpenPGPKey.OpenPGPSecretKey signingKey)
{
return get(signingKey.getPGPPublicKey());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.bouncycastle.openpgp.operator;

import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.api.KeyPassphraseProvider;
import org.bouncycastle.openpgp.api.OpenPGPKey;

public interface PGPContentSignerBuilderProviderFactory
{
PGPContentSignerBuilderProvider getPGPContentSignerBuilderProvider(
OpenPGPKey.OpenPGPSecretKey secretKey,
KeyPassphraseProvider keyPassphraseProvider,
int hashAlgorithmId)
throws PGPException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,11 @@ public byte[] getDigest()
}
};
}

@Override
public PGPContentSigner build(int signatureType)
throws PGPException
{
throw new PGPException("Needs to be implemented for hardware keys.");
}
}
Loading