-
Notifications
You must be signed in to change notification settings - Fork 324
Add ML-DSA-87. #1342
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 ML-DSA-87. #1342
Changes from all commits
a78f02b
8d7d145
a4daa54
2fa3714
6ec1445
a023122
aef6a2a
34c12db
3c9f077
ca7402b
e48bf07
91e3e4c
cfaee19
43f6a79
a2cecfa
07bf66c
53d44ba
7312418
a292bdf
2015f8d
d3567a3
0b36486
596895a
865981c
1dab318
645d1b0
1c4909c
6d34ac4
2e9317a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) { | ||
|
|
@@ -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(); | ||
| } | ||
|
|
||
|
|
@@ -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(); | ||
| } | ||
|
|
||
|
|
@@ -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; | ||
| } | ||
|
|
||
|
|
@@ -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; | ||
| } | ||
|
|
||
| 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", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
|
@@ -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"), | ||
|
|
||
| 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); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.