Skip to content

Commit 29578f0

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

5 files changed

Lines changed: 438 additions & 6 deletions

File tree

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

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

77
import org.bouncycastle.util.Arrays;
8-
import org.bouncycastle.util.io.Streams;
98

109
/**
1110
* Base class for OpenPGP secret (primary) keys.
@@ -64,14 +63,25 @@ public class SecretKeyPacket
6463
* Users should migrate to AEAD with all due speed.
6564
*/
6665
public static final int USAGE_AEAD = 0xfd;
67-
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+
6877
private PublicKeyPacket pubKeyPacket;
6978
private byte[] secKeyData;
7079
private int s2kUsage;
7180
private int encAlgorithm;
7281
private int aeadAlgorithm;
7382
private S2K s2k;
7483
private byte[] iv;
84+
private byte[] externalKeyLocatorHint;
7585

7686
/**
7787
* Parse a primary OpenPGP secret key packet from the given OpenPGP {@link BCPGInputStream}.
@@ -159,13 +169,19 @@ public class SecretKeyPacket
159169
s2kUsage = in.read();
160170

161171
int conditionalParameterLength = -1;
162-
if (version == PublicKeyPacket.LIBREPGP_5 ||
172+
if (version == PublicKeyPacket.LIBREPGP_5 ||
163173
(version == PublicKeyPacket.VERSION_6 && s2kUsage != USAGE_NONE))
164174
{
165175
// TODO: Use length to parse unknown parameters
166176
conditionalParameterLength = in.read();
167177
}
168178

179+
if (s2kUsage == USAGE_EXTERNAL)
180+
{
181+
externalKeyLocatorHint = in.readAll();
182+
return;
183+
}
184+
169185
if (s2kUsage == USAGE_CHECKSUM || s2kUsage == USAGE_SHA1 || s2kUsage == USAGE_AEAD)
170186
{
171187
encAlgorithm = in.read();
@@ -224,7 +240,7 @@ public class SecretKeyPacket
224240
if (encAlgorithm < 7)
225241
{
226242
iv = new byte[8];
227-
}
243+
}
228244
else
229245
{
230246
iv = new byte[16];
@@ -233,7 +249,7 @@ public class SecretKeyPacket
233249
}
234250
}
235251
}
236-
252+
237253
if (version == PublicKeyPacket.LIBREPGP_5)
238254
{
239255
long keyOctetCount = ((long) in.read() << 24) | ((long) in.read() << 16) | ((long) in.read() << 8) | in.read();
@@ -252,6 +268,40 @@ public class SecretKeyPacket
252268
}
253269
}
254270

271+
/**
272+
* Create a SecretKeyPacket representing an external secret key ({@link #USAGE_EXTERNAL}).
273+
*
274+
* @see <a href="https://datatracker.ietf.org/doc/draft-dkg-openpgp-external-secrets/">
275+
* OpenPGP External Secret Keys</a>
276+
* @param pubKeyPacket public key packet
277+
* @param locatorHint optional external key locator hint
278+
*/
279+
public SecretKeyPacket(
280+
PublicKeyPacket pubKeyPacket,
281+
byte[] locatorHint)
282+
{
283+
this(SECRET_KEY, pubKeyPacket, locatorHint);
284+
}
285+
286+
287+
/**
288+
* Create a SecretKeyPacket representing an external secret key ({@link #USAGE_EXTERNAL}).
289+
*
290+
* @see <a href="https://datatracker.ietf.org/doc/draft-dkg-openpgp-external-secrets/">
291+
* OpenPGP External Secret Keys</a>
292+
* @param keyTag key packet type
293+
* @param pubKeyPacket public key packet
294+
* @param locatorHint optional external key locator hint
295+
*/
296+
protected SecretKeyPacket(
297+
int keyTag,
298+
PublicKeyPacket pubKeyPacket,
299+
byte[] locatorHint)
300+
{
301+
this(keyTag, pubKeyPacket, 0, 0, USAGE_EXTERNAL, null, null, null);
302+
this.externalKeyLocatorHint = locatorHint == null ? new byte[0] : Arrays.clone(locatorHint);
303+
}
304+
255305
/**
256306
* Construct a {@link SecretKeyPacket}.
257307
* Note: <pre>secKeyData</pre> needs to be prepared by applying encryption/checksum beforehand.
@@ -445,6 +495,27 @@ public byte[] getSecretKeyData()
445495
return secKeyData;
446496
}
447497

498+
/**
499+
* If the key has external private key material (s2k usage {@link #USAGE_EXTERNAL}), return the locator hint data.
500+
* If the locator hint is empty, it is referred to as "best effort".
501+
* Otherwise, the first octet indicates the type of locator hint.
502+
*
503+
* @see <a href="https://www.ietf.org/archive/id/draft-dkg-openpgp-external-secrets-02.html#name-openpgp-external-secret-key">
504+
* OpenPGP External Secret Key Locator Hint type registry</a>
505+
* @return locator hints data
506+
*/
507+
public byte[] getExternalKeyLocatorHint()
508+
{
509+
if (s2kUsage == USAGE_EXTERNAL)
510+
{
511+
return externalKeyLocatorHint;
512+
}
513+
else
514+
{
515+
return null;
516+
}
517+
}
518+
448519
/**
449520
* Return the encoded packet content without packet frame.
450521
* @return encoded packet contents
@@ -462,7 +533,7 @@ public byte[] getEncodedContents()
462533

463534
// conditional parameters
464535
byte[] conditionalParameters = encodeConditionalParameters();
465-
if (pubKeyPacket.getVersion() == PublicKeyPacket.LIBREPGP_5 ||
536+
if (pubKeyPacket.getVersion() == PublicKeyPacket.LIBREPGP_5 ||
466537
(pubKeyPacket.getVersion() == PublicKeyPacket.VERSION_6 && s2kUsage != USAGE_NONE))
467538
{
468539
pOut.write(conditionalParameters.length);
@@ -495,6 +566,10 @@ private byte[] encodeConditionalParameters()
495566
{
496567
ByteArrayOutputStream conditionalParameters = new ByteArrayOutputStream();
497568
boolean hasS2KSpecifier = s2kUsage == USAGE_CHECKSUM || s2kUsage == USAGE_SHA1 || s2kUsage == USAGE_AEAD;
569+
if (s2kUsage == USAGE_EXTERNAL)
570+
{
571+
return getExternalKeyLocatorHint();
572+
}
498573

499574
if (hasS2KSpecifier)
500575
{

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)