SLH-DSA Verify Throws Exceptions on Malformed Inputs
CVSS v4.0
Score: 7.7 (High base, Medium contextual)
Vector: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N
Note: The base CVSS v4.0 score is 7.7, but contextual severity is assessed as Medium because this is a library-level vulnerability (not a directly exposed service), the impact is availability-only, and well-designed applications may already wrap cryptographic operations in try-catch blocks. However, the function's explicit purpose is to validate untrusted input, making the exception behavior particularly problematic.
CWE
CWE-754: Improper Check for Unusual or Exceptional Conditions
Affected Files
src/slh-dsa.ts:499-501—internal.verifydecodes publicKey and signature before performing length checkssrc/slh-dsa.ts:503— Dead code:sig.length !== sigCoder.bytesLencheck is unreachablesrc/slh-dsa.ts:573-576— Outerverifywrapper does not catch exceptions frominternal.verifysrc/slh-dsa.ts:591-594— Prehashverifywrapper also does not catch exceptions
Description
FIPS-205 Section 9.3 specifies that SLH-DSA.Verify should return false when the signature or public key is invalid. The function contract (Signer.verify) also declares a boolean return type, meaning callers expect either true or false.
In this implementation, internal.verify at line 500-501 calls publicCoder.decode(publicKey) and sigCoder.decode(sig) which internally invoke splitCoder.decode. The splitCoder.decode function at utils.ts:122 calls abytes_(buf, bytesLen, label) which throws an error if the buffer length doesn't match the expected length.
This means any signature or public key with an incorrect length causes an unhandled exception instead of returning false. The explicit length check at line 503 (if (sig.length !== sigCoder.bytesLen) return false) is dead code — the sigCoder.decode on line 501 always throws first for wrong-length signatures.
Neither the outer verify wrapper (line 573-576) nor the prehash verify wrapper (line 591-594) catches these exceptions. They both pass through directly to internal.verify.
Reproduction Steps
- Generate a valid SLH-DSA keypair and sign a message:
import { slh_dsa_sha2_128f } from './src/slh-dsa.ts'; const keys = slh_dsa_sha2_128f.keygen(); const msg = new Uint8Array([1, 2, 3]); const sig = slh_dsa_sha2_128f.sign(msg, keys.secretKey); - Call verify with a short signature:
slh_dsa_sha2_128f.verify(sig.slice(1), msg, keys.publicKey); // THROWS: "signature" expected Uint8Array of length 17088, got length=17087 - Call verify with a short public key:
slh_dsa_sha2_128f.verify(sig, msg, keys.publicKey.slice(1)); // THROWS: "publicKey" expected Uint8Array of length 32, got length=31 - Call verify with an empty signature:
slh_dsa_sha2_128f.verify(new Uint8Array(0), msg, keys.publicKey); // THROWS: "signature" expected Uint8Array of length 17088, got length=0 - The same behavior occurs through
slh_dsa_sha2_128f.prehash(sha512).verify(...).
Impact
- Denial of Service: Any application that calls
verifywithout wrapping it in a try-catch will crash when processing a malformed signature or public key. In server contexts (e.g., verifying post-quantum signatures on incoming requests), an attacker can trigger crashes by sending signatures or public keys with invalid lengths. - FIPS-205 non-compliance: The specification requires
verifyto returnfalsefor invalid inputs, not throw exceptions. - API contract violation: The
Signer.verifyinterface declares abooleanreturn type. Throwing an exception violates the declared contract, making the function unsafe to use without defensive exception handling.
Suggested Fix
Move the length checks before the decode calls, or wrap the decodes in a try-catch:
verify: (sig: Uint8Array, msg: Uint8Array, publicKey: Uint8Array) => {
if (sig.length !== sigCoder.bytesLen) return false;
if (publicKey.length !== publicCoder.bytesLen) return false;
const [pkSeed, pubRoot] = publicCoder.decode(publicKey);
const [random, forsVec, wotsVec] = sigCoder.decode(sig);
// ... rest unchanged
};