From f72d82fe3240c4f4aeac117b1325ead928bc5c52 Mon Sep 17 00:00:00 2001 From: "Matthias J. Kannwischer" Date: Wed, 31 Dec 2025 09:37:20 +0800 Subject: [PATCH] Constant-time: Make signature declassifications explicit in verification Previously, the signature would be declassified in the end of signing (which is the most common threat model). However, for rare use cases the signature may be secret in which case it is useful to know which part of the code has to be adapted. This commit removes the declassification from the end of signing, explicitly marks the signature as secret in the constant time tests (to also classify the previously declassified challenge c), and then adds necessary declassifications in verifiation. We may consider eliminating some of those declassifications later. In particular, the data dependencies in use_hint are not hard to eliminate (in fact, the native implementations are already constant time). The final comparison of the challenges can also easily be turned into a constant-time comparison. Signed-off-by: Matthias J. Kannwischer --- mldsa/src/sign.c | 24 +++++++++++++++++----- test/test_mldsa.c | 51 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/mldsa/src/sign.c b/mldsa/src/sign.c index 02dd6e192..d939a1382 100644 --- a/mldsa/src/sign.c +++ b/mldsa/src/sign.c @@ -616,10 +616,6 @@ __contract__( } /* All is well - write signature */ - /* Constant time: At this point it is clear that the signature is valid - it - * can, hence, be considered public. */ - MLD_CT_TESTING_DECLASSIFY(h, sizeof(*h)); - MLD_CT_TESTING_DECLASSIFY(z, sizeof(*z)); mld_pack_sig(sig, challenge_bytes, z, h, n); ret = 0; /* success */ @@ -874,6 +870,7 @@ int crypto_sign_verify_internal(const uint8_t *sig, size_t siglen, const uint8_t pk[MLDSA_CRYPTO_PUBLICKEYBYTES], int externalmu) { + uint32_t z_invalid; unsigned int i; int ret; MLD_ALLOC(buf, uint8_t, (MLDSA_K * MLDSA_POLYW1_PACKEDBYTES)); @@ -908,12 +905,24 @@ int crypto_sign_verify_internal(const uint8_t *sig, size_t siglen, /* mld_unpack_sig and mld_polyvecl_chknorm signal failure through a * single non-zero error code that's not yet aligned with MLD_ERR_XXX. * Map it to MLD_ERR_FAIL explicitly. */ + + /* Constant-time: The signature is commonly considered public. However, we + * mark it as secret to make explicit which parts of the code take time + * depending on the signature. + * + * mld_unpack_sig uses conditional branches to unpack the hint vector h. + * The h-component of the signature, hence, needs to be declassified. + */ if (mld_unpack_sig(c, z, h, sig)) { ret = MLD_ERR_FAIL; goto cleanup; } - if (mld_polyvecl_chknorm(z, MLDSA_GAMMA1 - MLDSA_BETA)) + + z_invalid = mld_polyvecl_chknorm(z, MLDSA_GAMMA1 - MLDSA_BETA); + /* Constant-time: We only leak if z is invalid or not. */ + MLD_CT_TESTING_DECLASSIFY(&z_invalid, sizeof(uint32_t)); + if (z_invalid) { ret = MLD_ERR_FAIL; goto cleanup; @@ -937,6 +946,8 @@ int crypto_sign_verify_internal(const uint8_t *sig, size_t siglen, } /* Matrix-vector multiplication; compute Az - c2^dt1 */ + /* Constant-time: poly_challenge uses conditional branches depending on c. */ + MLD_CT_TESTING_DECLASSIFY(c, MLDSA_CTILDEBYTES); mld_poly_challenge(cp, c); mld_polyvec_matrix_expand(mat, rho); @@ -955,6 +966,9 @@ int crypto_sign_verify_internal(const uint8_t *sig, size_t siglen, /* Reconstruct w1 */ mld_polyveck_caddq(w1); + + /* Constant-time: use_hint uses conditional branches depending on w1. */ + MLD_CT_TESTING_DECLASSIFY(w1, sizeof(mld_polyveck)); mld_polyveck_use_hint(tmp, w1, h); mld_polyveck_pack_w1(buf, tmp); /* Call random oracle and verify challenge */ diff --git a/test/test_mldsa.c b/test/test_mldsa.c index 5916e45dd..5eacce08e 100644 --- a/test/test_mldsa.c +++ b/test/test_mldsa.c @@ -29,6 +29,11 @@ } while (0) +#define MLD_CT_TESTING_DECLASSIFY_H(s) \ + MLD_CT_TESTING_DECLASSIFY( \ + (s) + MLDSA_CTILDEBYTES + MLDSA_L * MLDSA_POLYZ_PACKEDBYTES, \ + MLDSA_POLYVECH_PACKEDBYTES); + static int test_sign_core(uint8_t pk[MLDSA_CRYPTO_PUBLICKEYBYTES], uint8_t sk[MLDSA_CRYPTO_SECRETKEYBYTES], uint8_t sm[MLEN + MLDSA_CRYPTO_BYTES], @@ -49,10 +54,17 @@ static int test_sign_core(uint8_t pk[MLDSA_CRYPTO_PUBLICKEYBYTES], CHECK(crypto_sign(sm, &smlen, m, MLEN, ctx, CTXLEN, sk) == 0); + /* Mark signature as secret to force explcit declassification in verification + */ + MLD_CT_TESTING_SECRET(sm, MLEN + MLDSA_CRYPTO_BYTES); + /* Constant-time: The unpacking of the h-component of the signature requires + * conditional branches.*/ + MLD_CT_TESTING_DECLASSIFY_H(sm); + rc = crypto_sign_open(m2, &mlen, sm, smlen, ctx, CTXLEN, pk); /* Constant time: Declassify outputs to check them. */ - MLD_CT_TESTING_DECLASSIFY(rc, sizeof(int)); + MLD_CT_TESTING_DECLASSIFY(&rc, sizeof(int)); MLD_CT_TESTING_DECLASSIFY(m, MLEN); MLD_CT_TESTING_DECLASSIFY(m2, (MLEN + MLDSA_CRYPTO_BYTES)); @@ -120,6 +132,12 @@ static int test_sign_extmu(void) MLD_CT_TESTING_SECRET(mu, sizeof(mu)); CHECK(crypto_sign_signature_extmu(sig, &siglen, mu, sk) == 0); + /* Mark signature as secret to force explcit declassification in verification + */ + MLD_CT_TESTING_SECRET(sig, sizeof(sig)); + /* Constant-time: The unpacking of the h-component of the signature requires + * conditional branches.*/ + MLD_CT_TESTING_DECLASSIFY_H(sig); CHECK(crypto_sign_verify_extmu(sig, siglen, mu, pk) == 0); return 0; @@ -147,6 +165,12 @@ static int test_sign_pre_hash(void) CHECK(crypto_sign_signature_pre_hash_shake256(sig, &siglen, m, MLEN, ctx, CTXLEN, rnd, sk) == 0); + /* Mark signature as secret to force explcit declassification in verification + */ + MLD_CT_TESTING_SECRET(sig, sizeof(sig)); + /* Constant-time: The unpacking of the h-component of the signature requires + * conditional branches.*/ + MLD_CT_TESTING_DECLASSIFY_H(sig); CHECK(crypto_sign_verify_pre_hash_shake256(sig, siglen, m, MLEN, ctx, CTXLEN, pk) == 0); @@ -234,6 +258,13 @@ static int test_wrong_pk(void) CHECK(crypto_sign(sm, &smlen, m, MLEN, ctx, CTXLEN, sk) == 0); + /* Mark signature as secret to force explcit declassification in verification + */ + MLD_CT_TESTING_SECRET(sm, sizeof(sm)); + /* Constant-time: The unpacking of the h-component of the signature requires + * conditional branches.*/ + MLD_CT_TESTING_DECLASSIFY_H(sm); + /* flip bit in public key */ randombytes((uint8_t *)&idx, sizeof(size_t)); idx %= MLDSA_CRYPTO_PUBLICKEYBYTES; @@ -285,6 +316,13 @@ static int test_wrong_sig(void) CHECK(crypto_sign(sm, &smlen, m, MLEN, ctx, CTXLEN, sk) == 0); + /* Mark signature as secret to force explcit declassification in verification + */ + MLD_CT_TESTING_SECRET(sm, sizeof(sm)); + /* Constant-time: The unpacking of the h-component of the signature requires + * conditional branches.*/ + MLD_CT_TESTING_DECLASSIFY_H(sm); + /* flip bit in signed message */ randombytes((uint8_t *)&idx, sizeof(size_t)); idx %= MLEN + MLDSA_CRYPTO_BYTES; @@ -294,7 +332,7 @@ static int test_wrong_sig(void) rc = crypto_sign_open(m2, &mlen, sm, smlen, ctx, CTXLEN, pk); /* Constant time: Declassify outputs to check them. */ - MLD_CT_TESTING_DECLASSIFY(rc, sizeof(int)); + MLD_CT_TESTING_DECLASSIFY(&rc, sizeof(int)); MLD_CT_TESTING_DECLASSIFY(m2, sizeof(m2)); if (!rc) @@ -337,6 +375,13 @@ static int test_wrong_ctx(void) CHECK(crypto_sign(sm, &smlen, m, MLEN, ctx, CTXLEN, sk) == 0); + /* Mark signature as secret to force explcit declassification in verification + */ + MLD_CT_TESTING_SECRET(sm, sizeof(sm)); + /* Constant-time: The unpacking of the h-component of the signature requires + * conditional branches.*/ + MLD_CT_TESTING_DECLASSIFY_H(sm); + /* flip bit in ctx */ randombytes((uint8_t *)&idx, sizeof(size_t)); idx %= CTXLEN; @@ -346,7 +391,7 @@ static int test_wrong_ctx(void) rc = crypto_sign_open(m2, &mlen, sm, smlen, ctx, CTXLEN, pk); /* Constant time: Declassify outputs to check them. */ - MLD_CT_TESTING_DECLASSIFY(rc, sizeof(int)); + MLD_CT_TESTING_DECLASSIFY(&rc, sizeof(int)); MLD_CT_TESTING_DECLASSIFY(m2, sizeof(m2)); if (!rc)