diff --git a/pg/src/main/java/org/bouncycastle/bcpg/SecretKeyPacket.java b/pg/src/main/java/org/bouncycastle/bcpg/SecretKeyPacket.java
index 826b9bc4ba..b91f14e459 100644
--- a/pg/src/main/java/org/bouncycastle/bcpg/SecretKeyPacket.java
+++ b/pg/src/main/java/org/bouncycastle/bcpg/SecretKeyPacket.java
@@ -5,7 +5,6 @@
import java.io.IOException;
import org.bouncycastle.util.Arrays;
-import org.bouncycastle.util.io.Streams;
/**
* Base class for OpenPGP secret (primary) keys.
@@ -64,7 +63,17 @@ public class SecretKeyPacket
* Users should migrate to AEAD with all due speed.
*/
public static final int USAGE_AEAD = 0xfd;
-
+
+ /**
+ * Externally-backed secret key material.
+ * S2K-usage octet indicating that the secret key material is stored externally, e.g. on a hardware device.
+ * The draft specification is an alternative to GnuPGs proprietary {@link S2K#GNU_DUMMY_S2K} mechanism.
+ *
+ * @see
+ * OpenPGP External Secret Keys
+ */
+ public static final int USAGE_EXTERNAL = 0xfc;
+
private PublicKeyPacket pubKeyPacket;
private byte[] secKeyData;
private int s2kUsage;
@@ -72,6 +81,7 @@ public class SecretKeyPacket
private int aeadAlgorithm;
private S2K s2k;
private byte[] iv;
+ private byte[] externalKeyLocatorHint;
/**
* Parse a primary OpenPGP secret key packet from the given OpenPGP {@link BCPGInputStream}.
@@ -159,13 +169,19 @@ public class SecretKeyPacket
s2kUsage = in.read();
int conditionalParameterLength = -1;
- if (version == PublicKeyPacket.LIBREPGP_5 ||
+ if (version == PublicKeyPacket.LIBREPGP_5 ||
(version == PublicKeyPacket.VERSION_6 && s2kUsage != USAGE_NONE))
{
// TODO: Use length to parse unknown parameters
conditionalParameterLength = in.read();
}
+ if (s2kUsage == USAGE_EXTERNAL)
+ {
+ externalKeyLocatorHint = in.readAll();
+ return;
+ }
+
if (s2kUsage == USAGE_CHECKSUM || s2kUsage == USAGE_SHA1 || s2kUsage == USAGE_AEAD)
{
encAlgorithm = in.read();
@@ -224,7 +240,7 @@ public class SecretKeyPacket
if (encAlgorithm < 7)
{
iv = new byte[8];
- }
+ }
else
{
iv = new byte[16];
@@ -233,7 +249,7 @@ public class SecretKeyPacket
}
}
}
-
+
if (version == PublicKeyPacket.LIBREPGP_5)
{
long keyOctetCount = ((long) in.read() << 24) | ((long) in.read() << 16) | ((long) in.read() << 8) | in.read();
@@ -252,6 +268,40 @@ public class SecretKeyPacket
}
}
+ /**
+ * Create a SecretKeyPacket representing an external secret key ({@link #USAGE_EXTERNAL}).
+ *
+ * @see
+ * OpenPGP External Secret Keys
+ * @param pubKeyPacket public key packet
+ * @param locatorHint optional external key locator hint
+ */
+ public SecretKeyPacket(
+ PublicKeyPacket pubKeyPacket,
+ byte[] locatorHint)
+ {
+ this(SECRET_KEY, pubKeyPacket, locatorHint);
+ }
+
+
+ /**
+ * Create a SecretKeyPacket representing an external secret key ({@link #USAGE_EXTERNAL}).
+ *
+ * @see
+ * OpenPGP External Secret Keys
+ * @param keyTag key packet type
+ * @param pubKeyPacket public key packet
+ * @param locatorHint optional external key locator hint
+ */
+ protected SecretKeyPacket(
+ int keyTag,
+ PublicKeyPacket pubKeyPacket,
+ byte[] locatorHint)
+ {
+ this(keyTag, pubKeyPacket, 0, 0, USAGE_EXTERNAL, null, null, null);
+ this.externalKeyLocatorHint = locatorHint == null ? new byte[0] : Arrays.clone(locatorHint);
+ }
+
/**
* Construct a {@link SecretKeyPacket}.
* Note:
secKeyData
needs to be prepared by applying encryption/checksum beforehand.
@@ -445,6 +495,27 @@ public byte[] getSecretKeyData()
return secKeyData;
}
+ /**
+ * If the key has external private key material (s2k usage {@link #USAGE_EXTERNAL}), return the locator hint data.
+ * If the locator hint is empty, it is referred to as "best effort".
+ * Otherwise, the first octet indicates the type of locator hint.
+ *
+ * @see
+ * OpenPGP External Secret Key Locator Hint type registry
+ * @return locator hints data
+ */
+ public byte[] getExternalKeyLocatorHint()
+ {
+ if (s2kUsage == USAGE_EXTERNAL)
+ {
+ return externalKeyLocatorHint;
+ }
+ else
+ {
+ return null;
+ }
+ }
+
/**
* Return the encoded packet content without packet frame.
* @return encoded packet contents
@@ -462,7 +533,7 @@ public byte[] getEncodedContents()
// conditional parameters
byte[] conditionalParameters = encodeConditionalParameters();
- if (pubKeyPacket.getVersion() == PublicKeyPacket.LIBREPGP_5 ||
+ if (pubKeyPacket.getVersion() == PublicKeyPacket.LIBREPGP_5 ||
(pubKeyPacket.getVersion() == PublicKeyPacket.VERSION_6 && s2kUsage != USAGE_NONE))
{
pOut.write(conditionalParameters.length);
@@ -495,6 +566,10 @@ private byte[] encodeConditionalParameters()
{
ByteArrayOutputStream conditionalParameters = new ByteArrayOutputStream();
boolean hasS2KSpecifier = s2kUsage == USAGE_CHECKSUM || s2kUsage == USAGE_SHA1 || s2kUsage == USAGE_AEAD;
+ if (s2kUsage == USAGE_EXTERNAL)
+ {
+ return getExternalKeyLocatorHint();
+ }
if (hasS2KSpecifier)
{
diff --git a/pg/src/main/java/org/bouncycastle/bcpg/SecretSubkeyPacket.java b/pg/src/main/java/org/bouncycastle/bcpg/SecretSubkeyPacket.java
index 910d7b2f5b..4c09ec462f 100644
--- a/pg/src/main/java/org/bouncycastle/bcpg/SecretSubkeyPacket.java
+++ b/pg/src/main/java/org/bouncycastle/bcpg/SecretSubkeyPacket.java
@@ -28,6 +28,23 @@ public class SecretSubkeyPacket
{
super(SECRET_SUBKEY, in, newPacketFormat);
}
+
+ /**
+ * Create a SecretSubkeyPacket which has external private key material.
+ *
+ * @see
+ * OpenPGP External Secret Keys
+ *
+ * @param publicKeyPacket public key material
+ * @param locatorHints optional external key locator hints
+ */
+ public SecretSubkeyPacket(
+ PublicSubkeyPacket publicKeyPacket,
+ byte[] locatorHints)
+ {
+ super(SECRET_SUBKEY, publicKeyPacket, locatorHints);
+ }
+
/**
* Create a secret subkey packet.
* If the encryption algorithm is NOT {@link SymmetricKeyAlgorithmTags#NULL},
diff --git a/pg/src/main/java/org/bouncycastle/openpgp/PGPSecretKey.java b/pg/src/main/java/org/bouncycastle/openpgp/PGPSecretKey.java
index ed6a6c9af7..a080566855 100644
--- a/pg/src/main/java/org/bouncycastle/openpgp/PGPSecretKey.java
+++ b/pg/src/main/java/org/bouncycastle/openpgp/PGPSecretKey.java
@@ -446,11 +446,26 @@ public boolean isMasterKey()
*/
public boolean isPrivateKeyEmpty()
{
+ if (isExternalKey())
+ {
+ return true;
+ }
+
byte[] secKeyData = secret.getSecretKeyData();
return (secKeyData == null || secKeyData.length < 1);
}
+ public boolean isExternalKey()
+ {
+ return secret.getS2KUsage() == SecretKeyPacket.USAGE_EXTERNAL;
+ }
+
+ public byte[] getExternalKeyLocatorHint()
+ {
+ return secret.getExternalKeyLocatorHint();
+ }
+
/**
* return the algorithm the key is encrypted with.
*
@@ -510,6 +525,7 @@ public byte[] getFingerprint()
* {@link SecretKeyPacket#USAGE_CHECKSUM}: Password-protected using malleable CFB (deprecated)
* {@link SecretKeyPacket#USAGE_SHA1}: Password-protected using CFB
* {@link SecretKeyPacket#USAGE_AEAD}: Password-protected using AEAD (recommended)
+ * {@link SecretKeyPacket#USAGE_EXTERNAL}: Externally-backed private key, e.g. hardware token
*
*
* @return the key's S2K usage
@@ -564,6 +580,11 @@ private byte[] extractKeyData(PBESecretKeyDecryptor decryptorFactory)
{
byte[] encData = secret.getSecretKeyData();
+ if (isExternalKey())
+ {
+ throw new PGPException("Key is externally-backed and key-data cannot be extracted.");
+ }
+
if (secret.getEncAlgorithm() == SymmetricKeyAlgorithmTags.NULL)
{
return encData;
diff --git a/pg/src/test/java/org/bouncycastle/bcpg/test/ExternalSecretKeyPacketTest.java b/pg/src/test/java/org/bouncycastle/bcpg/test/ExternalSecretKeyPacketTest.java
new file mode 100644
index 0000000000..1ef189e611
--- /dev/null
+++ b/pg/src/test/java/org/bouncycastle/bcpg/test/ExternalSecretKeyPacketTest.java
@@ -0,0 +1,249 @@
+package org.bouncycastle.bcpg.test;
+
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.bcpg.ArmoredOutputStream;
+import org.bouncycastle.bcpg.BCPGInputStream;
+import org.bouncycastle.bcpg.BCPGOutputStream;
+import org.bouncycastle.bcpg.ContainedPacket;
+import org.bouncycastle.bcpg.PacketFormat;
+import org.bouncycastle.bcpg.PublicSubkeyPacket;
+import org.bouncycastle.bcpg.SecretKeyPacket;
+import org.bouncycastle.bcpg.SecretSubkeyPacket;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+public class ExternalSecretKeyPacketTest
+ extends AbstractPacketTest
+{
+ /**
+ * Example transferable secret key test vector. It includes unencrypted private key material for both
+ * its primary and subkey.
+ *
+ * @see
+ * Example Transferable Secret Key Test Vector
+ */
+ private static final String V4_TSK = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
+ "\n" +
+ "xVgEZgWtcxYJKwYBBAHaRw8BAQdAlLK6UPQsVHR2ETk1SwVIG3tBmpiEtikYYlCy\n" +
+ "1TIiqzYAAQCwm/O5cWsztxbUcwOHycBwszHpD4Oa+fK8XJDxLWH7dRIZzR08aGFy\n" +
+ "ZHdhcmUtc2VjcmV0QGV4YW1wbGUub3JnPsKNBBAWCAA1AhkBBQJmBa1zAhsDCAsJ\n" +
+ "CAcKDQwLBRUKCQgLAhYCFiEEXlP8Tur0WZR+f0I33/i9Uh4OHEkACgkQ3/i9Uh4O\n" +
+ "HEnryAD8CzH2ajJvASp46ApfI4pLPY57rjBX++d/2FQPRyqGHJUA/RLsNNgxiFYm\n" +
+ "K5cjtQe2/DgzWQ7R6PxPC6oa3XM7xPcCx10EZgWtcxIKKwYBBAGXVQEFAQEHQE1Y\n" +
+ "XOKeaklwG01Yab4xopP9wbu1E+pCrP1xQpiFZW5KAwEIBwAA/12uOubAQ5nhf1UF\n" +
+ "a51SQwFLpggB/Spn29qDnSQXOTzIDvPCeAQYFggAIAUCZgWtcwIbDBYhBF5T/E7q\n" +
+ "9FmUfn9CN9/4vVIeDhxJAAoJEN/4vVIeDhxJVTgA/1WaFrKdP3AgL0Ffdooc5XXb\n" +
+ "jQsj0uHo6FZSHRI4pchMAQCyJnKQ3RvW/0gm41JCqImyg2fxWG4hY0N5Q7Rc6Pyz\n" +
+ "DQ==\n" +
+ "=lYbx\n" +
+ "-----END PGP PRIVATE KEY BLOCK-----\n";
+ /**
+ * The same TSK as {@link #V4_TSK}, but with external secret keys for both the primary and subkey.
+ *
+ * @see
+ * External Key Test Vector
+ */
+ private static final String V4_TSK_AS_EXTERNAL_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
+ "\n" +
+ "xTQEZgWtcxYJKwYBBAHaRw8BAQdAlLK6UPQsVHR2ETk1SwVIG3tBmpiEtikYYlCy\n" +
+ "1TIiqzb8zR08aGFyZHdhcmUtc2VjcmV0QGV4YW1wbGUub3JnPsKNBBAWCAA1AhkB\n" +
+ "BQJmBa1zAhsDCAsJCAcKDQwLBRUKCQgLAhYCFiEEXlP8Tur0WZR+f0I33/i9Uh4O\n" +
+ "HEkACgkQ3/i9Uh4OHEnryAD8CzH2ajJvASp46ApfI4pLPY57rjBX++d/2FQPRyqG\n" +
+ "HJUA/RLsNNgxiFYmK5cjtQe2/DgzWQ7R6PxPC6oa3XM7xPcCxzkEZgWtcxIKKwYB\n" +
+ "BAGXVQEFAQEHQE1YXOKeaklwG01Yab4xopP9wbu1E+pCrP1xQpiFZW5KAwEIB/zC\n" +
+ "eAQYFggAIAUCZgWtcwIbDBYhBF5T/E7q9FmUfn9CN9/4vVIeDhxJAAoJEN/4vVIe\n" +
+ "DhxJVTgA/1WaFrKdP3AgL0Ffdooc5XXbjQsj0uHo6FZSHRI4pchMAQCyJnKQ3RvW\n" +
+ "/0gm41JCqImyg2fxWG4hY0N5Q7Rc6PyzDQ==\n" +
+ "=3w/O\n" +
+ "-----END PGP PRIVATE KEY BLOCK-----\n";
+
+ /**
+ * @see
+ * RFC9580: Sample Version 6 Secret Key
+ */
+ private static final String V6_TSK = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
+ "\n" +
+ "xYIGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laP9JgkC\n" +
+ "FARdb9ccngltHraRe25uHuyuAQQVtKipJ0+r5jL4dacGWSAheCWPpITYiyfyIOPS\n" +
+ "3gIDyg8f7strd1OB4+LZsUhcIjOMpVHgmiY/IutJkulneoBYwrEGHxsKAAAAQgWC\n" +
+ "Y4d/4wMLCQcFFQoOCAwCFgACmwMCHgkiIQbLGGxPBgmml+TVLfpscisMHx4nwYpW\n" +
+ "cI9lJewnutmsyQUnCQIHAgAAAACtKCAQPi19In7A5tfORHHbNr/JcIMlNpAnFJin\n" +
+ "7wV2wH+q4UWFs7kDsBJ+xP2i8CMEWi7Ha8tPlXGpZR4UruETeh1mhELIj5UeM8T/\n" +
+ "0z+5oX1RHu11j8bZzFDLX9eTsgOdWATHggZjh3/jGQAAACCGkySDZ/nlAV25Ivj0\n" +
+ "gJXdp4SYfy1ZhbEvutFsr15ENf0mCQIUBA5hhGgp2oaavg6mFUXcFMwBBBUuE8qf\n" +
+ "9Ock+xwusd+GAglBr5LVyr/lup3xxQvHXFSjjA2haXfoN6xUGRdDEHI6+uevKjVR\n" +
+ "v5oAxgu7eJpaXNjCmwYYGwoAAAAsBYJjh3/jApsMIiEGyxhsTwYJppfk1S36bHIr\n" +
+ "DB8eJ8GKVnCPZSXsJ7rZrMkAAAAABAEgpukYbZ1ZNfyP5WMUzbUnSGpaUSD5t2Ki\n" +
+ "Nacp8DkBClZRa2c3AMQzSDXa9jGhYzxjzVb5scHDzTkjyRZWRdTq8U6L4da+/+Kt\n" +
+ "ruh8m7Xo2ehSSFyWRSuTSZe5tm/KXgYG\n" +
+ "-----END PGP PRIVATE KEY BLOCK-----";
+
+ private static final String V6_TSK_AS_EXTERNAL_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
+ "\n" +
+ "xSwGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laP8AMKx\n" +
+ "Bh8bCgAAAEIFgmOHf+MDCwkHBRUKDggMAhYAApsDAh4JIiEGyxhsTwYJppfk1S36\n" +
+ "bHIrDB8eJ8GKVnCPZSXsJ7rZrMkFJwkCBwIAAAAArSggED4tfSJ+wObXzkRx2za/\n" +
+ "yXCDJTaQJxSYp+8FdsB/quFFhbO5A7ASfsT9ovAjBFoux2vLT5VxqWUeFK7hE3od\n" +
+ "ZoRCyI+VHjPE/9M/uaF9UR7tdY/G2cxQy1/Xk7IDnVgExywGY4d/4xkAAAAghpMk\n" +
+ "g2f55QFduSL49ICV3aeEmH8tWYWxL7rRbK9eRDX8AMKbBhgbCgAAACwFgmOHf+MC\n" +
+ "mwwiIQbLGGxPBgmml+TVLfpscisMHx4nwYpWcI9lJewnutmsyQAAAAAEASCm6Rht\n" +
+ "nVk1/I/lYxTNtSdIalpRIPm3YqI1pynwOQEKVlFrZzcAxDNINdr2MaFjPGPNVvmx\n" +
+ "wcPNOSPJFlZF1OrxTovh1r7/4q2u6HybtejZ6FJIXJZFK5NJl7m2b8peBgY=\n" +
+ "=1veT\n" +
+ "-----END PGP PRIVATE KEY BLOCK-----";
+
+ @Override
+ public String getName()
+ {
+ return "ExternalSecretKeyPacketTest";
+ }
+
+ @Override
+ public void performTest()
+ throws Exception
+ {
+ testPacketRoundTripping();
+ testV4PacketProperties();
+ testV6PacketProperties();
+ }
+
+ private String toExternalKey(String asciiArmoredKey) throws IOException {
+ ByteArrayInputStream bIn = new ByteArrayInputStream(asciiArmoredKey.getBytes(StandardCharsets.UTF_8));
+ ArmoredInputStream aIn = ArmoredInputStream.builder().build(bIn);
+ BCPGInputStream pIn = new BCPGInputStream(aIn);
+
+ ByteArrayOutputStream bOut = new ByteArrayOutputStream();
+ ArmoredOutputStream aOut = ArmoredOutputStream.builder().build(bOut);
+ BCPGOutputStream pOut = new BCPGOutputStream(aOut, PacketFormat.ROUNDTRIP);
+
+ ContainedPacket p;
+ while ((p = (ContainedPacket) pIn.readPacket()) != null)
+ {
+ if (p instanceof SecretSubkeyPacket)
+ {
+ SecretSubkeyPacket s = (SecretSubkeyPacket) p;
+ p = new SecretSubkeyPacket((PublicSubkeyPacket) s.getPublicKeyPacket(), new byte[0]);
+ }
+ else if (p instanceof SecretKeyPacket)
+ {
+ SecretKeyPacket s = (SecretKeyPacket) p;
+ p = new SecretKeyPacket(s.getPublicKeyPacket(), new byte[0]);
+ }
+ p.encode(pOut);
+ }
+
+ pOut.close();
+ aOut.close();
+
+ return bOut.toString();
+ }
+
+ private void testV4PacketProperties()
+ throws IOException
+ {
+ ByteArrayInputStream bIn = new ByteArrayInputStream(V4_TSK_AS_EXTERNAL_KEY.getBytes());
+ ArmoredInputStream aIn = ArmoredInputStream.builder()
+ .build(bIn);
+ BCPGInputStream pIn = new BCPGInputStream(aIn);
+
+ SecretKeyPacket primaryKey = (SecretKeyPacket) pIn.readPacket();
+ pIn.readPacket(); // skip uid
+ pIn.readPacket(); // skip uid sig
+ SecretSubkeyPacket subkey = (SecretSubkeyPacket) pIn.readPacket();
+
+ isTrue(primaryKey.getS2KUsage() == SecretKeyPacket.USAGE_EXTERNAL);
+ isTrue(subkey.getS2KUsage() == SecretKeyPacket.USAGE_EXTERNAL);
+ isEncodingEqual(new byte[0], primaryKey.getExternalKeyLocatorHint());
+ isEncodingEqual(new byte[0], subkey.getExternalKeyLocatorHint());
+
+ // Test with locator hint
+ byte[] hint = new byte[] {(byte) 0xca, (byte) 0xfe, (byte) 0xba, (byte) 0xbe};
+ primaryKey = new SecretKeyPacket(primaryKey.getPublicKeyPacket(), hint);
+ pIn = new BCPGInputStream(new ByteArrayInputStream(primaryKey.getEncoded()));
+ primaryKey = (SecretKeyPacket) pIn.readPacket();
+ isTrue(primaryKey.getS2KUsage() == SecretKeyPacket.USAGE_EXTERNAL);
+ isEncodingEqual(hint, primaryKey.getExternalKeyLocatorHint());
+
+ subkey = new SecretSubkeyPacket((PublicSubkeyPacket) subkey.getPublicKeyPacket(), hint);
+ pIn = new BCPGInputStream(new ByteArrayInputStream(subkey.getEncoded()));
+ subkey = (SecretSubkeyPacket) pIn.readPacket();
+ isTrue(subkey.getS2KUsage() == SecretKeyPacket.USAGE_EXTERNAL);
+ isEncodingEqual(hint, subkey.getExternalKeyLocatorHint());
+ }
+
+ private void testV6PacketProperties()
+ throws IOException
+ {
+ ByteArrayInputStream bIn = new ByteArrayInputStream(V6_TSK_AS_EXTERNAL_KEY.getBytes());
+ ArmoredInputStream aIn = ArmoredInputStream.builder()
+ .build(bIn);
+ BCPGInputStream pIn = new BCPGInputStream(aIn);
+
+ SecretKeyPacket primaryKey = (SecretKeyPacket) pIn.readPacket();
+ pIn.readPacket(); // skip dk sig
+ SecretSubkeyPacket subkey = (SecretSubkeyPacket) pIn.readPacket();
+
+ isTrue(primaryKey.getS2KUsage() == SecretKeyPacket.USAGE_EXTERNAL);
+ isTrue(subkey.getS2KUsage() == SecretKeyPacket.USAGE_EXTERNAL);
+ isEncodingEqual(new byte[0], primaryKey.getExternalKeyLocatorHint());
+ isEncodingEqual(new byte[0], subkey.getExternalKeyLocatorHint());
+
+ // Test with locator hint
+ byte[] hint = new byte[] {(byte) 0xca, (byte) 0xfe, (byte) 0xba, (byte) 0xbe};
+ primaryKey = new SecretKeyPacket(primaryKey.getPublicKeyPacket(), hint);
+ pIn = new BCPGInputStream(new ByteArrayInputStream(primaryKey.getEncoded()));
+ primaryKey = (SecretKeyPacket) pIn.readPacket();
+ isTrue(primaryKey.getS2KUsage() == SecretKeyPacket.USAGE_EXTERNAL);
+ isEncodingEqual(hint, primaryKey.getExternalKeyLocatorHint());
+
+ subkey = new SecretSubkeyPacket((PublicSubkeyPacket) subkey.getPublicKeyPacket(), hint);
+ pIn = new BCPGInputStream(new ByteArrayInputStream(subkey.getEncoded()));
+ subkey = (SecretSubkeyPacket) pIn.readPacket();
+ isTrue(subkey.getS2KUsage() == SecretKeyPacket.USAGE_EXTERNAL);
+ isEncodingEqual(hint, subkey.getExternalKeyLocatorHint());
+ }
+
+ private void testPacketRoundTripping()
+ throws IOException
+ {
+ assertPacketsCanBeRoundTripped(V4_TSK);
+ assertPacketsCanBeRoundTripped(V4_TSK_AS_EXTERNAL_KEY);
+ assertPacketsCanBeRoundTripped(V6_TSK);
+ assertPacketsCanBeRoundTripped(V6_TSK_AS_EXTERNAL_KEY);
+ }
+
+ private void assertPacketsCanBeRoundTripped(String asciiArmoredPackets)
+ throws IOException
+ {
+ ByteArrayInputStream bIn = new ByteArrayInputStream(asciiArmoredPackets.getBytes());
+ ArmoredInputStream aIn = ArmoredInputStream.builder()
+ .build(bIn);
+ ByteArrayOutputStream bOut = new ByteArrayOutputStream();
+ org.bouncycastle.util.io.Streams.pipeAll(aIn, bOut);
+ byte[] before = bOut.toByteArray();
+ bIn = new ByteArrayInputStream(before);
+ BCPGInputStream pIn = new BCPGInputStream(bIn);
+
+ bOut = new ByteArrayOutputStream();
+ BCPGOutputStream pOut = new BCPGOutputStream(bOut, PacketFormat.ROUNDTRIP);
+
+ ContainedPacket p;
+ while ((p = (ContainedPacket) pIn.readPacket()) != null)
+ {
+ p.encode(pOut);
+ }
+
+ pOut.close();
+
+ isEncodingEqual(before, bOut.toByteArray());
+ }
+
+ public static void main(String[] args)
+ {
+ runTest(new ExternalSecretKeyPacketTest());
+ }
+
+}
diff --git a/pg/src/test/java/org/bouncycastle/openpgp/test/ExternalPGPSecretKeyTest.java b/pg/src/test/java/org/bouncycastle/openpgp/test/ExternalPGPSecretKeyTest.java
new file mode 100644
index 0000000000..43e484680f
--- /dev/null
+++ b/pg/src/test/java/org/bouncycastle/openpgp/test/ExternalPGPSecretKeyTest.java
@@ -0,0 +1,70 @@
+package org.bouncycastle.openpgp.test;
+
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.bcpg.BCPGInputStream;
+import org.bouncycastle.openpgp.PGPKeyPair;
+import org.bouncycastle.openpgp.PGPObjectFactory;
+import org.bouncycastle.openpgp.PGPSecretKey;
+import org.bouncycastle.openpgp.PGPSecretKeyRing;
+import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
+import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder;
+import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+
+public class ExternalPGPSecretKeyTest
+ extends AbstractPgpKeyPairTest
+{
+ /**
+ * TSK with external secret keys for both the primary and subkey.
+ *
+ * @see
+ * External Key Test Vector
+ */
+ private static final String V4_TSK_AS_EXTERNAL_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
+ "\n" +
+ "xTQEZgWtcxYJKwYBBAHaRw8BAQdAlLK6UPQsVHR2ETk1SwVIG3tBmpiEtikYYlCy\n" +
+ "1TIiqzb8zR08aGFyZHdhcmUtc2VjcmV0QGV4YW1wbGUub3JnPsKNBBAWCAA1AhkB\n" +
+ "BQJmBa1zAhsDCAsJCAcKDQwLBRUKCQgLAhYCFiEEXlP8Tur0WZR+f0I33/i9Uh4O\n" +
+ "HEkACgkQ3/i9Uh4OHEnryAD8CzH2ajJvASp46ApfI4pLPY57rjBX++d/2FQPRyqG\n" +
+ "HJUA/RLsNNgxiFYmK5cjtQe2/DgzWQ7R6PxPC6oa3XM7xPcCxzkEZgWtcxIKKwYB\n" +
+ "BAGXVQEFAQEHQE1YXOKeaklwG01Yab4xopP9wbu1E+pCrP1xQpiFZW5KAwEIB/zC\n" +
+ "eAQYFggAIAUCZgWtcwIbDBYhBF5T/E7q9FmUfn9CN9/4vVIeDhxJAAoJEN/4vVIe\n" +
+ "DhxJVTgA/1WaFrKdP3AgL0Ffdooc5XXbjQsj0uHo6FZSHRI4pchMAQCyJnKQ3RvW\n" +
+ "/0gm41JCqImyg2fxWG4hY0N5Q7Rc6PyzDQ==\n" +
+ "=3w/O\n" +
+ "-----END PGP PRIVATE KEY BLOCK-----\n";
+
+ @Override
+ public String getName()
+ {
+ return "ExternalPGPSecretKeyTest";
+ }
+
+ @Override
+ public void performTest()
+ throws Exception
+ {
+ ByteArrayInputStream bIn = new ByteArrayInputStream(V4_TSK_AS_EXTERNAL_KEY.getBytes(StandardCharsets.UTF_8));
+ ArmoredInputStream aIn = ArmoredInputStream.builder().build(bIn);
+ BCPGInputStream pIn = new BCPGInputStream(aIn);
+ PGPObjectFactory objFac = new BcPGPObjectFactory(pIn);
+ PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) objFac.nextObject();
+
+ for (PGPSecretKey key : secretKeys)
+ {
+ isTrue(key.isPrivateKeyEmpty());
+ isTrue(key.isExternalKey());
+ PGPKeyPair kp = key.extractKeyPair(
+ new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider())
+ .build("arbitrary".toCharArray()));
+ isNull(kp.getPrivateKey());
+ }
+ }
+
+ public static void main(String[] args)
+ {
+ runTest(new ExternalPGPSecretKeyTest());
+ }
+}