Skip to content

ML-DSA signing ignores errors returned by mldsa_vec_expand_mask #11454

Description

@Uaenasxbpy

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:

  1. generation of the private random seed succeeds;
  2. mldsa_vec_expand_mask() returns -13;
  3. the signing implementation nevertheless continues with the NTT and subsequent signing computations;
  4. the later commitment hash returns -12;
  5. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions