Skip to content

Commit

Permalink
Add Ed25519 signatures to Conscrypt. (#1297)
Browse files Browse the repository at this point in the history
* Add Ed25519 Signatures to Conscrypt.

* Remove test keygenWithConscrypt_useWithDefaultProvider.

This is not a good test, its success depends on what the default
provider is.
  • Loading branch information
juergw authored Jan 29, 2025
1 parent 5db1caa commit 5473d34
Show file tree
Hide file tree
Showing 11 changed files with 966 additions and 40 deletions.
12 changes: 12 additions & 0 deletions common/src/main/java/org/conscrypt/OpenSSLProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ public OpenSSLProvider(String providerName) {
put("Alg.Alias.KeyPairGenerator.1.3.101.110", "XDH");
put("Alg.Alias.KeyPairGenerator.X25519", "XDH");

put("KeyPairGenerator.EdDSA", PREFIX + "OpenSslEdDsaKeyPairGenerator");
put("Alg.Alias.KeyPairGenerator.1.3.101.112", "EdDSA");
put("Alg.Alias.KeyPairGenerator.Ed25519", "EdDSA");

/* == KeyFactory == */
put("KeyFactory.RSA", PREFIX + "OpenSSLRSAKeyFactory");
put("Alg.Alias.KeyFactory.1.2.840.113549.1.1.1", "RSA");
Expand All @@ -226,6 +230,10 @@ public OpenSSLProvider(String providerName) {
put("Alg.Alias.KeyFactory.1.3.101.110", "XDH");
put("Alg.Alias.KeyFactory.X25519", "XDH");

put("KeyFactory.EdDSA", PREFIX + "OpenSslEdDsaKeyFactory");
put("Alg.Alias.KeyFactory.1.3.101.112", "EdDSA");
put("Alg.Alias.KeyFactory.Ed25519", "EdDSA");

/* == SecretKeyFactory == */
put("SecretKeyFactory.DESEDE", PREFIX + "DESEDESecretKeyFactory");
put("Alg.Alias.SecretKeyFactory.TDEA", "DESEDE");
Expand Down Expand Up @@ -348,6 +356,10 @@ public OpenSSLProvider(String providerName) {
putSignatureImplClass("SHA512withRSA/PSS", "OpenSSLSignature$SHA512RSAPSS");
put("Alg.Alias.Signature.SHA512withRSAandMGF1", "SHA512withRSA/PSS");

putSignatureImplClass("EdDSA", "OpenSslSignatureEdDsa");
put("Alg.Alias.Signature.1.3.101.112", "EdDSA");
put("Alg.Alias.Signature.Ed25519", "EdDSA");

/* === SecureRandom === */
/*
* We have to specify SHA1PRNG because various documentation mentions
Expand Down
156 changes: 156 additions & 0 deletions common/src/main/java/org/conscrypt/OpenSslEdDsaKeyFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* 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 java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactorySpi;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/** An implementation of a {@link KeyFactorySpi} for EdDSA keys based on BoringSSL. */
@Internal
public final class OpenSslEdDsaKeyFactory extends KeyFactorySpi {
public OpenSslEdDsaKeyFactory() {}

@Override
protected PublicKey engineGeneratePublic(KeySpec keySpec) throws InvalidKeySpecException {
if (keySpec == null) {
throw new InvalidKeySpecException("keySpec == null");
}
if (keySpec instanceof EncodedKeySpec) {
return new OpenSslEdDsaPublicKey((EncodedKeySpec) keySpec);
}
throw new InvalidKeySpecException("Must use X509EncodedKeySpec or Raw EncodedKeySpec; was "
+ keySpec.getClass().getName());
}

@Override
protected PrivateKey engineGeneratePrivate(KeySpec keySpec) throws InvalidKeySpecException {
if (keySpec == null) {
throw new InvalidKeySpecException("keySpec == null");
}
if (keySpec instanceof EncodedKeySpec) {
return new OpenSslEdDsaPrivateKey((EncodedKeySpec) keySpec);
}
throw new InvalidKeySpecException("Must use PKCS8EncodedKeySpec or Raw EncodedKeySpec; was "
+ keySpec.getClass().getName());
}

@Override
protected <T extends KeySpec> T engineGetKeySpec(Key key, Class<T> keySpec)
throws InvalidKeySpecException {
if (key == null) {
throw new InvalidKeySpecException("key == null");
}
if (keySpec == null) {
throw new InvalidKeySpecException("keySpec == null");
}
if (!key.getAlgorithm().equals("EdDSA") && !key.getAlgorithm().equals("Ed25519")) {
throw new InvalidKeySpecException("Key must be an EdDSA or Ed25519 key");
}
if (key.getEncoded() == null) {
throw new InvalidKeySpecException("Key is destroyed");
}
// Convert any "foreign" keys to our own type, this has the same requirements as
// converting to a KeySpec below, and is a no-op for our own keys.
try {
key = engineTranslateKey(key);
} catch (InvalidKeyException e) {
throw new InvalidKeySpecException("Unsupported key class: " + key.getClass(), e);
}

if (key instanceof OpenSslEdDsaPublicKey) {
OpenSslEdDsaPublicKey conscryptKey = (OpenSslEdDsaPublicKey) key;
if (X509EncodedKeySpec.class.isAssignableFrom(keySpec)) {
@SuppressWarnings("unchecked")
T result = (T) new X509EncodedKeySpec(key.getEncoded());
return result;
} else if (EncodedKeySpec.class.isAssignableFrom(keySpec)) {
return makeRawKeySpec(conscryptKey.getRaw(), keySpec);
}
} else if (key instanceof OpenSslEdDsaPrivateKey) {
OpenSslEdDsaPrivateKey conscryptKey = (OpenSslEdDsaPrivateKey) key;
if (PKCS8EncodedKeySpec.class.isAssignableFrom(keySpec)) {
@SuppressWarnings("unchecked")
T result = (T) new PKCS8EncodedKeySpec(key.getEncoded());
return result;
} else if (EncodedKeySpec.class.isAssignableFrom(keySpec)) {
return makeRawKeySpec(conscryptKey.getRaw(), keySpec);
}
}
throw new InvalidKeySpecException("Unsupported key type and key spec combination; key="
+ key.getClass().getName() + ", keySpec=" + keySpec.getName());
}

private <T extends KeySpec> T makeRawKeySpec(byte[] bytes, Class<T> keySpecClass)
throws InvalidKeySpecException {
try {
Constructor<T> constructor = keySpecClass.getConstructor(byte[].class);
T instance = constructor.newInstance((Object) bytes);
EncodedKeySpec spec = (EncodedKeySpec) instance;
if (!spec.getFormat().equalsIgnoreCase("raw")) {
throw new InvalidKeySpecException("EncodedKeySpec class must be raw format");
}
return instance;
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException
| IllegalAccessException e) {
throw new InvalidKeySpecException(
"Can't process KeySpec class " + keySpecClass.getName(), e);
}
}

@Override
protected Key engineTranslateKey(Key key) throws InvalidKeyException {
if (key == null) {
throw new InvalidKeyException("key == null");
}
if ((key instanceof OpenSslEdDsaPublicKey) || (key instanceof OpenSslEdDsaPrivateKey)) {
return key;
} else 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 must be XEC public or private key; was " + key.getClass().getName());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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 java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;

/**
* An implementation of {@link KeyPairGenerator} for XDH keys which uses BoringSSL to perform all
* the operations. This only supports EdDSA keys.
*/
@Internal
public final class OpenSslEdDsaKeyPairGenerator extends KeyPairGenerator {
private static final String ALGORITHM = "EdDSA";

public OpenSslEdDsaKeyPairGenerator() {
super(ALGORITHM);
}

@Override
public KeyPair generateKeyPair() {
byte[] publicKeyBytes = new byte[32];
byte[] privateKeyBytes = new byte[64];

NativeCrypto.ED25519_keypair(publicKeyBytes, privateKeyBytes);

// BoringSSL uses a 64-byte private key. We only need the seed, which is the first 32 bytes.
byte[] privateKeySeed = Arrays.copyOf(privateKeyBytes, 32);

return new KeyPair(new OpenSslEdDsaPublicKey(publicKeyBytes),
new OpenSslEdDsaPrivateKey(privateKeySeed));
}

@Override
public void initialize(int keysize, SecureRandom random) {
if (keysize != 255) {
throw new IllegalArgumentException("EdDSA only supports key size 255");
}
}

@Override
public void initialize(AlgorithmParameterSpec param, SecureRandom random)
throws InvalidAlgorithmParameterException {
throw new InvalidAlgorithmParameterException(
"No AlgorithmParameterSpec classes are supported");
}
}
120 changes: 120 additions & 0 deletions common/src/main/java/org/conscrypt/OpenSslEdDsaPrivateKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* 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 java.security.PrivateKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;

/** An OpenSSL EdDSA private key. */
public class OpenSslEdDsaPrivateKey implements PrivateKey {
private static final long serialVersionUID = -3136201500221850916L;
private static final byte[] pkcs8Preamble = new byte[] {
0x30, 0x2e, // Sequence: 46 bytes
0x02, 0x01, 0x00, // Integer: 0 (version)
0x30, 0x05, // Sequence: 5 bytes
0x06, 0x03, 0x2b, 0x65, 0x70, // OID: 1.3.101.112 (EdDSA)
0x04, 0x22, 0x04, 0x20, // Octet string: 32 bytes
// Key bytes follow directly
};

// BoringSSL uses a 64-byte private key. But this key here is only
// the 32-byte seed, as defined in RFC 8032.
static final int ED25519_PRIVATE_KEY_SIZE_BYTES = 32;

private byte[] privateKeyBytes;

public OpenSslEdDsaPrivateKey(EncodedKeySpec keySpec) throws InvalidKeySpecException {
byte[] encoded = keySpec.getEncoded();
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");
}
privateKeyBytes = Arrays.copyOfRange(encoded, pkcs8Preamble.length, encoded.length);
} else if (keySpec.getFormat().equalsIgnoreCase("raw")) {
privateKeyBytes = encoded;
} else {
throw new InvalidKeySpecException("Encoding must be in PKCS#8 or raw format");
}
if (privateKeyBytes.length != ED25519_PRIVATE_KEY_SIZE_BYTES) {
throw new InvalidKeySpecException("Invalid key size");
}
}

public OpenSslEdDsaPrivateKey(byte[] privateKeyBytes) {
if (privateKeyBytes.length != ED25519_PRIVATE_KEY_SIZE_BYTES) {
throw new IllegalArgumentException("Invalid key size");
}
this.privateKeyBytes = privateKeyBytes.clone();
}

@Override
public String getAlgorithm() {
return "EdDSA";
}

@Override
public String getFormat() {
return "PKCS#8";
}

@Override
public byte[] getEncoded() {
if (privateKeyBytes == null) {
throw new IllegalStateException("key is destroyed");
}
return ArrayUtils.concat(pkcs8Preamble, privateKeyBytes);
}

byte[] getRaw() {
if (privateKeyBytes == null) {
throw new IllegalStateException("key is destroyed");
}
return privateKeyBytes.clone();
}

@Override
public void destroy() {
if (privateKeyBytes != null) {
Arrays.fill(privateKeyBytes, (byte) 0);
privateKeyBytes = null;
}
}

@Override
public boolean isDestroyed() {
return privateKeyBytes == null;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof OpenSslEdDsaPrivateKey)) {
return false;
}
OpenSslEdDsaPrivateKey that = (OpenSslEdDsaPrivateKey) o;
return Arrays.equals(privateKeyBytes, that.privateKeyBytes);
}

@Override
public int hashCode() {
return Arrays.hashCode(privateKeyBytes);
}
}
Loading

0 comments on commit 5473d34

Please sign in to comment.