From 9c34b02842a3f776ab805af109e10ed863a0f6e3 Mon Sep 17 00:00:00 2001 From: juergw Date: Tue, 20 Jan 2026 11:51:40 +0000 Subject: [PATCH 1/2] Add support for PKCS8 and X.509 encoding of X-Wing keys. --- .../src/main/java/org/conscrypt/HpkeImpl.java | 18 ++- .../org/conscrypt/OpenSslXwingKeyFactory.java | 40 ++++- .../org/conscrypt/OpenSslXwingPrivateKey.java | 42 ++++- .../org/conscrypt/OpenSslXwingPublicKey.java | 47 +++++- .../test/java/org/conscrypt/XwingTest.java | 147 +++++++++++++++++- 5 files changed, 267 insertions(+), 27 deletions(-) diff --git a/common/src/main/java/org/conscrypt/HpkeImpl.java b/common/src/main/java/org/conscrypt/HpkeImpl.java index ebe7e29b9..ac50d1804 100644 --- a/common/src/main/java/org/conscrypt/HpkeImpl.java +++ b/common/src/main/java/org/conscrypt/HpkeImpl.java @@ -220,6 +220,8 @@ public X25519_CHACHA20() { } } + private static final OpenSslXwingKeyFactory xwingKeyFactory = new OpenSslXwingKeyFactory(); + private static class HpkeXwingImpl extends HpkeImpl { HpkeXwingImpl(HpkeSuite hpkeSuite) { super(hpkeSuite); @@ -227,20 +229,20 @@ private static class HpkeXwingImpl extends HpkeImpl { @Override byte[] getRecipientPublicKeyBytes(PublicKey publicKey) throws InvalidKeyException { - if (!(publicKey instanceof OpenSslXwingPublicKey)) { - throw new InvalidKeyException( - "Unsupported recipient key class: " + publicKey.getClass()); + Key translatedKey = xwingKeyFactory.engineTranslateKey(publicKey); + if (!(translatedKey instanceof OpenSslXwingPublicKey)) { + throw new IllegalStateException("Unexpected public key class"); } - return ((OpenSslXwingPublicKey) publicKey).getRaw(); + return ((OpenSslXwingPublicKey) translatedKey).getRaw(); } @Override byte[] getPrivateRecipientKeyBytes(PrivateKey recipientKey) throws InvalidKeyException { - if (!(recipientKey instanceof OpenSslXwingPrivateKey)) { - throw new InvalidKeyException( - "Unsupported recipient private key class: " + recipientKey.getClass()); + Key translatedKey = xwingKeyFactory.engineTranslateKey(recipientKey); + if (!(translatedKey instanceof OpenSslXwingPrivateKey)) { + throw new IllegalStateException("Unexpected private key class"); } - return ((OpenSslXwingPrivateKey) recipientKey).getRaw(); + return ((OpenSslXwingPrivateKey) translatedKey).getRaw(); } } diff --git a/common/src/main/java/org/conscrypt/OpenSslXwingKeyFactory.java b/common/src/main/java/org/conscrypt/OpenSslXwingKeyFactory.java index d65268ca3..b8bdaa95e 100644 --- a/common/src/main/java/org/conscrypt/OpenSslXwingKeyFactory.java +++ b/common/src/main/java/org/conscrypt/OpenSslXwingKeyFactory.java @@ -65,19 +65,26 @@ protected T engineGetKeySpec(Key key, Class keySpec) if (keySpec == null) { throw new InvalidKeySpecException("keySpec == null"); } + try { + key = engineTranslateKey(key); + } catch (InvalidKeyException e) { + throw new InvalidKeySpecException("Unsupported key class: " + key.getClass(), e); + } if (key instanceof OpenSslXwingPublicKey) { OpenSslXwingPublicKey conscryptKey = (OpenSslXwingPublicKey) key; if (X509EncodedKeySpec.class.isAssignableFrom(keySpec)) { - throw new UnsupportedOperationException( - "X509EncodedKeySpec is currently not supported"); + @SuppressWarnings("unchecked") // safe because of isAssignableFrom check above + T result = (T) new X509EncodedKeySpec(key.getEncoded()); + return result; } else if (EncodedKeySpec.class.isAssignableFrom(keySpec)) { return KeySpecUtil.makeRawKeySpec(conscryptKey.getRaw(), keySpec); } } else if (key instanceof OpenSslXwingPrivateKey) { OpenSslXwingPrivateKey conscryptKey = (OpenSslXwingPrivateKey) key; if (PKCS8EncodedKeySpec.class.isAssignableFrom(keySpec)) { - throw new UnsupportedOperationException( - "PKCS8EncodedKeySpec is currently not supported"); + @SuppressWarnings("unchecked") // safe because of isAssignableFrom check above + T result = (T) new PKCS8EncodedKeySpec(key.getEncoded()); + return result; } else if (EncodedKeySpec.class.isAssignableFrom(keySpec)) { return KeySpecUtil.makeRawKeySpec(conscryptKey.getRaw(), keySpec); } @@ -94,7 +101,28 @@ protected Key engineTranslateKey(Key key) throws InvalidKeyException { if ((key instanceof OpenSslXwingPublicKey) || (key instanceof OpenSslXwingPrivateKey)) { return key; } - throw new InvalidKeyException( - "Key must be OpenSslXwingPublicKey or OpenSslXwingPrivateKey"); + if ((key instanceof PrivateKey) && key.getFormat().equals("PKCS#8")) { + byte[] encoded = key.getEncoded(); + if (encoded == null) { + throw new InvalidKeyException("Key does not support encoding"); + } + try { + return engineGeneratePrivate(new PKCS8EncodedKeySpec(encoded)); + } catch (InvalidKeySpecException e) { + throw new InvalidKeyException(e); + } + } else if ((key instanceof PublicKey) && key.getFormat().equals("X.509")) { + byte[] encoded = key.getEncoded(); + if (encoded == null) { + throw new InvalidKeyException("Key does not support encoding"); + } + try { + return engineGeneratePublic(new X509EncodedKeySpec(encoded)); + } catch (InvalidKeySpecException e) { + throw new InvalidKeyException(e); + } + } else { + throw new InvalidKeyException("Key is not a XWING key"); + } } } diff --git a/common/src/main/java/org/conscrypt/OpenSslXwingPrivateKey.java b/common/src/main/java/org/conscrypt/OpenSslXwingPrivateKey.java index 50693744c..083cbaec5 100644 --- a/common/src/main/java/org/conscrypt/OpenSslXwingPrivateKey.java +++ b/common/src/main/java/org/conscrypt/OpenSslXwingPrivateKey.java @@ -30,11 +30,44 @@ public class OpenSslXwingPrivateKey implements PrivateKey { static final int PRIVATE_KEY_SIZE_BYTES = 32; + // The PKCS#8 encoding of a X-Wing private key is always the concatenation of a fixed + // prefix and the raw key. + private static final byte[] pkcs8Preamble = new byte[] { + 0x30, + 0x34, + 0x02, + 0x01, + 0x00, + 0x30, + 0x0d, + 0x06, + 0x0b, + 0x2b, + 0x06, + 0x01, + 0x04, + 0x01, + (byte) 0x83, + (byte) 0xe6, + 0x2d, + (byte) 0x81, + (byte) 0xc8, + (byte) 0x7a, + 0x04, + 0x20, + }; + private byte[] raw; public OpenSslXwingPrivateKey(EncodedKeySpec keySpec) throws InvalidKeySpecException { byte[] encoded = keySpec.getEncoded(); - if (keySpec.getFormat().equalsIgnoreCase("raw")) { + if (keySpec.getFormat().equals("PKCS#8")) { + byte[] preamble = Arrays.copyOf(encoded, pkcs8Preamble.length); + if (!Arrays.equals(preamble, pkcs8Preamble)) { + throw new InvalidKeySpecException("Invalid EdDSA PKCS8 key preamble"); + } + raw = Arrays.copyOfRange(encoded, pkcs8Preamble.length, encoded.length); + } else if (keySpec.getFormat().equalsIgnoreCase("raw")) { if (encoded.length != PRIVATE_KEY_SIZE_BYTES) { throw new InvalidKeySpecException("Invalid key size"); } @@ -58,12 +91,15 @@ public String getAlgorithm() { @Override public String getFormat() { - throw new UnsupportedOperationException("getFormat() not yet supported"); + return "PKCS#8"; } @Override public byte[] getEncoded() { - throw new UnsupportedOperationException("getEncoded() not yet supported"); + if (raw == null) { + throw new IllegalStateException("key is destroyed"); + } + return ArrayUtils.concat(pkcs8Preamble, raw); } byte[] getRaw() { diff --git a/common/src/main/java/org/conscrypt/OpenSslXwingPublicKey.java b/common/src/main/java/org/conscrypt/OpenSslXwingPublicKey.java index a7706bd34..ae5a6044e 100644 --- a/common/src/main/java/org/conscrypt/OpenSslXwingPublicKey.java +++ b/common/src/main/java/org/conscrypt/OpenSslXwingPublicKey.java @@ -29,11 +29,49 @@ public class OpenSslXwingPublicKey implements PublicKey { static final int PUBLIC_KEY_SIZE_BYTES = 1216; + // The X.509 encoding of a X-Wing public key is always the concatenation of a fixed + // prefix and the raw key. + private static final byte[] x509Preamble = new byte[] { + 0x30, + (byte) 0x82, + 0x04, + (byte) 0xd4, + 0x30, + 0x0d, + 0x06, + 0x0b, + 0x2b, + 0x06, + 0x01, + 0x04, + 0x01, + (byte) 0x83, + (byte) 0xe6, + 0x2d, + (byte) 0x81, + (byte) 0xc8, + (byte) 0x7a, + 0x03, + (byte) 0x82, + 0x04, + (byte) 0xc1, + 0x00, + }; + private final byte[] raw; public OpenSslXwingPublicKey(EncodedKeySpec keySpec) throws InvalidKeySpecException { byte[] encoded = keySpec.getEncoded(); - if (keySpec.getFormat().equalsIgnoreCase("raw")) { + if (keySpec.getFormat().equals("X.509")) { + if (!ArrayUtils.startsWith(encoded, x509Preamble)) { + throw new InvalidKeySpecException("Invalid format"); + } + int totalLength = x509Preamble.length + PUBLIC_KEY_SIZE_BYTES; + if (encoded.length < totalLength) { + throw new InvalidKeySpecException("Invalid key size"); + } + raw = Arrays.copyOfRange(encoded, x509Preamble.length, totalLength); + } else if (keySpec.getFormat().equalsIgnoreCase("raw")) { if (encoded.length != PUBLIC_KEY_SIZE_BYTES) { throw new InvalidKeySpecException("Invalid key size"); } @@ -57,12 +95,15 @@ public String getAlgorithm() { @Override public String getFormat() { - throw new UnsupportedOperationException("getFormat() not yet supported"); + return "X.509"; } @Override public byte[] getEncoded() { - throw new UnsupportedOperationException("getEncoded() not yet supported"); + if (raw == null) { + throw new IllegalStateException("key is destroyed"); + } + return ArrayUtils.concat(x509Preamble, raw); } byte[] getRaw() { diff --git a/common/src/test/java/org/conscrypt/XwingTest.java b/common/src/test/java/org/conscrypt/XwingTest.java index f11a4b8bf..1bb17a4c2 100644 --- a/common/src/test/java/org/conscrypt/XwingTest.java +++ b/common/src/test/java/org/conscrypt/XwingTest.java @@ -117,17 +117,119 @@ public void generatePublic_fromRawPublicKey_validatesSize() throws Exception { () -> keyFactory.generatePublic(new RawKeySpec(new byte[rawPublicKey.length + 1]))); } + /** Helper class to test KeyFactory.translateKey. */ + static class TestPublicKey implements PublicKey { + public TestPublicKey(byte[] x509encoded) { + this.x509encoded = x509encoded; + } + + private final byte[] x509encoded; + + @Override + public String getAlgorithm() { + return "XWING"; + } + + @Override + public String getFormat() { + return "X.509"; + } + + @Override + public byte[] getEncoded() { + return x509encoded; + } + } + + /** Helper class to test KeyFactory.translateKey. */ + static class TestPrivateKey implements PrivateKey { + public TestPrivateKey(byte[] pkcs8encoded) { + this.pkcs8encoded = pkcs8encoded; + } + + private final byte[] pkcs8encoded; + + @Override + public String getAlgorithm() { + return "XWING"; + } + + @Override + public String getFormat() { + return "PKCS#8"; + } + + @Override + public byte[] getEncoded() { + return pkcs8encoded; + } + } + @Test - public void x509AndPkcs8_areNotSupported() throws Exception { - KeyPairGenerator keyGen = KeyPairGenerator.getInstance("XWING", conscryptProvider); - KeyPair keyPair = keyGen.generateKeyPair(); + public void toAndFromX509AndPkcs8_works() throws Exception { + // from https://datatracker.ietf.org/doc/draft-connolly-cfrg-xwing-kem/, Appendix D + byte[] rawPrivateKey = TestUtils.decodeHex( + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"); + byte[] rawPublicKey = NativeCrypto.XWING_public_key_from_seed(rawPrivateKey); + + byte[] encodedPrivateKey = TestUtils.decodeBase64( + "MDQCAQAwDQYLKwYBBAGD5i2ByHoEIAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f"); + byte[] encodedPublicKey = TestUtils.decodeBase64( + "MIIE1DANBgsrBgEEAYPmLYHIegOCBMEAb1QJigoOZBFGYUtpYLpg2GA9YvRH+atJ" + + "m0e9aQbMQLBh2GNKPoiQbyhJWOdEHKbHJcu5cJW3ZxpGK2aByeZYC7yNYLFJ+mAm" + + "EEOvu6UvIFpgKDhIUVlq3zcavqmNM0c4PSu2c0OPZ4NhK/hwFPe5Gol0AmU0XfZ5" + + "NARz0cTBdohuXim48Fi7fHNTFmhs/1w764wmHLAJcKacGvzFS5TLhuHOY7pjbjlc" + + "pFEB4hx70EwxPqGa8kFB79KtREFqJbpPZZEO99iAnDCT8EqvAOPNluNcSqPIAsGK" + + "1vOdpLS42YyL15Atg6B7pFOWZ0pgJDyrk+gP2bHId3N2qcwNb6EV4mOTgLnGvnhI" + + "vRNYjGRwOgU10ZoPgWM6l2oKEFtm7ihdD9JV6CwDMZJfQ4O278dh72CZI1oLmHJj" + + "WKqdAbi4llGfkhR0u3wUuyIlK1wvENQSRsmyPnZEhJNn9UGhX2O8koo5u3vHPwe2" + + "ZcSWu2VYyPRUiacuxLrNNOnFlMM4cbcj8DSV6ItDkasm5DBD3rYRezkZ5FxMGxar" + + "KOR93XI2Y4VHZhkvwYBspwq7eGy9swky5oyKNwvPsHmDoBLDJmuT76YmV/S4ODdM" + + "sLuV4OwGVBsHZdmc8VO8a5YTXKeApVs2R3ieMZFeRig8+ce7boRT+2aCEFFB8dwN" + + "ANhe7XA7bGyWH3nIRSdrQkiUnAZ4LlE+spkbldlgQuOMvto1JEmytQhOvaUiamIG" + + "QAeJEwowlkSYSLYp/upKLCp0PEoN3Jyz89Z2/FY3MbJsShpm3IRZFwBW1XaX8UQ7" + + "gamjRBK7e/BfMydXWlkR3TAdYFOGfzwwgHEfG/EVh7C7KYQnayaF53ViEOSz+JVT" + + "hCMeVYxvUQyR4PxWtdGIX/KUnpWka8G+4fpx9QJ+EMRDsOkdD9dED0Z6JyISEuiP" + + "XGumQpbK4NIHv8YPiMfPtcRaoYOdGMs3xFhD5UJqSpDIArZCj5U8NZxKwGA0UvrA" + + "tzYeL9NdzIhakhRdT8oBWPG31wtLzRGOSipBVEON8xDESpobmepBWQcmeoiwYkJB" + + "V5wXIvRu1hwuPspUXJlwUXF1OZuADbJdo5WT0GSQ1xQsAOiNLbBH6YmL23rLftkH" + + "9uMEFswN5UokLAohJjAvXVTIW8Zqwvg8eXlFtQZ8qkK9LgwZypdQblB6sKXJ9WM3" + + "CEmcGfJK7FE705A6XXO27EmR98cuuZHBw3iJgFyx6jigzAIXayfFjWOM5aMmaEV8" + + "+bm+AnygIUBXlxcl1UEC6JlnFusq2CNFO2BbhVNwsbIbOTLN7UFgqplzx+uuWsR2" + + "TZTPfMlQbwd7rXMBLbtKyBQKOHRkEuszyVFFliBfcHY1hiIX2bYJGMYmjZNEkVuE" + + "eiR2waJw8VSlyEI0FlrPyGk5hwLOqemgfnsOmeqb3LeEH+nA+iXIM4CSVho+3dxw" + + "AfR4rWV4GmAkqtFl2baXmtrESKRGL1ZGhVJ/diQ0/ppCWoRDe0VzkuyoDJE1BhUe" + + "OhMjnzQvynZVtuquhFoiHOs+Z/VjnGGT9v3u9X45m4CLfzqitXQKre2QFj3F13XJ" + + "+vfx+9B12rNE6dfRRmRygfu6ezxWyv1YM7epMOxCBufDptd2T+gdeg=="); KeyFactory keyFactory = KeyFactory.getInstance("XWING", conscryptProvider); - assertThrows(UnsupportedOperationException.class, - () -> keyFactory.getKeySpec(keyPair.getPrivate(), PKCS8EncodedKeySpec.class)); - assertThrows(UnsupportedOperationException.class, - () -> keyFactory.getKeySpec(keyPair.getPublic(), X509EncodedKeySpec.class)); + // Check generatePrivate from PKCS8EncodedKeySpec works. + PrivateKey privateKey = + keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encodedPrivateKey)); + assertArrayEquals(encodedPrivateKey, privateKey.getEncoded()); + assertArrayEquals(rawPrivateKey, ((OpenSslXwingPrivateKey) privateKey).getRaw()); + + // Check generatePublic from X509EncodedKeySpec works. + PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedPublicKey)); + assertArrayEquals(encodedPublicKey, publicKey.getEncoded()); + assertArrayEquals(rawPublicKey, ((OpenSslXwingPublicKey) publicKey).getRaw()); + + // Check getKeySpec with works for both private and public keys. + EncodedKeySpec privateKeySpec = + keyFactory.getKeySpec(privateKey, PKCS8EncodedKeySpec.class); + assertEquals("PKCS#8", privateKeySpec.getFormat()); + assertArrayEquals(encodedPrivateKey, privateKeySpec.getEncoded()); + + EncodedKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey, X509EncodedKeySpec.class); + assertEquals("X.509", publicKeySpec.getFormat()); + assertArrayEquals(encodedPublicKey, publicKeySpec.getEncoded()); + + assertEquals(privateKey, keyFactory.translateKey(privateKey)); + assertEquals( + privateKey, keyFactory.translateKey(new TestPrivateKey(privateKey.getEncoded()))); + assertEquals(publicKey, keyFactory.translateKey(publicKey)); + assertEquals(publicKey, keyFactory.translateKey(new TestPublicKey(publicKey.getEncoded()))); } @Test @@ -168,6 +270,37 @@ public void sealAndOpen_works() throws Exception { } } + @Test + public void sealAndOpenWithForeignKeys_works() throws Exception { + byte[] info = TestUtils.decodeHex("aa"); + byte[] plaintext = TestUtils.decodeHex("bb"); + byte[] aad = TestUtils.decodeHex("cc"); + for (int aead : new int[] {AEAD_AES_128_GCM, AEAD_AES_256_GCM, AEAD_CHACHA20POLY1305}) { + HpkeSuite suite = new HpkeSuite(KEM_XWING, KDF_HKDF_SHA256, aead); + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("XWING", conscryptProvider); + KeyPair keyPairRecipient = keyGen.generateKeyPair(); + PublicKey foreignPublicKey = + new TestPublicKey(keyPairRecipient.getPublic().getEncoded()); + PrivateKey foreignPrivateKey = + new TestPrivateKey(keyPairRecipient.getPrivate().getEncoded()); + + HpkeContextSender ctxSender = + HpkeContextSender.getInstance(suite.name(), conscryptProvider); + ctxSender.init(foreignPublicKey, info); + + byte[] encapsulated = ctxSender.getEncapsulated(); + byte[] ciphertext = ctxSender.seal(plaintext, aad); + + HpkeContextRecipient foreignContextRecipient = + HpkeContextRecipient.getInstance(suite.name(), conscryptProvider); + foreignContextRecipient.init(encapsulated, foreignPrivateKey, info); + + byte[] foreignOutput = foreignContextRecipient.open(ciphertext, aad); + + assertArrayEquals(plaintext, foreignOutput); + } + } + @Test public void kemTestVectors_encapsulatedIsCorrect() throws Exception { HpkeSuite suite = new HpkeSuite(KEM_XWING, KDF_HKDF_SHA256, AEAD_AES_128_GCM); From e3ec30608b3df6edfe47d5895886958b0f69d507 Mon Sep 17 00:00:00 2001 From: juergw Date: Tue, 20 Jan 2026 12:00:42 +0000 Subject: [PATCH 2/2] Fix size checks, and some error messages. --- .../main/java/org/conscrypt/OpenSslXwingPrivateKey.java | 5 ++++- .../src/main/java/org/conscrypt/OpenSslXwingPublicKey.java | 7 +++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/org/conscrypt/OpenSslXwingPrivateKey.java b/common/src/main/java/org/conscrypt/OpenSslXwingPrivateKey.java index 083cbaec5..059e12ebb 100644 --- a/common/src/main/java/org/conscrypt/OpenSslXwingPrivateKey.java +++ b/common/src/main/java/org/conscrypt/OpenSslXwingPrivateKey.java @@ -64,9 +64,12 @@ public OpenSslXwingPrivateKey(EncodedKeySpec keySpec) throws InvalidKeySpecExcep if (keySpec.getFormat().equals("PKCS#8")) { byte[] preamble = Arrays.copyOf(encoded, pkcs8Preamble.length); if (!Arrays.equals(preamble, pkcs8Preamble)) { - throw new InvalidKeySpecException("Invalid EdDSA PKCS8 key preamble"); + throw new InvalidKeySpecException("Invalid X-Wing PKCS8 key preamble"); } raw = Arrays.copyOfRange(encoded, pkcs8Preamble.length, encoded.length); + if (raw.length != PRIVATE_KEY_SIZE_BYTES) { + throw new InvalidKeySpecException("Invalid key size"); + } } else if (keySpec.getFormat().equalsIgnoreCase("raw")) { if (encoded.length != PRIVATE_KEY_SIZE_BYTES) { throw new InvalidKeySpecException("Invalid key size"); diff --git a/common/src/main/java/org/conscrypt/OpenSslXwingPublicKey.java b/common/src/main/java/org/conscrypt/OpenSslXwingPublicKey.java index ae5a6044e..bfd1b26e2 100644 --- a/common/src/main/java/org/conscrypt/OpenSslXwingPublicKey.java +++ b/common/src/main/java/org/conscrypt/OpenSslXwingPublicKey.java @@ -64,13 +64,12 @@ public OpenSslXwingPublicKey(EncodedKeySpec keySpec) throws InvalidKeySpecExcept byte[] encoded = keySpec.getEncoded(); if (keySpec.getFormat().equals("X.509")) { if (!ArrayUtils.startsWith(encoded, x509Preamble)) { - throw new InvalidKeySpecException("Invalid format"); + throw new InvalidKeySpecException("Invalid X-Wing X.509 key preamble"); } - int totalLength = x509Preamble.length + PUBLIC_KEY_SIZE_BYTES; - if (encoded.length < totalLength) { + raw = Arrays.copyOfRange(encoded, x509Preamble.length, encoded.length); + if (raw.length != PUBLIC_KEY_SIZE_BYTES) { throw new InvalidKeySpecException("Invalid key size"); } - raw = Arrays.copyOfRange(encoded, x509Preamble.length, totalLength); } else if (keySpec.getFormat().equalsIgnoreCase("raw")) { if (encoded.length != PUBLIC_KEY_SIZE_BYTES) { throw new InvalidKeySpecException("Invalid key size");