Skip to content

Commit 85481b1

Browse files
vanitasvitaedghgit
authored andcommitted
pgsc: OpenPGP smart card signature support, with the raw sign and decrypt operations moved onto OpenPGPSmartCard so a backend can be emulated in software, incorporating github PR #2430.
1 parent 19b659d commit 85481b1

66 files changed

Lines changed: 3765 additions & 1863 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CONTRIBUTORS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ We also wish to acknowledge financial and collaborative support from [CISCO](htt
388388
- Adam Vartanian \<https://github.com/flooey\> use of ShortBuffer exception and buffer size pre-check in Cipher.doFinal().
389389
- Bernd \<https://github.com/ecki\> Fix to make PGPUtil.pipeFileContents use buffer and not leak file handle.
390390
- Shartung \<https://github.com/shartung\> Additional EC Key Agreement algorithms in support of German BSI TR-03111.
391-
- Paul Schaub \<https://github.com/vanitasvitae\> bringing PGPSecretKey.getUserIds() into line with PGPPublicKey.getUserIds(). Exception message fix in BcPublicKeyDataDecryptorFactory. Additional tests on PGP key ring generation. Improved functionality of PGPSignatureSubpacketGenerator, PGPPublicKeyRing. Tweaks to PGPDataEncryptorBuilder interface, fix for JcaPGP/BcPGP Ed25519 private key conversion. Added configurable CRC detection to ArmoredInputStream, additional control character skipping in ArmoredInputStream. Rewind code for PGPPBEEncryptedData, addition of PGPSignature.getDigestPrefix(). Wrong list traversal fix in PGPSecretKeyRing. Further improvement to use of generics in PGP API. General interop improvements. PGP Public / Secure keyring ignore marker packets when reading. Initial work on PGP session key handling, filtering literal data for canoncialization. Addition of direct key identified key-ring construction. PGPSecretKeyRing.insertOrReplacePublicKey addition. Addition of utility methods for joining/merging signatures and public keys. Addition of PGP regexp packet, PolicyURI packet handling, UTF8 comment testing. Efficiency improvements to TruncatedStream. Initial Argon2 support for OpenPGP. General cleanups. Fast CRC24 implementation, SHA3 addtions to BcImplProvider, improvements to One Pass Signature support, signatue validation, read() consistency in BCPGInputStream. Contributions to AEAD support (v6 & v5) in PGP API. Addition of PGP WildCard ID, moving the PGP example code into the 21st century. Security patches for encrypted data generation, initial thread safe certification verification. Support for V6 EC keys, V6 signatures, V6 encryption, V6 PKESK, PGP packet criticality, and Preferred AEAD CipherSuites sigsubpacket support. Introduce high-level OpenPGP API for message creation/consumption and certificate evaluation. OpenPGP fuzz testing. Fix to prevent a null pointer exception on processing a partial stripped key. Moving the Argon2 memory size exponent bounds check from S2K packet parsing to decryption time. ArmoredInputStream CSF dash-escape hardening. Report and initial patch for OnePassSignaturePacket defaulting to the Legacy packet format for v6 packets (github #2347). PGPKeyPairGenerator factory methods for the OpenPGP brainpool curves (github #2375). Support for OpenPGP External Secret Keys, and the initial OpenPGP smart card API (bcpgsc) with YubiKey and simulator backends (github #2339). Initial implementation of the JCE bindings for smart card decryption and the pluggable YubiKey decryptor factory provider (github #2374). Correcting MessageEncryptionMechanism.unencrypted() to report no encryption mode rather than SEIPDv1.
391+
- Paul Schaub \<https://github.com/vanitasvitae\> bringing PGPSecretKey.getUserIds() into line with PGPPublicKey.getUserIds(). Exception message fix in BcPublicKeyDataDecryptorFactory. Additional tests on PGP key ring generation. Improved functionality of PGPSignatureSubpacketGenerator, PGPPublicKeyRing. Tweaks to PGPDataEncryptorBuilder interface, fix for JcaPGP/BcPGP Ed25519 private key conversion. Added configurable CRC detection to ArmoredInputStream, additional control character skipping in ArmoredInputStream. Rewind code for PGPPBEEncryptedData, addition of PGPSignature.getDigestPrefix(). Wrong list traversal fix in PGPSecretKeyRing. Further improvement to use of generics in PGP API. General interop improvements. PGP Public / Secure keyring ignore marker packets when reading. Initial work on PGP session key handling, filtering literal data for canoncialization. Addition of direct key identified key-ring construction. PGPSecretKeyRing.insertOrReplacePublicKey addition. Addition of utility methods for joining/merging signatures and public keys. Addition of PGP regexp packet, PolicyURI packet handling, UTF8 comment testing. Efficiency improvements to TruncatedStream. Initial Argon2 support for OpenPGP. General cleanups. Fast CRC24 implementation, SHA3 addtions to BcImplProvider, improvements to One Pass Signature support, signatue validation, read() consistency in BCPGInputStream. Contributions to AEAD support (v6 & v5) in PGP API. Addition of PGP WildCard ID, moving the PGP example code into the 21st century. Security patches for encrypted data generation, initial thread safe certification verification. Support for V6 EC keys, V6 signatures, V6 encryption, V6 PKESK, PGP packet criticality, and Preferred AEAD CipherSuites sigsubpacket support. Introduce high-level OpenPGP API for message creation/consumption and certificate evaluation. OpenPGP fuzz testing. Fix to prevent a null pointer exception on processing a partial stripped key. Moving the Argon2 memory size exponent bounds check from S2K packet parsing to decryption time. ArmoredInputStream CSF dash-escape hardening. Report and initial patch for OnePassSignaturePacket defaulting to the Legacy packet format for v6 packets (github #2347). PGPKeyPairGenerator factory methods for the OpenPGP brainpool curves (github #2375). Support for OpenPGP External Secret Keys, and the initial OpenPGP smart card API (bcpgsc) with YubiKey and simulator backends (github #2339). Initial implementation of the JCE bindings for smart card decryption and the pluggable YubiKey decryptor factory provider (github #2374). Correcting MessageEncryptionMechanism.unencrypted() to report no encryption mode rather than SEIPDv1. Initial implementation of OpenPGP smart card signature support: the low-level sign and decrypt operations moved onto OpenPGPSmartCard so a backend can be emulated in software, external-key detection in the document signature generators, a pluggable PGPContentSignerBuilderProviderFactory, the generalised smart-card decryptor factories and the key conversion helpers (PR #2430).
392392
- Nick of Nexxar \<https://github.com/nros\> update to OpenPGP package to handle a broader range of EC curves.
393393
- catbref \<https://github.com/catbref\> sample implementation of RFC 7748/Ed25519 (incorporated work from github users Valodim and str4d as well).
394394
- gerlion \<https://github.com/gerlion\> detection of concurrency issue with pre-1.60 EC math library.

‎docs/releasenotes.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ Date: 2026, TBD
3535

3636
### 2.1.3 Additional Features and Functionality
3737

38+
- The OpenPGP smart card API (bcpgsc) can now create signatures on a card as well as decrypt with one. The raw sign and decrypt operations moved onto OpenPGPSmartCard, so a backend can be emulated in software and the simulator exercises the same paths a card does; AbstractOpenPGPDocumentSignatureGenerator detects an external key and routes it to the card, with the content signer supplied through a pluggable PGPContentSignerBuilderProviderFactory; the YubiKey-specific decryptor factories are replaced by generic smart-card ones, ExternalOpenPGPKeyUtils converts an OpenPGPKey or OpenPGPCertificate into an external key, and the YubiKit dependency moves to 3.2.0. YubiKit remains compileOnly, so bcpgsc still ships with no third-party dependencies (github PR #2430).
39+
3840
- The promoted LMS private key (org.bouncycastle.crypto.params.LMSPrivateKeyParameters) keeps its Merkle tree in two bounded tiers in place of the WeakHashMap that held every node it ever computed: the top 63 nodes - the same ones its encoding persists - in a fixed array for the life of the key, and the authentication path of the last one-time key signed with, together with that leaf's ancestors, advanced under the key's lock as each index is claimed. The old map keyed every node below the top on objects nothing retained, so a leaf was collectable the moment it was inserted and the map's hit rate below the top depended on when the collector next ran, while its table still grew to 2^(h+1) entries during a tree build. A run of consecutive signatures now costs about (h - 5) / 2 + 1 leaf derivations each rather than either a cache hit or a 2^(h - 5) rebuild, with the rebuild remaining as the worst case at a half-tree crossing; shards and repositioned keys inherit the parent's retained path, and the encoding is unchanged. The tree built for the public key is built in path form, so a freshly generated key already holds the path of its first signature rather than rebuilding it - that signature went from 2.6 s to about 1 ms at h=15. The deprecated org.bouncycastle.pqc.crypto.lms copy is untouched.
3941

4042
### 2.1.4 Additional Notes

‎pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureGenerator.java‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.bouncycastle.crypto.CryptoServicesRegistrar;
1919
import org.bouncycastle.openpgp.operator.PGPContentSigner;
2020
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
21+
import org.bouncycastle.openpgp.operator.PGPExternalContentSignerBuilder;
2122
import org.bouncycastle.util.Arrays;
2223
import org.bouncycastle.util.Strings;
2324

@@ -90,7 +91,7 @@ public PGPSignatureGenerator(
9091
}
9192

9293
/**
93-
* Initialise the generator for signing.
94+
* Initialize the generator for signing.
9495
*
9596
* @param signatureType type of signature
9697
* @param key private signing key
@@ -105,7 +106,20 @@ public void init(
105106
{
106107
throw new PGPException("Illegal signature type 0xFF provided.");
107108
}
108-
contentSigner = contentSignerBuilder.build(signatureType, key);
109+
110+
if (contentSignerBuilder instanceof PGPExternalContentSignerBuilder)
111+
{
112+
contentSigner = ((PGPExternalContentSignerBuilder)contentSignerBuilder).build(signatureType);
113+
}
114+
else if (key != null)
115+
{
116+
contentSigner = contentSignerBuilder.build(signatureType, key);
117+
}
118+
else
119+
{
120+
throw new PGPException("Missing private key.");
121+
}
122+
109123
sigOut = contentSigner.getOutputStream();
110124
sigType = contentSigner.getType();
111125
lastb = 0;
@@ -115,7 +129,7 @@ public void init(
115129
throw new PGPException("key algorithm mismatch");
116130
}
117131

118-
if (key.getPublicKeyPacket().getVersion() != version)
132+
if (key != null && key.getPublicKeyPacket().getVersion() != version)
119133
{
120134
throw new PGPException("Key version mismatch.");
121135
}

‎pg/src/main/java/org/bouncycastle/openpgp/PGPUtil.java‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@
1818
import org.bouncycastle.asn1.cryptlib.CryptlibObjectIdentifiers;
1919
import org.bouncycastle.asn1.edec.EdECObjectIdentifiers;
2020
import org.bouncycastle.asn1.gnu.GNUObjectIdentifiers;
21+
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
22+
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
2123
import org.bouncycastle.asn1.sec.SECObjectIdentifiers;
2224
import org.bouncycastle.asn1.teletrust.TeleTrusTObjectIdentifiers;
25+
import org.bouncycastle.asn1.x509.X509ObjectIdentifiers;
2326
import org.bouncycastle.asn1.x9.ECNamedCurveTable;
2427
import org.bouncycastle.bcpg.ArmoredInputStream;
2528
import org.bouncycastle.bcpg.BCPGInputStream;
@@ -83,6 +86,37 @@ public class PGPUtil
8386
}
8487
};
8588

89+
public static ASN1ObjectIdentifier getDigestIdentifier(int hashAlgorithmId)
90+
throws PGPException
91+
{
92+
switch (hashAlgorithmId)
93+
{
94+
case MD5:
95+
return PKCSObjectIdentifiers.md5;
96+
case SHA1:
97+
return X509ObjectIdentifiers.id_SHA1;
98+
case SHA224:
99+
return NISTObjectIdentifiers.id_sha224;
100+
case SHA256:
101+
return NISTObjectIdentifiers.id_sha256;
102+
case SHA384:
103+
return NISTObjectIdentifiers.id_sha384;
104+
case SHA512:
105+
return NISTObjectIdentifiers.id_sha512;
106+
case SHA3_224:
107+
return NISTObjectIdentifiers.id_sha3_224;
108+
case SHA3_256:
109+
return NISTObjectIdentifiers.id_sha3_256;
110+
case SHA3_384:
111+
return NISTObjectIdentifiers.id_sha3_384;
112+
case SHA3_512:
113+
return NISTObjectIdentifiers.id_sha3_512;
114+
case RIPEMD160:
115+
return TeleTrusTObjectIdentifiers.ripemd128;
116+
}
117+
throw new PGPException("unknown hash algorithm id: " + hashAlgorithmId);
118+
}
119+
86120
/**
87121
* Return an appropriate name for the hash algorithm represented by the passed
88122
* in hash algorithm ID number.

‎pg/src/main/java/org/bouncycastle/openpgp/api/AbstractOpenPGPDocumentSignatureGenerator.java‎

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@
33
import java.util.ArrayList;
44
import java.util.Date;
55
import java.util.Iterator;
6+
import java.util.LinkedHashSet;
67
import java.util.List;
8+
import java.util.Set;
79

810
import org.bouncycastle.bcpg.sig.PreferredAlgorithms;
911
import org.bouncycastle.openpgp.PGPException;
1012
import org.bouncycastle.openpgp.PGPKeyPair;
13+
import org.bouncycastle.openpgp.PGPPublicKey;
1114
import org.bouncycastle.openpgp.PGPSignatureGenerator;
1215
import org.bouncycastle.openpgp.api.exception.InvalidSigningKeyException;
1316
import org.bouncycastle.openpgp.api.exception.KeyPassphraseException;
17+
import org.bouncycastle.openpgp.api.operator.PGPContentSignerBuilderProviderFactory;
18+
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
19+
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProvider;
1420

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

2734
protected final KeyPassphraseProvider.DefaultKeyPassphraseProvider defaultKeyPassphraseProvider =
2835
new KeyPassphraseProvider.DefaultKeyPassphraseProvider();
@@ -81,6 +88,18 @@ public T addKeyPassphrase(char[] passphrase)
8188
return (T)this;
8289
}
8390

91+
/**
92+
* Add a custom {@link PGPContentSignerBuilderProviderFactory} for external key signing.
93+
* This is useful to allow e.g. signing with keys stored on hardware tokens or smart cards.
94+
* @param factory custom factory
95+
* @return this
96+
*/
97+
public T addCustomPGPContentSignerBuilderProviderFactory(PGPContentSignerBuilderProviderFactory factory)
98+
{
99+
customContentSignerBuilderProviderFactories.add(factory);
100+
return (T)this;
101+
}
102+
84103
/**
85104
* Add an {@link OpenPGPKey} for message signing.
86105
* The {@link #signingKeySelector} is responsible for selecting one or more subkeys of the key to sign with.
@@ -243,15 +262,59 @@ protected PGPSignatureGenerator initSignatureGenerator(
243262
throw new InvalidSigningKeyException(signingKey);
244263
}
245264

246-
char[] passphrase = passphraseProvider.getKeyPassword(signingKey);
247-
PGPKeyPair unlockedKey = signingKey.unlock(passphrase).getKeyPair();
248-
if (unlockedKey == null)
265+
if (signingKey.getPGPSecretKey().isExternalKey())
249266
{
250-
throw new KeyPassphraseException(signingKey, new PGPException("Cannot unlock secret key."));
267+
PGPPublicKey publicKey = signingKey.getPGPPublicKey();
268+
for (PGPContentSignerBuilderProviderFactory sigFac : customContentSignerBuilderProviderFactories)
269+
{
270+
PGPContentSignerBuilderProvider sigProv;
271+
try
272+
{
273+
sigProv = sigFac.getPGPContentSignerBuilderProvider(
274+
signingKey, passphraseProvider, parameters.getSignatureHashAlgorithmId());
275+
if (sigProv == null)
276+
{
277+
// no matching card found
278+
continue;
279+
}
280+
}
281+
catch (PGPException e)
282+
{
283+
// No matching card found
284+
continue;
285+
}
286+
287+
PGPContentSignerBuilder contentSignerBuilder;
288+
try
289+
{
290+
contentSignerBuilder = sigProv.get(publicKey);
291+
}
292+
catch (IllegalArgumentException e)
293+
{
294+
// Mismatched key
295+
continue;
296+
}
297+
298+
PGPSignatureGenerator sigGen = new PGPSignatureGenerator(contentSignerBuilder, publicKey);
299+
sigGen.init(parameters.getSignatureType(), null);
300+
301+
return Utils.applyDefaultSubpackets(publicKey, parameters, parameters.getSignatureCreationTime(), null, sigGen);
302+
}
303+
304+
throw new PGPException("Cannot initialize signature generator for external key " + signingKey.getKeyIdentifier());
251305
}
306+
else
307+
{
308+
char[] passphrase = passphraseProvider.getKeyPassword(signingKey);
309+
PGPKeyPair unlockedKey = signingKey.unlock(passphrase).getKeyPair();
310+
if (unlockedKey == null)
311+
{
312+
throw new KeyPassphraseException(signingKey, new PGPException("Cannot unlock secret key."));
313+
}
252314

253-
return Utils.getPgpSignatureGenerator(implementation, signingKey.getPGPPublicKey(),
254-
unlockedKey.getPrivateKey(), parameters, parameters.getSignatureCreationTime(), null);
315+
return Utils.getPgpSignatureGenerator(implementation, signingKey.getPGPPublicKey(),
316+
unlockedKey.getPrivateKey(), parameters, parameters.getSignatureCreationTime(), null);
317+
}
255318
}
256319

257320
private int getPreferredHashAlgorithm(OpenPGPCertificate.OpenPGPComponentKey key)

‎pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageProcessor.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,12 @@ void onException(PGPException e)
575575
}
576576
}
577577

578+
public OpenPGPMessageProcessor setExceptionCallback(PGPExceptionCallback callback)
579+
{
580+
this.configuration.exceptionCallback = callback;
581+
return this;
582+
}
583+
578584
/**
579585
* Recover the session key of the given PKESK using the given secret key.
580586
* <p>

‎pg/src/main/java/org/bouncycastle/openpgp/api/Utils.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ static PGPSignatureGenerator getPgpSignatureGenerator(OpenPGPImplementation impl
8888
publicKey);
8989
sigGen.init(parameters.getSignatureType(), privateKey);
9090

91+
return applyDefaultSubpackets(publicKey, parameters, date, operation, sigGen);
92+
}
93+
94+
static PGPSignatureGenerator applyDefaultSubpackets(PGPPublicKey publicKey,
95+
SignatureParameters parameters,
96+
Date date,
97+
HashedSubpacketsOperation operation,
98+
PGPSignatureGenerator sigGen)
99+
throws PGPException
100+
{
91101
final PGPSignatureSubpacketGenerator hashedSubpackets = new PGPSignatureSubpacketGenerator();
92102
hashedSubpackets.setIssuerFingerprint(true, publicKey);
93103
if (date != null)

‎pg/src/main/java/org/bouncycastle/openpgp/api/exception/KeyPassphraseException.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ public class KeyPassphraseException
77
{
88
private final Exception cause;
99

10+
public KeyPassphraseException(OpenPGPCertificate.OpenPGPComponentKey key, String message, Exception cause)
11+
{
12+
super(key, message + "\n" + componentKeyErrorMessage(key, cause));
13+
this.cause = cause;
14+
}
15+
1016
public KeyPassphraseException(OpenPGPCertificate.OpenPGPComponentKey key, Exception cause)
1117
{
1218
super(key, componentKeyErrorMessage(key, cause));

‎pg/src/main/java/org/bouncycastle/openpgp/api/jcajce/JcaOpenPGPImplementation.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ public JcaOpenPGPImplementation(Provider provider, SecureRandom secureRandom)
6565
this.secureRandom = secureRandom;
6666
}
6767

68+
public Provider getProvider()
69+
{
70+
return provider;
71+
}
72+
6873
@Override
6974
public PGPObjectFactory pgpObjectFactory(InputStream packetInputStream)
7075
{

0 commit comments

Comments
 (0)