eslint-plugin-kxco-pq
v1.0.1
Published
ESLint rule that flags direct .ml_dsa65/.ml_kem768 primitive access in code depending on kxco-post-quantum, catching the exact bypass bug found across the KXCO PQ package family in August 2026.
Maintainers
Readme
eslint-plugin-kxco-pq
One rule: catch code that calls a raw @noble/post-quantum primitive through kxco-post-quantum's re-export instead of the wrapper's own safety-checked API.
Why
kxco-post-quantum re-exports the raw @noble/post-quantum primitives as mlDsa.ml_dsa65 and mlKem.ml_kem768, for callers who genuinely need the lower-level API. In August 2026 we found nine KXCO packages calling mlDsa.ml_dsa65.sign() / .verify() or mlKem.ml_kem768.encapsulate() / .decapsulate() directly instead of the wrapper's own mlDsa.sign() / mlDsa.verify() / mlKem.encapsulate() / mlKem.decapsulate(). Every call site produced correct signatures, it wasn't a break, but it pinned those call sites to one @noble/post-quantum generation and left them without FIPS 204/205 context-parameter support. This rule catches the pattern at lint time instead of by hand-grepping nine repos.
.keygen() is deliberately never flagged. Random (non-deterministic) key generation has no wrapper equivalent, the wrapper only offers deterministic keypairFromMaster(master, info), so calling the raw primitive's .keygen() directly is the correct, only option.
Install
npm i -D eslint-plugin-kxco-pqUse (flat config, ESLint 9+)
// eslint.config.js
import kxcoPq from 'eslint-plugin-kxco-pq'
export default [
...kxcoPq.configs.recommended,
]Or enable just the rule without the shared config:
import kxcoPq from 'eslint-plugin-kxco-pq'
export default [
{
plugins: { 'kxco-pq': kxcoPq },
rules: { 'kxco-pq/no-raw-primitive': 'error' },
},
]Rules
kxco-pq/no-raw-primitive
Flags:
mlDsa.ml_dsa65.sign(secretKey, message) // ❌
mlDsa.ml_dsa65.verify(publicKey, msg, sig) // ❌
mlKem.ml_kem768.encapsulate(publicKey) // ❌
mlKem.ml_kem768.decapsulate(ct, secretKey) // ❌Use instead:
mlDsa.sign(secretKey, message) // ✓
mlDsa.verify(publicKey, msg, sigHex) // ✓
mlKem.encapsulate(publicKey) // ✓
mlKem.decapsulate(ct, secretKey) // ✓Not flagged (no wrapper equivalent exists):
mlDsa.ml_dsa65.keygen()
mlKem.ml_kem768.keygen()The rule matches on the .ml_dsa65 / .ml_kem768 property name and the method being called, not on the base object's identifier name, so it catches the pattern regardless of what the wrapper import is locally named.
What this isn't
A general-purpose @noble/post-quantum linter, or a guarantee that a codebase is bug-free. It's a narrow, single-purpose guard against one specific, previously-real bug class. See SECURITY.md in kxco-post-quantum for the project's actual security posture and disclosure process.
License
Apache-2.0
