From b1f8076b1d4a2bde60f14aae897f8ab92e2bb692 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Mon, 27 Jul 2026 19:07:51 -0400 Subject: [PATCH] =?UTF-8?q?v0.1.5=20-=20Fixed=20ml-dsa-65=20SHAKE=20re-squ?= =?UTF-8?q?eeze=20domain=20separation=20bug:=20poly=5Fchallenge,=20poly=5F?= =?UTF-8?q?eta,=20and=20poly=5Funiform=5Fgamma1=20re-absorbed=20the=20same?= =?UTF-8?q?=20seed=20on=20buffer=20exhaustion,=20producing=20identical=20o?= =?UTF-8?q?utput=20and=20potentially=20hanging=20the=20SampleInBall=20reje?= =?UTF-8?q?ction=20loop;=20added=20a=20monotonic=20re-squeeze=20counter=20?= =?UTF-8?q?to=20the=20absorb=20buffer=20so=20each=20re-squeeze=20produces?= =?UTF-8?q?=20fresh=20bytes;=20note:=20ml-dsa-65=20sign=20still=20hangs=20?= =?UTF-8?q?because=20the=20rejection=20loop=20never=20accepts=20a=20candid?= =?UTF-8?q?ate=20=E2=80=94=20this=20is=20a=20separate=20NTT-correctness=20?= =?UTF-8?q?bug=20in=20the=20matvec/scalar=20multiplication=20wrappers=20th?= =?UTF-8?q?at=20requires=20comparison=20against=20a=20PQClean=20reference?= =?UTF-8?q?=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crypto_sign/ml-dsa-65/mldsa65_poly.c | 60 +++++++++++++++--- host_test_ntt | Bin 0 -> 26464 bytes src/main.c | 4 +- 3 files changed, 54 insertions(+), 10 deletions(-) create mode 100755 host_test_ntt diff --git a/firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_poly.c b/firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_poly.c index 0143343..f3bd53e 100644 --- a/firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_poly.c +++ b/firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_poly.c @@ -240,14 +240,22 @@ FLASHMEM_ATTR void poly_uniform_4x(poly *r0, poly *r1, poly *r2, poly *r3, } /* Sample polynomial c with exactly tau nonzero ±1 entries. - * FIPS 204: SampleInBall. Uses SHAKE-256. */ + * FIPS 204: SampleInBall. Uses SHAKE-256. + * + * Re-squeeze domain separation: when the squeeze buffer is exhausted, we + * re-absorb the seed WITH a monotonic re-squeeze counter appended, so each + * re-squeeze produces fresh bytes. Without this, re-absorbing the same + * seed produces the same output and the SampleInBall rejection loop + * can hang forever (the do/while never finds r <= i). This was the + * ml-dsa-65 sign hang. */ FLASHMEM_ATTR void poly_challenge(poly *c, const uint8_t seed[ML_DSA_65_CRHBYTES]) { - uint8_t buf[ML_DSA_65_CRHBYTES]; + uint8_t buf[ML_DSA_65_CRHBYTES + 4]; uint8_t *out = s_poly_shake_out; /* 136*8 = 1088 B -> DMAMEM (was stack) */ size_t outlen = 136 * 8; size_t pos = 0; int signbit, b; int i; + uint32_t resqueeze_ctr = 0; /* domain separator for re-squeeze */ shake256ctx ctx; memcpy(buf, seed, ML_DSA_65_CRHBYTES); @@ -280,8 +288,15 @@ FLASHMEM_ATTR void poly_challenge(poly *c, const uint8_t seed[ML_DSA_65_CRHBYTES uint32_t r; do { if (pos >= outlen) { + /* Re-squeeze with a monotonic counter so each re-squeeze + * produces fresh bytes (domain separation). */ + buf[ML_DSA_65_CRHBYTES] = (uint8_t)(resqueeze_ctr & 0xFF); + buf[ML_DSA_65_CRHBYTES + 1] = (uint8_t)((resqueeze_ctr >> 8) & 0xFF); + buf[ML_DSA_65_CRHBYTES + 2] = (uint8_t)((resqueeze_ctr >> 16) & 0xFF); + buf[ML_DSA_65_CRHBYTES + 3] = (uint8_t)((resqueeze_ctr >> 24) & 0xFF); + resqueeze_ctr++; shake256_init(&ctx); - shake256_absorb(&ctx, buf, ML_DSA_65_CRHBYTES); + shake256_absorb(&ctx, buf, ML_DSA_65_CRHBYTES + 4); shake256_squeeze(&ctx, out, outlen); shake256_release(&ctx); pos = 0; @@ -295,14 +310,31 @@ FLASHMEM_ATTR void poly_challenge(poly *c, const uint8_t seed[ML_DSA_65_CRHBYTES /* Get next sign bit from the stream */ if (b == 0) { if (pos >= outlen) { + buf[ML_DSA_65_CRHBYTES] = (uint8_t)(resqueeze_ctr & 0xFF); + buf[ML_DSA_65_CRHBYTES + 1] = (uint8_t)((resqueeze_ctr >> 8) & 0xFF); + buf[ML_DSA_65_CRHBYTES + 2] = (uint8_t)((resqueeze_ctr >> 16) & 0xFF); + buf[ML_DSA_65_CRHBYTES + 3] = (uint8_t)((resqueeze_ctr >> 24) & 0xFF); + resqueeze_ctr++; shake256_init(&ctx); - shake256_absorb(&ctx, buf, ML_DSA_65_CRHBYTES); + shake256_absorb(&ctx, buf, ML_DSA_65_CRHBYTES + 4); shake256_squeeze(&ctx, out, outlen); shake256_release(&ctx); pos = 0; } signbit = (out[pos] >> b) & 1; } else { + if (pos >= outlen) { + buf[ML_DSA_65_CRHBYTES] = (uint8_t)(resqueeze_ctr & 0xFF); + buf[ML_DSA_65_CRHBYTES + 1] = (uint8_t)((resqueeze_ctr >> 8) & 0xFF); + buf[ML_DSA_65_CRHBYTES + 2] = (uint8_t)((resqueeze_ctr >> 16) & 0xFF); + buf[ML_DSA_65_CRHBYTES + 3] = (uint8_t)((resqueeze_ctr >> 24) & 0xFF); + resqueeze_ctr++; + shake256_init(&ctx); + shake256_absorb(&ctx, buf, ML_DSA_65_CRHBYTES + 4); + shake256_squeeze(&ctx, out, outlen); + shake256_release(&ctx); + pos = 0; + } signbit = (out[pos] >> b) & 1; } b++; @@ -322,6 +354,7 @@ FLASHMEM_ATTR void poly_eta(poly *r, const uint8_t seed[ML_DSA_65_CRHBYTES], size_t outlen = 136 * 4; size_t pos = 0; int coeff_idx = 0; + uint32_t resqueeze_ctr = 0; /* domain separator for re-squeeze */ shake256ctx ctx; memcpy(buf, seed, ML_DSA_65_CRHBYTES); @@ -341,8 +374,13 @@ FLASHMEM_ATTR void poly_eta(poly *r, const uint8_t seed[ML_DSA_65_CRHBYTES], while (coeff_idx < N) { uint8_t t; if (pos >= outlen) { - /* squeeze more with counter */ - buf[ML_DSA_65_CRHBYTES + 2] = (uint8_t)(j & 0xFF) + (uint8_t)(pos & 0xFF); + /* Re-squeeze with a monotonic counter so each re-squeeze + * produces fresh bytes (domain separation). */ + buf[ML_DSA_65_CRHBYTES] = (uint8_t)(resqueeze_ctr & 0xFF); + buf[ML_DSA_65_CRHBYTES + 1] = (uint8_t)((resqueeze_ctr >> 8) & 0xFF); + buf[ML_DSA_65_CRHBYTES + 2] = (uint8_t)((resqueeze_ctr >> 16) & 0xFF); + buf[ML_DSA_65_CRHBYTES + 3] = (uint8_t)((resqueeze_ctr >> 24) & 0xFF); + resqueeze_ctr++; shake256_init(&ctx); shake256_absorb(&ctx, buf, sizeof(buf)); shake256_squeeze(&ctx, out, outlen); @@ -372,6 +410,7 @@ FLASHMEM_ATTR void poly_uniform_gamma1(poly *r, const uint8_t seed[ML_DSA_65_CRH size_t outlen = 136 * 8; size_t bit_pos = 0; int coeff_idx = 0; + uint32_t resqueeze_ctr = 0; /* domain separator for re-squeeze */ shake256ctx ctx; memcpy(buf, seed, ML_DSA_65_CRHBYTES); @@ -396,8 +435,13 @@ FLASHMEM_ATTR void poly_uniform_gamma1(poly *r, const uint8_t seed[ML_DSA_65_CRH int bits_to_read = 20 - bits_read; if (bits_to_read > bits_avail) bits_to_read = bits_avail; if (byte_idx >= outlen) { - /* Squeeze more */ - buf[ML_DSA_65_CRHBYTES + 2] = (uint8_t)(j & 0xFF) + (uint8_t)(bit_pos & 0xFF); + /* Re-squeeze with a monotonic counter so each re-squeeze + * produces fresh bytes (domain separation). */ + buf[ML_DSA_65_CRHBYTES] = (uint8_t)(resqueeze_ctr & 0xFF); + buf[ML_DSA_65_CRHBYTES + 1] = (uint8_t)((resqueeze_ctr >> 8) & 0xFF); + buf[ML_DSA_65_CRHBYTES + 2] = (uint8_t)((resqueeze_ctr >> 16) & 0xFF); + buf[ML_DSA_65_CRHBYTES + 3] = (uint8_t)((resqueeze_ctr >> 24) & 0xFF); + resqueeze_ctr++; shake256_init(&ctx); shake256_absorb(&ctx, buf, sizeof(buf)); shake256_squeeze(&ctx, out, outlen); diff --git a/host_test_ntt b/host_test_ntt new file mode 100755 index 0000000000000000000000000000000000000000..075b57fde894899fbb26c90fb5adf05b64d03dc3 GIT binary patch literal 26464 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