Skip to content

Commit 7cbb302

Browse files
authored
Add X.509 and PKCS8 encoding for X-Wing keys (#1469)
* Add support for PKCS8 and X.509 encoding of X-Wing keys. * Fix size checks, and some error messages.
1 parent d6bbf71 commit 7cbb302

5 files changed

Lines changed: 269 additions & 27 deletions

File tree

common/src/main/java/org/conscrypt/HpkeImpl.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,27 +220,29 @@ public X25519_CHACHA20() {
220220
}
221221
}
222222

223+
private static final OpenSslXwingKeyFactory xwingKeyFactory = new OpenSslXwingKeyFactory();
224+
223225
private static class HpkeXwingImpl extends HpkeImpl {
224226
HpkeXwingImpl(HpkeSuite hpkeSuite) {
225227
super(hpkeSuite);
226228
}
227229

228230
@Override
229231
byte[] getRecipientPublicKeyBytes(PublicKey publicKey) throws InvalidKeyException {
230-
if (!(publicKey instanceof OpenSslXwingPublicKey)) {
231-
throw new InvalidKeyException(
232-
"Unsupported recipient key class: " + publicKey.getClass());
232+
Key translatedKey = xwingKeyFactory.engineTranslateKey(publicKey);
233+
if (!(translatedKey instanceof OpenSslXwingPublicKey)) {
234+
throw new IllegalStateException("Unexpected public key class");
233235
}
234-
return ((OpenSslXwingPublicKey) publicKey).getRaw();
236+
return ((OpenSslXwingPublicKey) translatedKey).getRaw();
235237
}
236238

237239
@Override
238240
byte[] getPrivateRecipientKeyBytes(PrivateKey recipientKey) throws InvalidKeyException {
239-
if (!(recipientKey instanceof OpenSslXwingPrivateKey)) {
240-
throw new InvalidKeyException(
241-
"Unsupported recipient private key class: " + recipientKey.getClass());
241+
Key translatedKey = xwingKeyFactory.engineTranslateKey(recipientKey);
242+
if (!(translatedKey instanceof OpenSslXwingPrivateKey)) {
243+
throw new IllegalStateException("Unexpected private key class");
242244
}
243-
return ((OpenSslXwingPrivateKey) recipientKey).getRaw();
245+
return ((OpenSslXwingPrivateKey) translatedKey).getRaw();
244246
}
245247
}
246248

common/src/main/java/org/conscrypt/OpenSslXwingKeyFactory.java

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,26 @@ protected <T extends KeySpec> T engineGetKeySpec(Key key, Class<T> keySpec)
6565
if (keySpec == null) {
6666
throw new InvalidKeySpecException("keySpec == null");
6767
}
68+
try {
69+
key = engineTranslateKey(key);
70+
} catch (InvalidKeyException e) {
71+
throw new InvalidKeySpecException("Unsupported key class: " + key.getClass(), e);
72+
}
6873
if (key instanceof OpenSslXwingPublicKey) {
6974
OpenSslXwingPublicKey conscryptKey = (OpenSslXwingPublicKey) key;
7075
if (X509EncodedKeySpec.class.isAssignableFrom(keySpec)) {
71-
throw new UnsupportedOperationException(
72-
"X509EncodedKeySpec is currently not supported");
76+
@SuppressWarnings("unchecked") // safe because of isAssignableFrom check above
77+
T result = (T) new X509EncodedKeySpec(key.getEncoded());
78+
return result;
7379
} else if (EncodedKeySpec.class.isAssignableFrom(keySpec)) {
7480
return KeySpecUtil.makeRawKeySpec(conscryptKey.getRaw(), keySpec);
7581
}
7682
} else if (key instanceof OpenSslXwingPrivateKey) {
7783
OpenSslXwingPrivateKey conscryptKey = (OpenSslXwingPrivateKey) key;
7884
if (PKCS8EncodedKeySpec.class.isAssignableFrom(keySpec)) {
79-
throw new UnsupportedOperationException(
80-
"PKCS8EncodedKeySpec is currently not supported");
85+
@SuppressWarnings("unchecked") // safe because of isAssignableFrom check above
86+
T result = (T) new PKCS8EncodedKeySpec(key.getEncoded());
87+
return result;
8188
} else if (EncodedKeySpec.class.isAssignableFrom(keySpec)) {
8289
return KeySpecUtil.makeRawKeySpec(conscryptKey.getRaw(), keySpec);
8390
}
@@ -94,7 +101,28 @@ protected Key engineTranslateKey(Key key) throws InvalidKeyException {
94101
if ((key instanceof OpenSslXwingPublicKey) || (key instanceof OpenSslXwingPrivateKey)) {
95102
return key;
96103
}
97-
throw new InvalidKeyException(
98-
"Key must be OpenSslXwingPublicKey or OpenSslXwingPrivateKey");
104+
if ((key instanceof PrivateKey) && key.getFormat().equals("PKCS#8")) {
105+
byte[] encoded = key.getEncoded();
106+
if (encoded == null) {
107+
throw new InvalidKeyException("Key does not support encoding");
108+
}
109+
try {
110+
return engineGeneratePrivate(new PKCS8EncodedKeySpec(encoded));
111+
} catch (InvalidKeySpecException e) {
112+
throw new InvalidKeyException(e);
113+
}
114+
} else if ((key instanceof PublicKey) && key.getFormat().equals("X.509")) {
115+
byte[] encoded = key.getEncoded();
116+
if (encoded == null) {
117+
throw new InvalidKeyException("Key does not support encoding");
118+
}
119+
try {
120+
return engineGeneratePublic(new X509EncodedKeySpec(encoded));
121+
} catch (InvalidKeySpecException e) {
122+
throw new InvalidKeyException(e);
123+
}
124+
} else {
125+
throw new InvalidKeyException("Key is not a XWING key");
126+
}
99127
}
100128
}

common/src/main/java/org/conscrypt/OpenSslXwingPrivateKey.java

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,47 @@ public class OpenSslXwingPrivateKey implements PrivateKey {
3030

3131
static final int PRIVATE_KEY_SIZE_BYTES = 32;
3232

33+
// The PKCS#8 encoding of a X-Wing private key is always the concatenation of a fixed
34+
// prefix and the raw key.
35+
private static final byte[] pkcs8Preamble = new byte[] {
36+
0x30,
37+
0x34,
38+
0x02,
39+
0x01,
40+
0x00,
41+
0x30,
42+
0x0d,
43+
0x06,
44+
0x0b,
45+
0x2b,
46+
0x06,
47+
0x01,
48+
0x04,
49+
0x01,
50+
(byte) 0x83,
51+
(byte) 0xe6,
52+
0x2d,
53+
(byte) 0x81,
54+
(byte) 0xc8,
55+
(byte) 0x7a,
56+
0x04,
57+
0x20,
58+
};
59+
3360
private byte[] raw;
3461

3562
public OpenSslXwingPrivateKey(EncodedKeySpec keySpec) throws InvalidKeySpecException {
3663
byte[] encoded = keySpec.getEncoded();
37-
if (keySpec.getFormat().equalsIgnoreCase("raw")) {
64+
if (keySpec.getFormat().equals("PKCS#8")) {
65+
byte[] preamble = Arrays.copyOf(encoded, pkcs8Preamble.length);
66+
if (!Arrays.equals(preamble, pkcs8Preamble)) {
67+
throw new InvalidKeySpecException("Invalid X-Wing PKCS8 key preamble");
68+
}
69+
raw = Arrays.copyOfRange(encoded, pkcs8Preamble.length, encoded.length);
70+
if (raw.length != PRIVATE_KEY_SIZE_BYTES) {
71+
throw new InvalidKeySpecException("Invalid key size");
72+
}
73+
} else if (keySpec.getFormat().equalsIgnoreCase("raw")) {
3874
if (encoded.length != PRIVATE_KEY_SIZE_BYTES) {
3975
throw new InvalidKeySpecException("Invalid key size");
4076
}
@@ -58,12 +94,15 @@ public String getAlgorithm() {
5894

5995
@Override
6096
public String getFormat() {
61-
throw new UnsupportedOperationException("getFormat() not yet supported");
97+
return "PKCS#8";
6298
}
6399

64100
@Override
65101
public byte[] getEncoded() {
66-
throw new UnsupportedOperationException("getEncoded() not yet supported");
102+
if (raw == null) {
103+
throw new IllegalStateException("key is destroyed");
104+
}
105+
return ArrayUtils.concat(pkcs8Preamble, raw);
67106
}
68107

69108
byte[] getRaw() {

common/src/main/java/org/conscrypt/OpenSslXwingPublicKey.java

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,48 @@ public class OpenSslXwingPublicKey implements PublicKey {
2929

3030
static final int PUBLIC_KEY_SIZE_BYTES = 1216;
3131

32+
// The X.509 encoding of a X-Wing public key is always the concatenation of a fixed
33+
// prefix and the raw key.
34+
private static final byte[] x509Preamble = new byte[] {
35+
0x30,
36+
(byte) 0x82,
37+
0x04,
38+
(byte) 0xd4,
39+
0x30,
40+
0x0d,
41+
0x06,
42+
0x0b,
43+
0x2b,
44+
0x06,
45+
0x01,
46+
0x04,
47+
0x01,
48+
(byte) 0x83,
49+
(byte) 0xe6,
50+
0x2d,
51+
(byte) 0x81,
52+
(byte) 0xc8,
53+
(byte) 0x7a,
54+
0x03,
55+
(byte) 0x82,
56+
0x04,
57+
(byte) 0xc1,
58+
0x00,
59+
};
60+
3261
private final byte[] raw;
3362

3463
public OpenSslXwingPublicKey(EncodedKeySpec keySpec) throws InvalidKeySpecException {
3564
byte[] encoded = keySpec.getEncoded();
36-
if (keySpec.getFormat().equalsIgnoreCase("raw")) {
65+
if (keySpec.getFormat().equals("X.509")) {
66+
if (!ArrayUtils.startsWith(encoded, x509Preamble)) {
67+
throw new InvalidKeySpecException("Invalid X-Wing X.509 key preamble");
68+
}
69+
raw = Arrays.copyOfRange(encoded, x509Preamble.length, encoded.length);
70+
if (raw.length != PUBLIC_KEY_SIZE_BYTES) {
71+
throw new InvalidKeySpecException("Invalid key size");
72+
}
73+
} else if (keySpec.getFormat().equalsIgnoreCase("raw")) {
3774
if (encoded.length != PUBLIC_KEY_SIZE_BYTES) {
3875
throw new InvalidKeySpecException("Invalid key size");
3976
}
@@ -57,12 +94,15 @@ public String getAlgorithm() {
5794

5895
@Override
5996
public String getFormat() {
60-
throw new UnsupportedOperationException("getFormat() not yet supported");
97+
return "X.509";
6198
}
6299

63100
@Override
64101
public byte[] getEncoded() {
65-
throw new UnsupportedOperationException("getEncoded() not yet supported");
102+
if (raw == null) {
103+
throw new IllegalStateException("key is destroyed");
104+
}
105+
return ArrayUtils.concat(x509Preamble, raw);
66106
}
67107

68108
byte[] getRaw() {

common/src/test/java/org/conscrypt/XwingTest.java

Lines changed: 140 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,119 @@ public void generatePublic_fromRawPublicKey_validatesSize() throws Exception {
117117
() -> keyFactory.generatePublic(new RawKeySpec(new byte[rawPublicKey.length + 1])));
118118
}
119119

120+
/** Helper class to test KeyFactory.translateKey. */
121+
static class TestPublicKey implements PublicKey {
122+
public TestPublicKey(byte[] x509encoded) {
123+
this.x509encoded = x509encoded;
124+
}
125+
126+
private final byte[] x509encoded;
127+
128+
@Override
129+
public String getAlgorithm() {
130+
return "XWING";
131+
}
132+
133+
@Override
134+
public String getFormat() {
135+
return "X.509";
136+
}
137+
138+
@Override
139+
public byte[] getEncoded() {
140+
return x509encoded;
141+
}
142+
}
143+
144+
/** Helper class to test KeyFactory.translateKey. */
145+
static class TestPrivateKey implements PrivateKey {
146+
public TestPrivateKey(byte[] pkcs8encoded) {
147+
this.pkcs8encoded = pkcs8encoded;
148+
}
149+
150+
private final byte[] pkcs8encoded;
151+
152+
@Override
153+
public String getAlgorithm() {
154+
return "XWING";
155+
}
156+
157+
@Override
158+
public String getFormat() {
159+
return "PKCS#8";
160+
}
161+
162+
@Override
163+
public byte[] getEncoded() {
164+
return pkcs8encoded;
165+
}
166+
}
167+
120168
@Test
121-
public void x509AndPkcs8_areNotSupported() throws Exception {
122-
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("XWING", conscryptProvider);
123-
KeyPair keyPair = keyGen.generateKeyPair();
169+
public void toAndFromX509AndPkcs8_works() throws Exception {
170+
// from https://datatracker.ietf.org/doc/draft-connolly-cfrg-xwing-kem/, Appendix D
171+
byte[] rawPrivateKey = TestUtils.decodeHex(
172+
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
173+
byte[] rawPublicKey = NativeCrypto.XWING_public_key_from_seed(rawPrivateKey);
174+
175+
byte[] encodedPrivateKey = TestUtils.decodeBase64(
176+
"MDQCAQAwDQYLKwYBBAGD5i2ByHoEIAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f");
177+
byte[] encodedPublicKey = TestUtils.decodeBase64(
178+
"MIIE1DANBgsrBgEEAYPmLYHIegOCBMEAb1QJigoOZBFGYUtpYLpg2GA9YvRH+atJ"
179+
+ "m0e9aQbMQLBh2GNKPoiQbyhJWOdEHKbHJcu5cJW3ZxpGK2aByeZYC7yNYLFJ+mAm"
180+
+ "EEOvu6UvIFpgKDhIUVlq3zcavqmNM0c4PSu2c0OPZ4NhK/hwFPe5Gol0AmU0XfZ5"
181+
+ "NARz0cTBdohuXim48Fi7fHNTFmhs/1w764wmHLAJcKacGvzFS5TLhuHOY7pjbjlc"
182+
+ "pFEB4hx70EwxPqGa8kFB79KtREFqJbpPZZEO99iAnDCT8EqvAOPNluNcSqPIAsGK"
183+
+ "1vOdpLS42YyL15Atg6B7pFOWZ0pgJDyrk+gP2bHId3N2qcwNb6EV4mOTgLnGvnhI"
184+
+ "vRNYjGRwOgU10ZoPgWM6l2oKEFtm7ihdD9JV6CwDMZJfQ4O278dh72CZI1oLmHJj"
185+
+ "WKqdAbi4llGfkhR0u3wUuyIlK1wvENQSRsmyPnZEhJNn9UGhX2O8koo5u3vHPwe2"
186+
+ "ZcSWu2VYyPRUiacuxLrNNOnFlMM4cbcj8DSV6ItDkasm5DBD3rYRezkZ5FxMGxar"
187+
+ "KOR93XI2Y4VHZhkvwYBspwq7eGy9swky5oyKNwvPsHmDoBLDJmuT76YmV/S4ODdM"
188+
+ "sLuV4OwGVBsHZdmc8VO8a5YTXKeApVs2R3ieMZFeRig8+ce7boRT+2aCEFFB8dwN"
189+
+ "ANhe7XA7bGyWH3nIRSdrQkiUnAZ4LlE+spkbldlgQuOMvto1JEmytQhOvaUiamIG"
190+
+ "QAeJEwowlkSYSLYp/upKLCp0PEoN3Jyz89Z2/FY3MbJsShpm3IRZFwBW1XaX8UQ7"
191+
+ "gamjRBK7e/BfMydXWlkR3TAdYFOGfzwwgHEfG/EVh7C7KYQnayaF53ViEOSz+JVT"
192+
+ "hCMeVYxvUQyR4PxWtdGIX/KUnpWka8G+4fpx9QJ+EMRDsOkdD9dED0Z6JyISEuiP"
193+
+ "XGumQpbK4NIHv8YPiMfPtcRaoYOdGMs3xFhD5UJqSpDIArZCj5U8NZxKwGA0UvrA"
194+
+ "tzYeL9NdzIhakhRdT8oBWPG31wtLzRGOSipBVEON8xDESpobmepBWQcmeoiwYkJB"
195+
+ "V5wXIvRu1hwuPspUXJlwUXF1OZuADbJdo5WT0GSQ1xQsAOiNLbBH6YmL23rLftkH"
196+
+ "9uMEFswN5UokLAohJjAvXVTIW8Zqwvg8eXlFtQZ8qkK9LgwZypdQblB6sKXJ9WM3"
197+
+ "CEmcGfJK7FE705A6XXO27EmR98cuuZHBw3iJgFyx6jigzAIXayfFjWOM5aMmaEV8"
198+
+ "+bm+AnygIUBXlxcl1UEC6JlnFusq2CNFO2BbhVNwsbIbOTLN7UFgqplzx+uuWsR2"
199+
+ "TZTPfMlQbwd7rXMBLbtKyBQKOHRkEuszyVFFliBfcHY1hiIX2bYJGMYmjZNEkVuE"
200+
+ "eiR2waJw8VSlyEI0FlrPyGk5hwLOqemgfnsOmeqb3LeEH+nA+iXIM4CSVho+3dxw"
201+
+ "AfR4rWV4GmAkqtFl2baXmtrESKRGL1ZGhVJ/diQ0/ppCWoRDe0VzkuyoDJE1BhUe"
202+
+ "OhMjnzQvynZVtuquhFoiHOs+Z/VjnGGT9v3u9X45m4CLfzqitXQKre2QFj3F13XJ"
203+
+ "+vfx+9B12rNE6dfRRmRygfu6ezxWyv1YM7epMOxCBufDptd2T+gdeg==");
124204

125205
KeyFactory keyFactory = KeyFactory.getInstance("XWING", conscryptProvider);
126206

127-
assertThrows(UnsupportedOperationException.class,
128-
() -> keyFactory.getKeySpec(keyPair.getPrivate(), PKCS8EncodedKeySpec.class));
129-
assertThrows(UnsupportedOperationException.class,
130-
() -> keyFactory.getKeySpec(keyPair.getPublic(), X509EncodedKeySpec.class));
207+
// Check generatePrivate from PKCS8EncodedKeySpec works.
208+
PrivateKey privateKey =
209+
keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encodedPrivateKey));
210+
assertArrayEquals(encodedPrivateKey, privateKey.getEncoded());
211+
assertArrayEquals(rawPrivateKey, ((OpenSslXwingPrivateKey) privateKey).getRaw());
212+
213+
// Check generatePublic from X509EncodedKeySpec works.
214+
PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedPublicKey));
215+
assertArrayEquals(encodedPublicKey, publicKey.getEncoded());
216+
assertArrayEquals(rawPublicKey, ((OpenSslXwingPublicKey) publicKey).getRaw());
217+
218+
// Check getKeySpec with works for both private and public keys.
219+
EncodedKeySpec privateKeySpec =
220+
keyFactory.getKeySpec(privateKey, PKCS8EncodedKeySpec.class);
221+
assertEquals("PKCS#8", privateKeySpec.getFormat());
222+
assertArrayEquals(encodedPrivateKey, privateKeySpec.getEncoded());
223+
224+
EncodedKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey, X509EncodedKeySpec.class);
225+
assertEquals("X.509", publicKeySpec.getFormat());
226+
assertArrayEquals(encodedPublicKey, publicKeySpec.getEncoded());
227+
228+
assertEquals(privateKey, keyFactory.translateKey(privateKey));
229+
assertEquals(
230+
privateKey, keyFactory.translateKey(new TestPrivateKey(privateKey.getEncoded())));
231+
assertEquals(publicKey, keyFactory.translateKey(publicKey));
232+
assertEquals(publicKey, keyFactory.translateKey(new TestPublicKey(publicKey.getEncoded())));
131233
}
132234

133235
@Test
@@ -168,6 +270,37 @@ public void sealAndOpen_works() throws Exception {
168270
}
169271
}
170272

273+
@Test
274+
public void sealAndOpenWithForeignKeys_works() throws Exception {
275+
byte[] info = TestUtils.decodeHex("aa");
276+
byte[] plaintext = TestUtils.decodeHex("bb");
277+
byte[] aad = TestUtils.decodeHex("cc");
278+
for (int aead : new int[] {AEAD_AES_128_GCM, AEAD_AES_256_GCM, AEAD_CHACHA20POLY1305}) {
279+
HpkeSuite suite = new HpkeSuite(KEM_XWING, KDF_HKDF_SHA256, aead);
280+
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("XWING", conscryptProvider);
281+
KeyPair keyPairRecipient = keyGen.generateKeyPair();
282+
PublicKey foreignPublicKey =
283+
new TestPublicKey(keyPairRecipient.getPublic().getEncoded());
284+
PrivateKey foreignPrivateKey =
285+
new TestPrivateKey(keyPairRecipient.getPrivate().getEncoded());
286+
287+
HpkeContextSender ctxSender =
288+
HpkeContextSender.getInstance(suite.name(), conscryptProvider);
289+
ctxSender.init(foreignPublicKey, info);
290+
291+
byte[] encapsulated = ctxSender.getEncapsulated();
292+
byte[] ciphertext = ctxSender.seal(plaintext, aad);
293+
294+
HpkeContextRecipient foreignContextRecipient =
295+
HpkeContextRecipient.getInstance(suite.name(), conscryptProvider);
296+
foreignContextRecipient.init(encapsulated, foreignPrivateKey, info);
297+
298+
byte[] foreignOutput = foreignContextRecipient.open(ciphertext, aad);
299+
300+
assertArrayEquals(plaintext, foreignOutput);
301+
}
302+
}
303+
171304
@Test
172305
public void kemTestVectors_encapsulatedIsCorrect() throws Exception {
173306
HpkeSuite suite = new HpkeSuite(KEM_XWING, KDF_HKDF_SHA256, AEAD_AES_128_GCM);

0 commit comments

Comments
 (0)