Skip to content

Commit 472d768

Browse files
committed
Add tests for conversion between JCA/BC keys
1 parent 4a10c27 commit 472d768

11 files changed

+1295
-1
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.bouncycastle.openpgp.test;
2+
3+
import org.bouncycastle.bcpg.test.AbstractPacketTest;
4+
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
5+
import org.bouncycastle.openpgp.PGPException;
6+
import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter;
7+
import org.bouncycastle.openpgp.operator.bc.BcPGPKeyPair;
8+
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyConverter;
9+
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair;
10+
11+
import java.security.KeyPair;
12+
import java.util.Date;
13+
14+
public abstract class AbstractPgpKeyPairTest
15+
extends AbstractPacketTest
16+
{
17+
18+
public Date currentTimeRounded()
19+
{
20+
Date now = new Date();
21+
return new Date((now.getTime() / 1000) * 1000); // rounded to seconds
22+
}
23+
24+
public BcPGPKeyPair toBcKeyPair(JcaPGPKeyPair keyPair)
25+
throws PGPException
26+
{
27+
BcPGPKeyConverter c = new BcPGPKeyConverter();
28+
return new BcPGPKeyPair(keyPair.getPublicKey().getAlgorithm(),
29+
new AsymmetricCipherKeyPair(
30+
c.getPublicKey(keyPair.getPublicKey()),
31+
c.getPrivateKey(keyPair.getPrivateKey())),
32+
keyPair.getPublicKey().getCreationTime());
33+
}
34+
35+
public JcaPGPKeyPair toJcaKeyPair(BcPGPKeyPair keyPair)
36+
throws PGPException
37+
{
38+
JcaPGPKeyConverter c = new JcaPGPKeyConverter();
39+
return new JcaPGPKeyPair(keyPair.getPublicKey().getAlgorithm(),
40+
new KeyPair(
41+
c.getPublicKey(keyPair.getPublicKey()),
42+
c.getPrivateKey(keyPair.getPrivateKey())),
43+
keyPair.getPublicKey().getCreationTime());
44+
}
45+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package org.bouncycastle.openpgp.test;
2+
3+
import org.bouncycastle.asn1.ASN1OctetString;
4+
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
5+
import org.bouncycastle.bcpg.*;
6+
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
7+
import org.bouncycastle.crypto.generators.X25519KeyPairGenerator;
8+
import org.bouncycastle.crypto.params.X25519KeyGenerationParameters;
9+
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters;
10+
import org.bouncycastle.jcajce.spec.XDHParameterSpec;
11+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
12+
import org.bouncycastle.openpgp.*;
13+
import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter;
14+
import org.bouncycastle.openpgp.operator.bc.BcPGPKeyPair;
15+
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyConverter;
16+
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair;
17+
import org.bouncycastle.util.Arrays;
18+
19+
import java.io.IOException;
20+
import java.security.*;
21+
import java.util.Date;
22+
23+
/**
24+
* Curve25519Legacy ECDH Secret Key Material uses big-endian MPI form,
25+
* while X25519 keys use little-endian native encoding.
26+
* This test verifies that legacy X25519 keys using ECDH are reverse-encoded,
27+
* while X25519 keys are natively encoded.
28+
*/
29+
public class Curve25519PrivateKeyEncodingTest
30+
extends AbstractPgpKeyPairTest
31+
{
32+
@Override
33+
public String getName()
34+
{
35+
return "Curve25519PrivateKeyEncodingTest";
36+
}
37+
38+
@Override
39+
public void performTest()
40+
throws Exception
41+
{
42+
containsTest();
43+
verifySecretKeyReverseEncoding();
44+
}
45+
46+
private void verifySecretKeyReverseEncoding()
47+
throws PGPException, IOException, InvalidAlgorithmParameterException, NoSuchAlgorithmException
48+
{
49+
bc_verifySecretKeyReverseEncoding();
50+
jca_verifySecretKeyReverseEncoding();
51+
}
52+
53+
/**
54+
* Verify that legacy ECDH keys over curve25519 encode the private key in reversed encoding,
55+
* while dedicated X25519 keys use native encoding for the private key material.
56+
* Test the JcaJce implementation.
57+
*
58+
* @throws NoSuchAlgorithmException
59+
* @throws InvalidAlgorithmParameterException
60+
* @throws PGPException
61+
* @throws IOException
62+
*/
63+
private void jca_verifySecretKeyReverseEncoding()
64+
throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, PGPException, IOException
65+
{
66+
JcaPGPKeyConverter c = new JcaPGPKeyConverter();
67+
68+
Date date = currentTimeRounded();
69+
KeyPairGenerator gen = KeyPairGenerator.getInstance("XDH", new BouncyCastleProvider());
70+
gen.initialize(new XDHParameterSpec("X25519"));
71+
KeyPair kp = gen.generateKeyPair();
72+
73+
byte[] rawPrivateKey = jcaNativePrivateKey(kp.getPrivate());
74+
75+
// Legacy key uses reversed encoding
76+
PGPKeyPair pgpECDHKeyPair = new JcaPGPKeyPair(PublicKeyAlgorithmTags.ECDH, kp, date);
77+
byte[] encodedECDHPrivateKey = pgpECDHKeyPair.getPrivateKey().getPrivateKeyDataPacket().getEncoded();
78+
isTrue(containsSubsequence(encodedECDHPrivateKey, Arrays.reverse(rawPrivateKey)));
79+
80+
byte[] decodedECDHPrivateKey = jcaNativePrivateKey(c.getPrivateKey(pgpECDHKeyPair.getPrivateKey()));
81+
isEncodingEqual(decodedECDHPrivateKey, rawPrivateKey);
82+
83+
// X25519 key uses native encoding
84+
PGPKeyPair pgpX25519KeyPair = new JcaPGPKeyPair(PublicKeyAlgorithmTags.X25519, kp, date);
85+
byte[] encodedX25519PrivateKey = pgpX25519KeyPair.getPrivateKey().getPrivateKeyDataPacket().getEncoded();
86+
isTrue(containsSubsequence(encodedX25519PrivateKey, rawPrivateKey));
87+
88+
byte[] decodedX25519PrivateKey = jcaNativePrivateKey(c.getPrivateKey(pgpX25519KeyPair.getPrivateKey()));
89+
isEncodingEqual(rawPrivateKey, decodedX25519PrivateKey);
90+
}
91+
92+
/**
93+
* Return the native encoding of the given private key.
94+
* @param privateKey private key
95+
* @return native encoding
96+
* @throws IOException
97+
*/
98+
private byte[] jcaNativePrivateKey(PrivateKey privateKey)
99+
throws IOException
100+
{
101+
PrivateKeyInfo kInfo = PrivateKeyInfo.getInstance(privateKey.getEncoded());
102+
return ASN1OctetString.getInstance(kInfo.parsePrivateKey()).getOctets();
103+
}
104+
105+
/**
106+
* Verify that legacy ECDH keys over curve25519 encode the private key in reversed encoding,
107+
* while dedicated X25519 keys use native encoding for the private key material.
108+
* Test the BC implementation.
109+
*/
110+
private void bc_verifySecretKeyReverseEncoding()
111+
throws PGPException
112+
{
113+
BcPGPKeyConverter c = new BcPGPKeyConverter();
114+
115+
Date date = currentTimeRounded();
116+
X25519KeyPairGenerator gen = new X25519KeyPairGenerator();
117+
gen.init(new X25519KeyGenerationParameters(new SecureRandom()));
118+
AsymmetricCipherKeyPair kp = gen.generateKeyPair();
119+
120+
byte[] rawPrivateKey = ((X25519PrivateKeyParameters) kp.getPrivate()).getEncoded();
121+
122+
// Legacy key uses reversed encoding
123+
PGPKeyPair pgpECDHKeyPair = new BcPGPKeyPair(PublicKeyAlgorithmTags.ECDH, kp, date);
124+
byte[] encodedECDHPrivateKey = pgpECDHKeyPair.getPrivateKey().getPrivateKeyDataPacket().getEncoded();
125+
isTrue(containsSubsequence(encodedECDHPrivateKey, Arrays.reverse(rawPrivateKey)));
126+
127+
byte[] decodedECDHPrivateKey = ((X25519PrivateKeyParameters) c.getPrivateKey(pgpECDHKeyPair.getPrivateKey())).getEncoded();
128+
isEncodingEqual(decodedECDHPrivateKey, rawPrivateKey);
129+
130+
// X25519 key uses native encoding
131+
PGPKeyPair pgpX25519KeyPair = new BcPGPKeyPair(PublicKeyAlgorithmTags.X25519, kp, date);
132+
byte[] encodedX25519PrivateKey = pgpX25519KeyPair.getPrivateKey().getPrivateKeyDataPacket().getEncoded();
133+
isTrue(containsSubsequence(encodedX25519PrivateKey, rawPrivateKey));
134+
135+
byte[] decodedX25519PrivateKey = ((X25519PrivateKeyParameters) c.getPrivateKey(pgpX25519KeyPair.getPrivateKey())).getEncoded();
136+
isEncodingEqual(rawPrivateKey, decodedX25519PrivateKey);
137+
}
138+
139+
/**
140+
* Return true, if the given sequence contains the given subsequence entirely.
141+
* @param sequence sequence
142+
* @param subsequence subsequence
143+
* @return true if subsequence is a subsequence of sequence
144+
*/
145+
public boolean containsSubsequence(byte[] sequence, byte[] subsequence)
146+
{
147+
outer: for (int i = 0; i < sequence.length - subsequence.length + 1; i++)
148+
{
149+
for (int j = 0; j < subsequence.length; j++)
150+
{
151+
if (sequence[i + j] != subsequence[j])
152+
{
153+
continue outer;
154+
}
155+
}
156+
return true;
157+
}
158+
return false;
159+
}
160+
161+
/**
162+
* Test proper functionality of the {@link #containsSubsequence(byte[], byte[])} method.
163+
*/
164+
private void containsTest() {
165+
// Make sure our containsSubsequence method functions correctly
166+
byte[] s = new byte[] {0x00, 0x01, 0x02, 0x03};
167+
isTrue(containsSubsequence(s, new byte[] {0x00, 0x01}));
168+
isTrue(containsSubsequence(s, new byte[] {0x01, 0x02}));
169+
isTrue(containsSubsequence(s, new byte[] {0x02, 0x03}));
170+
isTrue(containsSubsequence(s, new byte[] {0x00}));
171+
isTrue(containsSubsequence(s, new byte[] {0x03}));
172+
isTrue(containsSubsequence(s, new byte[] {0x00, 0x01, 0x02, 0x03}));
173+
isTrue(containsSubsequence(s, new byte[0]));
174+
isTrue(containsSubsequence(new byte[0], new byte[0]));
175+
176+
isFalse(containsSubsequence(s, new byte[] {0x00, 0x02}));
177+
isFalse(containsSubsequence(s, new byte[] {0x00, 0x00}));
178+
isFalse(containsSubsequence(s, new byte[] {0x00, 0x01, 0x02, 0x03, 0x04}));
179+
isFalse(containsSubsequence(s, new byte[] {0x04}));
180+
isFalse(containsSubsequence(new byte[0], new byte[] {0x00}));
181+
}
182+
183+
public static void main(String[] args)
184+
{
185+
runTest(new Curve25519PrivateKeyEncodingTest());
186+
}
187+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package org.bouncycastle.openpgp.test;
2+
3+
import org.bouncycastle.bcpg.*;
4+
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
5+
import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator;
6+
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
7+
import org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters;
8+
import org.bouncycastle.jcajce.spec.EdDSAParameterSpec;
9+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
10+
import org.bouncycastle.openpgp.PGPException;
11+
import org.bouncycastle.openpgp.PGPPublicKey;
12+
import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
13+
import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter;
14+
import org.bouncycastle.openpgp.operator.bc.BcPGPKeyPair;
15+
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyConverter;
16+
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair;
17+
import org.bouncycastle.util.Pack;
18+
import org.bouncycastle.util.encoders.Hex;
19+
20+
import java.io.IOException;
21+
import java.security.*;
22+
import java.util.Date;
23+
24+
public class DedicatedEd25519KeyPairTest
25+
extends AbstractPgpKeyPairTest
26+
{
27+
@Override
28+
public String getName()
29+
{
30+
return "DedicatedEd25519KeyPairTest";
31+
}
32+
33+
@Override
34+
public void performTest()
35+
throws Exception
36+
{
37+
testConversionOfJcaKeyPair();
38+
testConversionOfBcKeyPair();
39+
40+
testConversionOfTestVectorKey();
41+
}
42+
43+
private void testConversionOfJcaKeyPair()
44+
throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, PGPException, IOException
45+
{
46+
Date date = currentTimeRounded();
47+
KeyPairGenerator gen = KeyPairGenerator.getInstance("EDDSA", new BouncyCastleProvider());
48+
gen.initialize(new EdDSAParameterSpec("Ed25519"));
49+
KeyPair kp = gen.generateKeyPair();
50+
51+
JcaPGPKeyPair j1 = new JcaPGPKeyPair(PublicKeyAlgorithmTags.Ed25519, kp, date);
52+
byte[] pubEnc = j1.getPublicKey().getEncoded();
53+
byte[] privEnc = j1.getPrivateKey().getPrivateKeyDataPacket().getEncoded();
54+
isTrue("Dedicated Ed25519 public key MUST be instanceof Ed25519PublicBCPGKey",
55+
j1.getPublicKey().getPublicKeyPacket().getKey() instanceof Ed25519PublicBCPGKey);
56+
isTrue("Dedicated Ed25519 secret key MUST be instanceof Ed25519SecretBCPGKey",
57+
j1.getPrivateKey().getPrivateKeyDataPacket() instanceof Ed25519SecretBCPGKey);
58+
59+
BcPGPKeyPair b1 = toBcKeyPair(j1);
60+
isEncodingEqual(pubEnc, b1.getPublicKey().getEncoded());
61+
isEncodingEqual(privEnc, b1.getPrivateKey().getPrivateKeyDataPacket().getEncoded());
62+
isTrue("Dedicated Ed25519 public key MUST be instanceof Ed25519PublicBCPGKey",
63+
b1.getPublicKey().getPublicKeyPacket().getKey() instanceof Ed25519PublicBCPGKey);
64+
isTrue("Dedicated Ed25519 secret key MUST be instanceof Ed25519SecretBCPGKey",
65+
b1.getPrivateKey().getPrivateKeyDataPacket() instanceof Ed25519SecretBCPGKey);
66+
67+
JcaPGPKeyPair j2 = toJcaKeyPair(b1);
68+
isEncodingEqual(pubEnc, j2.getPublicKey().getEncoded());
69+
isEncodingEqual(privEnc, j2.getPrivateKey().getPrivateKeyDataPacket().getEncoded());
70+
isTrue("Dedicated Ed25519 public key MUST be instanceof Ed25519PublicBCPGKey",
71+
j2.getPublicKey().getPublicKeyPacket().getKey() instanceof Ed25519PublicBCPGKey);
72+
isTrue("Dedicated Ed25519 secret key MUST be instanceof Ed25519SecretBCPGKey",
73+
j2.getPrivateKey().getPrivateKeyDataPacket() instanceof Ed25519SecretBCPGKey);
74+
75+
BcPGPKeyPair b2 = toBcKeyPair(j2);
76+
isEncodingEqual(pubEnc, b2.getPublicKey().getEncoded());
77+
isEncodingEqual(privEnc, b2.getPrivateKey().getPrivateKeyDataPacket().getEncoded());
78+
isTrue("Dedicated Ed25519 public key MUST be instanceof Ed25519PublicBCPGKey",
79+
b2.getPublicKey().getPublicKeyPacket().getKey() instanceof Ed25519PublicBCPGKey);
80+
isTrue("Dedicated Ed25519 secret key MUST be instanceof Ed25519SecretBCPGKey",
81+
b2.getPrivateKey().getPrivateKeyDataPacket() instanceof Ed25519SecretBCPGKey);
82+
83+
isEquals("Creation time is preserved",
84+
date.getTime(), b2.getPublicKey().getCreationTime().getTime());
85+
}
86+
87+
private void testConversionOfBcKeyPair()
88+
throws PGPException, IOException
89+
{
90+
Date date = currentTimeRounded();
91+
Ed25519KeyPairGenerator gen = new Ed25519KeyPairGenerator();
92+
gen.init(new Ed25519KeyGenerationParameters(new SecureRandom()));
93+
AsymmetricCipherKeyPair kp = gen.generateKeyPair();
94+
95+
BcPGPKeyPair b1 = new BcPGPKeyPair(PublicKeyAlgorithmTags.Ed25519, kp, date);
96+
byte[] pubEnc = b1.getPublicKey().getEncoded();
97+
byte[] privEnc = b1.getPrivateKey().getPrivateKeyDataPacket().getEncoded();
98+
isTrue("Dedicated Ed25519 public key MUST be instanceof Ed25519PublicBCPGKey",
99+
b1.getPublicKey().getPublicKeyPacket().getKey() instanceof Ed25519PublicBCPGKey);
100+
isTrue("Dedicated Ed25519 secret key MUST be instanceof Ed25519SecretBCPGKey",
101+
b1.getPrivateKey().getPrivateKeyDataPacket() instanceof Ed25519SecretBCPGKey);
102+
103+
JcaPGPKeyPair j1 = toJcaKeyPair(b1);
104+
isEncodingEqual(pubEnc, j1.getPublicKey().getEncoded());
105+
isEncodingEqual(privEnc, j1.getPrivateKey().getPrivateKeyDataPacket().getEncoded());
106+
isTrue("Dedicated Ed25519 public key MUST be instanceof Ed25519PublicBCPGKey",
107+
j1.getPublicKey().getPublicKeyPacket().getKey() instanceof Ed25519PublicBCPGKey);
108+
isTrue("Dedicated Ed25519 secret key MUST be instanceof Ed25519SecretBCPGKey",
109+
j1.getPrivateKey().getPrivateKeyDataPacket() instanceof Ed25519SecretBCPGKey);
110+
111+
BcPGPKeyPair b2 = toBcKeyPair(j1);
112+
isEncodingEqual(pubEnc, b2.getPublicKey().getEncoded());
113+
isEncodingEqual(privEnc, b2.getPrivateKey().getPrivateKeyDataPacket().getEncoded());
114+
isTrue("Dedicated Ed25519 public key MUST be instanceof Ed25519PublicBCPGKey",
115+
b2.getPublicKey().getPublicKeyPacket().getKey() instanceof Ed25519PublicBCPGKey);
116+
isTrue("Dedicated Ed25519 secret key MUST be instanceof Ed25519SecretBCPGKey",
117+
b2.getPrivateKey().getPrivateKeyDataPacket() instanceof Ed25519SecretBCPGKey);
118+
119+
JcaPGPKeyPair j2 = toJcaKeyPair(b2);
120+
isEncodingEqual(pubEnc, j2.getPublicKey().getEncoded());
121+
isEncodingEqual(privEnc, j2.getPrivateKey().getPrivateKeyDataPacket().getEncoded());
122+
isTrue("Dedicated Ed25519 public key MUST be instanceof Ed25519PublicBCPGKey",
123+
j2.getPublicKey().getPublicKeyPacket().getKey() instanceof Ed25519PublicBCPGKey);
124+
isTrue("Dedicated Ed25519 secret key MUST be instanceof Ed25519SecretBCPGKey",
125+
j2.getPrivateKey().getPrivateKeyDataPacket() instanceof Ed25519SecretBCPGKey);
126+
127+
isEquals("Creation time is preserved",
128+
date.getTime(), j2.getPublicKey().getCreationTime().getTime());
129+
}
130+
131+
private void testConversionOfTestVectorKey() throws PGPException, IOException {
132+
JcaPGPKeyConverter jc = new JcaPGPKeyConverter().setProvider(new BouncyCastleProvider());
133+
BcPGPKeyConverter bc = new BcPGPKeyConverter();
134+
// ed25519 public key from https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-hashed-data-stream-for-sign
135+
// just adapted to be a version 4 key.
136+
Date creationTime = new Date(Pack.bigEndianToInt(Hex.decode("63877fe3"), 0) * 1000L);
137+
byte[] k = Hex.decode("f94da7bb48d60a61e567706a6587d0331999bb9d891a08242ead84543df895a3");
138+
PGPPublicKey v4k = new PGPPublicKey(
139+
new PublicKeyPacket(PublicKeyAlgorithmTags.Ed25519, creationTime, new Ed25519PublicBCPGKey(k)),
140+
new BcKeyFingerprintCalculator()
141+
);
142+
143+
// convert parsed key to Jca public key
144+
PublicKey jcpk = jc.getPublicKey(v4k);
145+
PGPPublicKey jck = jc.getPGPPublicKey(PublicKeyAlgorithmTags.Ed25519, jcpk, creationTime);
146+
isEncodingEqual(v4k.getEncoded(), jck.getEncoded());
147+
148+
// convert parsed key to Bc public key
149+
AsymmetricKeyParameter bcpk = bc.getPublicKey(v4k);
150+
PGPPublicKey bck = bc.getPGPPublicKey(PublicKeyAlgorithmTags.Ed25519, null, bcpk, creationTime);
151+
isEncodingEqual(v4k.getEncoded(), bck.getEncoded());
152+
}
153+
154+
public static void main(String[] args)
155+
{
156+
runTest(new DedicatedEd25519KeyPairTest());
157+
}
158+
}

0 commit comments

Comments
 (0)