From 1b5d11962438bef1576eb49a04b4e0b6ff8a7a8c Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Sat, 11 Apr 2026 23:42:51 -0700 Subject: [PATCH 1/2] fix: validate s1/s2 bounds in wc_dilithium_check_key After decoding s1 and s2 from the private key, check that every coefficient lies within [-eta, +eta]. 3-bit values 5/6/7 (eta=2) and 4-bit nibbles 9-15 (eta=4) are out of spec per FIPS 204 and must be rejected. Without this check, internally-consistent but spec-invalid keys pass the mathematical consistency test. Catches the InvalidPrivateKey case in Wycheproof ML-DSA mldsa_*_sign_noseed_test.json vectors. --- wolfcrypt/src/dilithium.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/wolfcrypt/src/dilithium.c b/wolfcrypt/src/dilithium.c index e459683a33c..2ac0651f68b 100644 --- a/wolfcrypt/src/dilithium.c +++ b/wolfcrypt/src/dilithium.c @@ -11456,6 +11456,23 @@ int wc_dilithium_check_key(dilithium_key* key) /* Get s1, s2 and t0 from private key. */ dilithium_vec_decode_eta_bits(s1p, params->eta, s1, params->l); dilithium_vec_decode_eta_bits(s2p, params->eta, s2, params->k); + /* Validate s1 and s2 coefficients are within [-eta, eta]. */ + { + sword32 eta = (sword32)params->eta; + word32 c; + for (c = 0; c < (word32)(params->l * DILITHIUM_N); c++) { + if (s1[c] < -eta || s1[c] > eta) { + ret = PUBLIC_KEY_E; + break; + } + } + for (c = 0; (ret == 0) && (c < (word32)(params->k * DILITHIUM_N)); c++) { + if (s2[c] < -eta || s2[c] > eta) { + ret = PUBLIC_KEY_E; + break; + } + } + } dilithium_vec_decode_t0(t0p, params->k, t0); /* Get t1 from public key. */ From 3d992cc90d9240b4fb3b3cd9853b6c243d2a0045 Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Sun, 12 Apr 2026 09:50:12 -0700 Subject: [PATCH 2/2] fix: skip NTT when s1/s2 bounds check fails in check_key After the coefficient bounds check sets ret=PUBLIC_KEY_E, wrap the subsequent NTT and matrix-multiply block in if (ret == 0) so that invalid coefficient data is never passed to NTT functions. --- wolfcrypt/src/dilithium.c | 60 ++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/wolfcrypt/src/dilithium.c b/wolfcrypt/src/dilithium.c index 2ac0651f68b..dfd90a25a5d 100644 --- a/wolfcrypt/src/dilithium.c +++ b/wolfcrypt/src/dilithium.c @@ -11473,39 +11473,41 @@ int wc_dilithium_check_key(dilithium_key* key) } } } - dilithium_vec_decode_t0(t0p, params->k, t0); + if (ret == 0) { + dilithium_vec_decode_t0(t0p, params->k, t0); - /* Get t1 from public key. */ - dilithium_vec_decode_t1(t1p, params->k, t1); + /* Get t1 from public key. */ + dilithium_vec_decode_t1(t1p, params->k, t1); - /* Calcaluate t = NTT-1(A o NTT(s1)) + s2 */ - dilithium_vec_ntt_small_full(s1, params->l); - dilithium_matrix_mul(t, a, s1, params->k, params->l); - #ifdef WOLFSSL_DILITHIUM_SMALL - dilithium_vec_red(t, params->k); - #endif - dilithium_vec_invntt_full(t, params->k); - dilithium_vec_add(t, s2, params->k); - /* Subtract t0 from t. */ - dilithium_vec_sub(t, t0, params->k); - /* Make t positive to match t1. */ - dilithium_vec_make_pos(t, params->k); - - /* Check t - t0 and t1 are the same. */ - for (i = 0; i < params->k; i++) { - for (j = 0; j < DILITHIUM_N; j++) { - x |= tt[j] ^ t1[j]; + /* Calcaluate t = NTT-1(A o NTT(s1)) + s2 */ + dilithium_vec_ntt_small_full(s1, params->l); + dilithium_matrix_mul(t, a, s1, params->k, params->l); + #ifdef WOLFSSL_DILITHIUM_SMALL + dilithium_vec_red(t, params->k); + #endif + dilithium_vec_invntt_full(t, params->k); + dilithium_vec_add(t, s2, params->k); + /* Subtract t0 from t. */ + dilithium_vec_sub(t, t0, params->k); + /* Make t positive to match t1. */ + dilithium_vec_make_pos(t, params->k); + + /* Check t - t0 and t1 are the same. */ + for (i = 0; i < params->k; i++) { + for (j = 0; j < DILITHIUM_N; j++) { + x |= tt[j] ^ t1[j]; + } + tt += DILITHIUM_N; + t1 += DILITHIUM_N; + } + /* Check the public seed is the same in private and public key. */ + for (i = 0; i < DILITHIUM_PUB_SEED_SZ; i++) { + x |= key->p[i] ^ key->k[i]; } - tt += DILITHIUM_N; - t1 += DILITHIUM_N; - } - /* Check the public seed is the same in private and public key. */ - for (i = 0; i < DILITHIUM_PUB_SEED_SZ; i++) { - x |= key->p[i] ^ key->k[i]; - } - if ((ret == 0) && (x != 0)) { - ret = PUBLIC_KEY_E; + if (x != 0) { + ret = PUBLIC_KEY_E; + } } }