Skip to content

Commit 033b499

Browse files
author
Administrator
committed
Merge branch 'openpgp-smartcard-decryption' into 'main'
Implement support for OpenPGP External Secret Keys See merge request root/bc-java!305
2 parents e246329 + f9a2bf1 commit 033b499

52 files changed

Lines changed: 5010 additions & 115 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.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@
450450
<li>Adam Vartanian &lt;https://github.com/flooey&gt; use of ShortBuffer exception and buffer size pre-check in Cipher.doFinal().</li>
451451
<li>Bernd &lt;https://github.com/ecki&gt; Fix to make PGPUtil.pipeFileContents use buffer and not leak file handle.</li>
452452
<li>Shartung &lt;https://github.com/shartung&gt; Additional EC Key Agreement algorithms in support of German BSI TR-03111.</li>
453-
<li>Paul Schaub &lt;https://github.com/vanitasvitae&gt; 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).</li>
453+
<li>Paul Schaub &lt;https://github.com/vanitasvitae&gt; 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, #2374).</li>
454454
<li>Nick of Nexxar &lt;https://github.com/nros&gt; update to OpenPGP package to handle a broader range of EC curves.</li>
455455
<li>catbref &lt;https://github.com/catbref&gt; sample implementation of RFC 7748/Ed25519 (incorporated work from github users Valodim and str4d as well).</li>
456456
<li>gerlion &lt;https://github.com/gerlion&gt; detection of concurrency issue with pre-1.60 EC math library.</li>

‎build.gradle‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ configure(subprojects.findAll {it.name != 'bom'}) {
316316
}
317317

318318

319-
test.dependsOn([':core:test', ':prov:test', ':prov:test11', ':prov:test15', ':prov:test17', ':pkix:test', 'pg:test', ':tls:test', ':tls-klog:test', 'mls:test', 'mail:test', 'jmail:test'])
319+
test.dependsOn([':core:test', ':prov:test', ':prov:test11', ':prov:test15', ':prov:test17', ':pkix:test', 'pg:test', ':pgsc:test', ':tls:test', ':tls-klog:test', 'mls:test', 'mail:test', 'jmail:test'])
320320

321321
// Aggregate all published jars (main, sources, javadoc) into a top-level dist/
322322
// directory so consumers have a single place to pick up the build outputs.

‎docs/releasenotes.html‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ <h3>2.1.2 Defects Fixed</h3>
8383

8484
<h3>2.1.3 Additional Features and Functionality</h3>
8585
<ul>
86+
<li>OpenPGP secret keys whose private key material is held outside the key - on a hardware token - are now supported, following <a href="https://datatracker.ietf.org/doc/draft-dkg-openpgp-external-secrets/">draft-dkg-openpgp-external-secrets</a>. org.bouncycastle.bcpg.SecretKeyPacket recognises the External S2K usage octet (SecretKeyPacket.USAGE_EXTERNAL) and its locator hint, PGPSecretKey exposes isExternalKey() / getExternalKeyLocatorHint(), and a decryption backend is plugged into the high level API by registering an org.bouncycastle.openpgp.api.PublicKeyDataDecryptorFactoryProvider with OpenPGPMessageProcessor. On the lightweight side BcPublicKeyDataDecryptorFactory now isolates the raw private-key operation behind BcPublicKeyCryptoCallback, so a subclass of the new BcExternalPublicKeyDataDecryptorFactory routes only the RSA decryption or the ECDH/X25519 agreement to a device while inheriting all packet parsing, KDF and key-unwrap logic. Note the External S2K usage octet is provisional: the draft records it as "TBD (252?)" and IANA has not assigned it.</li>
87+
<li>A new module, bcpgsc (org.bouncycastle.openpgp.smartcard), provides an OpenPGP smart-card API on top of the external-secret-key support: listing cards across pluggable backends, uploading key material, and decrypting messages with a card-held key. Two backends ship - a YubiKey backend built on the YubiKit libraries, and an in-memory simulator for testing without hardware. The YubiKit libraries are a compile-only dependency, so the published bcpgsc jar carries no third-party runtime dependency; an application using org.bouncycastle.openpgp.smartcard.yubikey must add them to its own classpath. Contributed by Paul Schaub.</li>
8688
<li>org.bouncycastle.openpgp.operator.PGPKeyPairGenerator gained named convenience methods for the three brainpool curves RFC 9580 sec. 9.2 permits for OpenPGP - generateBrainpoolP256r1ECDHKeyPair / generateBrainpoolP384r1ECDHKeyPair / generateBrainpoolP512r1ECDHKeyPair and the matching ECDSA variants - alongside the existing NIST P-256/P-384/P-521 methods. The generated ECDH keys carry the per-curve KDF hash and KEK symmetric algorithm required by sec. 11.5.1, which the key-pair generator tests now assert for the brainpool and the NIST curves alike. Contributed by Paul Schaub.</li>
8789
<li>Further extended key usages are now available as KeyPurposeId constants: id-kp-secureShellClient and id-kp-secureShellServer (RFC 6187 sec. 2.2.2, id-kp 21/22), id-kp-cmcArchive (RFC 6402 sec. 2.10, id-kp 29) and id-kp-bundleSecurity (RFC 9174, id-kp 35).</li>
8890
<li>The two RFC 4556 (PKINIT) extended key usages are now available as KeyPurposeId constants: id-pkinit-KPClientAuth (1.3.6.1.5.2.3.4, sec. 3.2.2) as KeyPurposeId.id_kp_pkinitClientAuth and id-pkinit-KPKdc (1.3.6.1.5.2.3.5, sec. 3.2.4) as KeyPurposeId.id_kp_pkinitKdc. Note these sit under the Kerberos id-pkinit arc rather than the PKIX id-kp arc, and are distinct from the Microsoft smartcard logon usage already available as id_kp_smartcardlogon.</li>

‎pg/src/main/java/org/bouncycastle/bcpg/PublicKeyUtils.java‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.bouncycastle.bcpg;
22

3+
import org.bouncycastle.asn1.cryptlib.CryptlibObjectIdentifiers;
4+
35
/**
46
* Utility methods related to OpenPGP public key algorithms.
57
*/
@@ -52,4 +54,33 @@ public static boolean isEncryptionAlgorithm(int publicKeyAlgorithm)
5254
return false;
5355
}
5456
}
57+
58+
/**
59+
* Return true, if the passed in {@link PublicKeyPacket} is based on X25519, either the legacy variant
60+
* via {@link PublicKeyAlgorithmTags#ECDH} over curve25519, or the modern
61+
* {@link PublicKeyAlgorithmTags#X25519}.
62+
*
63+
* @param publicKeyPacket public key packet
64+
* @return true if the key is an X25519 key
65+
*/
66+
public static boolean isX25519Key(PublicKeyPacket publicKeyPacket)
67+
{
68+
int algorithm = publicKeyPacket.getAlgorithm();
69+
if (algorithm == PublicKeyAlgorithmTags.X25519)
70+
{
71+
return true;
72+
}
73+
if (algorithm != PublicKeyAlgorithmTags.ECDH)
74+
{
75+
return false;
76+
}
77+
// the algorithm tag and the key packet body can disagree on malformed input, so type-check
78+
// rather than cast: an ECDH tag over a non-EC body is simply not an X25519 key.
79+
BCPGKey key = publicKeyPacket.getKey();
80+
if (!(key instanceof ECPublicBCPGKey))
81+
{
82+
return false;
83+
}
84+
return CryptlibObjectIdentifiers.curvey25519.equals(((ECPublicBCPGKey)key).getCurveOID());
85+
}
5586
}

‎pg/src/main/java/org/bouncycastle/bcpg/SecretKeyPacket.java‎

Lines changed: 131 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.io.IOException;
66

77
import org.bouncycastle.util.Arrays;
8+
import org.bouncycastle.util.io.Streams;
89

910
/**
1011
* Base class for OpenPGP secret (primary) keys.
@@ -63,14 +64,42 @@ public class SecretKeyPacket
6364
* Users should migrate to AEAD with all due speed.
6465
*/
6566
public static final int USAGE_AEAD = 0xfd;
66-
67+
68+
/**
69+
* Externally-backed secret key material.
70+
* S2K-usage octet indicating that the secret key material is stored externally, e.g. on a hardware device.
71+
* The draft specification is an alternative to GnuPGs proprietary {@link S2K#GNU_DUMMY_S2K} mechanism.
72+
* <p>
73+
* NOTE: this code point is <em>provisional</em>. draft-dkg-openpgp-external-secrets-03 sec. 2 records
74+
* it as "TBD (252?)" and IANA has not yet assigned it, so the value here tracks the draft's suggestion
75+
* and is subject to change if a different octet is allocated. Do not rely on it for long-term storage.
76+
*
77+
* @see <a href="https://datatracker.ietf.org/doc/draft-dkg-openpgp-external-secrets/">
78+
* OpenPGP External Secret Keys</a>
79+
*/
80+
public static final int USAGE_EXTERNAL = 0xfc;
81+
82+
/**
83+
* Maximum accepted length of the external key locator hint of a version 4 secret key packet, whose
84+
* hint is not length-prefixed and so is bounded only by the packet. Mirrors
85+
* {@link SignaturePacket#MAX_SUBPACKET_LEN}.
86+
*/
87+
public static final int MAX_EXTERNAL_LOCATOR_HINT_LEN = 2 * 1024 * 1024;
88+
89+
/**
90+
* Maximum length of the external key locator hint of a version 5 or 6 secret key packet, whose
91+
* conditional parameters are prefixed with a one-octet count (RFC 9580 sec. 5.5.3).
92+
*/
93+
public static final int MAX_V6_EXTERNAL_LOCATOR_HINT_LEN = 255;
94+
6795
private PublicKeyPacket pubKeyPacket;
6896
private byte[] secKeyData;
6997
private int s2kUsage;
7098
private int encAlgorithm;
7199
private int aeadAlgorithm;
72100
private S2K s2k;
73101
private byte[] iv;
102+
private byte[] externalKeyLocatorHint;
74103

75104
/**
76105
* Parse a primary OpenPGP secret key packet from the given OpenPGP {@link BCPGInputStream}.
@@ -140,13 +169,33 @@ public class SecretKeyPacket
140169
s2kUsage = in.read();
141170

142171
int conditionalParameterLength = -1;
143-
if (version == PublicKeyPacket.LIBREPGP_5 ||
172+
if (version == PublicKeyPacket.LIBREPGP_5 ||
144173
(version == PublicKeyPacket.VERSION_6 && s2kUsage != USAGE_NONE))
145174
{
146175
// TODO: Use length to parse unknown parameters
147176
conditionalParameterLength = in.read();
148177
}
149178

179+
if (s2kUsage == USAGE_EXTERNAL)
180+
{
181+
if (conditionalParameterLength >= 0)
182+
{
183+
// v5/v6 carry an explicit count of the conditional parameters, which for an external
184+
// key is exactly the locator hint - honour it, rather than reading to end of stream.
185+
externalKeyLocatorHint = new byte[conditionalParameterLength];
186+
in.readFully(externalKeyLocatorHint);
187+
}
188+
else
189+
{
190+
// v4 has no count: the hint is the remainder of the packet (draft sec. 2). Bound the
191+
// read - a partial-length packet body shares the underlying stream (BCPGInputStream
192+
// hands back "this"), so an unbounded readAll() would consume the rest of the input
193+
// and turn a short header into an arbitrary allocation.
194+
externalKeyLocatorHint = Streams.readAllLimited(in, MAX_EXTERNAL_LOCATOR_HINT_LEN);
195+
}
196+
return;
197+
}
198+
150199
if (s2kUsage == USAGE_CHECKSUM || s2kUsage == USAGE_SHA1 || s2kUsage == USAGE_AEAD)
151200
{
152201
encAlgorithm = in.read();
@@ -205,7 +254,7 @@ public class SecretKeyPacket
205254
if (encAlgorithm < 7)
206255
{
207256
iv = new byte[8];
208-
}
257+
}
209258
else
210259
{
211260
iv = new byte[16];
@@ -214,7 +263,7 @@ public class SecretKeyPacket
214263
}
215264
}
216265
}
217-
266+
218267
if (version == PublicKeyPacket.LIBREPGP_5)
219268
{
220269
long keyOctetCount = ((long) in.read() << 24) | ((long) in.read() << 16) | ((long) in.read() << 8) | in.read();
@@ -233,6 +282,59 @@ public class SecretKeyPacket
233282
}
234283
}
235284

285+
/**
286+
* Create a SecretKeyPacket representing an external secret key ({@link #USAGE_EXTERNAL}).
287+
*
288+
* @see <a href="https://datatracker.ietf.org/doc/draft-dkg-openpgp-external-secrets/">
289+
* OpenPGP External Secret Keys</a>
290+
* @param pubKeyPacket public key packet
291+
* @param locatorHint optional external key locator hint
292+
*/
293+
public SecretKeyPacket(
294+
PublicKeyPacket pubKeyPacket,
295+
byte[] locatorHint)
296+
{
297+
this(SECRET_KEY, pubKeyPacket, locatorHint);
298+
}
299+
300+
301+
/**
302+
* Create a SecretKeyPacket representing an external secret key ({@link #USAGE_EXTERNAL}).
303+
*
304+
* @see <a href="https://datatracker.ietf.org/doc/draft-dkg-openpgp-external-secrets/">
305+
* OpenPGP External Secret Keys</a>
306+
* @param keyTag key packet type
307+
* @param pubKeyPacket public key packet
308+
* @param locatorHint optional external key locator hint
309+
*/
310+
protected SecretKeyPacket(
311+
int keyTag,
312+
PublicKeyPacket pubKeyPacket,
313+
byte[] locatorHint)
314+
{
315+
this(keyTag, pubKeyPacket, 0, 0, USAGE_EXTERNAL, null, null, null);
316+
317+
byte[] hint = locatorHint == null ? new byte[0] : Arrays.clone(locatorHint);
318+
int version = pubKeyPacket.getVersion();
319+
if (version == PublicKeyPacket.LIBREPGP_5 || version == PublicKeyPacket.VERSION_6)
320+
{
321+
// a v5/v6 secret key prefixes its conditional parameters - here the locator hint - with a
322+
// one-octet count, so a longer hint could not be encoded: the count would wrap while the
323+
// hint was still written in full, producing a packet that does not round-trip
324+
if (hint.length > MAX_V6_EXTERNAL_LOCATOR_HINT_LEN)
325+
{
326+
throw new IllegalArgumentException("external key locator hint too long for a version "
327+
+ version + " secret key: " + hint.length + " > " + MAX_V6_EXTERNAL_LOCATOR_HINT_LEN);
328+
}
329+
}
330+
else if (hint.length > MAX_EXTERNAL_LOCATOR_HINT_LEN)
331+
{
332+
throw new IllegalArgumentException("external key locator hint too long: " + hint.length
333+
+ " > " + MAX_EXTERNAL_LOCATOR_HINT_LEN);
334+
}
335+
this.externalKeyLocatorHint = hint;
336+
}
337+
236338
/**
237339
* Construct a {@link SecretKeyPacket}.
238340
* Note: <pre>secKeyData</pre> needs to be prepared by applying encryption/checksum beforehand.
@@ -426,6 +528,24 @@ public byte[] getSecretKeyData()
426528
return secKeyData;
427529
}
428530

531+
/**
532+
* If the key has external private key material (s2k usage {@link #USAGE_EXTERNAL}), return the locator hint data.
533+
* If the locator hint is empty, it is referred to as "best effort".
534+
* Otherwise, the first octet indicates the type of locator hint.
535+
*
536+
* @see <a href="https://www.ietf.org/archive/id/draft-dkg-openpgp-external-secrets-03.html#name-openpgp-external-secret-key">
537+
* OpenPGP External Secret Key Locator Hint type registry</a>
538+
* @return locator hints data
539+
*/
540+
public byte[] getExternalKeyLocatorHint()
541+
{
542+
if (s2kUsage != USAGE_EXTERNAL)
543+
{
544+
return null;
545+
}
546+
return Arrays.clone(externalKeyLocatorHint);
547+
}
548+
429549
/**
430550
* Return the encoded packet content without packet frame.
431551
* @return encoded packet contents
@@ -443,7 +563,7 @@ public byte[] getEncodedContents()
443563

444564
// conditional parameters
445565
byte[] conditionalParameters = encodeConditionalParameters();
446-
if (pubKeyPacket.getVersion() == PublicKeyPacket.LIBREPGP_5 ||
566+
if (pubKeyPacket.getVersion() == PublicKeyPacket.LIBREPGP_5 ||
447567
(pubKeyPacket.getVersion() == PublicKeyPacket.VERSION_6 && s2kUsage != USAGE_NONE))
448568
{
449569
pOut.write(conditionalParameters.length);
@@ -474,6 +594,12 @@ public byte[] getEncodedContents()
474594
private byte[] encodeConditionalParameters()
475595
throws IOException
476596
{
597+
if (s2kUsage == USAGE_EXTERNAL)
598+
{
599+
// for an external key the conditional parameters are exactly the locator hint
600+
return Arrays.clone(externalKeyLocatorHint);
601+
}
602+
477603
ByteArrayOutputStream conditionalParameters = new ByteArrayOutputStream();
478604
boolean hasS2KSpecifier = s2kUsage == USAGE_CHECKSUM || s2kUsage == USAGE_SHA1 || s2kUsage == USAGE_AEAD;
479605

0 commit comments

Comments
 (0)