Skip to content

Commit 5fbf4b0

Browse files
committed
Implement support for OpenPGP External Secret Keys
See https://datatracker.ietf.org/doc/draft-dkg-openpgp-external-secrets/
1 parent d3d00d9 commit 5fbf4b0

5 files changed

Lines changed: 438 additions & 5 deletions

File tree

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

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,25 @@ public class SecretKeyPacket
6363
* Users should migrate to AEAD with all due speed.
6464
*/
6565
public static final int USAGE_AEAD = 0xfd;
66-
66+
67+
/**
68+
* Externally-backed secret key material.
69+
* S2K-usage octet indicating that the secret key material is stored externally, e.g. on a hardware device.
70+
* The draft specification is an alternative to GnuPGs proprietary {@link S2K#GNU_DUMMY_S2K} mechanism.
71+
*
72+
* @see <a href="https://datatracker.ietf.org/doc/draft-dkg-openpgp-external-secrets/">
73+
* OpenPGP External Secret Keys</a>
74+
*/
75+
public static final int USAGE_EXTERNAL = 0xfc;
76+
6777
private PublicKeyPacket pubKeyPacket;
6878
private byte[] secKeyData;
6979
private int s2kUsage;
7080
private int encAlgorithm;
7181
private int aeadAlgorithm;
7282
private S2K s2k;
7383
private byte[] iv;
84+
private byte[] externalKeyLocatorHint;
7485

7586
/**
7687
* Parse a primary OpenPGP secret key packet from the given OpenPGP {@link BCPGInputStream}.
@@ -140,13 +151,19 @@ public class SecretKeyPacket
140151
s2kUsage = in.read();
141152

142153
int conditionalParameterLength = -1;
143-
if (version == PublicKeyPacket.LIBREPGP_5 ||
154+
if (version == PublicKeyPacket.LIBREPGP_5 ||
144155
(version == PublicKeyPacket.VERSION_6 && s2kUsage != USAGE_NONE))
145156
{
146157
// TODO: Use length to parse unknown parameters
147158
conditionalParameterLength = in.read();
148159
}
149160

161+
if (s2kUsage == USAGE_EXTERNAL)
162+
{
163+
externalKeyLocatorHint = in.readAll();
164+
return;
165+
}
166+
150167
if (s2kUsage == USAGE_CHECKSUM || s2kUsage == USAGE_SHA1 || s2kUsage == USAGE_AEAD)
151168
{
152169
encAlgorithm = in.read();
@@ -205,7 +222,7 @@ public class SecretKeyPacket
205222
if (encAlgorithm < 7)
206223
{
207224
iv = new byte[8];
208-
}
225+
}
209226
else
210227
{
211228
iv = new byte[16];
@@ -214,7 +231,7 @@ public class SecretKeyPacket
214231
}
215232
}
216233
}
217-
234+
218235
if (version == PublicKeyPacket.LIBREPGP_5)
219236
{
220237
long keyOctetCount = ((long) in.read() << 24) | ((long) in.read() << 16) | ((long) in.read() << 8) | in.read();
@@ -233,6 +250,40 @@ public class SecretKeyPacket
233250
}
234251
}
235252

253+
/**
254+
* Create a SecretKeyPacket representing an external secret key ({@link #USAGE_EXTERNAL}).
255+
*
256+
* @see <a href="https://datatracker.ietf.org/doc/draft-dkg-openpgp-external-secrets/">
257+
* OpenPGP External Secret Keys</a>
258+
* @param pubKeyPacket public key packet
259+
* @param locatorHint optional external key locator hint
260+
*/
261+
public SecretKeyPacket(
262+
PublicKeyPacket pubKeyPacket,
263+
byte[] locatorHint)
264+
{
265+
this(SECRET_KEY, pubKeyPacket, locatorHint);
266+
}
267+
268+
269+
/**
270+
* Create a SecretKeyPacket representing an external secret key ({@link #USAGE_EXTERNAL}).
271+
*
272+
* @see <a href="https://datatracker.ietf.org/doc/draft-dkg-openpgp-external-secrets/">
273+
* OpenPGP External Secret Keys</a>
274+
* @param keyTag key packet type
275+
* @param pubKeyPacket public key packet
276+
* @param locatorHint optional external key locator hint
277+
*/
278+
protected SecretKeyPacket(
279+
int keyTag,
280+
PublicKeyPacket pubKeyPacket,
281+
byte[] locatorHint)
282+
{
283+
this(keyTag, pubKeyPacket, 0, 0, USAGE_EXTERNAL, null, null, null);
284+
this.externalKeyLocatorHint = locatorHint == null ? new byte[0] : Arrays.clone(locatorHint);
285+
}
286+
236287
/**
237288
* Construct a {@link SecretKeyPacket}.
238289
* Note: <pre>secKeyData</pre> needs to be prepared by applying encryption/checksum beforehand.
@@ -426,6 +477,27 @@ public byte[] getSecretKeyData()
426477
return secKeyData;
427478
}
428479

480+
/**
481+
* If the key has external private key material (s2k usage {@link #USAGE_EXTERNAL}), return the locator hint data.
482+
* If the locator hint is empty, it is referred to as "best effort".
483+
* Otherwise, the first octet indicates the type of locator hint.
484+
*
485+
* @see <a href="https://www.ietf.org/archive/id/draft-dkg-openpgp-external-secrets-02.html#name-openpgp-external-secret-key">
486+
* OpenPGP External Secret Key Locator Hint type registry</a>
487+
* @return locator hints data
488+
*/
489+
public byte[] getExternalKeyLocatorHint()
490+
{
491+
if (s2kUsage == USAGE_EXTERNAL)
492+
{
493+
return externalKeyLocatorHint;
494+
}
495+
else
496+
{
497+
return null;
498+
}
499+
}
500+
429501
/**
430502
* Return the encoded packet content without packet frame.
431503
* @return encoded packet contents
@@ -443,7 +515,7 @@ public byte[] getEncodedContents()
443515

444516
// conditional parameters
445517
byte[] conditionalParameters = encodeConditionalParameters();
446-
if (pubKeyPacket.getVersion() == PublicKeyPacket.LIBREPGP_5 ||
518+
if (pubKeyPacket.getVersion() == PublicKeyPacket.LIBREPGP_5 ||
447519
(pubKeyPacket.getVersion() == PublicKeyPacket.VERSION_6 && s2kUsage != USAGE_NONE))
448520
{
449521
pOut.write(conditionalParameters.length);
@@ -476,6 +548,10 @@ private byte[] encodeConditionalParameters()
476548
{
477549
ByteArrayOutputStream conditionalParameters = new ByteArrayOutputStream();
478550
boolean hasS2KSpecifier = s2kUsage == USAGE_CHECKSUM || s2kUsage == USAGE_SHA1 || s2kUsage == USAGE_AEAD;
551+
if (s2kUsage == USAGE_EXTERNAL)
552+
{
553+
return getExternalKeyLocatorHint();
554+
}
479555

480556
if (hasS2KSpecifier)
481557
{

pg/src/main/java/org/bouncycastle/bcpg/SecretSubkeyPacket.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,23 @@ public class SecretSubkeyPacket
2828
{
2929
super(SECRET_SUBKEY, in, newPacketFormat);
3030
}
31+
32+
/**
33+
* Create a SecretSubkeyPacket which has external private key material.
34+
*
35+
* @see <a href="https://datatracker.ietf.org/doc/draft-dkg-openpgp-external-secrets/">
36+
* OpenPGP External Secret Keys</a>
37+
*
38+
* @param publicKeyPacket public key material
39+
* @param locatorHints optional external key locator hints
40+
*/
41+
public SecretSubkeyPacket(
42+
PublicSubkeyPacket publicKeyPacket,
43+
byte[] locatorHints)
44+
{
45+
super(SECRET_SUBKEY, publicKeyPacket, locatorHints);
46+
}
47+
3148
/**
3249
* Create a secret subkey packet.
3350
* If the encryption algorithm is NOT {@link SymmetricKeyAlgorithmTags#NULL},

pg/src/main/java/org/bouncycastle/openpgp/PGPSecretKey.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,11 +446,26 @@ public boolean isMasterKey()
446446
*/
447447
public boolean isPrivateKeyEmpty()
448448
{
449+
if (isExternalKey())
450+
{
451+
return true;
452+
}
453+
449454
byte[] secKeyData = secret.getSecretKeyData();
450455

451456
return (secKeyData == null || secKeyData.length < 1);
452457
}
453458

459+
public boolean isExternalKey()
460+
{
461+
return secret.getS2KUsage() == SecretKeyPacket.USAGE_EXTERNAL;
462+
}
463+
464+
public byte[] getExternalKeyLocatorHint()
465+
{
466+
return secret.getExternalKeyLocatorHint();
467+
}
468+
454469
/**
455470
* return the algorithm the key is encrypted with.
456471
*
@@ -510,6 +525,7 @@ public byte[] getFingerprint()
510525
* <li>{@link SecretKeyPacket#USAGE_CHECKSUM}: Password-protected using malleable CFB (deprecated)</li>
511526
* <li>{@link SecretKeyPacket#USAGE_SHA1}: Password-protected using CFB</li>
512527
* <li>{@link SecretKeyPacket#USAGE_AEAD}: Password-protected using AEAD (recommended)</li>
528+
* <li>{@link SecretKeyPacket#USAGE_EXTERNAL}: Externally-backed private key, e.g. hardware token</li>
513529
* </ul>
514530
*
515531
* @return the key's S2K usage
@@ -564,6 +580,11 @@ private byte[] extractKeyData(PBESecretKeyDecryptor decryptorFactory)
564580
{
565581
byte[] encData = secret.getSecretKeyData();
566582

583+
if (isExternalKey())
584+
{
585+
throw new PGPException("Key is externally-backed and key-data cannot be extracted.");
586+
}
587+
567588
if (secret.getEncAlgorithm() == SymmetricKeyAlgorithmTags.NULL)
568589
{
569590
return encData;

0 commit comments

Comments
 (0)