Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a78f02b
Add ML-DSA-87 to NativeCrypto.
juergw Jun 2, 2025
8d7d145
Add more tests for ML-DSA-87.
juergw Jun 2, 2025
a4daa54
Add ML-DSA-87.
juergw Jun 2, 2025
2fa3714
Fix formatting.
juergw Jun 2, 2025
6ec1445
Fix formating.
juergw Jun 2, 2025
a023122
Fix ML-DSA-65 calls.
juergw Jun 2, 2025
aef6a2a
Use ExposedByteArrayOutputStream.
juergw Jun 2, 2025
34c12db
Fix copy-paste error.
juergw Jun 2, 2025
3c9f077
Fix KeyPairGeneratorTest.
juergw Jun 2, 2025
ca7402b
Fix SignatureTest.
juergw Jun 2, 2025
e48bf07
Fix formatting.
juergw Jun 2, 2025
91e3e4c
Add missing license.
juergw Jun 5, 2025
cfaee19
Throw an exception when MLDSA87_parse_public_key fails.
juergw Jun 10, 2025
43f6a79
Merge branch 'google:master' into master
juergw Jun 10, 2025
a2cecfa
Add JNI_TRACE call when MLDSA_sign couldn't allocate output.
juergw Jun 10, 2025
07bf66c
Merge branch 'master' of https://github.com/juergw/conscrypt
juergw Jun 10, 2025
53d44ba
Fix whitespace.
juergw Jun 10, 2025
7312418
Avoid some memcopy.
juergw Jun 17, 2025
a292bdf
Fix formatting, and one error message.
juergw Jun 17, 2025
2015f8d
Change the serialVersionUID of the private and the public key.
juergw Jun 17, 2025
d3567a3
Fix formatting.
juergw Jun 17, 2025
0b36486
Remove unused variable.
juergw Jun 17, 2025
596895a
Add test vector for ML-DSA.
juergw Jun 17, 2025
865981c
Keep serialization of ML-DSA private and public key compatible.
juergw Jun 23, 2025
1dab318
Add missing imports.
juergw Jun 23, 2025
645d1b0
Merge branch 'google:master' into master
juergw Jun 25, 2025
1c4909c
Merge remote-tracking branch 'upstream/master'
juergw Jun 26, 2025
6d34ac4
Remove remaining conflict marks.
juergw Jun 26, 2025
2e9317a
Merge branch 'google:master' into master
juergw Jun 26, 2025
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
192 changes: 171 additions & 21 deletions common/src/jni/main/cpp/conscrypt/native_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2544,17 +2544,6 @@ static jbyteArray NativeCrypto_MLDSA65_public_key_from_seed(JNIEnv* env, jclass,
return nullptr;
}

CBB cbb;
size_t size;
uint8_t public_key_bytes[MLDSA65_PUBLIC_KEY_BYTES];
if (!CBB_init_fixed(&cbb, public_key_bytes, MLDSA65_PUBLIC_KEY_BYTES) ||
!MLDSA65_marshal_public_key(&cbb, &publicKey) || !CBB_finish(&cbb, nullptr, &size) ||
size != MLDSA65_PUBLIC_KEY_BYTES) {
JNI_TRACE("Failed to serialize ML-DSA public key.");
conscrypt::jniutil::throwExceptionFromBoringSSLError(env, "MLDSA65_marshal_public_key");
return nullptr;
}

ScopedLocalRef<jbyteArray> publicKeyRef(
env, env->NewByteArray(static_cast<jsize>(MLDSA65_PUBLIC_KEY_BYTES)));
if (publicKeyRef.get() == nullptr) {
Expand All @@ -2565,7 +2554,18 @@ static jbyteArray NativeCrypto_MLDSA65_public_key_from_seed(JNIEnv* env, jclass,
if (publicKeyArray.get() == nullptr) {
return nullptr;
}
memcpy(publicKeyArray.get(), public_key_bytes, MLDSA65_PUBLIC_KEY_BYTES);

CBB cbb;
size_t size;
if (!CBB_init_fixed(&cbb, reinterpret_cast<uint8_t*>(publicKeyArray.get()),
MLDSA65_PUBLIC_KEY_BYTES) ||
!MLDSA65_marshal_public_key(&cbb, &publicKey) || !CBB_finish(&cbb, nullptr, &size) ||
size != MLDSA65_PUBLIC_KEY_BYTES) {
JNI_TRACE("Failed to serialize ML-DSA public key.");
conscrypt::jniutil::throwExceptionFromBoringSSLError(env, "MLDSA65_marshal_public_key");
return nullptr;
}

return publicKeyRef.release();
}

Expand Down Expand Up @@ -2598,25 +2598,26 @@ static jbyteArray NativeCrypto_MLDSA65_sign(JNIEnv* env, jclass, jbyteArray data
return nullptr;
}

uint8_t result[MLDSA65_SIGNATURE_BYTES];
if (!MLDSA65_sign(result, &privateKey, reinterpret_cast<const unsigned char*>(dataArray.get()),
dataLen, /* context */ NULL, /* context_len */ 0)) {
JNI_TRACE("MLDSA65_sign failed");
conscrypt::jniutil::throwExceptionFromBoringSSLError(env, "MLDSA65_sign");
return nullptr;
}

ScopedLocalRef<jbyteArray> resultRef(
env, env->NewByteArray(static_cast<jsize>(MLDSA65_SIGNATURE_BYTES)));
if (resultRef.get() == nullptr) {
JNI_TRACE("NativeCrypto_MLDSA65_sign: byte array creation failed");
return nullptr;
}

ScopedByteArrayRW resultArray(env, resultRef.get());
if (resultArray.get() == nullptr) {
return nullptr;
}
memcpy(resultArray.get(), result, MLDSA65_SIGNATURE_BYTES);

if (!MLDSA65_sign(reinterpret_cast<unsigned char*>(resultArray.get()), &privateKey,
reinterpret_cast<const unsigned char*>(dataArray.get()), dataLen,
/* context */ NULL, /* context_len */ 0)) {
JNI_TRACE("MLDSA65_sign failed");
conscrypt::jniutil::throwExceptionFromBoringSSLError(env, "MLDSA65_sign");
return nullptr;
}

return resultRef.release();
}

Expand All @@ -2635,6 +2636,7 @@ static jint NativeCrypto_MLDSA65_verify(JNIEnv* env, jclass, jbyteArray data, ji
MLDSA65_public_key pubkey;
if (!MLDSA65_parse_public_key(&pubkey, &cbs)) {
JNI_TRACE("MLDSA65_parse_public_key failed");
conscrypt::jniutil::throwIllegalArgumentException(env, "MLDSA65_parse_public_key failed");
return -1;
}

Expand Down Expand Up @@ -2663,6 +2665,151 @@ static jint NativeCrypto_MLDSA65_verify(JNIEnv* env, jclass, jbyteArray data, ji
return static_cast<jint>(result);
}

static jbyteArray NativeCrypto_MLDSA87_public_key_from_seed(JNIEnv* env, jclass,
jbyteArray privateKeySeed) {
CHECK_ERROR_QUEUE_ON_RETURN;

ScopedByteArrayRO seedArray(env, privateKeySeed);
if (seedArray.get() == nullptr) {
JNI_TRACE("NativeCrypto_MLDSA87_public_key_from_seed => privateKeySeed == null");
return nullptr;
}

MLDSA87_private_key privateKey;
if (!MLDSA87_private_key_from_seed(
&privateKey, reinterpret_cast<const uint8_t*>(seedArray.get()), seedArray.size())) {
JNI_TRACE("MLDSA87_private_key_from_seed failed");
conscrypt::jniutil::throwExceptionFromBoringSSLError(env, "MLDSA87_private_key_from_seed");
return nullptr;
}

MLDSA87_public_key publicKey;
if (!MLDSA87_public_from_private(&publicKey, &privateKey)) {
JNI_TRACE("MLDSA87_public_from_private failed");
conscrypt::jniutil::throwExceptionFromBoringSSLError(env, "MLDSA87_public_from_private");
return nullptr;
}

ScopedLocalRef<jbyteArray> publicKeyRef(
env, env->NewByteArray(static_cast<jsize>(MLDSA87_PUBLIC_KEY_BYTES)));
if (publicKeyRef.get() == nullptr) {
return nullptr;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason that we don't have JNI_TRACE here? I'd expect to have that macro invoked whenever you return nullptr.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a JNI_TRACE.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I investigated if we need to manually throw an exception here. It seems that this is not needed: when I set the number of bytes to the max value, i already get an out-of-memory exception.

}

ScopedByteArrayRW publicKeyArray(env, publicKeyRef.get());
if (publicKeyArray.get() == nullptr) {
return nullptr;
}

CBB cbb;
size_t size;
if (!CBB_init_fixed(&cbb, reinterpret_cast<uint8_t*>(publicKeyArray.get()),
MLDSA87_PUBLIC_KEY_BYTES) ||
!MLDSA87_marshal_public_key(&cbb, &publicKey) || !CBB_finish(&cbb, nullptr, &size) ||
size != MLDSA87_PUBLIC_KEY_BYTES) {
JNI_TRACE("Failed to serialize ML-DSA public key.");
conscrypt::jniutil::throwExceptionFromBoringSSLError(env, "MLDSA87_marshal_public_key");
return nullptr;
}
return publicKeyRef.release();
}

static jbyteArray NativeCrypto_MLDSA87_sign(JNIEnv* env, jclass, jbyteArray data, jint dataLen,
jbyteArray privateKeySeed) {
CHECK_ERROR_QUEUE_ON_RETURN;

ScopedByteArrayRO seedArray(env, privateKeySeed);
if (seedArray.get() == nullptr) {
JNI_TRACE("NativeCrypto_MLDSA87_sign => privateKeySeed == null");
return nullptr;
}

MLDSA87_private_key privateKey;
if (!MLDSA87_private_key_from_seed(
&privateKey, reinterpret_cast<const uint8_t*>(seedArray.get()), seedArray.size())) {
JNI_TRACE("MLDSA87_private_key_from_seed failed");
conscrypt::jniutil::throwExceptionFromBoringSSLError(env, "MLDSA87_private_key_from_seed");
return nullptr;
}

ScopedByteArrayRO dataArray(env, data);
if (dataArray.get() == nullptr) {
return nullptr;
}

if (ARRAY_OFFSET_LENGTH_INVALID(dataArray, 0, dataLen)) {
conscrypt::jniutil::throwException(env, "java/lang/ArrayIndexOutOfBoundsException",
"dataLen");
return nullptr;
}

ScopedLocalRef<jbyteArray> resultRef(
env, env->NewByteArray(static_cast<jsize>(MLDSA87_SIGNATURE_BYTES)));
if (resultRef.get() == nullptr) {
JNI_TRACE("NativeCrypto_MLDSA87_sign: byte array creation failed");
return nullptr;
}

ScopedByteArrayRW resultArray(env, resultRef.get());
if (resultArray.get() == nullptr) {
return nullptr;
}

if (!MLDSA87_sign(reinterpret_cast<unsigned char*>(resultArray.get()), &privateKey,
reinterpret_cast<const unsigned char*>(dataArray.get()), dataLen,
/* context */ NULL, /* context_len */ 0)) {
JNI_TRACE("MLDSA87_sign failed");
conscrypt::jniutil::throwExceptionFromBoringSSLError(env, "MLDSA87_sign");
return nullptr;
}

return resultRef.release();
}

static jint NativeCrypto_MLDSA87_verify(JNIEnv* env, jclass, jbyteArray data, jint dataLen,
jbyteArray sig, jbyteArray publicKey) {
CHECK_ERROR_QUEUE_ON_RETURN;

ScopedByteArrayRO publicKeyArray(env, publicKey);
if (publicKeyArray.get() == nullptr) {
JNI_TRACE("NativeCrypto_MLDSA87_verify => publicKey == null");
return -1;
}

CBS cbs;
CBS_init(&cbs, reinterpret_cast<const uint8_t*>(publicKeyArray.get()), publicKeyArray.size());
MLDSA87_public_key pubkey;
if (!MLDSA87_parse_public_key(&pubkey, &cbs)) {
JNI_TRACE("MLDSA87_parse_public_key failed");
conscrypt::jniutil::throwIllegalArgumentException(env, "MLDSA87_parse_public_key failed");
return -1;
}

ScopedByteArrayRO dataArray(env, data);
if (dataArray.get() == nullptr) {
return -1;
}

if (ARRAY_OFFSET_LENGTH_INVALID(dataArray, 0, dataLen)) {
conscrypt::jniutil::throwException(env, "java/lang/ArrayIndexOutOfBoundsException",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, this I actually also fail to understand. Here we throw an exception, which makes sense. But then why don't we throw in other parts where the input is invalid (e.g. "MLSDSA87_parse_public_key failed" could be an IllegalArgumentException.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that MLSDSA87_parse_public_key should probably also throw instead of just returing -1. I've changed it, and also changed the tests in NativeCryptoTest.

If I read the boringSSL code correctly, it seems that parsing only fails when the size is incorrect. And we already check that in Java, when we create the key class. So I think we don't end up in this case, and throwing will now change the behavior.

I've also did the same change for MLSDSA65_parse_public_key.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But note that when ScopedByteArray returns null, then it already calls jniThrowNullPointerException, so we don't have to do this ourselves.

"dataLen");
return -1;
}

ScopedByteArrayRO sigArray(env, sig);
if (sigArray.get() == nullptr) {
return -1;
}

int result =
MLDSA87_verify(&pubkey, reinterpret_cast<const unsigned char*>(sigArray.get()),
sigArray.size(), reinterpret_cast<const unsigned char*>(dataArray.get()),
dataLen, /*context=*/NULL, /*context_len=*/0);

JNI_TRACE("MLDSA87_verify(%p, %p, %p, %d) => %d", publicKey, sig, data, dataLen, result);
return static_cast<jint>(result);
}

static void NativeCrypto_SLHDSA_SHA2_128S_generate_key(JNIEnv* env, jclass,
jbyteArray outPublicArray,
jbyteArray outPrivateArray) {
Expand Down Expand Up @@ -11720,6 +11867,9 @@ static JNINativeMethod sNativeCryptoMethods[] = {
CONSCRYPT_NATIVE_METHOD(MLDSA65_public_key_from_seed, "([B)[B"),
CONSCRYPT_NATIVE_METHOD(MLDSA65_sign, "([BI[B)[B"),
CONSCRYPT_NATIVE_METHOD(MLDSA65_verify, "([BI[B[B)I"),
CONSCRYPT_NATIVE_METHOD(MLDSA87_public_key_from_seed, "([B)[B"),
CONSCRYPT_NATIVE_METHOD(MLDSA87_sign, "([BI[B)[B"),
CONSCRYPT_NATIVE_METHOD(MLDSA87_verify, "([BI[B[B)I"),
CONSCRYPT_NATIVE_METHOD(SLHDSA_SHA2_128S_generate_key, "([B[B)V"),
CONSCRYPT_NATIVE_METHOD(SLHDSA_SHA2_128S_sign, "([BI[B)[B"),
CONSCRYPT_NATIVE_METHOD(SLHDSA_SHA2_128S_verify, "([BI[B[B)I"),
Expand Down
51 changes: 51 additions & 0 deletions common/src/main/java/org/conscrypt/MlDsaAlgorithm.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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;

/** ML-DSA algorithm. */
public enum MlDsaAlgorithm {
ML_DSA_65("ML-DSA-65", 1952),
ML_DSA_87("ML-DSA-87", 2592);

private final String name;
private final int publicKeySize;

private MlDsaAlgorithm(String name, int publicKeySize) {
this.name = name;
this.publicKeySize = publicKeySize;
}

@Override
public String toString() {
return name;
}

public int publicKeySize() {
return publicKeySize;
}

public static MlDsaAlgorithm parse(String name) {
switch (name) {
case "ML-DSA-65":
return ML_DSA_65;
case "ML-DSA-87":
return ML_DSA_87;
default:
throw new IllegalArgumentException("Unsupported algorithm: " + name);
}
}
}
8 changes: 8 additions & 0 deletions common/src/main/java/org/conscrypt/NativeCrypto.java
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,14 @@ static native int ECDH_compute_key(byte[] out, int outOffset, NativeRef.EVP_PKEY

static native int MLDSA65_verify(byte[] data, int dataLen, byte[] sig, byte[] publicKey);

// --- MLDSA87 --------------------------------------------------------------

static native byte[] MLDSA87_public_key_from_seed(byte[] privateKeySeed);

static native byte[] MLDSA87_sign(byte[] data, int dataLen, byte[] privateKeySeed);

static native int MLDSA87_verify(byte[] data, int dataLen, byte[] sig, byte[] publicKey);

// --- SLHDSA_SHA2_128S --------------------------------------------------------------

static native void SLHDSA_SHA2_128S_generate_key(byte[] outPublicKey, byte[] outPrivateKey);
Expand Down
15 changes: 9 additions & 6 deletions common/src/main/java/org/conscrypt/OpenSSLProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +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");
put("KeyPairGenerator.ML-DSA", PREFIX + "OpenSslMlDsaKeyPairGenerator$MlDsa");
put("KeyPairGenerator.ML-DSA-65", PREFIX + "OpenSslMlDsaKeyPairGenerator$MlDsa65");
put("KeyPairGenerator.ML-DSA-87", PREFIX + "OpenSslMlDsaKeyPairGenerator$MlDsa87");

// We don't support SLH-DSA, because it's not clear which algorithm to use.
put("KeyPairGenerator.SLH-DSA-SHA2-128S", PREFIX + "OpenSslSlhDsaKeyPairGenerator");
Expand All @@ -240,8 +241,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");
put("KeyFactory.ML-DSA", PREFIX + "OpenSslMlDsaKeyFactory$MlDsa");
put("KeyFactory.ML-DSA-65", PREFIX + "OpenSslMlDsaKeyFactory$MlDsa65");
put("KeyFactory.ML-DSA-87", PREFIX + "OpenSslMlDsaKeyFactory$MlDsa87");

// We don't support SLH-DSA, because it's not clear which algorithm to use.
put("KeyFactory.SLH-DSA-SHA2-128S", PREFIX + "OpenSslSlhDsaKeyFactory");
Expand Down Expand Up @@ -372,8 +374,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");
putSignatureImplClass("ML-DSA", "OpenSslSignatureMlDsa$MlDsa");
putSignatureImplClass("ML-DSA-65", "OpenSslSignatureMlDsa$MlDsa65");
putSignatureImplClass("ML-DSA-87", "OpenSslSignatureMlDsa$MlDsa87");

// We don't support SLH-DSA, because it's not clear which algorithm to use.
putSignatureImplClass("SLH-DSA-SHA2-128S", "OpenSslSignatureSlhDsa");
Expand Down
Loading
Loading