From 29578f09d25286ff098469e896495388a4b63215 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 28 Jun 2026 21:18:28 +0200 Subject: [PATCH] Implement support for OpenPGP External Secret Keys See https://datatracker.ietf.org/doc/draft-dkg-openpgp-external-secrets/ --- .../bouncycastle/bcpg/SecretKeyPacket.java | 87 +++++- .../bouncycastle/bcpg/SecretSubkeyPacket.java | 17 ++ .../bouncycastle/openpgp/PGPSecretKey.java | 21 ++ .../test/ExternalSecretKeyPacketTest.java | 249 ++++++++++++++++++ .../test/ExternalPGPSecretKeyTest.java | 70 +++++ 5 files changed, 438 insertions(+), 6 deletions(-) create mode 100644 pg/src/test/java/org/bouncycastle/bcpg/test/ExternalSecretKeyPacketTest.java create mode 100644 pg/src/test/java/org/bouncycastle/openpgp/test/ExternalPGPSecretKeyTest.java 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()); + } +}