Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ExternalMu mode for pre-hash ML-DSA #2113

Merged
merged 36 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5abd396
ml-dsa extmu
jakemas Jan 10, 2025
00b3ba1
CR fixes from nevine and will
jakemas Jan 13, 2025
0461f48
spacing nits
jakemas Jan 13, 2025
21006fd
evp documentation
jakemas Jan 13, 2025
9f17f86
Merge branch 'main' into extmu-ml-dsa
jakemas Jan 13, 2025
7823f93
removed use of internal functions in test file
jakemas Jan 13, 2025
7baa8c1
spacing nits
jakemas Jan 13, 2025
9de6f9d
mem leak in test code
jakemas Jan 13, 2025
dddd501
duplicated line
jakemas Jan 13, 2025
bd4e629
added negative testing for extmu
jakemas Jan 13, 2025
9a90332
added ACVP access for extmu internal and KAT framework
jakemas Jan 14, 2025
cb40bdc
spacing nit
jakemas Jan 14, 2025
ba50e2c
Merge branch 'main' into extmu-ml-dsa
jakemas Jan 14, 2025
ba1856b
CR fixes
jakemas Jan 14, 2025
255aa97
Merge branch 'extmu-ml-dsa' of github.com:jakemas/aws-lc into extmu-m…
jakemas Jan 14, 2025
89d4744
spotted typo
jakemas Jan 14, 2025
49580d6
updated message names to mu
jakemas Jan 14, 2025
a97e54b
Merge branch 'main' into extmu-ml-dsa
jakemas Jan 15, 2025
318f0c1
internal naming
jakemas Jan 15, 2025
6d214b5
readme updates
jakemas Jan 15, 2025
195779c
readme update
jakemas Jan 15, 2025
75ccaf5
Merge branch 'main' into extmu-ml-dsa
jakemas Jan 16, 2025
3d98fb6
remove KAT
jakemas Jan 16, 2025
b347ee6
added source files
jakemas Jan 16, 2025
3b6e302
updated gensrc
jakemas Jan 16, 2025
2f8729a
update readme to discuss KAT
jakemas Jan 16, 2025
ce4f764
update gen src
jakemas Jan 16, 2025
957523e
added ACVP test vectors
jakemas Jan 16, 2025
76a71f1
update readme
jakemas Jan 16, 2025
807aca4
Merge branch 'main' into extmu-ml-dsa
jakemas Jan 17, 2025
201b096
update internal name
jakemas Jan 17, 2025
6762c2a
Merge branch 'extmu-ml-dsa' of github.com:jakemas/aws-lc into extmu-m…
jakemas Jan 17, 2025
0dfc1f2
typo
jakemas Jan 17, 2025
e56a143
missed space
jakemas Jan 17, 2025
49d0b08
missed space
jakemas Jan 17, 2025
36e9ae2
Merge branch 'main' into extmu-ml-dsa
jakemas Jan 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 70 additions & 19 deletions crypto/evp_extra/p_pqdsa.c
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ static int pkey_pqdsa_keygen(EVP_PKEY_CTX *ctx, EVP_PKEY *pkey) {
return 1;
}

static int pkey_pqdsa_sign_message(EVP_PKEY_CTX *ctx, uint8_t *sig,
size_t *sig_len, const uint8_t *message,
size_t message_len) {
static int pkey_pqdsa_sign_generic(EVP_PKEY_CTX *ctx, uint8_t *sig,
size_t *sig_len, const uint8_t *message,
size_t message_len, int prehash) {
Copy link
Contributor

Choose a reason for hiding this comment

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

To make it clear that this is not HashML-DSA, I suggest replacing prehash with external_mu.

Copy link
Contributor Author

@jakemas jakemas Jan 14, 2025

Choose a reason for hiding this comment

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

Not a good idea. This is the PQDSA logic for ALL PQDSA types. ExternalMu isn't defined for any PQDSA scheme other than ML-DSA. For example, why would SLH-DSA or even HashML-DSA have a flag for ExternalMu?

I have changed all the ML-DSA specific uses of a pre-hash flag to use external_mu but the ones that remain called prehash are intentional so that we can use the flag for the purposes of indicating any pre-hash method for any PQDSA signature scheme.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree this shouldn't be external_mu, but it isn't clear if prehash means the input has been pre-hashed or if AWS-LC should hash the input before signing it, can this follow the method names and use sign_message or sign_digest 0/1?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks Andrew, have I understood you correctly with this update: 318f0c1

PQDSA_PKEY_CTX *dctx = ctx->data;
const PQDSA *pqdsa = dctx->pqdsa;
if (pqdsa == NULL) {
Expand Down Expand Up @@ -96,16 +96,43 @@ static int pkey_pqdsa_sign_message(EVP_PKEY_CTX *ctx, uint8_t *sig,
return 0;
}

if (!pqdsa->method->pqdsa_sign(key->private_key, sig, sig_len, message, message_len, NULL, 0)) {
OPENSSL_PUT_ERROR(EVP, ERR_R_INTERNAL_ERROR);
return 0;
// |prehash| is a flag we use to indicate that the message to be signed has already
jakemas marked this conversation as resolved.
Show resolved Hide resolved
// been pre-processed (i.e, the context string is included) and hashed.
// Pure mode
if (!prehash) {
if (!pqdsa->method->pqdsa_sign_message(key->private_key, sig, sig_len, message, message_len, NULL, 0)) {
OPENSSL_PUT_ERROR(EVP, ERR_R_INTERNAL_ERROR);
return 0;
}
}
// Pre-hash mode
else {
if (!pqdsa->method->pqdsa_sign(key->private_key, sig, sig_len, message, message_len)) {
OPENSSL_PUT_ERROR(EVP, ERR_R_INTERNAL_ERROR);
return 0;
}
}

return 1;
}

static int pkey_pqdsa_verify_signature(EVP_PKEY_CTX *ctx, const uint8_t *sig,
size_t sig_len, const uint8_t *message,
size_t message_len) {
// DIGEST signing
static int pkey_pqdsa_sign(EVP_PKEY_CTX *ctx, uint8_t *sig,
size_t *sig_len, const uint8_t *message,
size_t message_len) {
return pkey_pqdsa_sign_generic(ctx, sig, sig_len, message, message_len, 1);
}

// RAW message signing
static int pkey_pqdsa_sign_message(EVP_PKEY_CTX *ctx, uint8_t *sig,
size_t *sig_len, const uint8_t *message,
size_t message_len) {
return pkey_pqdsa_sign_generic(ctx, sig, sig_len, message, message_len, 0);
}

static int pkey_pqdsa_verify_generic(EVP_PKEY_CTX *ctx, const uint8_t *sig,
size_t sig_len, const uint8_t *message,
size_t message_len, int prehash) {
PQDSA_PKEY_CTX *dctx = ctx->data;
const PQDSA *pqdsa = dctx->pqdsa;

Expand All @@ -127,15 +154,42 @@ static int pkey_pqdsa_verify_signature(EVP_PKEY_CTX *ctx, const uint8_t *sig,

PQDSA_KEY *key = ctx->pkey->pkey.pqdsa_key;

if (sig_len != pqdsa->signature_len ||
!pqdsa->method->pqdsa_verify(key->public_key, sig, sig_len, message, message_len, NULL, 0)) {
OPENSSL_PUT_ERROR(EVP, EVP_R_INVALID_SIGNATURE);
return 0;
// |prehash| is a flag we use to indicate that the message to be signed has already
// been pre-processed (i.e, the context string is included) and hashed.
// Pure mode
if(!prehash) {
if (sig_len != pqdsa->signature_len ||
!pqdsa->method->pqdsa_verify_message(key->public_key, sig, sig_len, message, message_len, NULL, 0)) {
OPENSSL_PUT_ERROR(EVP, EVP_R_INVALID_SIGNATURE);
return 0;
}
}
// Pre-hash mode
else {
if (sig_len != pqdsa->signature_len ||
!pqdsa->method->pqdsa_verify(key->public_key, sig, sig_len, message, message_len)) {
OPENSSL_PUT_ERROR(EVP, EVP_R_INVALID_SIGNATURE);
return 0;
}
}

return 1;
}

// DIGEST verification
static int pkey_pqdsa_verify(EVP_PKEY_CTX *ctx, const uint8_t *sig,
size_t sig_len, const uint8_t *message,
size_t message_len) {
return pkey_pqdsa_verify_generic(ctx, sig, sig_len, message, message_len, 1);
}

// RAW message verification
static int pkey_pqdsa_verify_message(EVP_PKEY_CTX *ctx, const uint8_t *sig,
size_t sig_len, const uint8_t *message,
size_t message_len) {
return pkey_pqdsa_verify_generic(ctx, sig, sig_len, message, message_len, 0);
}

// Additional PQDSA specific EVP functions.

// This function sets pqdsa parameters defined by |nid| in |pkey|.
Expand Down Expand Up @@ -267,11 +321,11 @@ const EVP_PKEY_METHOD pqdsa_pkey_meth = {
pkey_pqdsa_cleanup,
pkey_pqdsa_keygen,
NULL,
NULL,
pkey_pqdsa_sign,
pkey_pqdsa_sign_message,
NULL,
NULL,
pkey_pqdsa_verify_signature,
pkey_pqdsa_verify,
pkey_pqdsa_verify_message,
NULL,
NULL,
NULL,
Expand All @@ -284,6 +338,3 @@ const EVP_PKEY_METHOD pqdsa_pkey_meth = {
NULL,
NULL,
};



231 changes: 230 additions & 1 deletion crypto/evp_extra/p_pqdsa_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -961,7 +961,7 @@ struct PQDSATestVector {
uint8_t *sig, size_t *sig_len,
const uint8_t *message, size_t message_len,
const uint8_t *pre, size_t pre_len,
uint8_t *rnd);
const uint8_t *rnd);

int (*verify)(const uint8_t *public_key,
const uint8_t *sig, size_t sig_len,
Expand Down Expand Up @@ -1515,3 +1515,232 @@ TEST_P(PQDSAParameterTest, ParsePublicKey) {
bssl::UniquePtr<EVP_PKEY> pkey_from_der(EVP_parse_public_key(&cbs));
ASSERT_TRUE(pkey_from_der);
}

// ML-DSA specific test framework to test pre-hash modes only applicable to ML-DSA
struct KnownMLDSA {
const char name[20];
const int nid;
const size_t public_key_len;
const size_t private_key_len;
const size_t signature_len;
const char *kat_filename;

int (*keygen)(uint8_t *public_key, uint8_t *private_key, const uint8_t *seed);

int (*sign)(const uint8_t *private_key,
uint8_t *sig, size_t *sig_len,
const uint8_t *message, size_t message_len,
const uint8_t *pre, size_t pre_len,
const uint8_t *rnd);

int (*verify)(const uint8_t *public_key,
const uint8_t *sig, size_t sig_len,
const uint8_t *message, size_t message_len,
const uint8_t *pre, size_t pre_len);
};

static const struct KnownMLDSA kMLDSAs[] = {
{
"MLDSA44",
NID_MLDSA44,
1312,
2560,
2420,
"ml_dsa/kat/MLDSA_EXTMU_44_hedged_pure.txt",
ml_dsa_44_keypair_internal,
ml_dsa_extmu_44_sign_internal,
ml_dsa_extmu_44_verify_internal
},
{
"MLDSA65",
NID_MLDSA65,
1952,
4032,
3309,
"ml_dsa/kat/MLDSA_EXTMU_65_hedged_pure.txt",
ml_dsa_65_keypair_internal,
ml_dsa_extmu_65_sign_internal,
ml_dsa_extmu_65_verify_internal
},
{
"MLDSA87",
NID_MLDSA87,
2592,
4896,
4627,
"ml_dsa/kat/MLDSA_EXTMU_87_hedged_pure.txt",
ml_dsa_87_keypair_internal,
ml_dsa_extmu_87_sign_internal,
ml_dsa_extmu_87_verify_internal
},
};

class PerMLDSATest : public testing::TestWithParam<KnownMLDSA> {};

INSTANTIATE_TEST_SUITE_P(All, PerMLDSATest, testing::ValuesIn(kMLDSAs),
[](const testing::TestParamInfo<KnownMLDSA> &params)
-> std::string { return params.param.name; });

TEST_P(PerMLDSATest, ExternalMu) {
// ---- 1. Setup phase: generate PQDSA EVP KEY and sign/verify contexts ----
bssl::UniquePtr<EVP_PKEY> pkey(generate_key_pair(GetParam().nid));
bssl::UniquePtr<EVP_PKEY_CTX> ctx(EVP_PKEY_CTX_new(pkey.get(), nullptr));
bssl::UniquePtr<EVP_MD_CTX> md_ctx_mu(EVP_MD_CTX_new()), md_ctx_pk(EVP_MD_CTX_new());
bssl::ScopedEVP_MD_CTX md_ctx_verify;

std::vector<uint8_t> msg1 = {
0x4a, 0x41, 0x4b, 0x45, 0x20, 0x4d, 0x41, 0x53, 0x53, 0x49,
0x4d, 0x4f, 0x20, 0x41, 0x57, 0x53, 0x32, 0x30, 0x32, 0x32, 0x2e};

// ---- 2. Pre-hash setup phase: compute tr, mu ----
size_t TRBYTES = 64;
size_t CRHBYTES = 64;
size_t pk_len = GetParam().public_key_len;

std::vector<uint8_t> pk(pk_len);
std::vector<uint8_t> tr(TRBYTES);
std::vector<uint8_t> mu(TRBYTES);

uint8_t pre[2];
pre[0] = 0;
pre[1] = 0;

//get public key and hash it
ASSERT_TRUE(EVP_PKEY_get_raw_public_key(pkey.get(), pk.data(), &pk_len));
ASSERT_TRUE(EVP_DigestInit_ex(md_ctx_pk.get(), EVP_shake256(), nullptr));
ASSERT_TRUE(EVP_DigestUpdate(md_ctx_pk.get(), pk.data(), pk_len));
ASSERT_TRUE(EVP_DigestFinalXOF(md_ctx_pk.get(), tr.data(), TRBYTES));

// compute mu
ASSERT_TRUE(EVP_DigestInit_ex(md_ctx_mu.get(), EVP_shake256(), nullptr));
ASSERT_TRUE(EVP_DigestUpdate(md_ctx_mu.get(), tr.data(), TRBYTES));
ASSERT_TRUE(EVP_DigestUpdate(md_ctx_mu.get(), pre, 2));
ASSERT_TRUE(EVP_DigestUpdate(md_ctx_mu.get(), msg1.data(), msg1.size()));
ASSERT_TRUE(EVP_DigestFinalXOF(md_ctx_mu.get(), mu.data(), CRHBYTES));

// ---- 2. Init signing, get signature size and allocate signature buffer ----
size_t sig_len = GetParam().signature_len;
std::vector<uint8_t> sig1(sig_len);

// ---- 3. Sign mu ----
ASSERT_TRUE(EVP_PKEY_sign_init(ctx.get()));
ASSERT_TRUE(EVP_PKEY_sign(ctx.get(), sig1.data(), &sig_len, mu.data(), mu.size()));

// ---- 4. Verify mu (pre-hash) ----
ASSERT_TRUE(EVP_PKEY_verify_init(ctx.get()));
ASSERT_TRUE(EVP_PKEY_verify(ctx.get(), sig1.data(), sig_len, mu.data(), mu.size()));

// ---- 5. Bonus: Verify raw message with digest verify (no pre-hash) ----
ASSERT_TRUE(EVP_DigestVerifyInit(md_ctx_verify.get(), nullptr, nullptr, nullptr, pkey.get()));
ASSERT_TRUE(EVP_DigestVerify(md_ctx_verify.get(), sig1.data(), sig_len, msg1.data(), msg1.size()));

// reset the contexts between tests
md_ctx_verify.Reset();

// ---- 6. Test signature failure modes: invalid keys and signatures ----
// Check that verification fails upon providing a signature of invalid length
sig_len = GetParam().signature_len - 1;
ASSERT_FALSE(EVP_PKEY_verify(ctx.get(), sig1.data(), sig_len, mu.data(), mu.size()));
GET_ERR_AND_CHECK_REASON(EVP_R_INVALID_SIGNATURE);

sig_len = GetParam().signature_len + 1;
ASSERT_FALSE(EVP_PKEY_verify(ctx.get(), sig1.data(), sig_len, mu.data(), mu.size()));
GET_ERR_AND_CHECK_REASON(EVP_R_INVALID_SIGNATURE);

// Check that verification fails upon providing a different public key
// than the one that was used to sign.
bssl::UniquePtr<EVP_PKEY> new_pkey(generate_key_pair(GetParam().nid));
bssl::UniquePtr<EVP_PKEY_CTX> new_ctx(EVP_PKEY_CTX_new(new_pkey.get(), nullptr));

ASSERT_TRUE(EVP_PKEY_verify_init(new_ctx.get()));
ASSERT_FALSE(EVP_PKEY_verify(new_ctx.get(), sig1.data(), sig_len, mu.data(), mu.size()));
GET_ERR_AND_CHECK_REASON(EVP_R_INVALID_SIGNATURE);
md_ctx_verify.Reset();
}

TEST_P(PerMLDSATest, KAT) {
std::string kat_filepath = "crypto/";
kat_filepath += GetParam().kat_filename;

FileTestGTest(kat_filepath.c_str(), [&](FileTest *t) {
std::string count, mlen, smlen;
std::vector<uint8_t> xi, rng, seed, msg, pk, sk, mu, sm, ctxstr;

ASSERT_TRUE(t->GetAttribute(&count, "count"));
ASSERT_TRUE(t->GetBytes(&xi, "xi"));
ASSERT_TRUE(t->GetBytes(&rng, "rng"));
ASSERT_TRUE(t->GetBytes(&seed, "seed"));
ASSERT_TRUE(t->GetBytes(&pk, "pk"));
ASSERT_TRUE(t->GetBytes(&sk, "sk"));
ASSERT_TRUE(t->GetBytes(&msg, "msg"));
ASSERT_TRUE(t->GetAttribute(&mlen, "mlen"));
ASSERT_TRUE(t->GetBytes(&sm, "sm"));
ASSERT_TRUE(t->GetAttribute(&smlen, "smlen"));
ASSERT_TRUE(t->GetBytes(&ctxstr, "ctx"));
ASSERT_TRUE(t->GetBytes(&mu, "mu"));

size_t pk_len = GetParam().public_key_len;
size_t sk_len = GetParam().private_key_len;
size_t sig_len = GetParam().signature_len;

std::vector<uint8_t> pub(pk_len);
std::vector<uint8_t> priv(sk_len);
std::vector<uint8_t> signature(sig_len);

std::string name = GetParam().name;
sm.resize(sig_len);

// Generate key pair from seed xi and assert that public and private keys
// are equal to expected values from KAT
ASSERT_TRUE(GetParam().keygen(pub.data(), priv.data(), xi.data()));
EXPECT_EQ(Bytes(pub), Bytes(pk));
EXPECT_EQ(Bytes(priv), Bytes(sk));

// reconstruct mu, tr, and check it is equal to expected value
size_t TRBYTES = 64;
size_t CRHBYTES = 64;
bssl::UniquePtr<EVP_MD_CTX> md_ctx_mu(EVP_MD_CTX_new()), md_ctx_pk(EVP_MD_CTX_new());
std::vector<uint8_t> tr(TRBYTES);
std::vector<uint8_t> mu2(CRHBYTES);

// construct tr: get public key and hash it
ASSERT_TRUE(EVP_DigestInit_ex(md_ctx_pk.get(), EVP_shake256(), nullptr));
ASSERT_TRUE(EVP_DigestUpdate(md_ctx_pk.get(), pub.data(), pub.size()));
ASSERT_TRUE(EVP_DigestFinalXOF(md_ctx_pk.get(), tr.data(), TRBYTES));

// Prepare m_prime = (0 || ctxlen || ctx)
// See both FIPS 204: Algorithm 2 line 10 and FIPS 205: Algorithm 22 line 8
uint8_t m_prime[257];
size_t m_prime_len = ctxstr.size() + 2;
m_prime[0] = 0;
m_prime[1] = ctxstr.size();
ASSERT_TRUE(ctxstr.size() <= 255);
OPENSSL_memcpy(m_prime + 2 , ctxstr.data(), ctxstr.size());

// reconstruct mu
ASSERT_TRUE(EVP_DigestInit_ex(md_ctx_mu.get(), EVP_shake256(), nullptr));
ASSERT_TRUE(EVP_DigestUpdate(md_ctx_mu.get(), tr.data(), TRBYTES));
ASSERT_TRUE(EVP_DigestUpdate(md_ctx_mu.get(), m_prime, m_prime_len));
ASSERT_TRUE(EVP_DigestUpdate(md_ctx_mu.get(), msg.data(), msg.size()));
ASSERT_TRUE(EVP_DigestFinalXOF(md_ctx_mu.get(), mu2.data(), CRHBYTES));

// assert equal to expected value
ASSERT_EQ(Bytes(mu), Bytes(mu2));

// Generate signature by signing |mu2|.
ASSERT_TRUE(GetParam().sign(priv.data(),
signature.data(), &sig_len,
mu2.data(), mu.size(),
nullptr, 0,
rng.data()));

// Assert that signature is equal to expected signature
ASSERT_EQ(Bytes(signature), Bytes(sm));

// Assert that the signature verifies correctly.
ASSERT_TRUE(GetParam().verify(pub.data(),
signature.data(), sig_len,
mu2.data(), mu2.size(),
m_prime, m_prime_len));
});
}
Loading
Loading