Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for ML-DSA-65 signatures. #1305

Merged
merged 6 commits into from
Feb 24, 2025
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
9 changes: 9 additions & 0 deletions common/src/main/java/org/conscrypt/OpenSSLProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ public OpenSSLProvider(String providerName) {
put("Alg.Alias.KeyPairGenerator.1.3.101.112", "EdDSA");
put("Alg.Alias.KeyPairGenerator.Ed25519", "EdDSA");

put("KeyPairGenerator.ML-DSA", PREFIX + "OpenSslMlDsaKeyPairGenerator");
put("Alg.Alias.KeyPairGenerator.ML-DSA-65", "ML-DSA");

/* == KeyFactory == */
put("KeyFactory.RSA", PREFIX + "OpenSSLRSAKeyFactory");
put("Alg.Alias.KeyFactory.1.2.840.113549.1.1.1", "RSA");
Expand All @@ -234,6 +237,9 @@ public OpenSSLProvider(String providerName) {
put("Alg.Alias.KeyFactory.1.3.101.112", "EdDSA");
put("Alg.Alias.KeyFactory.Ed25519", "EdDSA");

put("KeyFactory.ML-DSA", PREFIX + "OpenSslMlDsaKeyFactory");
put("Alg.Alias.KeyFactory.ML-DSA-65", "ML-DSA");

/* == SecretKeyFactory == */
put("SecretKeyFactory.DESEDE", PREFIX + "DESEDESecretKeyFactory");
put("Alg.Alias.SecretKeyFactory.TDEA", "DESEDE");
Expand Down Expand Up @@ -360,6 +366,9 @@ public OpenSSLProvider(String providerName) {
put("Alg.Alias.Signature.1.3.101.112", "EdDSA");
put("Alg.Alias.Signature.Ed25519", "EdDSA");

putSignatureImplClass("ML-DSA", "OpenSslSignatureMlDsa");
put("Alg.Alias.Signature.ML-DSA-65", "ML-DSA");

/* === SecureRandom === */
/*
* We have to specify SHA1PRNG because various documentation mentions
Expand Down
122 changes: 122 additions & 0 deletions common/src/main/java/org/conscrypt/OpenSslMlDsaKeyFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright (C) 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 MLDSA keys based on BoringSSL. */
@Internal
public final class OpenSslMlDsaKeyFactory extends KeyFactorySpi {
public OpenSslMlDsaKeyFactory() {}

@Override
protected PublicKey engineGeneratePublic(KeySpec keySpec) throws InvalidKeySpecException {
if (keySpec == null) {
throw new InvalidKeySpecException("keySpec == null");
}
if (keySpec instanceof EncodedKeySpec) {
return new OpenSslMlDsaPublicKey((EncodedKeySpec) keySpec);
}
throw new InvalidKeySpecException(
"Currently only EncodedKeySpec is supported; 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 OpenSslMlDsaPrivateKey((EncodedKeySpec) keySpec);
}
throw new InvalidKeySpecException(
"Currently only EncodedKeySpec is supported; 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("ML-DSA")) {
throw new InvalidKeySpecException("Key must be an ML-DSA key");
}
if (key instanceof OpenSslMlDsaPublicKey) {
OpenSslMlDsaPublicKey conscryptKey = (OpenSslMlDsaPublicKey) key;
if (X509EncodedKeySpec.class.isAssignableFrom(keySpec)) {
throw new UnsupportedOperationException(
"X509EncodedKeySpec is currently not supported");
} else if (EncodedKeySpec.class.isAssignableFrom(keySpec)) {
return makeRawKeySpec(conscryptKey.getRaw(), keySpec);
}
} else if (key instanceof OpenSslMlDsaPrivateKey) {
OpenSslMlDsaPrivateKey conscryptKey = (OpenSslMlDsaPrivateKey) key;
if (PKCS8EncodedKeySpec.class.isAssignableFrom(keySpec)) {
throw new UnsupportedOperationException(
"PKCS8EncodedKeySpec is currently not supported");
} else if (EncodedKeySpec.class.isAssignableFrom(keySpec)) {
return makeRawKeySpec(conscryptKey.getSeed(), 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 OpenSslMlDsaPublicKey) || (key instanceof OpenSslMlDsaPrivateKey)) {
return key;
}
throw new InvalidKeyException(
"Key must be OpenSslMlDsaPublicKey or OpenSslMlDsaPrivateKey");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (C) 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.InvalidParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;

/**
* An implementation of {@link KeyPairGenerator} for ML-DSA keys which uses BoringSSL to perform all
* the operations.
*/
@Internal
public final class OpenSslMlDsaKeyPairGenerator extends KeyPairGenerator {
private static final String ALGORITHM = "ML-DSA";

public OpenSslMlDsaKeyPairGenerator() {
super(ALGORITHM);
}

@Override
public void initialize(int bits) throws InvalidParameterException {
if (bits != -1) {
throw new InvalidParameterException("ML-DSA only supports -1 for bits");
}
}

@Override
public KeyPair generateKeyPair() {
byte[] privateKeyBytes = new byte[32];
NativeCrypto.RAND_bytes(privateKeyBytes);
byte[] publicKeyBytes = NativeCrypto.MLDSA65_public_key_from_seed(privateKeyBytes);

return new KeyPair(new OpenSslMlDsaPublicKey(publicKeyBytes),
new OpenSslMlDsaPrivateKey(privateKeyBytes));
}
}
92 changes: 92 additions & 0 deletions common/src/main/java/org/conscrypt/OpenSslMlDsaPrivateKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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 ML-DSA private key. */
public class OpenSslMlDsaPrivateKey implements PrivateKey {
private byte[] seed;

public OpenSslMlDsaPrivateKey(EncodedKeySpec keySpec) throws InvalidKeySpecException {
byte[] encoded = keySpec.getEncoded();
if ("raw".equalsIgnoreCase(keySpec.getFormat())) {
seed = encoded;
} else {
throw new InvalidKeySpecException("Encoding must be in raw format");
}
}

public OpenSslMlDsaPrivateKey(byte[] seed) {
this.seed = seed.clone();
}

@Override
public String getAlgorithm() {
return "ML-DSA";
}

@Override
public String getFormat() {
throw new UnsupportedOperationException("getFormat() not yet supported");
}

@Override
public byte[] getEncoded() {
throw new UnsupportedOperationException("getEncoded() not yet supported");
}

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

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

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

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

@Override
public int hashCode() {
return Arrays.hashCode(seed);
}
}
88 changes: 88 additions & 0 deletions common/src/main/java/org/conscrypt/OpenSslMlDsaPublicKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* 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.PublicKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;

/** An OpenSSL ML-DSA public key. */
public class OpenSslMlDsaPublicKey implements PublicKey {
private static final long serialVersionUID = 453861992373478445L;

private final byte[] raw;

public OpenSslMlDsaPublicKey(EncodedKeySpec keySpec) throws InvalidKeySpecException {
byte[] encoded = keySpec.getEncoded();
if ("raw".equalsIgnoreCase(keySpec.getFormat())) {
raw = encoded;
} else {
throw new InvalidKeySpecException("Encoding must be in raw format");
}
}

public OpenSslMlDsaPublicKey(byte[] raw) {
this.raw = raw.clone();
}

@Override
public String getAlgorithm() {
return "ML-DSA";
}

@Override
public String getFormat() {
throw new UnsupportedOperationException("getFormat() not yet supported");
}

@Override
public byte[] getEncoded() {
throw new UnsupportedOperationException("getEncoded() not yet supported");
}

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

@Override
public boolean equals(Object o) {
if (raw == null) {
throw new IllegalStateException("key is destroyed");
}

if (this == o) {
return true;
}
if (!(o instanceof OpenSslMlDsaPublicKey)) {
return false;
}
OpenSslMlDsaPublicKey that = (OpenSslMlDsaPublicKey) o;
return Arrays.equals(raw, that.raw);
}

@Override
public int hashCode() {
if (raw == null) {
throw new IllegalStateException("key is destroyed");
}
return Arrays.hashCode(raw);
}
}
Loading