ML-DSA: reject private key with out-of-range s1/s2 coefficients on decode

This commit is contained in:
aidan garske
2026-06-04 15:36:32 -07:00
parent 477754024d
commit a1bdacbb83
2 changed files with 78 additions and 0 deletions
+59
View File
@@ -11669,6 +11669,55 @@ int wc_MlDsaKey_ImportPubRaw(wc_MlDsaKey* key, const byte* in, word32 inLen)
#ifdef WOLFSSL_MLDSA_PRIVATE_KEY
/* Check the s1 and s2 vectors of a private key are in range [-eta, eta].
*
* FIPS 204, Algorithm 25 skDecode: s1 and s2 are BitPack encodings of
* (eta - coeff), so each packed value must be no greater than 2*eta. Reject
* a private key with any value outside this range rather than silently
* accepting a non-conforming key.
*
* @param [in] p Encoded s1 followed by s2.
* @param [in] eta Coefficient range specifier (2 or 4).
* @param [in] len Number of encoded bytes covering s1 and s2.
* @return 0 when all values are in range.
* @return PUBLIC_KEY_E when at least one value is out of range.
*/
static int mldsa_check_eta_range(const byte* p, byte eta, word32 len)
{
int ret = 0;
word32 i;
word32 j;
word32 bits;
byte max = (byte)(2 * eta);
if (eta == MLDSA_ETA_4) {
/* 4 bits per coefficient, two coefficients per byte. */
for (i = 0; i < len; i++) {
if (((p[i] & 0xf) > max) || ((p[i] >> 4) > max)) {
ret = PUBLIC_KEY_E;
break;
}
}
}
else {
/* 3 bits per coefficient, eight coefficients per three bytes. len
* (s1EncSz + s2EncSz) is always a multiple of 3, so no trailing
* partial group is skipped. */
for (i = 0; (ret == 0) && (i + 3 <= len); i += 3) {
bits = (word32)p[i] | ((word32)p[i + 1] << 8) |
((word32)p[i + 2] << 16);
for (j = 0; j < 8; j++) {
if (((bits >> (3 * j)) & 0x7) > max) {
ret = PUBLIC_KEY_E;
break;
}
}
}
}
return ret;
}
/* Set the private key data into key.
*
* @param [in] priv Private key data.
@@ -11677,6 +11726,7 @@ int wc_MlDsaKey_ImportPubRaw(wc_MlDsaKey* key, const byte* in, word32 inLen)
* @return 0 on success.
* @return BAD_FUNC_ARG when private key size is invalid.
* @return MEMORY_E when dynamic memory allocation fails.
* @return PUBLIC_KEY_E when an s1 or s2 coefficient is out of range.
* @return Other negative on hash error.
*/
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,
}
#endif
if (ret == 0) {
/* Reject a private key whose s1 or s2 coefficients are out of range
* before copying it in, so a failed import never overwrites an
* existing key or leaves the object in an inconsistent state. */
const byte* s1p = priv + MLDSA_PUB_SEED_SZ + MLDSA_K_SZ + MLDSA_TR_SZ;
ret = mldsa_check_eta_range(s1p, key->params->eta,
(word32)key->params->s1EncSz + key->params->s2EncSz);
}
if (ret == 0) {
/* Copy the private key data in or copy pointer. */
#ifdef WOLFSSL_MLDSA_ASSIGN_KEY
+19
View File
@@ -55860,6 +55860,25 @@ static wc_test_ret_t test_mldsa_decode_level(const byte* rawKey,
ret = WC_TEST_RET_ENC_NC;
}
#endif /* !WOLFSSL_MLDSA_FIPS204_DRAFT */
#ifdef WOLFSSL_MLDSA_PRIVATE_KEY
/* Negative: a private key with an out-of-range s1 coefficient must be
* rejected. s1 follows rho || K || tr; force its first byte out of range. */
if ((ret == 0) && (!isPublicOnlyKey)) {
XMEMCPY(der, rawKey, rawKeySz);
der[MLDSA_PUB_SEED_SZ + MLDSA_K_SZ + MLDSA_TR_SZ] = 0xff;
wc_MlDsaKey_Free(key);
ret = wc_MlDsaKey_Init(key, NULL, devId);
if (ret == 0) {
ret = wc_MlDsaKey_SetParams(key, expectedLevel);
}
if (ret == 0) {
if (wc_MlDsaKey_ImportPrivRaw(key, der, rawKeySz) != PUBLIC_KEY_E) {
ret = WC_TEST_RET_ENC_NC;
}
}
}
#endif
#endif /* !WOLFSSL_MLDSA_NO_ASN1 && WOLFSSL_ASN_TEMPLATE */
/* Cleanup */