ML-DSA signing ignores errors returned by mldsa_vec_expand_mask
Summary
The ML-DSA signing implementation calls mldsa_vec_expand_mask() without checking its return value.
mldsa_vec_expand_mask() can return a negative error when SHAKE initialization, absorption, or squeezing fails. In the portable C implementation, expansion stops when mldsa_squeeze256() fails, so the output vector y may only be partially generated.
However, the signing code proceeds with NTT and the subsequent signing computations even when this expansion has failed.
This affects both portable signing paths.
At commit 8b845f38105d9c8659056456970af7eaec9a1b90, the relevant calls are in:
wolfcrypt/src/wc_mldsa.c:8999-9000
wolfcrypt/src/wc_mldsa.c:9288-9289
The calls currently have the form:
mldsa_vec_expand_mask(&key->shake, priv_rand_seed, kappa,
params->gamma1_bits, y, params->l, key->heap);
The return value is neither stored nor checked.
Why this matters
mldsa_vec_expand_mask() has an error-returning interface:
@return 0 on success.
@return Negative on hash error.
Its portable implementation calls mldsa_squeeze256() while generating each polynomial of y and stops expansion when that operation returns an error.
Therefore, if SHAKE fails while generating y, the caller should stop the signing operation before using y.
Instead, the current signing path continues into operations such as:
ExpandMask
|
| returns error
v
return value discarded
|
v
NTT(y)
|
v
matrix multiplication
|
v
inverse NTT
|
v
decomposition / w1 encoding
|
v
commitment hash
Reproduced error path
I verified this behavior using a controlled error-path model against the original C/LLVM implementation with SAW and Z3.
The proof assigns distinguishable error values to the relevant operations:
- generation of the private random seed succeeds;
mldsa_vec_expand_mask() returns -13;
- the signing implementation nevertheless continues with the NTT and subsequent signing computations;
- the later commitment hash returns
-12;
- the signing function returns
-12.
Thus, the original -13 error from mldsa_vec_expand_mask() is discarded and overwritten by a later failure.
This path was verified independently for:
- ML-DSA-44
- ML-DSA-65
- ML-DSA-87
The proof was performed on the portable, non-small-memory implementation. The other portable signing path contains the same unchecked call, although it was not included in that LLVM proof configuration.
Impact
The confirmed effects are:
- an error returned by
mldsa_vec_expand_mask() is not propagated to the caller;
- cryptographic computation continues after mask generation has failed;
- subsequent code may consume a partially generated or otherwise invalid
y;
- a later error can overwrite the original SHAKE failure, causing the caller to receive an unrelated error code.
If all subsequent operations succeed, the implementation may continue the signing procedure using an incorrectly generated mask. I have not established that this results in a successful invalid signature or a practical cryptographic attack.
This issue requires an underlying SHAKE/allocation error path and does not affect ordinary successful SHAKE executions.
Expected behavior
The return value of mldsa_vec_expand_mask() should be preserved and signing should stop immediately when mask generation fails.
For example:
ret = mldsa_vec_expand_mask(&key->shake, priv_rand_seed, kappa,
params->gamma1_bits, y, params->l, key->heap);
if (ret != 0) {
break;
}
Simply assigning the result to ret without immediately leaving the rejection-sampling iteration does not appear sufficient, because not all subsequent NTT/decomposition operations in the regular path are protected by a common ret == 0 condition.
Breaking at this point preserves the original error and allows the existing cleanup path to run.
Verification scope
The demonstrated counterexample is an error-path correctness issue rather than an ordinary-input signature forgery.
The formal result establishes the following property of the original implementation:
ExpandMask error
->
error ignored
->
subsequent signing computation executes
->
later error may replace the original error
The SAW/Z3 proof covers the portable non-small-memory implementation for all three standardized ML-DSA parameter sets.
I can provide the relevant proof harness and proof-log excerpt if that would be useful for reproducing the result.
ML-DSA signing ignores errors returned by
mldsa_vec_expand_maskSummary
The ML-DSA signing implementation calls
mldsa_vec_expand_mask()without checking its return value.mldsa_vec_expand_mask()can return a negative error when SHAKE initialization, absorption, or squeezing fails. In the portable C implementation, expansion stops whenmldsa_squeeze256()fails, so the output vectorymay only be partially generated.However, the signing code proceeds with NTT and the subsequent signing computations even when this expansion has failed.
This affects both portable signing paths.
At commit
8b845f38105d9c8659056456970af7eaec9a1b90, the relevant calls are in:wolfcrypt/src/wc_mldsa.c:8999-9000wolfcrypt/src/wc_mldsa.c:9288-9289The calls currently have the form:
The return value is neither stored nor checked.
Why this matters
mldsa_vec_expand_mask()has an error-returning interface:Its portable implementation calls
mldsa_squeeze256()while generating each polynomial ofyand stops expansion when that operation returns an error.Therefore, if SHAKE fails while generating
y, the caller should stop the signing operation before usingy.Instead, the current signing path continues into operations such as:
Reproduced error path
I verified this behavior using a controlled error-path model against the original C/LLVM implementation with SAW and Z3.
The proof assigns distinguishable error values to the relevant operations:
mldsa_vec_expand_mask()returns-13;-12;-12.Thus, the original
-13error frommldsa_vec_expand_mask()is discarded and overwritten by a later failure.This path was verified independently for:
The proof was performed on the portable, non-small-memory implementation. The other portable signing path contains the same unchecked call, although it was not included in that LLVM proof configuration.
Impact
The confirmed effects are:
mldsa_vec_expand_mask()is not propagated to the caller;y;If all subsequent operations succeed, the implementation may continue the signing procedure using an incorrectly generated mask. I have not established that this results in a successful invalid signature or a practical cryptographic attack.
This issue requires an underlying SHAKE/allocation error path and does not affect ordinary successful SHAKE executions.
Expected behavior
The return value of
mldsa_vec_expand_mask()should be preserved and signing should stop immediately when mask generation fails.For example:
Simply assigning the result to
retwithout immediately leaving the rejection-sampling iteration does not appear sufficient, because not all subsequent NTT/decomposition operations in the regular path are protected by a commonret == 0condition.Breaking at this point preserves the original error and allows the existing cleanup path to run.
Verification scope
The demonstrated counterexample is an error-path correctness issue rather than an ordinary-input signature forgery.
The formal result establishes the following property of the original implementation:
The SAW/Z3 proof covers the portable non-small-memory implementation for all three standardized ML-DSA parameter sets.
I can provide the relevant proof harness and proof-log excerpt if that would be useful for reproducing the result.