ML-DSA Verify Throws Exception for Wrong-Length Public Keys
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
| Metric | Value | Rationale |
|---|---|---|
| AV | Network | Attacker can provide malformed public key over the network to a server verifying ML-DSA signatures |
| AC | Low | No security mechanisms to bypass; simply provide wrong-length public key |
| AT | Present | Requires application to pass untrusted public key input to verify() without try-catch; many deployments use trusted key stores |
| PR | None | No authentication needed to submit a public key for verification |
| UI | None | No user interaction required |
| VC | None | No data disclosed |
| VI | None | No data modified |
| VA | High | Process crashes with unhandled exception, complete denial of service |
| SC | None | No downstream systems affected by the crash |
| SI | None | No downstream integrity impact |
| SA | None | No downstream availability impact |
| E | PoC | Working PoC demonstrates the exception throw |
CWE
CWE-754: Improper Check for Unusual or Exceptional Conditions
Affected Files
src/ml-dsa.ts:541—publicCoder.decode(publicKey)throws before any length validationsrc/ml-dsa.ts:544— Signature length check returnsfalsecorrectly, but analogous check for publicKey is missingsrc/ml-dsa.ts:600-603— Outerverifywrapper does not catch exceptions frominternal.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
- 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); - Verify with a correct-length but wrong signature (returns false correctly):
ml_dsa44.verify(sig.slice(1), msg, keys.publicKey); // Returns: false - 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 - 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
- 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. - FIPS-204 non-compliance: The specification requires
verifyto return a boolean for all inputs. - API contract violation: The
Signer.verifyinterface declaresbooleanreturn type. Throwing an exception violates this contract. - 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
};