Skip to content

Add ML-DSA-87.#1342

Merged
miguelaranda0 merged 29 commits into
google:masterfrom
juergw:master
Jun 30, 2025
Merged

Add ML-DSA-87.#1342
miguelaranda0 merged 29 commits into
google:masterfrom
juergw:master

Conversation

@juergw

@juergw juergw commented Jun 2, 2025

Copy link
Copy Markdown
Collaborator

Currently, the ML-DSA implementation only supports the default ML-DSA-65. This pull request now adds support for ML-DSA-87.

Note that the functions in native_crypto.cc for ML-DSA-87 are a copy of the functions for ML-DSA-65, but using different functions.

We add an enum class that is used to indicate which algorithm is used.

We also add validation of the key size to the private and public keys.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is missing the license

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.

Thanks. Done.

@tholenst tholenst left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'm not too familiar with Conscrypt, so I still might miss important things.

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.

}

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.

}
OpenSslMlDsaPrivateKey that = (OpenSslMlDsaPrivateKey) o;
return Arrays.equals(seed, that.seed);
return algorithm.equals(that.algorithm) && Arrays.equals(seed, that.seed);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Would it make sense to call BoringSSL for this here? Calling "Arrays.equals" is so obvious non-constant time it feels scary.

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, but I'd prefer to do this in a different pull request. Because this should also be changed in other places.

@juergw
juergw requested a review from tholenst June 13, 2025 09:47
if (publicKeyArray.get() == nullptr) {
return nullptr;
}
memcpy(publicKeyArray.get(), public_key_bytes, MLDSA87_PUBLIC_KEY_BYTES);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Let's instead simply write directly into publicKeyArray.get(): move the construction of publicKeyArray before the call to MLDSA87_marshal_public_key, and initialize the CBB with publicKeyArray.get().

(Or is there a reason not to do that?)

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.

You are right, that is both simpler and faster.

if (resultArray.get() == nullptr) {
return nullptr;
}
memcpy(resultArray.get(), result, MLDSA87_SIGNATURE_BYTES);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Also here, we can avoid this memcpy

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.

Done. Thanks.

private static final long serialVersionUID = 453861992373478445L;

private final byte[] raw;
private final MlDsaAlgorithm algorithm;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do we need to make this transient? I don't know if Conscrypt offers anything in terms of stability of the serialization.

If we change it, let's at least change the serialVersionUID to ensure that things properly fail.

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.

This is a good question. I think it would be cumbersome to keep the same serialization as before. So I think we should just choose a new version ID.

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 found a way to keep the serialization as it is. I'll do that, I think it's better to keep serialization working even if it's unlikely that anybody is using it.

putKeySize("XDH", 255);
putKeySize("EdDSA", 255);
putKeySize("ML-DSA", -1);
putKeySize("ML-DSA-44", -1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I don't understand why we have this here; just for future cases?

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.

It's needed if you run the test with a OpenJDK (version 24 and newer) that has ML-DSA-44 implemented.

byte[] signature = vector.getBytes("signature");

assertEquals(errMsg + ", algorithm:", "ML-DSA-65", algorithm);
if (!algorithm.equals("ML-DSA-65") && !algorithm.equals("ML-DSA-87")) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should we also have a test vector where this is just ML-DSA?

juergw added 5 commits June 17, 2025 13:45
Because we need to add a new field to these keys, it would be cumbersome
to keep the serialization exactly the same. And because the serialization
changes, we need to change the serialVersionUID.
@juergw
juergw requested a review from miguelaranda0 June 18, 2025 06:40
@juergw
juergw marked this pull request as draft June 19, 2025 12:09
@tholenst
tholenst self-requested a review June 19, 2025 12:15
@juergw
juergw marked this pull request as ready for review June 23, 2025 12:52
oos.writeObject(privateKey);
}

String expectedHexEncoding = "aced000573720024"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actually I think this isn't necessary, but I do think we should add this test before rewriting the class. If it's part of the public API and we want to keep it, let's do it properly.

@miguelaranda0
miguelaranda0 merged commit 40b0b67 into google:master Jun 30, 2025
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants