Skip to content

FIPS 203 (ML-KEM) — Conformance Matrix ​

Synced from docs/compliance/FIPS-203-MATRIX.md — edit there, not here.

Object of assessment: @pqc-sdk/core @ 0.8.3, commit 3d76be9, branch mainNormative document: FIPS 203, Module-Lattice-Based Key-Encapsulation Mechanism Standard (NIST, August 2024) Primitive provider: @noble/post-quantum@0.7.1 (exact pin), with the ML-KEM surface vendored into packages/core/src/vendor/ml-kem/ — see §1.3 Assessment date: 2026-09-07 (initial), 2026-09-07 (corrective pass), 2026-09-09 (corrective pass — see §4.4) Method: static source analysis. Rows recorded as CONFORMING on the strength of a correction cite the specific test that demonstrates it; those tests were executed.


1. Scope and reading instructions ​

This matrix records, requirement by requirement, the state of FIPS 203 conformance for the ML-KEM surface of packages/core.

It began as a pure findings document, per the project's findings-before-fixes rule: the assessment pass changed nothing. Remediation then landed separately, referencing the F203-nn identifiers, and this document now also records which findings were closed, by what change, and on what evidence. A row moves to CONFORMING only when a named, executed test demonstrates it — never on the strength of a code change alone, and never because a fix became possible (see §3.8 as of 8c135fa for a row deliberately left open on exactly that ground).

1.1 Keyword semantics ​

FIPS 203 §2.1 (Terms and Definitions) defines exactly two normative keywords:

shall — Used to indicate a requirement of this standard. should — Used to indicate a strong recommendation but not a requirement of this standard.

The word must is not defined by FIPS 203, yet it appears in the body text (for example, §3.3: "A fresh string of random bytes must be generated for every such invocation"). Statements resting solely on an undefined must are recorded here with their normative force marked MUST (undefined) and are not treated as discharged or as breached; their interpretation remains open.

1.2 Status vocabulary ​

StatusMeaning
CONFORMINGRequirement is applicable, satisfied, and supported by objective evidence in this repository.
CONFORMING (delegated)Requirement is satisfied by the pinned provider, verified by source inspection, but not guarded by any test in this repository.
PARTIALLY CONFORMINGSatisfied for part of the declared scope only.
NONCONFORMINGApplicable requirement demonstrably not satisfied.
INDETERMINATEApplicability or satisfaction cannot be settled from the evidence available in this pass.
NOT APPLICABLERequirement addresses a construct this SDK does not implement or expose.

This set is closed. Where a row needs a narrower reading than a bare state conveys, it carries a parenthetical qualifier on one of these six — CONFORMING (delegated), CONFORMING (with documented language limitation) — never a new state. A qualifier is part of the status and must be reproduced wherever the status is cited; dropping it changes the claim.

1.3 Boundary of responsibility ​

@pqc-sdk/core does not implement lattice arithmetic itself. It is a wrapper: key lifecycle, algorithm registry, envelope framing, AEAD sealing, and error mapping. The arithmetic requirements of FIPS 203 §4–§7 are discharged — or not — by the primitive. Rows are labelled SDK, Vendored or Provider in the SDK Component column.

The distinction between the last two is what makes several rows closable at all, and it must not be lost. The published dist imports @noble/post-quantum as an external runtime import rather than inlining it, so consumers execute their own registry-resolved copy. No patch, overrides entry, or pnpm patchedDependencies entry in this repository can reach that copy — a Provider-scope finding is therefore not closable from here at all.

Vendored rows are different. As of 2026-09-07 the ML-KEM surface of @noble/post-quantum@0.7.1 (ml-kem.ts, _crystals.ts, utils.ts) is copied into packages/core/src/vendor/ml-kem/ under its MIT license, and KEM_ALGORITHMS['ml-kem-768'] resolves to that copy. Because src/ is bundled into dist by tsup, a correction made there does ship to consumers.

As of 2026-09-09, KEM_ALGORITHMS['x-wing']'s embedded ML-KEM-768 also resolves to that copy, via packages/core/src/x-wing.ts (see §3.10 as of 8c135fa) — X-Wing's own X25519 component and the generic hybrid combiner it uses (combineKEMS/expandSeedXof/_ecdhKem) still come from the npm package and remain Provider-scope, since neither performs ML-KEM arithmetic and neither carries an ML-KEM finding. ML-DSA is vendored separately (see FIPS-204-MATRIX.md §1.3); SLH-DSA is not yet implemented by this SDK and, when it is, will resolve entirely against the npm package and remain Provider-scope. See packages/core/src/vendor/ml-kem/NOTICE.md for provenance, digests and the maintenance procedure.

Vendoring is a maintenance liability, not a free win: it forks the ML-KEM implementation, and every upstream release must now be re-vendored and re-verified rather than picked up by a version bump. It was accepted here only because two shall statements could not otherwise be satisfied for consumers by any means available to this repository.


2. Compliance matrix ​

Req IDFIPS 203 QuoteSectionNormative ForceApplicabilitySDK ComponentEvidence RequiredCurrent Status
F203-01"ML-KEM comes equipped with three parameter sets: ML-KEM-512 (security category 1) • ML-KEM-768 (security category 3) • ML-KEM-1024 (security category 5)"§7 (intro)DefinitionalProject scope declares all three; FIPS mandates nonetypes.ts:12 (KemAlgorithm), algorithms.ts:82 (KEM_ALGORITHMS)Registry entry, exported type member, round-trip vector per setPARTIALLY CONFORMING — ML-KEM-512/1024 registered at algorithms.ts:85, :107; ACVP vectors nist-vectors.test.ts:13-18 (#93, a5d00d9); see the F203-01 note below the table; historical: §3.1 as of 8c135fa
F203-02"NIST recommends using ML-KEM-768 as the default parameter set, as it provides a large security margin at a reasonable performance cost."§8RecommendationApplicablealgorithms.ts:76-86The sole FIPS 203 set offered is 768; parameters match Table 2CONFORMING
F203-03"a combined KEM that includes ML-KEM as a component might not meet IND-CCA2 security. Implementers should assess the security of any procedure in which …"§3.3SHOULDApplicable — x-wing is a combined KEMindex.ts (FIPS_ALGORITHMS, SUPPORTED_ALGORITHMS); algorithms.ts:59-68An exported constant (FIPS_ALGORITHMS) that excludes x-wing and names only NIST-standardized entries, with JSDoc citing FIPS 203 §3.3CONFORMING — closed 2026-09-09 (corrective pass 2) in #96 (976651a): FIPS_ALGORITHMS at index.ts:94; historical: §3.2 as of 8c135fa
F203-04"(Type check) If ek is not an array of bytes of length 384k + 32 … then input checking failed."§7.2SHALL (via §3.3)Applicablealgorithms.ts (requireKey); vendor/ml-kem/ml-kem.ts (abytes in encapsulate)Negative test: wrong-length encapsulation key rejected with a documented PqcErrorCONFORMING — requireKey (algorithms.ts:215, called at encrypt.ts:41); wrong-length key test encrypt.test.ts:58; historical: §3.3 as of 8c135fa
F203-05"(Modulus check) Perform the computation test ← ByteEncode12(ByteDecode12(ek[0:384k])). If test ≠ ek[0:384k], then input checking failed."§7.2SHALL (via §3.3)Applicablevendor/ml-kem/ml-kem.ts (validateModulus, invoked from encapsulate)Negative test: encapsulation key with a coefficient ≥ q injected and rejected at encapsulate — fips203-input-checks.test.tsCONFORMING — closed 2026-09-09 (corrective pass 2) in #96 (976651a): fips203-input-checks.test.ts:45-72; historical: §3.3 as of 8c135fa
F203-06"ML-KEM.Encaps shall not be run with an encapsulation key that has not been checked as above."§7.2SHALLApplicableencrypt.ts:41 → algorithms.ts:153 (requireKey); stream.ts:194,199Both checks precede Encaps_internal on every call pathCONFORMING (delegated)
F203-07"Implementers shall ensure that ML-KEM.Encaps and ML-KEM.Decaps are only executed on inputs that have been checked, as described in Section 7."§3.3SHALLApplicableencrypt.ts:41,88; stream.ts:194,338No call path reaches the primitive without traversing requireKeyCONFORMING
F203-08"(Ciphertext type check) If c is not a byte array of length 32(du·k + dv) … then input checking has failed." + "Ciphertext checking shall be performed with every execution of ML-KEM.Decaps."§7.3SHALLApplicableencrypt.ts (minLength), vendor/ml-kem/ml-kem.ts (abytes on cipherText)Negative test: truncated / over-length ciphertext rejected on every decapsulationCONFORMING
F203-09"(Decapsulation key type check) If dk is not a byte array of length 768k + 96 … then input checking has failed."§7.3SHALL (via §3.3)Applicablealgorithms.ts (requireKey); vendor/ml-kem/ml-kem.ts (abytes on secretKey)Negative test: wrong-length decapsulation key rejected with a documented PqcErrorCONFORMING
F203-10"(Hash check) Perform the computation test ← H(dk[384k : 768k+32]). If test ≠ dk[768k+32 : 768k+64], then input checking has failed."§7.3SHALL (via §3.3)Applicablevendor/ml-kem/ml-kem.ts (hash check in decapsulate)Negative test: stored H(ek) bit-flipped, decapsulate throws; stored hash independently verified against SHA3-256(ek) — fips203-input-checks.test.tsCONFORMING — closed 2026-09-09 (corrective pass 2) in #96 (976651a): fips203-input-checks.test.ts:86-129; historical: §3.3 as of 8c135fa
F203-11"2: if m == NULL then / 3: return ⊥ ▷ return an error indication if random bit generation failed"§7.2, Alg. 20DefinitionalApplicablevendor/ml-kem/ml-kem.ts (sampleRandomness), algorithms.ts (encapsulateTo)RBG failure surfaces as a distinct, non-key-attributed error conditionCONFORMING — closed 2026-09-07, see §3.4 as of 8c135fa
F203-12"These random bytes shall be generated using an approved RBG, as prescribed in SP 800-90A, SP 800-90B, and SP 800-90C."§3.3SHALLApplicable to the deployed module; not determinable hereHost runtime crypto.getRandomValues — outside this repositoryAn RBG validated under SP 800-90A/B/C, inside a cryptographic module boundaryINDETERMINATE — reclassified 2026-09-08, see §3.5 as of 8c135fa
F203-13"this RBG shall have a security strength of at least … 192 bits for ML-KEM-768"§3.3SHALLApplicableHost RBG (out of repository control)Documented security-strength claim for the host entropy source on each supported runtimeINDETERMINATE — depends on F203-12
F203-14"A fresh string of random bytes must be generated for every such invocation."§3.3MUST (undefined)Applicablevendor/ml-kem/ml-kem.ts:646, :713 (m sampled per call via sampleRandomness); x-wing.ts:98No caching or reuse of encapsulation randomness across invocations. No test isolates encapsulation freshness: encrypt.test.ts:26 compares whole default (X-Wing) envelopes, which the random AES-GCM nonce alone makes differINDETERMINATE — reclassified 2026-10-27 per §1.1: statements resting solely on an undefined must "are not treated as discharged or as breached; their interpretation remains open"
F203-15"The interfaces for these functions should not be made available to applications other than for testing purposes."§3.3SHOULDApplicablekeys.ts:60 (generateKeyPairFromSeed), index.ts:22-34Seeded / derandomized entry points absent from the package export mapCONFORMING — generateKeyPairFromSeed (keys.ts:106), KEM_ALGORITHMS and encapsulateTo are not re-exported from index.ts; packages/core/package.json exports only "."; the seed parameter is at algorithms.ts:40-43; the uncompilable JSDoc example noted there is at algorithms.ts:254; historical: §3.6 as of 8c135fa (its line numbers are stale)
F203-16"the sampling of random values required for key generation … and encapsulation … shall be performed by the cryptographic module."§3.3SHALLApplicablekeys.ts:53 (randomBytes), provider EncapsRandomness sampled inside the boundary, never supplied by the caller in production pathsCONFORMING
F203-17"these values … can be used directly as a shared secret key for symmetric cryptography. If further key derivation is needed, the final symmetric keys shall be derived … in an approved manner"§3.3SHALL (conditional)Applicableencrypt.ts:58 (gcm(sharedSecret, …)), stream.tsK used directly as an AES-256-GCM key; no non-approved KDF interposedCONFORMING — gcm(sharedSecret, nonce, header) at encrypt.ts:81 (moved from :58 by #96 976651a and #113 f3f1d28); historical: §3.7 as of 8c135fa
F203-18"implementers shall ensure that intermediate data is destroyed as soon as it is no longer needed … only the designated output can be retained in memory"§3.3, §6.3SHALLApplicablevendor/ml-kem/ml-kem.ts (selectSharedSecret, both decapsulate sites)Zeroization of intermediate state, including the implicit-reject flagCONFORMING (with documented language limitation) — closed 2026-09-08, see §3.8 as of 8c135fa
F203-19"Division and rounding in the computation of these functions are performed in the set of rational numbers. Floating-point computations shall not be used."§3.3, §4.2.1SHALLApplicable — Compress_d runs on every Encaps and Decapsvendor/ml-kem/ml-kem.ts (compress)Integer-only implementation of Compress_d / Decompress_dCONFORMING — closed 2026-09-07, see §3.9 as of 8c135fa
F203-20"The public-key encryption scheme K-PKE described in Section 5 shall not be used as a stand-alone cryptographic scheme."§3.3SHALLApplicablePublic surface (index.ts), package export mapNo K-PKE entry point reachable from @pqc-sdk/coreCONFORMING
F203-21"For every algorithm that is specified in this standard, a conforming implementation may replace the given set of steps with any mathematically equivalent set of steps."§3.3MAYPermissive — considered, and not relied on, for F203-19vendor/ml-kem/ml-kem.ts (compress)Proof of output equivalence over the full input domainNOT APPLICABLE — see §3.9 as of 8c135fa

F203-01 note (2026-10-27). The status stays PARTIALLY CONFORMING. #93 (a5d00d9) recorded this row as CONFORMING on the ground that the three parameter sets share the same vendored primitive (historical: §3.1 as of 8c135fa). A requirement-by-requirement determination for ML-KEM-512 and ML-KEM-1024 is still missing.

4.2 Validation status ​

No claim of FIPS validation is made or implied by this document. Nothing in this repository has been submitted to, or evaluated under, the NIST Cryptographic Module Validation Program or the Cryptographic Algorithm Validation Program. This matrix records the result of a self-assessment by static analysis. Conformance, validation-readiness, and official validation status are three separate things, and only the first is addressed here.

ACVP known-answer vectors (packages/core/src/vectors/mlkem768-*.json, exercised by nist-vectors.test.ts) are assurance evidence. They demonstrate input/output agreement with the standard on the tested paths. They are not automatic proof of every internal normative condition — F203-19 is the standing counterexample: those vectors passed for the entire period the requirement was breached, and still pass now that it is not. A known-answer test cannot see the difference, which is exactly why closing that row required a source-level assertion and not only a behavioural one.

The 31 ACVP cases now execute against the vendored primitive, because generateKeyPairFromSeed and pqc.decrypt route through algorithms.ts. Their passing is what establishes that vendoring changed no ML-KEM output.

4.3 Next actions (open) ​

  1. F203-05, F203-10 — closed 2026-09-09 (corrective pass 2): fips203-input-checks.test.ts exercises both checks directly. No further action.
  2. F203-18 — assessor decision on the proposed status (§3.8 as of 8c135fa): CONFORMING (with documented language limitation) as proposed, or NONCONFORMING on the ground that the shall cannot be met in a language with no zeroization primitive. No further code change is proposed either way.
  3. F203-03 — closed 2026-09-09 (corrective pass 2): FIPS_ALGORITHMS exported from index.ts with JSDoc citing FIPS 203 §3.3 and this matrix §3.2 (index.ts:84; historical: §3.2 as of 8c135fa, which predates this closure and still reads INDETERMINATE). No further action.
  4. F203-01 — sets exposed in #93 (a5d00d9): envelopes v3/v4, streaming versions 5/6, ACVP and golden vectors. #93 recorded F203-01 as CONFORMING (§3.1 at a5d00d9); #96 (976651a) reverted the row to PARTIALLY CONFORMING without mentioning it. The status stays PARTIALLY CONFORMING until a requirement-by-requirement determination exists for ML-KEM-512 and ML-KEM-1024; see the F203-01 note.

4.4 Change log ​

DateChange
2026-09-07Initial assessment. 21 rows recorded; F203-11, F203-12, F203-18, F203-19 NONCONFORMING.
2026-09-07Corrective pass. ML-KEM surface vendored (§1.3); F203-11 and F203-19 closed with the evidence cited in §3.4 as of 8c135fa, §3.9 as of 8c135fa. Keyword-definition citation corrected from §1.3 to §2.1.
2026-09-08Corrective pass. F203-18: implicit-reject selection made branch-free at both decapsulate sites, both candidates now destroyed unconditionally; proposed CONFORMING (with documented language limitation), pending assessor review (§3.8 as of 8c135fa). F203-12: reclassified NONCONFORMING → INDETERMINATE — a classification correction, no code change; runtime randomness sources documented here and in SECURITY.md (§3.5 as of 8c135fa). Qualifier convention recorded in §1.2.
2026-09-09Corrective pass 1. Found and closed a scope gap (§3.10 as of 8c135fa): X-Wing's embedded ML-KEM-768 — pqc.keys.generate()'s default path — still ran the unpatched floating-point Compress_d (F203-19) and the unpatched double-ternary implicit-reject selection (F203-18) after both rows had already closed for the ml-kem-768 algorithm entry alone. packages/core/src/x-wing.ts repoints X-Wing at the vendored ml_kem768, reusing @noble/post-quantum/hybrid.js's own combineKEMS/expandSeedXof/_ecdhKem (Provider-scope, no ML-KEM finding applies to them) rather than vendoring them; evidence in x-wing.test.ts. §1.3 and §3.2 (historical: §3.2 as of 8c135fa; the current boundary is §1.3 and x-wing.ts) updated to describe the new boundary. Row statuses for F203-03, F203-18 and F203-19 are unchanged by this pass.
2026-09-09Adversarial verification pass. Found F203-11 had a second gap on X-Wing's default path. Closed by wrapping encapsulate in x-wing.ts to sample via sampleRandomness before the unpatched combineKEMS default parameter ever evaluates. Evidence: new default-path case in rbg-attribution.test.ts; xwing-vectors.test.ts 18 KAT cases re-verified unaffected. See historical: §3.11 as of 8c135fa; the keys.ts gap it records as open was closed in #96 (976651a): keys.ts:89 samples through sampleRandomness, tested at rbg-attribution.test.ts:334.
2026-09-09Corrective pass 2. Closed three open rows: F203-03 — FIPS_ALGORITHMS constant exported from index.ts, listing only NIST-standardized entries (ml-kem-768, ml-dsa-44/65/87) and excluding x-wing, with JSDoc citing FIPS 203 §3.3 and this matrix §3.2 (index.ts:84; historical: §3.2 as of 8c135fa); status INDETERMINATE → CONFORMING. F203-05 — fips203-input-checks.test.ts: ML-KEM-768 encapsulation key with coefficient q=3329 injected; encapsulate throws; status CONFORMING (delegated) → CONFORMING. F203-10 — same file: stored H(ek) bit-flipped in decapsulation key; decapsulate throws; stored hash re-derived independently and confirmed equal to SHA3-256(ek); status CONFORMING (delegated) → CONFORMING. Also: F204-06/07 RBG path closed for ML-DSA (keys.ts, sign.ts); F204-05 hedged-signing test added; F204-09/21 provider tripwires added (provider-tripwires.test.ts). Summary counts updated.
2026-10-25F203-11 consistency (no status change): the one-shot AES-GCM nonce in encrypt.ts is now drawn through sampleRandomness, so an RBG failure at that step also surfaces as PqcError('RBG_FAILURE') instead of the host's raw error (#113). The nonce is not an ML-KEM draw, so this extends the SDK's RBG-failure rule rather than FIPS 203 Alg. 20 itself. Regression tests in vendor/ml-kem/__tests__/rbg-attribution.test.ts break only the nonce draw for all four KEMs in the throw/absent/short modes.
2026-10-26Not recorded here: an RBG failure on X-Wing's X25519 scalar-blinding draw (@noble/curves 2.4.0) surfaces as a raw error on keygen, INVALID_KEY on encapsulation and DECRYPTION_FAILED on decapsulation. Out of scope for this matrix per §1 ("FIPS 203 conformance for the ML-KEM surface", line 18) and §1.3 (X25519 and the hybrid combiner are Provider-scope and carry no ML-KEM finding, lines 81–83); neither FIPS 203 nor draft-connolly-cfrg-xwing-kem-10 requires an error type for this case. Documented in SECURITY.md ("Threat model: known limits") and docs/security/AUDIT-SCOPE.md §5. Citation fix: section 3 ("Finding detail") was removed from this file in #96 (976651a); the two §3.4 citations now link to it as of 8c135fa.
2026-10-27Evidence linked, not restored. #96 (976651a) removed section 3 ("Finding detail", §3.1–§3.11 as of 8c135fa), ## 4. Summary and §4.1 from this file, and changed F203-01 from CONFORMING to PARTIALLY CONFORMING with no recorded justification (neither its commit message nor its PR description mentions F203-01 or the removal). This PR links that evidence to 8c135fa (#94, the last version that has it) instead of restoring it, because it contradicts the current state (for example, §3.2 still reads INDETERMINATE for F203-03, and §3.11 records the keys.ts RBG gap as open). Each of the 22 remaining §3.x citations now links to its section at 8c135fa; where the row or its evidence changed after 8c135fa, the citation names the current evidence and marks the link historical. F203-01 keeps PARTIALLY CONFORMING; see the note below §2. §4.3 item 4 updated.
2026-10-27F203-14 classified INDETERMINATE. The row carried the label "NOT EVALUATED", which is outside the closed set of six states in §1.2, so it had no valid status. It is classified INDETERMINATE under §1.1, which treats statements resting solely on the undefined must as neither discharged nor breached; of the six states, only INDETERMINATE describes that. Classification correction, no code change. Evidence pointer updated from encrypt.ts:53 (now a comment) to the current m sampling sites; no test isolates encapsulation freshness.