Skip to content

Commit eb936b1

Browse files
authoredMar 3, 2025
Encode ML-DSA priv key as seed, expose MlDsaUtils (#434)
See [PR #434][1] for a detailed description of this commit. [1]: #434
1 parent c4a382d commit eb936b1

17 files changed

+430
-74
lines changed
 

‎CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* [PR 426:](https://github.com/corretto/amazon-corretto-crypto-provider/pull/426) Add null check to AesCbcSpi
1414
* [PR 427:](https://github.com/corretto/amazon-corretto-crypto-provider/pull/427) Add provider info string
1515
* [PR 432:](https://github.com/corretto/amazon-corretto-crypto-provider/pull/432) Support Ed25519ph, bump AWS-LC to v1.46.0yy
16+
* [PR 434:](https://github.com/corretto/amazon-corretto-crypto-provider/pull/434) Encode ML-DSA priv key as seed, expose MlDsaUtils
1617

1718
## 2.4.1
1819

‎CMakeLists.txt

+3-1
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,12 @@ add_custom_command(
161161
# detected by CMake.
162162
if (${CMAKE_VERSION} VERSION_LESS "3.12.0")
163163
file(GLOB_RECURSE ACCP_SRC "src/com/amazon/corretto/crypto/provider/*.java")
164+
file(GLOB_RECURSE ACCP_UTILS_SRC "src/com/amazon/corretto/crypto/utils/*.java")
164165
else()
165166
file(GLOB_RECURSE ACCP_SRC CONFIGURE_DEPENDS "src/com/amazon/corretto/crypto/provider/*.java")
167+
file(GLOB_RECURSE ACCP_UTILS_SRC CONFIGURE_DEPENDS "src/com/amazon/corretto/crypto/utils/*.java")
166168
endif()
167-
set(ACCP_SRC ${ACCP_SRC} ${GENERATED_JAVA_SRC})
169+
set(ACCP_SRC ${ACCP_SRC} ${ACCP_UTILS_SRC} ${GENERATED_JAVA_SRC})
168170

169171
set(BASE_JAVA_COMPILE_FLAGS ${CMAKE_JAVA_COMPILE_FLAGS} -h "${JNI_HEADER_DIR}" -Werror -Xlint)
170172

‎README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ ACCP did not track a FIPS branch/release version of AWS-LC until ACCP v2.3.0. Be
133133
| 2.3.3 | 1.17.0 | 2.0.2 |
134134
| 2.4.0 | 1.30.1 | 2.0.13 |
135135
| 2.4.1 | 1.30.1 | 2.0.13 |
136-
| 2.5.0 | 1.46.0 | 3.0.0 |
136+
| 2.5.0 | 1.47.0 | 3.0.0 |
137137

138138
Notable differences between ACCP and ACCP-FIPS:
139139
* ACCP uses [the latest release of AWS-LC](https://github.com/aws/aws-lc/releases), whereas, ACCP-FIPS uses [the fips-2022-11-02 branch of AWS-LC](https://github.com/aws/aws-lc/tree/fips-2022-11-02).

‎aws-lc

‎build.gradle

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ plugins {
1414

1515
group = 'software.amazon.cryptools'
1616
version = '2.5.0'
17-
ext.awsLcMainTag = 'v1.46.0'
17+
ext.awsLcMainTag = 'v1.47.0'
1818
ext.awsLcFipsTag = 'AWS-LC-FIPS-3.0.0'
1919
ext.isExperimentalFips = Boolean.getBoolean('EXPERIMENTAL_FIPS')
2020
ext.isFips = ext.isExperimentalFips || Boolean.getBoolean('FIPS')
@@ -78,6 +78,7 @@ spotless {
7878
target 'csrc/*'
7979
licenseHeaderFile 'build-tools/license-headers/LicenseHeader.h'
8080
clangFormat(clangFormatVersion)
81+
toggleOffOn()
8182
}
8283
}
8384
}

‎csrc/auto_free.h

+5
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ class OPENSSL_buffer_auto {
117117
{
118118
}
119119

120+
explicit OPENSSL_buffer_auto(size_t buf_size)
121+
: buf((unsigned char*)OPENSSL_malloc(buf_size))
122+
{
123+
}
124+
120125
virtual ~OPENSSL_buffer_auto() { OPENSSL_free(buf); }
121126

122127
operator unsigned char*() { return buf; }

‎csrc/java_evp_keys.cpp

+49
Original file line numberDiff line numberDiff line change
@@ -666,3 +666,52 @@ JNIEXPORT jbyteArray JNICALL Java_com_amazon_corretto_crypto_provider_EvpRsaPriv
666666

667667
return result;
668668
}
669+
670+
#if !defined(FIPS_BUILD) || defined(EXPERIMENTAL_FIPS_BUILD)
671+
/*
672+
* Class: com_amazon_corretto_crypto_provider_EvpRsaPrivateKey
673+
* Method: encodeRsaPrivateKey
674+
* Signature: (J)[B
675+
*/
676+
JNIEXPORT jbyteArray JNICALL Java_com_amazon_corretto_crypto_provider_EvpMlDsaPrivateKey_encodeMlDsaPrivateKey(
677+
JNIEnv* pEnv, jclass, jlong keyHandle)
678+
{
679+
jbyteArray result = NULL;
680+
681+
try {
682+
raii_env env(pEnv);
683+
684+
EVP_PKEY* key = reinterpret_cast<EVP_PKEY*>(keyHandle);
685+
CHECK_OPENSSL(EVP_PKEY_id(key) == EVP_PKEY_PQDSA);
686+
687+
uint8_t* der;
688+
size_t der_len;
689+
CBB cbb;
690+
CHECK_OPENSSL(CBB_init(&cbb, 0));
691+
// Failure below may just indicate that we don't have the seed, so retry with |encodeExpandedMLDSAPrivateKey|
692+
// and encode in PKCS8 (RFC 5208) format after clearing the error queue.
693+
if (EVP_marshal_private_key(&cbb, key)) {
694+
if (!CBB_finish(&cbb, &der, &der_len)) {
695+
OPENSSL_free(der);
696+
throw_java_ex(EX_RUNTIME_CRYPTO, "Error finalizing seed ML-DSA key");
697+
}
698+
} else {
699+
ERR_clear_error();
700+
der_len = encodeExpandedMLDSAPrivateKey(key, &der);
701+
}
702+
CBB_cleanup(&cbb);
703+
704+
if (!(result = env->NewByteArray(der_len))) {
705+
OPENSSL_free(der);
706+
throw_java_ex(EX_OOM, "Unable to allocate DER array");
707+
}
708+
// This may throw, if it does we'll just keep the exception state as we return.
709+
env->SetByteArrayRegion(result, 0, der_len, (const jbyte*)der);
710+
OPENSSL_free(der);
711+
} catch (java_ex& ex) {
712+
ex.throw_to_java(pEnv);
713+
}
714+
715+
return result;
716+
}
717+
#endif // !defined(FIPS_BUILD) || defined(EXPERIMENTAL_FIPS_BUILD)

‎csrc/keyutils.cpp

+51
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,55 @@ RSA* new_private_RSA_key_with_no_e(BIGNUM const* n, BIGNUM const* d)
179179
return result;
180180
}
181181

182+
#if !defined(FIPS_BUILD) || defined(EXPERIMENTAL_FIPS_BUILD)
183+
size_t encodeExpandedMLDSAPrivateKey(const EVP_PKEY* key, uint8_t** out)
184+
{
185+
CHECK_OPENSSL(key);
186+
CHECK_OPENSSL(EVP_PKEY_id(key) == EVP_PKEY_PQDSA);
187+
CHECK_OPENSSL(out);
188+
size_t raw_len;
189+
int nid = NID_undef;
190+
// See Section 4, Table 2 of https://nvlpubs.nist.gov/nistpubs/fips/nist.fips.204.pdf
191+
switch (EVP_PKEY_size(key)) { // switch on signature size for |key|'s algorithm
192+
case 2420:
193+
nid = NID_MLDSA44;
194+
raw_len = 2560;
195+
break;
196+
case 3309:
197+
nid = NID_MLDSA65;
198+
raw_len = 4032;
199+
break;
200+
case 4627:
201+
nid = NID_MLDSA87;
202+
raw_len = 4896;
203+
break;
204+
default:
205+
throw_java_ex(EX_ILLEGAL_ARGUMENT, "Invalid ML-DSA signature size");
206+
}
207+
OPENSSL_buffer_auto raw_expanded(raw_len);
208+
CHECK_OPENSSL(EVP_PKEY_get_raw_private_key(key, raw_expanded, &raw_len));
209+
CBB cbb, pkcs8, algorithm, priv, expanded;
210+
CBB_init(&cbb, 0);
211+
// Encoding below is based on expandedKey CHOICE member of PrivateKey ASN.1 structures in:
212+
// https://github.com/lamps-wg/dilithium-certificates/blob/main/X509-ML-DSA-2025.asn
213+
// spotless:off
214+
if (!CBB_add_asn1(&cbb, &pkcs8, CBS_ASN1_SEQUENCE) ||
215+
!CBB_add_asn1_uint64(&pkcs8, 0) ||
216+
!CBB_add_asn1(&pkcs8, &algorithm, CBS_ASN1_SEQUENCE) ||
217+
!OBJ_nid2cbb(&algorithm, nid) ||
218+
!CBB_add_asn1(&pkcs8, &priv, CBS_ASN1_OCTETSTRING) ||
219+
!CBB_add_asn1(&priv, &expanded, CBS_ASN1_OCTETSTRING) ||
220+
!CBB_add_bytes(&expanded, raw_expanded, raw_len)) {
221+
throw_java_ex(EX_RUNTIME_CRYPTO, "Error serializing expanded ML-DSA key");
222+
}
223+
// spotless:on
224+
size_t out_len;
225+
if (!CBB_finish(&cbb, out, &out_len)) {
226+
OPENSSL_free(*out);
227+
throw_java_ex(EX_RUNTIME_CRYPTO, "Error finalizing expanded ML-DSA key");
228+
}
229+
return out_len;
230+
}
231+
#endif // !defined(FIPS_BUILD) || defined(EXPERIMENTAL_FIPS_BUILD)
232+
182233
}

‎csrc/keyutils.h

+7
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ const EVP_MD* digestFromJstring(raii_env& env, jstring digestName);
145145

146146
RSA* new_private_RSA_key_with_no_e(BIGNUM const* n, BIGNUM const* d);
147147

148+
#if !defined(FIPS_BUILD) || defined(EXPERIMENTAL_FIPS_BUILD)
149+
// Expands ML-DSA |key|, allocates appropriately sized buffer to |*out|, writes the PKCS8-encoded expanded key to
150+
// |*out|, and returns the size of |*out| on success and throws an unchecked exception on failure. The caller takes
151+
// ownership of |*out|.
152+
size_t encodeExpandedMLDSAPrivateKey(const EVP_PKEY* key, uint8_t** out);
153+
#endif
154+
148155
}
149156

150157
#endif

‎csrc/util_class.cpp

+101
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,105 @@ JNIEXPORT jint JNICALL Java_com_amazon_corretto_crypto_provider_Utils_getDigestL
8383
{
8484
return EVP_MD_size(reinterpret_cast<const EVP_MD*>(evpMd));
8585
}
86+
87+
#if !defined(FIPS_BUILD) || defined(EXPERIMENTAL_FIPS_BUILD)
88+
/*
89+
* Class: com_amazon_corretto_crypto_utils_MlDsaUtils
90+
* Method: expandPrivateKeyInternal
91+
* Signature: ([B)[B
92+
*/
93+
JNIEXPORT jbyteArray JNICALL Java_com_amazon_corretto_crypto_utils_MlDsaUtils_expandPrivateKeyInternal(
94+
JNIEnv* pEnv, jclass, jbyteArray keyBytes)
95+
{
96+
jbyteArray result = NULL;
97+
try {
98+
raii_env env(pEnv);
99+
jsize key_der_len = env->GetArrayLength(keyBytes);
100+
101+
if (key_der_len > 54) { // If they key is already expanded, return it
102+
return keyBytes;
103+
}
104+
CHECK_OPENSSL(key_der_len == 54); // seed-only keys are always 54 bytes when PKCS8-encoded
105+
uint8_t* key_der = (uint8_t*)env->GetByteArrayElements(keyBytes, nullptr);
106+
CHECK_OPENSSL(key_der);
107+
108+
// Parse the seed key
109+
BIO* key_bio = BIO_new_mem_buf(key_der, key_der_len);
110+
CHECK_OPENSSL(key_bio);
111+
PKCS8_PRIV_KEY_INFO_auto pkcs8 = PKCS8_PRIV_KEY_INFO_auto::from(d2i_PKCS8_PRIV_KEY_INFO_bio(key_bio, nullptr));
112+
CHECK_OPENSSL(pkcs8.isInitialized());
113+
EVP_PKEY_auto key = EVP_PKEY_auto::from(EVP_PKCS82PKEY(pkcs8));
114+
115+
// Expand the seed key and encode it before returning
116+
OPENSSL_buffer_auto new_der;
117+
int new_der_len = encodeExpandedMLDSAPrivateKey(key, &new_der);
118+
CHECK_OPENSSL(new_der_len > 0);
119+
if (!(result = env->NewByteArray(new_der_len))) {
120+
throw_java_ex(EX_OOM, "Unable to allocate DER array");
121+
}
122+
env->SetByteArrayRegion(result, 0, new_der_len, (const jbyte*)new_der);
123+
} catch (java_ex& ex) {
124+
ex.throw_to_java(pEnv);
125+
return 0;
126+
}
127+
return result;
128+
}
129+
130+
/*
131+
* Class: com_amazon_corretto_crypto_utils_MlDsaUtils
132+
* Method: computeMuInternal
133+
* Signature: ([B[B)[B
134+
*/
135+
JNIEXPORT jbyteArray JNICALL Java_com_amazon_corretto_crypto_utils_MlDsaUtils_computeMuInternal(
136+
JNIEnv* pEnv, jclass, jbyteArray pubKeyEncodedArr, jbyteArray messageArr)
137+
{
138+
try {
139+
raii_env env(pEnv);
140+
jsize pub_key_der_len = env->GetArrayLength(pubKeyEncodedArr);
141+
jsize message_len = env->GetArrayLength(messageArr);
142+
uint8_t* pub_key_der = (uint8_t*)env->GetByteArrayElements(pubKeyEncodedArr, nullptr);
143+
CHECK_OPENSSL(pub_key_der);
144+
uint8_t* message = (uint8_t*)env->GetByteArrayElements(messageArr, nullptr);
145+
CHECK_OPENSSL(message);
146+
147+
CBS cbs;
148+
CBS_init(&cbs, pub_key_der, pub_key_der_len);
149+
EVP_PKEY_auto pkey = EVP_PKEY_auto::from((EVP_parse_public_key(&cbs)));
150+
EVP_PKEY_CTX_auto ctx = EVP_PKEY_CTX_auto::from(EVP_PKEY_CTX_new(pkey.get(), nullptr));
151+
EVP_MD_CTX_auto md_ctx_mu = EVP_MD_CTX_auto::from(EVP_MD_CTX_new());
152+
EVP_MD_CTX_auto md_ctx_pk = EVP_MD_CTX_auto::from(EVP_MD_CTX_new());
153+
154+
size_t pk_len; // fetch the public key length
155+
CHECK_OPENSSL(EVP_PKEY_get_raw_public_key(pkey.get(), nullptr, &pk_len));
156+
std::vector<uint8_t> pk(pk_len);
157+
CHECK_OPENSSL(EVP_PKEY_get_raw_public_key(pkey.get(), pk.data(), &pk_len));
158+
uint8_t tr[64] = { 0 };
159+
uint8_t mu[64] = { 0 };
160+
uint8_t pre[2] = { 0 };
161+
162+
// get raw public key and hash it
163+
CHECK_OPENSSL(EVP_DigestInit_ex(md_ctx_pk.get(), EVP_shake256(), nullptr));
164+
CHECK_OPENSSL(EVP_DigestUpdate(md_ctx_pk.get(), pk.data(), pk_len));
165+
CHECK_OPENSSL(EVP_DigestFinalXOF(md_ctx_pk.get(), tr, sizeof(tr)));
166+
167+
// compute mu as defined on line 6 of Algorithm 7 in FIPS 204
168+
// https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf
169+
CHECK_OPENSSL(EVP_DigestInit_ex(md_ctx_mu.get(), EVP_shake256(), nullptr));
170+
CHECK_OPENSSL(EVP_DigestUpdate(md_ctx_mu.get(), tr, sizeof(tr)));
171+
CHECK_OPENSSL(EVP_DigestUpdate(md_ctx_mu.get(), pre, sizeof(pre)));
172+
CHECK_OPENSSL(EVP_DigestUpdate(md_ctx_mu.get(), message, message_len));
173+
CHECK_OPENSSL(EVP_DigestFinalXOF(md_ctx_mu.get(), mu, sizeof(mu)));
174+
175+
env->ReleaseByteArrayElements(pubKeyEncodedArr, (jbyte*)pub_key_der, 0);
176+
env->ReleaseByteArrayElements(messageArr, (jbyte*)message, 0);
177+
178+
jbyteArray ret = env->NewByteArray(sizeof(mu));
179+
env->SetByteArrayRegion(ret, 0, sizeof(mu), (const jbyte*)mu);
180+
return ret;
181+
} catch (java_ex& ex) {
182+
ex.throw_to_java(pEnv);
183+
return 0;
184+
}
86185
}
186+
#endif // !defined(FIPS_BUILD) || defined(EXPERIMENTAL_FIPS_BUILD)
187+
}

‎src/com/amazon/corretto/crypto/provider/EvpMlDsaPrivateKey.java

+19
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
class EvpMlDsaPrivateKey extends EvpMlDsaKey implements PrivateKey {
88
private static final long serialVersionUID = 1;
99

10+
private static native byte[] encodeMlDsaPrivateKey(long ptr);
11+
1012
EvpMlDsaPrivateKey(final long ptr) {
1113
this(new InternalKey(ptr));
1214
}
@@ -22,4 +24,21 @@ public EvpMlDsaPublicKey getPublicKey() {
2224
result.sharedKey = true;
2325
return result;
2426
}
27+
28+
@Override
29+
protected byte[] internalGetEncoded() {
30+
// ML-DSA private keys have special logic to handle presence/absence of seed
31+
assertNotDestroyed();
32+
byte[] result = encoded;
33+
if (result == null) {
34+
synchronized (this) {
35+
result = encoded;
36+
if (result == null) {
37+
result = use(EvpMlDsaPrivateKey::encodeMlDsaPrivateKey);
38+
encoded = result;
39+
}
40+
}
41+
}
42+
return result;
43+
}
2544
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package com.amazon.corretto.crypto.utils;
4+
5+
import java.security.PrivateKey;
6+
import java.security.PublicKey;
7+
8+
/** Public utility methods */
9+
public final class MlDsaUtils {
10+
private MlDsaUtils() {} // private constructor to prevent instantiation
11+
12+
private static native byte[] computeMuInternal(byte[] pubKeyEncoded, byte[] message);
13+
14+
private static native byte[] expandPrivateKeyInternal(byte[] key);
15+
16+
/**
17+
* Computes mu as defined on line 6 of Algorithm 7 and line 7 of Algorithm 8 in NIST FIPS 204.
18+
*
19+
* <p>See <a href="https://csrc.nist.gov/pubs/fips/204/final">FIPS 204</a>
20+
*
21+
* @param publicKey ML-DSA public key
22+
* @param message byte array of the message over which to compute mu
23+
* @return a byte[] of length 64 containing mu
24+
*/
25+
public static byte[] computeMu(PublicKey publicKey, byte[] message) {
26+
if (publicKey == null || !publicKey.getAlgorithm().startsWith("ML-DSA") || message == null) {
27+
throw new IllegalArgumentException();
28+
}
29+
return computeMuInternal(publicKey.getEncoded(), message);
30+
}
31+
32+
/**
33+
* Returns an expanded ML-DSA private key, whether the key passed in is based on a seed or
34+
* expanded. It returns the PKCS8-encoded expanded key.
35+
*
36+
* @param key an ML-DSA private key
37+
* @return a byte[] containing the PKCS8-encoded seed private key
38+
*/
39+
public static byte[] expandPrivateKey(PrivateKey key) {
40+
if (key == null || !key.getAlgorithm().startsWith("ML-DSA")) {
41+
throw new IllegalArgumentException();
42+
}
43+
return expandPrivateKeyInternal(key.getEncoded());
44+
}
45+
}

‎src/module-info.java

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
requires java.logging;
66

77
exports com.amazon.corretto.crypto.provider;
8+
exports com.amazon.corretto.crypto.utils;
89

910
provides java.security.Provider with
1011
com.amazon.corretto.crypto.provider.ServiceProviderFactory;

‎tst/com/amazon/corretto/crypto/provider/test/EvpKeyFactoryTest.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public static void setupParameters() throws Exception {
9090
if (algorithm.startsWith("ML-DSA")) {
9191
// JCE doesn't support ML-DSA until JDK24, and BouncyCastle currently
9292
// serializes ML-DSA private keys via seeds. TODO: switch to
93-
// BouncyCastle once we support deserializing private keys from seed.
93+
// BouncyCastle once BC supports CHOICE-encoded private keys
9494
kpg = KeyPairGenerator.getInstance(algorithm, NATIVE_PROVIDER);
9595
} else {
9696
kpg = KeyPairGenerator.getInstance(algorithm);
@@ -238,7 +238,7 @@ public void testX509Encoding(final KeyPair keyPair, final String testName) throw
238238
if (algorithm.startsWith("ML-DSA")) {
239239
// JCE doesn't support ML-DSA until JDK24, and BouncyCastle currently
240240
// serializes ML-DSA private keys via seeds. TODO: switch to
241-
// BouncyCastle once we support deserializing private keys from seed.
241+
// BouncyCastle once BC supports CHOICE-encoded private keys
242242
jceFactory = KeyFactory.getInstance(algorithm, NATIVE_PROVIDER);
243243
} else {
244244
jceFactory = KeyFactory.getInstance(algorithm);
@@ -315,7 +315,7 @@ public void testPKCS8Encoding(final KeyPair keyPair, final String testName) thro
315315
if (algorithm.startsWith("ML-DSA")) {
316316
// JCE doesn't support ML-DSA until JDK24, and BouncyCastle currently
317317
// serializes ML-DSA private keys via seeds. TODO: switch to
318-
// BouncyCastle once we support deserializing private keys from seed.
318+
// BouncyCastle once BC supports CHOICE-encoded private keys
319319
jceFactory = KeyFactory.getInstance(algorithm, NATIVE_PROVIDER);
320320
} else {
321321
jceFactory = KeyFactory.getInstance(algorithm);

‎tst/com/amazon/corretto/crypto/provider/test/MLDSATest.java

+36-48
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import static org.junit.jupiter.api.Assertions.assertTrue;
1212

1313
import com.amazon.corretto.crypto.provider.AmazonCorrettoCryptoProvider;
14+
import com.amazon.corretto.crypto.utils.MlDsaUtils;
1415
import java.security.InvalidKeyException;
1516
import java.security.KeyFactory;
1617
import java.security.KeyPair;
@@ -24,6 +25,7 @@
2425
import java.util.ArrayList;
2526
import java.util.Arrays;
2627
import java.util.List;
28+
import org.junit.jupiter.api.Disabled;
2729
import org.junit.jupiter.api.Test;
2830
import org.junit.jupiter.api.condition.DisabledIf;
2931
import org.junit.jupiter.api.extension.ExtendWith;
@@ -88,7 +90,9 @@ private static List<TestParams> getParams() throws Exception {
8890
// support non-Bouncy-Castle keys.
8991
KeyFactory bcKf = KeyFactory.getInstance("ML-DSA", TestUtil.BC_PROVIDER);
9092
PublicKey bcPub = bcKf.generatePublic(new X509EncodedKeySpec(nativePub.getEncoded()));
91-
PrivateKey bcPriv = bcKf.generatePrivate(new PKCS8EncodedKeySpec(nativePriv.getEncoded()));
93+
// TODO uncomment below once BC supports CHOICE-encoded private keys
94+
// PrivateKey bcPriv = bcKf.generatePrivate(new
95+
// PKCS8EncodedKeySpec(nativePriv.getEncoded()));
9296

9397
Provider nativeProv = NATIVE_PROVIDER;
9498
Provider bcProv = TestUtil.BC_PROVIDER;
@@ -98,8 +102,8 @@ private static List<TestParams> getParams() throws Exception {
98102

99103
params.add(new TestParams(nativeProv, nativeProv, nativePriv, nativePub, message));
100104
params.add(new TestParams(nativeProv, bcProv, nativePriv, bcPub, message));
101-
params.add(new TestParams(bcProv, nativeProv, bcPriv, nativePub, message));
102-
params.add(new TestParams(bcProv, bcProv, bcPriv, bcPub, message));
105+
// params.add(new TestParams(bcProv, nativeProv, bcPriv, nativePub, message));
106+
// params.add(new TestParams(bcProv, bcProv, bcPriv, bcPub, message));
103107
}
104108
}
105109
return params;
@@ -200,47 +204,49 @@ public void testInvalidKeyInitialization() {
200204
});
201205
}
202206

207+
@Disabled("until BC updates to newer CHOICE priv key encoding format")
203208
@Test
204209
public void documentBouncyCastleDifferences() throws Exception {
205-
// ACCP and BouncyCastle both encode the public key in full form, but BC FIPS encodes the
206-
// private key as its 32 byte
207-
// seed while ACCP encodes the fully expanded key. Key sizes don't precisely match the spec's
208-
// sizes due to X509/PKCS9 ASN.1 encoding overhead.
209-
// https://openquantumsafe.org/liboqs/algorithms/sig/ml-dsa.html
210+
// ACCP and BouncyCastle both encode ML-DSA public keys in "expanded "form and ML-DSA private
211+
// keys in "seed" form.
212+
KeyFactory bcKf = KeyFactory.getInstance("ML-DSA", TestUtil.BC_PROVIDER);
210213
KeyPair nativePair =
211214
KeyPairGenerator.getInstance("ML-DSA-44", NATIVE_PROVIDER).generateKeyPair();
212-
KeyPair bcPair =
213-
KeyPairGenerator.getInstance("ML-DSA-44", TestUtil.BC_PROVIDER).generateKeyPair();
214-
assertEquals(
215-
nativePair.getPublic().getEncoded().length, bcPair.getPublic().getEncoded().length);
216-
assertEquals(2584, nativePair.getPrivate().getEncoded().length);
217-
assertEquals(52, bcPair.getPrivate().getEncoded().length);
215+
PublicKey nativePub = nativePair.getPublic();
216+
PrivateKey nativePriv = nativePair.getPrivate();
217+
PublicKey bcPub = bcKf.generatePublic(new X509EncodedKeySpec(nativePub.getEncoded()));
218+
PrivateKey bcPriv = bcKf.generatePrivate(new PKCS8EncodedKeySpec(nativePriv.getEncoded()));
219+
TestUtil.assertArraysHexEquals(bcPub.getEncoded(), nativePub.getEncoded());
220+
assertEquals(bcPriv.getEncoded().length + 2, nativePriv.getEncoded().length);
221+
TestUtil.assertArraysHexEquals(bcPriv.getEncoded(), nativePriv.getEncoded());
218222

219223
nativePair = KeyPairGenerator.getInstance("ML-DSA-65", NATIVE_PROVIDER).generateKeyPair();
220-
bcPair = KeyPairGenerator.getInstance("ML-DSA-65", TestUtil.BC_PROVIDER).generateKeyPair();
221-
assertEquals(
222-
nativePair.getPublic().getEncoded().length, bcPair.getPublic().getEncoded().length);
223-
assertEquals(4056, nativePair.getPrivate().getEncoded().length);
224-
assertEquals(52, bcPair.getPrivate().getEncoded().length);
224+
nativePub = nativePair.getPublic();
225+
nativePriv = nativePair.getPrivate();
226+
bcPub = bcKf.generatePublic(new X509EncodedKeySpec(nativePub.getEncoded()));
227+
bcPriv = bcKf.generatePrivate(new PKCS8EncodedKeySpec(nativePriv.getEncoded()));
228+
TestUtil.assertArraysHexEquals(bcPub.getEncoded(), nativePub.getEncoded());
229+
assertEquals(bcPriv.getEncoded().length + 2, nativePriv.getEncoded().length);
230+
TestUtil.assertArraysHexEquals(bcPriv.getEncoded(), nativePriv.getEncoded());
225231

226232
nativePair = KeyPairGenerator.getInstance("ML-DSA-87", NATIVE_PROVIDER).generateKeyPair();
227-
bcPair = KeyPairGenerator.getInstance("ML-DSA-87", TestUtil.BC_PROVIDER).generateKeyPair();
228-
assertEquals(
229-
nativePair.getPublic().getEncoded().length, bcPair.getPublic().getEncoded().length);
230-
assertEquals(4920, nativePair.getPrivate().getEncoded().length);
231-
assertEquals(52, bcPair.getPrivate().getEncoded().length);
233+
nativePub = nativePair.getPublic();
234+
nativePriv = nativePair.getPrivate();
235+
bcPub = bcKf.generatePublic(new X509EncodedKeySpec(nativePub.getEncoded()));
236+
bcPriv = bcKf.generatePrivate(new PKCS8EncodedKeySpec(nativePriv.getEncoded()));
237+
TestUtil.assertArraysHexEquals(bcPub.getEncoded(), nativePub.getEncoded());
238+
TestUtil.assertArraysHexEquals(bcPriv.getEncoded(), nativePriv.getEncoded());
232239

233240
// BouncyCastle Signatures don't accept keys from other providers
234241
Signature bcSignature = Signature.getInstance("ML-DSA", TestUtil.BC_PROVIDER);
235-
final KeyPair finalNativePair = nativePair;
236-
assertThrows(
237-
InvalidKeyException.class, () -> bcSignature.initSign(finalNativePair.getPrivate()));
242+
final PrivateKey finalNativePriv = nativePriv;
243+
assertThrows(InvalidKeyException.class, () -> bcSignature.initSign(finalNativePriv));
238244

239245
// However, ACCP can use BouncyCastle KeyPairs with seed-encoded PrivateKeys
240246
Signature nativeSignature = Signature.getInstance("ML-DSA", NATIVE_PROVIDER);
241-
nativeSignature.initSign(bcPair.getPrivate());
247+
nativeSignature.initSign(bcPriv);
242248
byte[] sigBytes = nativeSignature.sign();
243-
nativeSignature.initVerify(bcPair.getPublic());
249+
nativeSignature.initVerify(bcPub);
244250
assertTrue(nativeSignature.verify(sigBytes));
245251
}
246252

@@ -258,7 +264,7 @@ public void testExtMu(TestParams params) throws Exception {
258264
PublicKey pub = params.pub;
259265

260266
byte[] message = Arrays.copyOf(params.message, params.message.length);
261-
byte[] mu = TestUtil.computeMLDSAMu(pub, message);
267+
byte[] mu = MlDsaUtils.computeMu(pub, message);
262268
assertEquals(64, mu.length);
263269
byte[] fakeMu = new byte[64];
264270
Arrays.fill(fakeMu, (byte) 0);
@@ -304,22 +310,4 @@ public void testExtMu(TestParams params) throws Exception {
304310
extMuVerifier.update(mu);
305311
assertFalse(extMuVerifier.verify(signatureBytes));
306312
}
307-
308-
@ParameterizedTest
309-
@ValueSource(strings = {"ML-DSA-44", "ML-DSA-65", "ML-DSA-87"})
310-
public void testComputeMLDSAExtMu(String algorithm) throws Exception {
311-
KeyPair keyPair = KeyPairGenerator.getInstance(algorithm, NATIVE_PROVIDER).generateKeyPair();
312-
PublicKey nativePub = keyPair.getPublic();
313-
KeyFactory bcKf = KeyFactory.getInstance("ML-DSA", TestUtil.BC_PROVIDER);
314-
PublicKey bcPub = bcKf.generatePublic(new X509EncodedKeySpec(nativePub.getEncoded()));
315-
316-
byte[] message = new byte[256];
317-
Arrays.fill(message, (byte) 0x41);
318-
byte[] mu = TestUtil.computeMLDSAMu(nativePub, message);
319-
assertEquals(64, mu.length);
320-
// We don't have any other implementations of mu calculation to test against, so just assert
321-
// that mu is equivalent
322-
// generated from both ACCP and BouncyCastle keys.
323-
assertArrayEquals(mu, TestUtil.computeMLDSAMu(bcPub, message));
324-
}
325313
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package com.amazon.corretto.crypto.provider.test;
4+
5+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
6+
import static org.junit.jupiter.api.Assertions.assertEquals;
7+
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
9+
import com.amazon.corretto.crypto.provider.AmazonCorrettoCryptoProvider;
10+
import com.amazon.corretto.crypto.utils.MlDsaUtils;
11+
import java.security.KeyFactory;
12+
import java.security.KeyPair;
13+
import java.security.KeyPairGenerator;
14+
import java.security.PrivateKey;
15+
import java.security.Provider;
16+
import java.security.PublicKey;
17+
import java.security.Signature;
18+
import java.security.spec.PKCS8EncodedKeySpec;
19+
import java.security.spec.X509EncodedKeySpec;
20+
import java.util.Arrays;
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.condition.DisabledIf;
23+
import org.junit.jupiter.api.extension.ExtendWith;
24+
import org.junit.jupiter.api.parallel.Execution;
25+
import org.junit.jupiter.api.parallel.ExecutionMode;
26+
import org.junit.jupiter.api.parallel.ResourceAccessMode;
27+
import org.junit.jupiter.api.parallel.ResourceLock;
28+
import org.junit.jupiter.params.ParameterizedTest;
29+
import org.junit.jupiter.params.provider.ValueSource;
30+
31+
@Execution(ExecutionMode.CONCURRENT)
32+
@ExtendWith(TestResultLogger.class)
33+
@ResourceLock(value = TestUtil.RESOURCE_GLOBAL, mode = ResourceAccessMode.READ)
34+
public class MlDsaUtilsTest {
35+
private static final Provider NATIVE_PROVIDER = AmazonCorrettoCryptoProvider.INSTANCE;
36+
37+
// TODO: remove this disablement when ACCP consumes an AWS-LC-FIPS release with ML-DSA
38+
private static boolean mlDsaDisabled() {
39+
return AmazonCorrettoCryptoProvider.INSTANCE.isFips()
40+
&& !AmazonCorrettoCryptoProvider.INSTANCE.isExperimentalFips();
41+
}
42+
43+
@ParameterizedTest
44+
@ValueSource(strings = {"ML-DSA-44", "ML-DSA-65", "ML-DSA-87"})
45+
@DisabledIf("mlDsaDisabled")
46+
public void testComputeMu(String algorithm) throws Exception {
47+
KeyPair keyPair = KeyPairGenerator.getInstance(algorithm, NATIVE_PROVIDER).generateKeyPair();
48+
PublicKey nativePub = keyPair.getPublic();
49+
KeyFactory bcKf = KeyFactory.getInstance("ML-DSA", TestUtil.BC_PROVIDER);
50+
PublicKey bcPub = bcKf.generatePublic(new X509EncodedKeySpec(nativePub.getEncoded()));
51+
52+
byte[] message = new byte[256];
53+
Arrays.fill(message, (byte) 0x41);
54+
byte[] mu = MlDsaUtils.computeMu(nativePub, message);
55+
assertEquals(64, mu.length);
56+
// We don't have any other implementations of mu calculation to test against, so just assert
57+
// that mu is equivalent generated from both ACCP and BouncyCastle keys.
58+
assertArrayEquals(mu, MlDsaUtils.computeMu(bcPub, message));
59+
}
60+
61+
@Test
62+
@DisabledIf("mlDsaDisabled")
63+
public void testExpandPrivateKey() throws Exception {
64+
KeyFactory kf = KeyFactory.getInstance("ML-DSA", TestUtil.NATIVE_PROVIDER);
65+
66+
// Parsing expanded keys discards the seed, so after expansion we're no longer dealing with
67+
// the seed. There are 24 bytes of PKCS8 overhead for each key. Raw private key sizes below.
68+
// https://openquantumsafe.org/liboqs/algorithms/sig/ml-dsa.html
69+
KeyPair nativePair =
70+
KeyPairGenerator.getInstance("ML-DSA-44", NATIVE_PROVIDER).generateKeyPair();
71+
assertEquals(54, nativePair.getPrivate().getEncoded().length);
72+
byte[] expanded = MlDsaUtils.expandPrivateKey(nativePair.getPrivate());
73+
assertEquals(2588, expanded.length);
74+
PrivateKey expandedPriv = kf.generatePrivate(new PKCS8EncodedKeySpec(expanded));
75+
assertEquals(2588, expandedPriv.getEncoded().length);
76+
77+
nativePair = KeyPairGenerator.getInstance("ML-DSA-65", NATIVE_PROVIDER).generateKeyPair();
78+
assertEquals(54, nativePair.getPrivate().getEncoded().length);
79+
expanded = MlDsaUtils.expandPrivateKey(nativePair.getPrivate());
80+
assertEquals(4060, expanded.length);
81+
expandedPriv = kf.generatePrivate(new PKCS8EncodedKeySpec(expanded));
82+
assertEquals(4060, expandedPriv.getEncoded().length);
83+
84+
nativePair = KeyPairGenerator.getInstance("ML-DSA-87", NATIVE_PROVIDER).generateKeyPair();
85+
assertEquals(54, nativePair.getPrivate().getEncoded().length);
86+
expanded = MlDsaUtils.expandPrivateKey(nativePair.getPrivate());
87+
assertEquals(4924, expanded.length);
88+
expandedPriv = kf.generatePrivate(new PKCS8EncodedKeySpec(expanded));
89+
assertEquals(4924, expandedPriv.getEncoded().length);
90+
91+
// Lastly, do a sign/verify round trip with the expanded key
92+
nativePair = KeyPairGenerator.getInstance("ML-DSA-44", NATIVE_PROVIDER).generateKeyPair();
93+
expanded = MlDsaUtils.expandPrivateKey(nativePair.getPrivate());
94+
expandedPriv = kf.generatePrivate(new PKCS8EncodedKeySpec(expanded));
95+
final byte[] message = new byte[256];
96+
Arrays.fill(message, (byte) 0x41);
97+
Signature signature = Signature.getInstance("ML-DSA", NATIVE_PROVIDER);
98+
signature.initSign(expandedPriv);
99+
signature.update(message);
100+
byte[] signatureBytes = signature.sign();
101+
signature.initVerify(nativePair.getPublic());
102+
signature.update(message);
103+
assertTrue(signature.verify(signatureBytes));
104+
}
105+
}

‎tst/com/amazon/corretto/crypto/provider/test/TestUtil.java

-19
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import java.nio.ByteBuffer;
1818
import java.security.NoSuchAlgorithmException;
1919
import java.security.Provider;
20-
import java.security.PublicKey;
2120
import java.security.SecureRandom;
2221
import java.security.Security;
2322
import java.util.ArrayList;
@@ -842,22 +841,4 @@ static boolean edKeyFactoryRegistered() {
842841
return "true"
843842
.equals(System.getProperty("com.amazon.corretto.crypto.provider.registerEdKeyFactory"));
844843
}
845-
846-
private static native byte[] computeMLDSAMuInternal(byte[] pubKeyEncoded, byte[] message);
847-
848-
/**
849-
* Computes mu as defined on line 6 of Algorithm 7 and line 7 of Algorithm 8 in NIST FIPS 204.
850-
*
851-
* <p>See <a href="https://csrc.nist.gov/pubs/fips/204/final">FIPS 204</a>
852-
*
853-
* @param publicKey ML-DSA public key
854-
* @param message byte array of the message over which to compute mu
855-
* @return a byte[] of length 64 containing mu
856-
*/
857-
static byte[] computeMLDSAMu(PublicKey publicKey, byte[] message) {
858-
if (publicKey == null || !publicKey.getAlgorithm().startsWith("ML-DSA") || message == null) {
859-
throw new IllegalArgumentException();
860-
}
861-
return computeMLDSAMuInternal(publicKey.getEncoded(), message);
862-
}
863844
}

0 commit comments

Comments
 (0)
Please sign in to comment.