Add ML-DSA-87.#1342
Conversation
Also, add MlDsaAlgorithm enum, which we will need later.
There was a problem hiding this comment.
This is missing the license
tholenst
left a comment
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
I added a JNI_TRACE.
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Would it make sense to call BoringSSL for this here? Calling "Arrays.equals" is so obvious non-constant time it feels scary.
There was a problem hiding this comment.
I agree, but I'd prefer to do this in a different pull request. Because this should also be changed in other places.
And also do this for MLDSA65.
| if (publicKeyArray.get() == nullptr) { | ||
| return nullptr; | ||
| } | ||
| memcpy(publicKeyArray.get(), public_key_bytes, MLDSA87_PUBLIC_KEY_BYTES); |
There was a problem hiding this comment.
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?)
There was a problem hiding this comment.
You are right, that is both simpler and faster.
| if (resultArray.get() == nullptr) { | ||
| return nullptr; | ||
| } | ||
| memcpy(resultArray.get(), result, MLDSA87_SIGNATURE_BYTES); |
| private static final long serialVersionUID = 453861992373478445L; | ||
|
|
||
| private final byte[] raw; | ||
| private final MlDsaAlgorithm algorithm; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
I don't understand why we have this here; just for future cases?
There was a problem hiding this comment.
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")) { |
There was a problem hiding this comment.
Should we also have a test vector where this is just ML-DSA?
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.
| oos.writeObject(privateKey); | ||
| } | ||
|
|
||
| String expectedHexEncoding = "aced000573720024" |
There was a problem hiding this comment.
Do we want to decode this more?
This would be "STREAM_MAGIC" (https://docs.oracle.com/en/java/javase/11/docs/specs/serialization/protocol.html#grammar-for-the-stream-format)
There was a problem hiding this comment.
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.
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.