Skip to content

Commit

Permalink
Encode ML-DSA priv key as seed, expose MlDsaUtils (#434)
Browse files Browse the repository at this point in the history
See [PR #434][1] for a detailed description of this commit.

[1]: #434
  • Loading branch information
WillChilds-Klein authored Mar 3, 2025
1 parent c4a382d commit eb936b1
Show file tree
Hide file tree
Showing 17 changed files with 430 additions and 74 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* [PR 426:](https://github.com/corretto/amazon-corretto-crypto-provider/pull/426) Add null check to AesCbcSpi
* [PR 427:](https://github.com/corretto/amazon-corretto-crypto-provider/pull/427) Add provider info string
* [PR 432:](https://github.com/corretto/amazon-corretto-crypto-provider/pull/432) Support Ed25519ph, bump AWS-LC to v1.46.0yy
* [PR 434:](https://github.com/corretto/amazon-corretto-crypto-provider/pull/434) Encode ML-DSA priv key as seed, expose MlDsaUtils

## 2.4.1

Expand Down
4 changes: 3 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,12 @@ add_custom_command(
# detected by CMake.
if (${CMAKE_VERSION} VERSION_LESS "3.12.0")
file(GLOB_RECURSE ACCP_SRC "src/com/amazon/corretto/crypto/provider/*.java")
file(GLOB_RECURSE ACCP_UTILS_SRC "src/com/amazon/corretto/crypto/utils/*.java")
else()
file(GLOB_RECURSE ACCP_SRC CONFIGURE_DEPENDS "src/com/amazon/corretto/crypto/provider/*.java")
file(GLOB_RECURSE ACCP_UTILS_SRC CONFIGURE_DEPENDS "src/com/amazon/corretto/crypto/utils/*.java")
endif()
set(ACCP_SRC ${ACCP_SRC} ${GENERATED_JAVA_SRC})
set(ACCP_SRC ${ACCP_SRC} ${ACCP_UTILS_SRC} ${GENERATED_JAVA_SRC})

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

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ ACCP did not track a FIPS branch/release version of AWS-LC until ACCP v2.3.0. Be
| 2.3.3 | 1.17.0 | 2.0.2 |
| 2.4.0 | 1.30.1 | 2.0.13 |
| 2.4.1 | 1.30.1 | 2.0.13 |
| 2.5.0 | 1.46.0 | 3.0.0 |
| 2.5.0 | 1.47.0 | 3.0.0 |

Notable differences between ACCP and ACCP-FIPS:
* 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).
Expand Down
2 changes: 1 addition & 1 deletion aws-lc
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ plugins {

group = 'software.amazon.cryptools'
version = '2.5.0'
ext.awsLcMainTag = 'v1.46.0'
ext.awsLcMainTag = 'v1.47.0'
ext.awsLcFipsTag = 'AWS-LC-FIPS-3.0.0'
ext.isExperimentalFips = Boolean.getBoolean('EXPERIMENTAL_FIPS')
ext.isFips = ext.isExperimentalFips || Boolean.getBoolean('FIPS')
Expand Down Expand Up @@ -78,6 +78,7 @@ spotless {
target 'csrc/*'
licenseHeaderFile 'build-tools/license-headers/LicenseHeader.h'
clangFormat(clangFormatVersion)
toggleOffOn()
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions csrc/auto_free.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ class OPENSSL_buffer_auto {
{
}

explicit OPENSSL_buffer_auto(size_t buf_size)
: buf((unsigned char*)OPENSSL_malloc(buf_size))
{
}

virtual ~OPENSSL_buffer_auto() { OPENSSL_free(buf); }

operator unsigned char*() { return buf; }
Expand Down
49 changes: 49 additions & 0 deletions csrc/java_evp_keys.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -666,3 +666,52 @@ JNIEXPORT jbyteArray JNICALL Java_com_amazon_corretto_crypto_provider_EvpRsaPriv

return result;
}

#if !defined(FIPS_BUILD) || defined(EXPERIMENTAL_FIPS_BUILD)
/*
* Class: com_amazon_corretto_crypto_provider_EvpRsaPrivateKey
* Method: encodeRsaPrivateKey
* Signature: (J)[B
*/
JNIEXPORT jbyteArray JNICALL Java_com_amazon_corretto_crypto_provider_EvpMlDsaPrivateKey_encodeMlDsaPrivateKey(
JNIEnv* pEnv, jclass, jlong keyHandle)
{
jbyteArray result = NULL;

try {
raii_env env(pEnv);

EVP_PKEY* key = reinterpret_cast<EVP_PKEY*>(keyHandle);
CHECK_OPENSSL(EVP_PKEY_id(key) == EVP_PKEY_PQDSA);

uint8_t* der;
size_t der_len;
CBB cbb;
CHECK_OPENSSL(CBB_init(&cbb, 0));
// Failure below may just indicate that we don't have the seed, so retry with |encodeExpandedMLDSAPrivateKey|
// and encode in PKCS8 (RFC 5208) format after clearing the error queue.
if (EVP_marshal_private_key(&cbb, key)) {
if (!CBB_finish(&cbb, &der, &der_len)) {
OPENSSL_free(der);
throw_java_ex(EX_RUNTIME_CRYPTO, "Error finalizing seed ML-DSA key");
}
} else {
ERR_clear_error();
der_len = encodeExpandedMLDSAPrivateKey(key, &der);
}
CBB_cleanup(&cbb);

if (!(result = env->NewByteArray(der_len))) {
OPENSSL_free(der);
throw_java_ex(EX_OOM, "Unable to allocate DER array");
}
// This may throw, if it does we'll just keep the exception state as we return.
env->SetByteArrayRegion(result, 0, der_len, (const jbyte*)der);
OPENSSL_free(der);
} catch (java_ex& ex) {
ex.throw_to_java(pEnv);
}

return result;
}
#endif // !defined(FIPS_BUILD) || defined(EXPERIMENTAL_FIPS_BUILD)
51 changes: 51 additions & 0 deletions csrc/keyutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,55 @@ RSA* new_private_RSA_key_with_no_e(BIGNUM const* n, BIGNUM const* d)
return result;
}

#if !defined(FIPS_BUILD) || defined(EXPERIMENTAL_FIPS_BUILD)
size_t encodeExpandedMLDSAPrivateKey(const EVP_PKEY* key, uint8_t** out)
{
CHECK_OPENSSL(key);
CHECK_OPENSSL(EVP_PKEY_id(key) == EVP_PKEY_PQDSA);
CHECK_OPENSSL(out);
size_t raw_len;
int nid = NID_undef;
// See Section 4, Table 2 of https://nvlpubs.nist.gov/nistpubs/fips/nist.fips.204.pdf
switch (EVP_PKEY_size(key)) { // switch on signature size for |key|'s algorithm
case 2420:
nid = NID_MLDSA44;
raw_len = 2560;
break;
case 3309:
nid = NID_MLDSA65;
raw_len = 4032;
break;
case 4627:
nid = NID_MLDSA87;
raw_len = 4896;
break;
default:
throw_java_ex(EX_ILLEGAL_ARGUMENT, "Invalid ML-DSA signature size");
}
OPENSSL_buffer_auto raw_expanded(raw_len);
CHECK_OPENSSL(EVP_PKEY_get_raw_private_key(key, raw_expanded, &raw_len));
CBB cbb, pkcs8, algorithm, priv, expanded;
CBB_init(&cbb, 0);
// Encoding below is based on expandedKey CHOICE member of PrivateKey ASN.1 structures in:
// https://github.com/lamps-wg/dilithium-certificates/blob/main/X509-ML-DSA-2025.asn
// spotless:off
if (!CBB_add_asn1(&cbb, &pkcs8, CBS_ASN1_SEQUENCE) ||
!CBB_add_asn1_uint64(&pkcs8, 0) ||
!CBB_add_asn1(&pkcs8, &algorithm, CBS_ASN1_SEQUENCE) ||
!OBJ_nid2cbb(&algorithm, nid) ||
!CBB_add_asn1(&pkcs8, &priv, CBS_ASN1_OCTETSTRING) ||
!CBB_add_asn1(&priv, &expanded, CBS_ASN1_OCTETSTRING) ||
!CBB_add_bytes(&expanded, raw_expanded, raw_len)) {
throw_java_ex(EX_RUNTIME_CRYPTO, "Error serializing expanded ML-DSA key");
}
// spotless:on
size_t out_len;
if (!CBB_finish(&cbb, out, &out_len)) {
OPENSSL_free(*out);
throw_java_ex(EX_RUNTIME_CRYPTO, "Error finalizing expanded ML-DSA key");
}
return out_len;
}
#endif // !defined(FIPS_BUILD) || defined(EXPERIMENTAL_FIPS_BUILD)

}
7 changes: 7 additions & 0 deletions csrc/keyutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ const EVP_MD* digestFromJstring(raii_env& env, jstring digestName);

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

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

}

#endif
101 changes: 101 additions & 0 deletions csrc/util_class.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,105 @@ JNIEXPORT jint JNICALL Java_com_amazon_corretto_crypto_provider_Utils_getDigestL
{
return EVP_MD_size(reinterpret_cast<const EVP_MD*>(evpMd));
}

#if !defined(FIPS_BUILD) || defined(EXPERIMENTAL_FIPS_BUILD)
/*
* Class: com_amazon_corretto_crypto_utils_MlDsaUtils
* Method: expandPrivateKeyInternal
* Signature: ([B)[B
*/
JNIEXPORT jbyteArray JNICALL Java_com_amazon_corretto_crypto_utils_MlDsaUtils_expandPrivateKeyInternal(
JNIEnv* pEnv, jclass, jbyteArray keyBytes)
{
jbyteArray result = NULL;
try {
raii_env env(pEnv);
jsize key_der_len = env->GetArrayLength(keyBytes);

if (key_der_len > 54) { // If they key is already expanded, return it
return keyBytes;
}
CHECK_OPENSSL(key_der_len == 54); // seed-only keys are always 54 bytes when PKCS8-encoded
uint8_t* key_der = (uint8_t*)env->GetByteArrayElements(keyBytes, nullptr);
CHECK_OPENSSL(key_der);

// Parse the seed key
BIO* key_bio = BIO_new_mem_buf(key_der, key_der_len);
CHECK_OPENSSL(key_bio);
PKCS8_PRIV_KEY_INFO_auto pkcs8 = PKCS8_PRIV_KEY_INFO_auto::from(d2i_PKCS8_PRIV_KEY_INFO_bio(key_bio, nullptr));
CHECK_OPENSSL(pkcs8.isInitialized());
EVP_PKEY_auto key = EVP_PKEY_auto::from(EVP_PKCS82PKEY(pkcs8));

// Expand the seed key and encode it before returning
OPENSSL_buffer_auto new_der;
int new_der_len = encodeExpandedMLDSAPrivateKey(key, &new_der);
CHECK_OPENSSL(new_der_len > 0);
if (!(result = env->NewByteArray(new_der_len))) {
throw_java_ex(EX_OOM, "Unable to allocate DER array");
}
env->SetByteArrayRegion(result, 0, new_der_len, (const jbyte*)new_der);
} catch (java_ex& ex) {
ex.throw_to_java(pEnv);
return 0;
}
return result;
}

/*
* Class: com_amazon_corretto_crypto_utils_MlDsaUtils
* Method: computeMuInternal
* Signature: ([B[B)[B
*/
JNIEXPORT jbyteArray JNICALL Java_com_amazon_corretto_crypto_utils_MlDsaUtils_computeMuInternal(
JNIEnv* pEnv, jclass, jbyteArray pubKeyEncodedArr, jbyteArray messageArr)
{
try {
raii_env env(pEnv);
jsize pub_key_der_len = env->GetArrayLength(pubKeyEncodedArr);
jsize message_len = env->GetArrayLength(messageArr);
uint8_t* pub_key_der = (uint8_t*)env->GetByteArrayElements(pubKeyEncodedArr, nullptr);
CHECK_OPENSSL(pub_key_der);
uint8_t* message = (uint8_t*)env->GetByteArrayElements(messageArr, nullptr);
CHECK_OPENSSL(message);

CBS cbs;
CBS_init(&cbs, pub_key_der, pub_key_der_len);
EVP_PKEY_auto pkey = EVP_PKEY_auto::from((EVP_parse_public_key(&cbs)));
EVP_PKEY_CTX_auto ctx = EVP_PKEY_CTX_auto::from(EVP_PKEY_CTX_new(pkey.get(), nullptr));
EVP_MD_CTX_auto md_ctx_mu = EVP_MD_CTX_auto::from(EVP_MD_CTX_new());
EVP_MD_CTX_auto md_ctx_pk = EVP_MD_CTX_auto::from(EVP_MD_CTX_new());

size_t pk_len; // fetch the public key length
CHECK_OPENSSL(EVP_PKEY_get_raw_public_key(pkey.get(), nullptr, &pk_len));
std::vector<uint8_t> pk(pk_len);
CHECK_OPENSSL(EVP_PKEY_get_raw_public_key(pkey.get(), pk.data(), &pk_len));
uint8_t tr[64] = { 0 };
uint8_t mu[64] = { 0 };
uint8_t pre[2] = { 0 };

// get raw public key and hash it
CHECK_OPENSSL(EVP_DigestInit_ex(md_ctx_pk.get(), EVP_shake256(), nullptr));
CHECK_OPENSSL(EVP_DigestUpdate(md_ctx_pk.get(), pk.data(), pk_len));
CHECK_OPENSSL(EVP_DigestFinalXOF(md_ctx_pk.get(), tr, sizeof(tr)));

// compute mu as defined on line 6 of Algorithm 7 in FIPS 204
// https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf
CHECK_OPENSSL(EVP_DigestInit_ex(md_ctx_mu.get(), EVP_shake256(), nullptr));
CHECK_OPENSSL(EVP_DigestUpdate(md_ctx_mu.get(), tr, sizeof(tr)));
CHECK_OPENSSL(EVP_DigestUpdate(md_ctx_mu.get(), pre, sizeof(pre)));
CHECK_OPENSSL(EVP_DigestUpdate(md_ctx_mu.get(), message, message_len));
CHECK_OPENSSL(EVP_DigestFinalXOF(md_ctx_mu.get(), mu, sizeof(mu)));

env->ReleaseByteArrayElements(pubKeyEncodedArr, (jbyte*)pub_key_der, 0);
env->ReleaseByteArrayElements(messageArr, (jbyte*)message, 0);

jbyteArray ret = env->NewByteArray(sizeof(mu));
env->SetByteArrayRegion(ret, 0, sizeof(mu), (const jbyte*)mu);
return ret;
} catch (java_ex& ex) {
ex.throw_to_java(pEnv);
return 0;
}
}
#endif // !defined(FIPS_BUILD) || defined(EXPERIMENTAL_FIPS_BUILD)
}
19 changes: 19 additions & 0 deletions src/com/amazon/corretto/crypto/provider/EvpMlDsaPrivateKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
class EvpMlDsaPrivateKey extends EvpMlDsaKey implements PrivateKey {
private static final long serialVersionUID = 1;

private static native byte[] encodeMlDsaPrivateKey(long ptr);

EvpMlDsaPrivateKey(final long ptr) {
this(new InternalKey(ptr));
}
Expand All @@ -22,4 +24,21 @@ public EvpMlDsaPublicKey getPublicKey() {
result.sharedKey = true;
return result;
}

@Override
protected byte[] internalGetEncoded() {
// ML-DSA private keys have special logic to handle presence/absence of seed
assertNotDestroyed();
byte[] result = encoded;
if (result == null) {
synchronized (this) {
result = encoded;
if (result == null) {
result = use(EvpMlDsaPrivateKey::encodeMlDsaPrivateKey);
encoded = result;
}
}
}
return result;
}
}
45 changes: 45 additions & 0 deletions src/com/amazon/corretto/crypto/utils/MlDsaUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package com.amazon.corretto.crypto.utils;

import java.security.PrivateKey;
import java.security.PublicKey;

/** Public utility methods */
public final class MlDsaUtils {
private MlDsaUtils() {} // private constructor to prevent instantiation

private static native byte[] computeMuInternal(byte[] pubKeyEncoded, byte[] message);

private static native byte[] expandPrivateKeyInternal(byte[] key);

/**
* Computes mu as defined on line 6 of Algorithm 7 and line 7 of Algorithm 8 in NIST FIPS 204.
*
* <p>See <a href="https://csrc.nist.gov/pubs/fips/204/final">FIPS 204</a>
*
* @param publicKey ML-DSA public key
* @param message byte array of the message over which to compute mu
* @return a byte[] of length 64 containing mu
*/
public static byte[] computeMu(PublicKey publicKey, byte[] message) {
if (publicKey == null || !publicKey.getAlgorithm().startsWith("ML-DSA") || message == null) {
throw new IllegalArgumentException();
}
return computeMuInternal(publicKey.getEncoded(), message);
}

/**
* Returns an expanded ML-DSA private key, whether the key passed in is based on a seed or
* expanded. It returns the PKCS8-encoded expanded key.
*
* @param key an ML-DSA private key
* @return a byte[] containing the PKCS8-encoded seed private key
*/
public static byte[] expandPrivateKey(PrivateKey key) {
if (key == null || !key.getAlgorithm().startsWith("ML-DSA")) {
throw new IllegalArgumentException();
}
return expandPrivateKeyInternal(key.getEncoded());
}
}
1 change: 1 addition & 0 deletions src/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
requires java.logging;

exports com.amazon.corretto.crypto.provider;
exports com.amazon.corretto.crypto.utils;

provides java.security.Provider with
com.amazon.corretto.crypto.provider.ServiceProviderFactory;
Expand Down
Loading

0 comments on commit eb936b1

Please sign in to comment.