Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions common/src/main/java/org/conscrypt/OpenSSLX25519PrivateKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

import org.conscrypt.OpenSSLX509CertificateFactory.ParsingException;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.InvalidKeyException;
import java.security.PrivateKey;
import java.security.spec.EncodedKeySpec;
Expand Down Expand Up @@ -114,4 +117,15 @@ public boolean equals(Object o) {
public int hashCode() {
return Arrays.hashCode(uCoordinate);
}

private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject(); // reads "uCoordinate"
if (uCoordinate.length != X25519_KEY_SIZE_BYTES) {
throw new IOException("Invalid key size");
}
}

private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject(); // writes "uCoordinate"
}
}
14 changes: 14 additions & 0 deletions common/src/main/java/org/conscrypt/OpenSSLX25519PublicKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

package org.conscrypt;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.PublicKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
Expand Down Expand Up @@ -108,4 +111,15 @@ public int hashCode() {
}
return Arrays.hashCode(uCoordinate);
}

private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject(); // reads "uCoordinate"
if (uCoordinate.length != X25519_KEY_SIZE_BYTES) {
throw new IOException("Invalid key size");
}
}

private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject(); // writes "uCoordinate"
}
}
311 changes: 311 additions & 0 deletions common/src/test/java/org/conscrypt/X25519Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.conscrypt;

import static org.conscrypt.TestUtils.decodeHex;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;

import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

import javax.crypto.KeyAgreement;

@RunWith(JUnit4.class)
public class X25519Test {
private final Provider conscryptProvider = TestUtils.getConscryptProvider();

@BeforeClass
public static void setUp() {
TestUtils.assumeAllowsUnsignedCrypto();
}

/** Implements a KeySpec that contains the raw bytes of a key. */
public static final class RawKeySpec extends EncodedKeySpec {
public RawKeySpec(byte[] encoded) {
super(encoded);
}

@Override
public String getFormat() {
return "raw";
}
}

@Test
public void generateKeyPairKeyAgreement_works() throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("X25519", conscryptProvider);
KeyPair keyPair1 = keyGen.generateKeyPair();
KeyPair keyPair2 = keyGen.generateKeyPair();

KeyAgreement ka1 = KeyAgreement.getInstance("X25519", conscryptProvider);
ka1.init(keyPair1.getPrivate());
ka1.doPhase(keyPair2.getPublic(), true);

KeyAgreement ka2 = KeyAgreement.getInstance("X25519", conscryptProvider);
ka2.init(keyPair2.getPrivate());
ka2.doPhase(keyPair1.getPublic(), true);

assertArrayEquals(ka1.generateSecret(), ka2.generateSecret());
}

@Test
public void generateKeyPairWithWrongKeySize_throws() throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("X25519", conscryptProvider);
assertThrows(IllegalArgumentException.class, () -> keyGen.initialize(256));
}

@Test
public void keyAgreement_rfc7748_success() throws Exception {
// Test vector from RFC 7748, Section 5.2
byte[] rawPrivateKey =
decodeHex("a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4");
byte[] rawPublicKey =
decodeHex("e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c");
byte[] expectedSecret =
decodeHex("c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552");

KeyFactory keyFactory = KeyFactory.getInstance("X25519", conscryptProvider);
PrivateKey privateKey = keyFactory.generatePrivate(new RawKeySpec(rawPrivateKey));
PublicKey publicKey = keyFactory.generatePublic(new RawKeySpec(rawPublicKey));

KeyAgreement ka = KeyAgreement.getInstance("X25519", conscryptProvider);
ka.init(privateKey);
ka.doPhase(publicKey, true);
assertArrayEquals(expectedSecret, ka.generateSecret());
}

@Test
public void convertPrivateKeyToAndFromKeySpec_works() throws Exception {
byte[] rawPrivateKey =
decodeHex("a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4");
KeyFactory keyFactory = KeyFactory.getInstance("X25519", conscryptProvider);
PrivateKey privateKey = keyFactory.generatePrivate(new RawKeySpec(rawPrivateKey));
assertEquals("XDH", privateKey.getAlgorithm());

// RawKeySpec returns the raw private key.
RawKeySpec rawPrivateKeySpec = keyFactory.getKeySpec(privateKey, RawKeySpec.class);
assertEquals("raw", rawPrivateKeySpec.getFormat());
assertArrayEquals(rawPrivateKey, rawPrivateKeySpec.getEncoded());

// PKCS8EncodedKeySpec returns the same encoding as getEncoded().
PKCS8EncodedKeySpec privateKeySpec =
keyFactory.getKeySpec(privateKey, PKCS8EncodedKeySpec.class);
assertEquals("PKCS#8", privateKeySpec.getFormat());
assertArrayEquals(privateKey.getEncoded(), privateKeySpec.getEncoded());

PrivateKey privateKey2 =
keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKey.getEncoded()));
assertArrayEquals(privateKey.getEncoded(), privateKey2.getEncoded());

assertThrows(InvalidKeySpecException.class,
() -> keyFactory.getKeySpec(privateKey, X509EncodedKeySpec.class));
}

@Test
public void generateKey_invalidEncoding_throwsInvalidKeySpecException() throws Exception {
byte[] invalidEncoding = decodeHex("012345");
KeyFactory keyFactory = KeyFactory.getInstance("X25519", conscryptProvider);
assertThrows(InvalidKeySpecException.class,
() -> keyFactory.generatePrivate(new PKCS8EncodedKeySpec(invalidEncoding)));
assertThrows(InvalidKeySpecException.class,
() -> keyFactory.generatePublic(new X509EncodedKeySpec(invalidEncoding)));
assertThrows(InvalidKeySpecException.class,
() -> keyFactory.generatePrivate(new RawKeySpec(invalidEncoding)));
assertThrows(InvalidKeySpecException.class,
() -> keyFactory.generatePublic(new RawKeySpec(invalidEncoding)));
}

@Test
public void convertPublicKeyToFromKeySpec_works() throws Exception {
byte[] rawPublicKey =
decodeHex("e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c");
KeyFactory keyFactory = KeyFactory.getInstance("X25519", conscryptProvider);
PublicKey publicKey = keyFactory.generatePublic(new RawKeySpec(rawPublicKey));
assertEquals("XDH", publicKey.getAlgorithm());

// RawKeySpec returns the raw public key.
RawKeySpec rawPublicKeySpec = keyFactory.getKeySpec(publicKey, RawKeySpec.class);
assertEquals("raw", rawPublicKeySpec.getFormat());
assertArrayEquals(rawPublicKey, rawPublicKeySpec.getEncoded());

// X509EncodedKeySpec returns the same encoding as getEncoded().
X509EncodedKeySpec publicKeySpec =
keyFactory.getKeySpec(publicKey, X509EncodedKeySpec.class);
assertEquals("X.509", publicKeySpec.getFormat());
assertArrayEquals(publicKey.getEncoded(), publicKeySpec.getEncoded());

PublicKey publicKey2 =
keyFactory.generatePublic(new X509EncodedKeySpec(publicKey.getEncoded()));
assertArrayEquals(publicKey.getEncoded(), publicKey2.getEncoded());

assertThrows(InvalidKeySpecException.class,
() -> keyFactory.getKeySpec(publicKey, PKCS8EncodedKeySpec.class));
}

@Test
public void serializeAndDeserialize_works() throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("X25519", conscryptProvider);
KeyPair keyPair = keyGen.generateKeyPair();

ByteArrayOutputStream baos = new ByteArrayOutputStream(16384);
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(keyPair.getPrivate());
oos.writeObject(keyPair.getPublic());
}

ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
PrivateKey inflatedPrivateKey = (PrivateKey) ois.readObject();
PublicKey inflatedPublicKey = (PublicKey) ois.readObject();

assertEquals(inflatedPrivateKey, keyPair.getPrivate());
assertEquals(inflatedPublicKey, keyPair.getPublic());
}

@Test
public void serializePrivateKey_isEqualToTestVector() throws Exception {
byte[] rawPrivateKey =
decodeHex("a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4");
KeyFactory keyFactory = KeyFactory.getInstance("X25519", conscryptProvider);
PrivateKey privateKey = keyFactory.generatePrivate(new RawKeySpec(rawPrivateKey));

ByteArrayOutputStream baos = new ByteArrayOutputStream(16384);
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(privateKey);
}

String classNameHex = TestUtils.encodeHex(
privateKey.getClass().getName().getBytes(StandardCharsets.UTF_8));
String expectedHexEncoding = "aced0005737200"
+ Integer.toHexString(privateKey.getClass().getName().length()) + classNameHex
+ "d479f95a133abadc" // serialVersionUID
+ "0300015b000b"
+ "75436f6f7264696e617465" // hex("uCoordinate")
+ "7400025b427870757200025b42acf317f8060854e0020000787000000020"
+ "a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4"
+ "78";
assertEquals(expectedHexEncoding, TestUtils.encodeHex(baos.toByteArray()));
}

@Test
public void serializePublicKey_isEqualToTestVector() throws Exception {
byte[] rawPublicKey =
decodeHex("e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c");
KeyFactory keyFactory = KeyFactory.getInstance("X25519", conscryptProvider);
PublicKey publicKey = keyFactory.generatePublic(new RawKeySpec(rawPublicKey));

ByteArrayOutputStream baos = new ByteArrayOutputStream(16384);
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(publicKey);
}

String classNameHex = TestUtils.encodeHex(
publicKey.getClass().getName().getBytes(StandardCharsets.UTF_8));
String expectedHexEncoding = "aced0005737200"
+ Integer.toHexString(publicKey.getClass().getName().length()) + classNameHex
+ "064c7113d078e42d" // serialVersionUID
+ "0300015b000b"
+ "75436f6f7264696e617465" // hex("uCoordinate")
+ "7400025b427870757200025b42acf317f8060854e0020000787000000020"
+ "e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c"
+ "78";
assertEquals(expectedHexEncoding, TestUtils.encodeHex(baos.toByteArray()));
}

@Test
public void deserializeInvalidPrivateKey_fails() throws Exception {
byte[] rawPrivateKey =
decodeHex("a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4");
KeyFactory keyFactory = KeyFactory.getInstance("X25519", conscryptProvider);
PrivateKey privateKey = keyFactory.generatePrivate(new RawKeySpec(rawPrivateKey));

ByteArrayOutputStream baos = new ByteArrayOutputStream(16384);
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(privateKey);
}

String classNameHex = TestUtils.encodeHex(
privateKey.getClass().getName().getBytes(StandardCharsets.UTF_8));
String invalidPrivateKeySerialized = "aced0005737200"
+ Integer.toHexString(privateKey.getClass().getName().length()) + classNameHex
+ "d479f95a133abadc" // serialVersionUID
+ "0300015b000b"
+ "75436f6f7264696e617465" // hex("uCoordinate")
+ "7400025b427870757200025b42acf317f8060854e00200007870000000"
+ "1f" // size of private key (is 31, which is invalid)
+ "a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449a"
+ "78";

ByteArrayInputStream bais =
new ByteArrayInputStream(TestUtils.decodeHex(invalidPrivateKeySerialized));
ObjectInputStream ois = new ObjectInputStream(bais);

assertThrows(IOException.class, () -> ois.readObject());
}

@Test
public void deserializeInvalidPublicKey_fails() throws Exception {
byte[] rawPublicKey =
decodeHex("e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c");
KeyFactory keyFactory = KeyFactory.getInstance("X25519", conscryptProvider);
PublicKey publicKey = keyFactory.generatePublic(new RawKeySpec(rawPublicKey));

ByteArrayOutputStream baos = new ByteArrayOutputStream(16384);
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(publicKey);
}

String classNameHex = TestUtils.encodeHex(
publicKey.getClass().getName().getBytes(StandardCharsets.UTF_8));
String invalidPublicKeySerialized = "aced0005737200"
+ Integer.toHexString(publicKey.getClass().getName().length()) + classNameHex
+ "064c7113d078e42d" // serialVersionUID
+ "0300015b000b"
+ "75436f6f7264696e617465" // hex("uCoordinate")
+ "7400025b427870757200025b42acf317f8060854e00200007870000000"
+ "1f" // size of public key (is 31, which is invalid)
+ "e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c"
+ "78";

ByteArrayInputStream bais =
new ByteArrayInputStream(TestUtils.decodeHex(invalidPublicKeySerialized));
ObjectInputStream ois = new ObjectInputStream(bais);

assertThrows(IOException.class, () -> ois.readObject());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@
SlhDsaTest.class,
TestSessionBuilderTest.class,
TrustManagerImplTest.class,
X25519Test.class,
XwingTest.class,
// org.conscrypt.ct tests
VerifierTest.class,
Expand Down