Back to Cases
MediumCWE-754

ML-DSA Verify Throws Exception for Wrong-Length Public Keys

noble-post-quantum2026-02-23CWE-754Medium

CVSS v4.0

Score: CVSS-BT 6.0 / Medium Vector: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:P

MetricValueRationale
AVNetworkAttacker can provide malformed public key over the network to a server verifying ML-DSA signatures
ACLowNo security mechanisms to bypass; simply provide wrong-length public key
ATPresentRequires application to pass untrusted public key input to verify() without try-catch; many deployments use trusted key stores
PRNoneNo authentication needed to submit a public key for verification
UINoneNo user interaction required
VCNoneNo data disclosed
VINoneNo data modified
VAHighProcess crashes with unhandled exception, complete denial of service
SCNoneNo downstream systems affected by the crash
SINoneNo downstream integrity impact
SANoneNo downstream availability impact
EPoCWorking PoC demonstrates the exception throw

CWE

CWE-754: Improper Check for Unusual or Exceptional Conditions

Affected Files

  • src/ml-dsa.ts:541publicCoder.decode(publicKey) throws before any length validation
  • src/ml-dsa.ts:544 — Signature length check returns false correctly, but analogous check for publicKey is missing
  • src/ml-dsa.ts:600-603 — Outer verify wrapper does not catch exceptions from internal.verify

Description

ML-DSA's internal.verify function at line 541 calls publicCoder.decode(publicKey) which internally uses splitCoder.decode. The splitCoder.decode at utils.ts:122 calls abytes_(buf, bytesLen, label) which throws an exception if the public key length doesn't match the expected length.

In contrast, the signature length is correctly validated at line 544 with an explicit check that returns false:

if (sig.length !== sigCoder.bytesLen) return false;

No analogous check exists for publicKey. This creates an asymmetry where:

  • Wrong-length signature -> returns false (correct behavior)
  • Wrong-length public key -> throws an exception (incorrect behavior)
  • Empty signature -> returns false (correct behavior)
  • Empty public key -> throws an exception (incorrect behavior)

FIPS-204 Section 6.3 specifies that ML-DSA.Verify returns a boolean. The function contract (Signer.verify) also declares a boolean return type. The outer verify wrapper at lines 600-603 does not catch these exceptions.

Reproduction Steps

  1. Generate a valid ML-DSA keypair and sign a message:
    import { ml_dsa44 } from './src/ml-dsa.ts';
    const keys = ml_dsa44.keygen();
    const msg = new Uint8Array([1, 2, 3, 4, 5]);
    const sig = ml_dsa44.sign(msg, keys.secretKey);
    
  2. Verify with a correct-length but wrong signature (returns false correctly):
    ml_dsa44.verify(sig.slice(1), msg, keys.publicKey);
    // Returns: false
    
  3. Verify with a wrong-length public key (throws):
    ml_dsa44.verify(sig, msg, keys.publicKey.slice(1));
    // THROWS: "publicKey" expected Uint8Array of length 1312, got length=1311
    
  4. Verify with an empty public key (throws):
    ml_dsa44.verify(sig, msg, new Uint8Array(0));
    // THROWS: "publicKey" expected Uint8Array of length 1312, got length=0
    

Impact

  1. Denial of Service: Applications that call ml_dsa44.verify(sig, msg, pk) without try-catch will crash when given a malformed public key. In a server verifying signatures from untrusted sources, an attacker can trigger a crash by providing a wrong-length public key.
  2. FIPS-204 non-compliance: The specification requires verify to return a boolean for all inputs.
  3. API contract violation: The Signer.verify interface declares boolean return type. Throwing an exception violates this contract.
  4. Inconsistency: The function correctly handles wrong-length signatures by returning false, but incorrectly throws for wrong-length public keys, suggesting an oversight.

Affected Variants

All three ML-DSA variants are affected (ml_dsa44, ml_dsa65, ml_dsa87) as they share the same getDilithium() implementation. The prehash verify (HashML-DSA.Verify) at ml-dsa.ts:619-622 also calls internal.verify without catching exceptions and is similarly affected.

Suggested Fix

Add an explicit public key length check before the decode:

verify: (sig, msg, publicKey, opts = {}) => {
  validateInternalOpts(opts);
  if (publicKey.length !== publicCoder.bytesLen) return false;
  if (sig.length !== sigCoder.bytesLen) return false;
  const [rho, t1] = publicCoder.decode(publicKey);
  // ... rest unchanged
};