Skip to content

Commit

Permalink
Add support to export ML-DSA key-pairs in seed format (#2194)
Browse files Browse the repository at this point in the history
Add support to encode ML-DSA private keys in seed format

- Update ML-DSA key generation to preserve seed used within ML-DSA-KeyGen_internal
- Modify ASN.1 encoding/decoding to handle seed format for ML-DSA private keys
- Pass OID separately in key decoding functions to better handle algorithm params
- Update tests to verify seed encoding functionality

The commit is following the RFC draft spec for ML-DSA certificates to store private keys as 
seed values rather than expanded form, https://datatracker.ietf.org/doc/draft-ietf-lamps-dilithium-certificates.
  • Loading branch information
jakemas authored Feb 28, 2025
1 parent 4898adb commit cbcba97
Show file tree
Hide file tree
Showing 19 changed files with 492 additions and 412 deletions.
36 changes: 15 additions & 21 deletions crypto/evp_extra/evp_asn1.c
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,20 @@
#include "../fipsmodule/pqdsa/internal.h"

// parse_key_type takes the algorithm cbs sequence |cbs| and extracts the OID.
// The extracted OID will be set on |out_oid| so that it may be used later in
// specific key type implementations like PQDSA.
// The OID is then searched against ASN.1 methods for a method with that OID.
// As the |OID| is read from |cbs| the buffer is advanced.
// For the case of |NID_rsa| the method |rsa_asn1_meth| is returned.
// For the case of |EVP_PKEY_PQDSA| the method |pqdsa_asn1.meth| is returned, as
// the OID is not returned (and the |cbs| buffer is advanced) we return the OID
// as |cbs|. (This allows the specific OID, e.g. NID_MLDSA65 to be parsed by
// the type-specific decoding functions within the algorithm parameter.)
static const EVP_PKEY_ASN1_METHOD *parse_key_type(CBS *cbs) {
// For the case of |EVP_PKEY_PQDSA| the method |pqdsa_asn1.meth| is returned.
static const EVP_PKEY_ASN1_METHOD *parse_key_type(CBS *cbs, CBS *out_oid) {
CBS oid;
if (!CBS_get_asn1(cbs, &oid, CBS_ASN1_OBJECT)) {
return NULL;
}

CBS_init(out_oid, CBS_data(&oid), CBS_len(&oid));

const EVP_PKEY_ASN1_METHOD *const *asn1_methods =
AWSLC_non_fips_pkey_evp_asn1_methods();
for (size_t i = 0; i < ASN1_EVP_PKEY_METHODS; i++) {
Expand All @@ -103,18 +104,7 @@ static const EVP_PKEY_ASN1_METHOD *parse_key_type(CBS *cbs) {
// The pkey_id for the pqdsa_asn1_meth is EVP_PKEY_PQDSA, as this holds all
// asn1 functions for pqdsa types. However, the incoming CBS has the OID for
// the specific algorithm. So we must search explicitly for the algorithm.
const EVP_PKEY_ASN1_METHOD * ret = PQDSA_find_asn1_by_nid(OBJ_cbs2nid(&oid));
if (ret != NULL) {
// if |cbs| is empty after parsing |oid| from it, we overwrite the contents
// with |oid| so that we can call pub_decode/priv_decode with the |algorithm|
// populated as |oid|.
if (CBS_len(cbs) == 0) {
OPENSSL_memcpy(cbs, &oid, sizeof(oid));
return ret;
}
}

return NULL;
return PQDSA_find_asn1_by_nid(OBJ_cbs2nid(&oid));
}

EVP_PKEY *EVP_parse_public_key(CBS *cbs) {
Expand All @@ -129,7 +119,9 @@ EVP_PKEY *EVP_parse_public_key(CBS *cbs) {
return NULL;
}

const EVP_PKEY_ASN1_METHOD *method = parse_key_type(&algorithm);
CBS oid;

const EVP_PKEY_ASN1_METHOD *method = parse_key_type(&algorithm, &oid);
if (method == NULL) {
OPENSSL_PUT_ERROR(EVP, EVP_R_UNSUPPORTED_ALGORITHM);
return NULL;
Expand All @@ -154,7 +146,7 @@ EVP_PKEY *EVP_parse_public_key(CBS *cbs) {
OPENSSL_PUT_ERROR(EVP, EVP_R_UNSUPPORTED_ALGORITHM);
goto err;
}
if (!ret->ameth->pub_decode(ret, &algorithm, &key)) {
if (!ret->ameth->pub_decode(ret, &oid, &algorithm, &key)) {
goto err;
}

Expand Down Expand Up @@ -195,7 +187,9 @@ EVP_PKEY *EVP_parse_private_key(CBS *cbs) {
return NULL;
}

const EVP_PKEY_ASN1_METHOD *method = parse_key_type(&algorithm);
CBS oid;

const EVP_PKEY_ASN1_METHOD *method = parse_key_type(&algorithm, &oid);
if (method == NULL) {
OPENSSL_PUT_ERROR(EVP, EVP_R_UNSUPPORTED_ALGORITHM);
return NULL;
Expand Down Expand Up @@ -236,7 +230,7 @@ EVP_PKEY *EVP_parse_private_key(CBS *cbs) {
goto err;
}

if (!ret->ameth->priv_decode(ret, &algorithm, &key,
if (!ret->ameth->priv_decode(ret, &oid, &algorithm, &key,
has_pub ? &public_key : NULL)) {
goto err;
}
Expand Down
651 changes: 339 additions & 312 deletions crypto/evp_extra/evp_extra_test.cc

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crypto/evp_extra/p_dh_asn1.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ static int dh_pub_encode(CBB *out, const EVP_PKEY *key) {
return 1;
}

static int dh_pub_decode(EVP_PKEY *out, CBS *params, CBS *key) {
static int dh_pub_decode(EVP_PKEY *out, CBS *oid, CBS *params, CBS *key) {
// RFC 2786
BIGNUM *pubkey = NULL;
DH *dh = NULL;
Expand Down
4 changes: 2 additions & 2 deletions crypto/evp_extra/p_dsa_asn1.c
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
#include "internal.h"


static int dsa_pub_decode(EVP_PKEY *out, CBS *params, CBS *key) {
static int dsa_pub_decode(EVP_PKEY *out, CBS *oid, CBS *params, CBS *key) {
// See RFC 3279, section 2.3.2.

// Parameters may or may not be present.
Expand Down Expand Up @@ -127,7 +127,7 @@ static int dsa_pub_encode(CBB *out, const EVP_PKEY *key) {
return 1;
}

static int dsa_priv_decode(EVP_PKEY *out, CBS *params, CBS *key, CBS *pubkey) {
static int dsa_priv_decode(EVP_PKEY *out, CBS *oid, CBS *params, CBS *key, CBS *pubkey) {
// See PKCS#11, v2.40, section 2.5.
if(pubkey) {
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR);
Expand Down
4 changes: 2 additions & 2 deletions crypto/evp_extra/p_ec_asn1.c
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ static int eckey_pub_encode(CBB *out, const EVP_PKEY *key) {
return 1;
}

static int eckey_pub_decode(EVP_PKEY *out, CBS *params, CBS *key) {
static int eckey_pub_decode(EVP_PKEY *out, CBS *oid, CBS *params, CBS *key) {
// See RFC 5480, section 2.

// The parameters are a named curve.
Expand Down Expand Up @@ -139,7 +139,7 @@ static int eckey_pub_cmp(const EVP_PKEY *a, const EVP_PKEY *b) {
}
}

static int eckey_priv_decode(EVP_PKEY *out, CBS *params, CBS *key, CBS *pubkey) {
static int eckey_priv_decode(EVP_PKEY *out, CBS *oid, CBS *params, CBS *key, CBS *pubkey) {
// See RFC 5915.
if(pubkey) {
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR);
Expand Down
4 changes: 2 additions & 2 deletions crypto/evp_extra/p_ed25519_asn1.c
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ static int ed25519_get_pub_raw(const EVP_PKEY *pkey, uint8_t *out,
return 1;
}

static int ed25519_pub_decode(EVP_PKEY *out, CBS *params, CBS *key) {
static int ed25519_pub_decode(EVP_PKEY *out, CBS *oid, CBS *params, CBS *key) {
// See RFC 8410, section 4.

// The parameters must be omitted. Public keys have length 32.
Expand Down Expand Up @@ -166,7 +166,7 @@ static int ed25519_pub_cmp(const EVP_PKEY *a, const EVP_PKEY *b) {
b_key->key + ED25519_PUBLIC_KEY_OFFSET, ED25519_PUBLIC_KEY_LEN) == 0;
}

static int ed25519_priv_decode(EVP_PKEY *out, CBS *params, CBS *key, CBS *pubkey) {
static int ed25519_priv_decode(EVP_PKEY *out, CBS *oid, CBS *params, CBS *key, CBS *pubkey) {
// See RFC 8410, section 7.

// Parameters must be empty. The key is a 32-byte value wrapped in an extra
Expand Down
80 changes: 49 additions & 31 deletions crypto/evp_extra/p_pqdsa_asn1.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ static void pqdsa_free(EVP_PKEY *pkey) {

static int pqdsa_get_priv_raw(const EVP_PKEY *pkey, uint8_t *out,
size_t *out_len) {
GUARD_PTR(pkey);
GUARD_PTR(out_len);

if (pkey->pkey.pqdsa_key == NULL) {
OPENSSL_PUT_ERROR(EVP, EVP_R_NO_PARAMETERS_SET);
return 0;
Expand Down Expand Up @@ -88,16 +91,15 @@ static int pqdsa_get_pub_raw(const EVP_PKEY *pkey, uint8_t *out,
return 1;
}

static int pqdsa_pub_decode(EVP_PKEY *out, CBS *params, CBS *key) {
static int pqdsa_pub_decode(EVP_PKEY *out, CBS *oid, CBS *params, CBS *key) {
// See https://datatracker.ietf.org/doc/draft-ietf-lamps-dilithium-certificates/
// section 4. the only parameter that can be included is the OID which has
// length 9
if (CBS_len(params) != 9) {
// section 4. There should be no parameters
if (CBS_len(params) > 0) {
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR);
return 0;
}
// Set the pqdsa params on |out|.
if (!EVP_PKEY_pqdsa_set_params(out, OBJ_cbs2nid(params))) {
if (!EVP_PKEY_pqdsa_set_params(out, OBJ_cbs2nid(oid))) {
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR);
return 0;
}
Expand Down Expand Up @@ -138,64 +140,80 @@ static int pqdsa_pub_cmp(const EVP_PKEY *a, const EVP_PKEY *b) {
a->pkey.pqdsa_key->pqdsa->public_key_len) == 0;
}

static int pqdsa_priv_decode(EVP_PKEY *out, CBS *params, CBS *key, CBS *pubkey) {
static int pqdsa_priv_decode(EVP_PKEY *out, CBS *oid, CBS *params, CBS *key, CBS *pubkey) {
// See https://datatracker.ietf.org/doc/draft-ietf-lamps-dilithium-certificates/
// section 6. the only parameter that can be included is the OID which has
// length 9.
if (CBS_len(params) != 9) {
// section 6. There should be no parameters.
if (CBS_len(params) > 0) {
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR);
return 0;
}

// Set the pqdsa params on |out|.
if (!EVP_PKEY_pqdsa_set_params(out, OBJ_cbs2nid(params))) {
if (!EVP_PKEY_pqdsa_set_params(out, OBJ_cbs2nid(oid))) {
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR);
return 0;
}

// check the size of the provided input against the private key and seed len
if (CBS_len(key) != out->pkey.pqdsa_key->pqdsa->private_key_len &&
CBS_len(key) != out->pkey.pqdsa_key->pqdsa->keygen_seed_len) {
OPENSSL_PUT_ERROR(EVP, EVP_R_INVALID_BUFFER_SIZE);
return 0;
}
// Try to parse as one of the three ASN.1 formats defined in ML-DSA-XX-PrivateKey
// Currently only the following cases are supported:
// Case 1: seed [0] OCTET STRING
// Case 2: expandedKey OCTET STRING

// See https://datatracker.ietf.org/doc/draft-ietf-lamps-dilithium-certificates/
// The caller can either provide the full key of size |private_key_len| or
// |keygen_seed_len|.
if (CBS_len(key) == out->pkey.pqdsa_key->pqdsa->private_key_len) {
// Once https://datatracker.ietf.org/doc/draft-ietf-lamps-dilithium-certificates/
// is stable we will implement:
// Case 3: both SEQUENCE { seed, expandedKey }

// Set the private key
if (!PQDSA_KEY_set_raw_private_key(out->pkey.pqdsa_key, key)) {
// PQDSA_KEY_set_raw_private_key sets the appropriate error.
if (CBS_peek_asn1_tag(key, CBS_ASN1_CONTEXT_SPECIFIC | 0)) {
// Case 1: seed [0] OCTET STRING
CBS seed;
if (!CBS_get_asn1(key, &seed, CBS_ASN1_CONTEXT_SPECIFIC | 0)) {
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR);
return 0;
}

} else if (CBS_len(key) == out->pkey.pqdsa_key->pqdsa->keygen_seed_len) {
if (!PQDSA_KEY_set_raw_keypair_from_seed(out->pkey.pqdsa_key, key)) {
// PQDSA_KEY_set_raw_keypair_from_seed sets the appropriate error.
if (CBS_len(&seed) != out->pkey.pqdsa_key->pqdsa->keygen_seed_len) {
OPENSSL_PUT_ERROR(EVP, EVP_R_INVALID_BUFFER_SIZE);
return 0;
}

return PQDSA_KEY_set_raw_keypair_from_seed(out->pkey.pqdsa_key, &seed);
} else if (CBS_peek_asn1_tag(key, CBS_ASN1_OCTETSTRING)) {
// Case 2: expandedKey OCTET STRING
CBS expanded_key;
if (!CBS_get_asn1(key, &expanded_key, CBS_ASN1_OCTETSTRING)) {
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR);
return 0;
}

if (CBS_len(&expanded_key) != out->pkey.pqdsa_key->pqdsa->private_key_len) {
OPENSSL_PUT_ERROR(EVP, EVP_R_INVALID_BUFFER_SIZE);
return 0;
}

return PQDSA_KEY_set_raw_private_key(out->pkey.pqdsa_key, &expanded_key);
} else {
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR);
return 0;
}
return 1;
}

static int pqdsa_priv_encode(CBB *out, const EVP_PKEY *pkey) {
PQDSA_KEY *key = pkey->pkey.pqdsa_key;
const PQDSA *pqdsa = key->pqdsa;
if (key->private_key == NULL) {
if (key->seed == NULL) {
OPENSSL_PUT_ERROR(EVP, EVP_R_NOT_A_PRIVATE_KEY);
return 0;
}
// See https://datatracker.ietf.org/doc/draft-ietf-lamps-dilithium-certificates/ section 6.
CBB pkcs8, algorithm, oid, private_key;
CBB pkcs8, algorithm, oid, private_key, seed_choice;
if (!CBB_add_asn1(out, &pkcs8, CBS_ASN1_SEQUENCE) ||
!CBB_add_asn1_uint64(&pkcs8, 0 /* version */) ||
!CBB_add_asn1_uint64(&pkcs8, PKCS8_VERSION_ONE /* version */) ||
!CBB_add_asn1(&pkcs8, &algorithm, CBS_ASN1_SEQUENCE) ||
!CBB_add_asn1(&algorithm, &oid, CBS_ASN1_OBJECT) ||
!CBB_add_bytes(&oid, pqdsa->oid, pqdsa->oid_len) ||
!CBB_add_asn1(&pkcs8, &private_key, CBS_ASN1_OCTETSTRING) ||
!CBB_add_bytes(&private_key, key->private_key, pqdsa->private_key_len) ||
!CBB_add_asn1(&private_key, &seed_choice, CBS_ASN1_CONTEXT_SPECIFIC | 0) ||
!CBB_add_bytes(&seed_choice, key->seed, pqdsa->keygen_seed_len) ||
!CBB_flush(out)) {
OPENSSL_PUT_ERROR(EVP, EVP_R_ENCODE_ERROR);
return 0;
Expand Down
28 changes: 18 additions & 10 deletions crypto/evp_extra/p_pqdsa_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1089,20 +1089,20 @@ const char *mldsa_87_pub_pem_str =
// C.1. Example Private Key
const char *mldsa_44_priv_pem_str =
"-----BEGIN PRIVATE KEY-----\n"
"MDICAQAwCwYJYIZIAWUDBAMRBCAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRob\n"
"HB0eHw==\n"
"MDQCAQAwCwYJYIZIAWUDBAMRBCKAIAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZ\n"
"GhscHR4f\n"
"-----END PRIVATE KEY-----\n";

const char *mldsa_65_priv_pem_str =
"-----BEGIN PRIVATE KEY-----\n"
"MDICAQAwCwYJYIZIAWUDBAMSBCAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRob\n"
"HB0eHw==\n"
"MDQCAQAwCwYJYIZIAWUDBAMSBCKAIAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZ\n"
"GhscHR4f\n"
"-----END PRIVATE KEY-----\n";

const char *mldsa_87_priv_pem_str =
"-----BEGIN PRIVATE KEY-----\n"
"MDICAQAwCwYJYIZIAWUDBAMTBCAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRob\n"
"HB0eHw==\n"
"MDQCAQAwCwYJYIZIAWUDBAMTBCKAIAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZ\n"
"GhscHR4f\n"
"-----END PRIVATE KEY-----\n";

struct PQDSATestVector {
Expand Down Expand Up @@ -1463,11 +1463,10 @@ TEST_P(PQDSAParameterTest, RawFunctions) {
EXPECT_NE(private_pkey->pkey.pqdsa_key->private_key, nullptr);

// ---- 5. Test get_raw public/private failure modes ----
uint8_t *buf = nullptr;
size_t buf_size;
std::vector<uint8_t> get_sk(sk_len);

// Attempting to get a private key that is not present must fail correctly
EXPECT_FALSE(EVP_PKEY_get_raw_private_key(public_pkey.get(), buf, &buf_size));
EXPECT_FALSE(EVP_PKEY_get_raw_private_key(public_pkey.get(), get_sk.data(), &sk_len));
GET_ERR_AND_CHECK_REASON(EVP_R_NOT_A_PRIVATE_KEY);

// Null PKEY must fail correctly.
Expand Down Expand Up @@ -1754,6 +1753,15 @@ TEST_P(PQDSAParameterTest, ParsePrivateKey) {
// the public key that was parsed from PEM.
ASSERT_EQ(1, EVP_PKEY_cmp(pkey1.get(), pkey2.get()));

// ---- 5. test failure modes ----
// Test case in which a parsed key does not contain a seed
bssl::ScopedCBB cbb;
void *tmp = (void*) pkey1.get()->pkey.pqdsa_key->seed;
pkey1.get()->pkey.pqdsa_key->seed =nullptr;
ASSERT_TRUE(CBB_init(cbb.get(), 0));
ASSERT_FALSE(EVP_marshal_private_key(cbb.get(), pkey1.get()));
pkey1.get()->pkey.pqdsa_key->seed = (uint8_t *)tmp;

// Clean up
OPENSSL_free(der_pub);
OPENSSL_free(der_priv);
Expand All @@ -1780,7 +1788,7 @@ TEST_P(PQDSAParameterTest, KeyConsistencyTest) {
// ---- 3. Generate a raw public key from the raw private key ----
ASSERT_TRUE(GetParam().pack_key(pk.data(), sk.data()));

// ---- 4. Generate a raw public key from the raw private key ----
// ---- 4. Test that the calculated pk is equal to original pkey ----
CMP_VEC_AND_PKEY_PUBLIC(pk, pkey, pk_len);
}

Expand Down
8 changes: 4 additions & 4 deletions crypto/evp_extra/p_rsa_asn1.c
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ static int rsa_pub_encode(CBB *out, const EVP_PKEY *key) {
return 1;
}

static int rsa_pub_decode(EVP_PKEY *out, CBS *params, CBS *key) {
static int rsa_pub_decode(EVP_PKEY *out, CBS *oid, CBS *params, CBS *key) {
// The IETF specification defines that the parameters must be
// NULL. See RFC 3279, section 2.3.1.
// There is also an ITU-T X.509 specification that is rarely seen,
Expand All @@ -105,7 +105,7 @@ static int rsa_pub_decode(EVP_PKEY *out, CBS *params, CBS *key) {
return 1;
}

static int rsa_pss_pub_decode(EVP_PKEY *out, CBS *params, CBS *key) {
static int rsa_pss_pub_decode(EVP_PKEY *out, CBS *oid, CBS *params, CBS *key) {
RSASSA_PSS_PARAMS *pss = NULL;
if (!RSASSA_PSS_parse_params(params, &pss)) {
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR);
Expand Down Expand Up @@ -152,7 +152,7 @@ static int rsa_priv_encode(CBB *out, const EVP_PKEY *key) {
return 1;
}

static int rsa_priv_decode(EVP_PKEY *out, CBS *params, CBS *key, CBS *pubkey) {
static int rsa_priv_decode(EVP_PKEY *out, CBS *oid, CBS *params, CBS *key, CBS *pubkey) {
if(pubkey) {
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR);
return 0;
Expand All @@ -178,7 +178,7 @@ static int rsa_priv_decode(EVP_PKEY *out, CBS *params, CBS *key, CBS *pubkey) {
return 1;
}

static int rsa_pss_priv_decode(EVP_PKEY *out, CBS *params, CBS *key, CBS *pubkey) {
static int rsa_pss_priv_decode(EVP_PKEY *out, CBS *oid, CBS *params, CBS *key, CBS *pubkey) {
RSASSA_PSS_PARAMS *pss = NULL;
if (!RSASSA_PSS_parse_params(params, &pss)) {
OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR);
Expand Down
Loading

0 comments on commit cbcba97

Please sign in to comment.