Skip to content

Commit 3c34a97

Browse files
authored
Merge pull request #10603 from aidangarske/pqc-decode-validation
PQC enforce modulus and eta range checks in DecodePrivateKey
2 parents 29f14ed + 514e39e commit 3c34a97

5 files changed

Lines changed: 117 additions & 10 deletions

File tree

wolfcrypt/src/wc_mldsa.c

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11669,6 +11669,55 @@ int wc_MlDsaKey_ImportPubRaw(wc_MlDsaKey* key, const byte* in, word32 inLen)
1166911669

1167011670
#ifdef WOLFSSL_MLDSA_PRIVATE_KEY
1167111671

11672+
/* Check the s1 and s2 vectors of a private key are in range [-eta, eta].
11673+
*
11674+
* FIPS 204, Algorithm 25 skDecode: s1 and s2 are BitPack encodings of
11675+
* (eta - coeff), so each packed value must be no greater than 2*eta. Reject
11676+
* a private key with any value outside this range rather than silently
11677+
* accepting a non-conforming key.
11678+
*
11679+
* @param [in] p Encoded s1 followed by s2.
11680+
* @param [in] eta Coefficient range specifier (2 or 4).
11681+
* @param [in] len Number of encoded bytes covering s1 and s2.
11682+
* @return 0 when all values are in range.
11683+
* @return PUBLIC_KEY_E when at least one value is out of range.
11684+
*/
11685+
static int mldsa_check_eta_range(const byte* p, byte eta, word32 len)
11686+
{
11687+
int ret = 0;
11688+
word32 i;
11689+
word32 j;
11690+
word32 bits;
11691+
byte max = (byte)(2 * eta);
11692+
11693+
if (eta == MLDSA_ETA_4) {
11694+
/* 4 bits per coefficient, two coefficients per byte. */
11695+
for (i = 0; i < len; i++) {
11696+
if (((p[i] & 0xf) > max) || ((p[i] >> 4) > max)) {
11697+
ret = PUBLIC_KEY_E;
11698+
break;
11699+
}
11700+
}
11701+
}
11702+
else {
11703+
/* 3 bits per coefficient, eight coefficients per three bytes. len
11704+
* (s1EncSz + s2EncSz) is always a multiple of 3, so no trailing
11705+
* partial group is skipped. */
11706+
for (i = 0; (ret == 0) && (i + 3 <= len); i += 3) {
11707+
bits = (word32)p[i] | ((word32)p[i + 1] << 8) |
11708+
((word32)p[i + 2] << 16);
11709+
for (j = 0; j < 8; j++) {
11710+
if (((bits >> (3 * j)) & 0x7) > max) {
11711+
ret = PUBLIC_KEY_E;
11712+
break;
11713+
}
11714+
}
11715+
}
11716+
}
11717+
11718+
return ret;
11719+
}
11720+
1167211721
/* Set the private key data into key.
1167311722
*
1167411723
* @param [in] priv Private key data.
@@ -11677,6 +11726,7 @@ int wc_MlDsaKey_ImportPubRaw(wc_MlDsaKey* key, const byte* in, word32 inLen)
1167711726
* @return 0 on success.
1167811727
* @return BAD_FUNC_ARG when private key size is invalid.
1167911728
* @return MEMORY_E when dynamic memory allocation fails.
11729+
* @return PUBLIC_KEY_E when an s1 or s2 coefficient is out of range.
1168011730
* @return Other negative on hash error.
1168111731
*/
1168211732
static int mldsa_set_priv_key(const byte* priv, word32 privSz,
@@ -11706,6 +11756,15 @@ static int mldsa_set_priv_key(const byte* priv, word32 privSz,
1170611756
}
1170711757
#endif
1170811758

11759+
if (ret == 0) {
11760+
/* Reject a private key whose s1 or s2 coefficients are out of range
11761+
* before copying it in, so a failed import never overwrites an
11762+
* existing key or leaves the object in an inconsistent state. */
11763+
const byte* s1p = priv + MLDSA_PUB_SEED_SZ + MLDSA_K_SZ + MLDSA_TR_SZ;
11764+
ret = mldsa_check_eta_range(s1p, key->params->eta,
11765+
(word32)key->params->s1EncSz + key->params->s2EncSz);
11766+
}
11767+
1170911768
if (ret == 0) {
1171011769
/* Copy the private key data in or copy pointer. */
1171111770
#ifdef WOLFSSL_MLDSA_ASSIGN_KEY

wolfcrypt/src/wc_mlkem.c

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2043,7 +2043,9 @@ static void mlkemkey_decode_public(sword16* pub, byte* pubSeed, const byte* p,
20432043
* @return BAD_FUNC_ARG when key or in is NULL.
20442044
* @return NOT_COMPILED_IN when key type is not supported.
20452045
* @return BUFFER_E when len is not the correct size.
2046-
* @return PUBLIC_KEY_E when public key data doesn't match parameters.
2046+
* @return PUBLIC_KEY_E when the private or public vector has a coefficient
2047+
* that is not reduced modulo q, or public key data doesn't match
2048+
* parameters.
20472049
* @return MLKEM_PUB_HASH_E when public key hash doesn't match stored hash.
20482050
* @return MEMORY_E when dynamic memory allocation failed.
20492051
*/
@@ -2130,15 +2132,24 @@ int wc_MlKemKey_DecodePrivateKey(MlKemKey* key, const unsigned char* in,
21302132
}
21312133
#endif
21322134
if (ret == 0) {
2135+
/* Clear the key-set flags first so any failure below (size, reduction
2136+
* check, or hash) leaves a reused key object consistently unusable
2137+
* rather than flagged-set with zeroed material. */
2138+
key->flags &= ~(MLKEM_FLAG_BOTH_SET | MLKEM_FLAG_H_SET);
2139+
21332140
/* Decode private key that is vector of polynomials.
21342141
* Alg 18 Step 1: dk_PKE <- dk[0 : 384k]
21352142
* Alg 15 Step 5: s_hat <- ByteDecode_12(dk_PKE) */
21362143
mlkem_from_bytes(key->priv, p, (int)k);
21372144
p += k * WC_ML_KEM_POLY_SIZE;
21382145

2139-
/* Decode the public key that is after the private key. */
2140-
mlkemkey_decode_public(key->pub, key->pubSeed, p, k);
2141-
ret = mlkem_check_public(key->pub, (int)k);
2146+
/* Both vectors must decode to coefficients reduced modulo q. */
2147+
ret = mlkem_check_reduced(key->priv, (int)k);
2148+
if (ret == 0) {
2149+
/* Decode the public key that is after the private key. */
2150+
mlkemkey_decode_public(key->pub, key->pubSeed, p, k);
2151+
ret = mlkem_check_reduced(key->pub, (int)k);
2152+
}
21422153
if (ret != 0) {
21432154
ForceZero(key->priv, k * MLKEM_N * sizeof(sword16));
21442155
}
@@ -2263,7 +2274,7 @@ int wc_MlKemKey_DecodePublicKey(MlKemKey* key, const unsigned char* in,
22632274
if (ret == 0) {
22642275
/* Decode public key and check public key matches parameters. */
22652276
mlkemkey_decode_public(key->pub, key->pubSeed, p, k);
2266-
ret = mlkem_check_public(key->pub, (int)k);
2277+
ret = mlkem_check_reduced(key->pub, (int)k);
22672278
}
22682279
if (ret == 0) {
22692280
/* Calculate public hash. */

wolfcrypt/src/wc_mlkem_poly.c

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6096,14 +6096,17 @@ void mlkem_to_bytes(byte* b, sword16* p, int k)
60966096
}
60976097

60986098
/**
6099-
* Check the public key values are smaller than the modulus.
6099+
* Check the vector coefficients are reduced modulo q.
61006100
*
6101-
* @param [in] p Public key - vector.
6101+
* FIPS 203, Sections 7.2 and 7.3: encapsulation and decapsulation keys must
6102+
* decode to coefficients in Z_q; reject any that are not reduced.
6103+
*
6104+
* @param [in] p Key - vector of polynomials.
61026105
* @param [in] k Number of polynomials in vector.
61036106
* @return 0 when all values are in range.
61046107
* @return PUBLIC_KEY_E when at least one value is out of range.
61056108
*/
6106-
int mlkem_check_public(const sword16* p, int k)
6109+
int mlkem_check_reduced(const sword16* p, int k)
61076110
{
61086111
int ret = 0;
61096112
int i;

wolfcrypt/test/test.c

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52299,9 +52299,23 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t mlkem_test(void)
5229952299
if (ret != 0)
5230052300
ERROR_OUT(WC_TEST_RET_ENC_I(i), out);
5230152301

52302-
if (XMEMCMP(priv, priv2, testData[i][2]) != 0)
52302+
if (XMEMCMP(priv, priv2, testData[i][1]) != 0)
5230352303
ERROR_OUT(WC_TEST_RET_ENC_I(i), out);
5230452304

52305+
/* FIPS 203 modulus check: a private key whose first coefficient is
52306+
* not reduced (>= q) must be rejected on decode. Free first so the
52307+
* reinit does not leak the decoded dynamic priv/pub buffers. */
52308+
wc_MlKemKey_Free(key);
52309+
ret = wc_MlKemKey_Init(key, testData[i][0], HEAP_HINT, devId);
52310+
if (ret != 0)
52311+
ERROR_OUT(WC_TEST_RET_ENC_I(i), out);
52312+
priv[0] = 0xff;
52313+
priv[1] |= 0x0f;
52314+
ret = wc_MlKemKey_DecodePrivateKey(key, priv, testData[i][1]);
52315+
if (ret != WC_NO_ERR_TRACE(PUBLIC_KEY_E))
52316+
ERROR_OUT(WC_TEST_RET_ENC_I(i), out);
52317+
ret = 0;
52318+
5230552319
#if !defined(WOLFSSL_NO_MALLOC) && !defined(WC_NO_CONSTRUCTORS)
5230652320
tmpKey = wc_MlKemKey_New(testData[i][0], HEAP_HINT, devId);
5230752321
if (tmpKey == NULL)
@@ -55846,6 +55860,26 @@ static wc_test_ret_t test_mldsa_decode_level(const byte* rawKey,
5584655860
ret = WC_TEST_RET_ENC_NC;
5584755861
}
5584855862
#endif /* !WOLFSSL_MLDSA_FIPS204_DRAFT */
55863+
55864+
#ifdef WOLFSSL_MLDSA_PRIVATE_KEY
55865+
/* Negative: a private key with an out-of-range s1 coefficient must be
55866+
* rejected. s1 follows rho || K || tr; force its first byte out of range. */
55867+
if ((ret == 0) && (!isPublicOnlyKey)) {
55868+
XMEMCPY(der, rawKey, rawKeySz);
55869+
der[MLDSA_PUB_SEED_SZ + MLDSA_K_SZ + MLDSA_TR_SZ] = 0xff;
55870+
wc_MlDsaKey_Free(key);
55871+
ret = wc_MlDsaKey_Init(key, NULL, devId);
55872+
if (ret == 0) {
55873+
ret = wc_MlDsaKey_SetParams(key, expectedLevel);
55874+
}
55875+
if (ret == 0) {
55876+
if (wc_MlDsaKey_ImportPrivRaw(key, der, rawKeySz) !=
55877+
WC_NO_ERR_TRACE(PUBLIC_KEY_E)) {
55878+
ret = WC_TEST_RET_ENC_NC;
55879+
}
55880+
}
55881+
}
55882+
#endif
5584955883
#endif /* !WOLFSSL_MLDSA_NO_ASN1 && WOLFSSL_ASN_TEMPLATE */
5585055884

5585155885
/* Cleanup */

wolfssl/wolfcrypt/wc_mlkem.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,7 @@ void mlkem_from_bytes(sword16* p, const byte* b, int k);
576576
WOLFSSL_LOCAL
577577
void mlkem_to_bytes(byte* b, sword16* p, int k);
578578
WOLFSSL_LOCAL
579-
int mlkem_check_public(const sword16* p, int k);
579+
int mlkem_check_reduced(const sword16* p, int k);
580580

581581
#ifdef USE_INTEL_SPEEDUP
582582
WOLFSSL_LOCAL

0 commit comments

Comments
 (0)