npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

kxco-post-quantum

v1.13.0

Published

NIST post-quantum cryptography for Node.js and browsers: ML-DSA, ML-KEM and SLH-DSA (FIPS 203, 204, 205). 1,793 NIST ACVP vectors passed, 0 failed. OpenSSL 3.5 native on Node 24+, the CNSA 2.0 parameter sets, reproducible builds with provenance.

Downloads

2,566

Readme

kxco-post-quantum

NIST post-quantum signatures and key exchange for Node.js and the browser, proven against NIST's own test vectors.

npm downloads NIST ACVP npm provenance OpenSSF Scorecard CI conformance license

  • All three NIST standards. ML-DSA-87 and ML-DSA-65 (FIPS 204), ML-KEM-1024 and ML-KEM-768 (FIPS 203) and SLH-DSA-SHA2-192s (FIPS 205). ML-DSA-87 and ML-KEM-1024 are the sets recommended for new keys, and ML-DSA-65 and ML-KEM-768 stay for keys that already exist.
  • The CNSA 2.0 parameter sets ship. ML-DSA-87 and ML-KEM-1024 at Category 5, with the same API as the Category 3 sets. KXCO does not claim CNSA 2.0 compliance.
  • 1,793 NIST ACVP vectors passed, 0 failed. The other 310 are pairings the library refuses as weaker than the parameter set. See CONFORMANCE.md.
  • Interoperable by test. 225 checks against liboqs, Bouncy Castle and the Python reference implementations, in both directions, 0 failed. See CONFORMANCE.md.
  • Native speed on Node 24. The maths runs in OpenSSL 3.5 on Node 24 and later, and in JavaScript on Node 22 and in browsers, with identical bytes on the wire.
  • Speaks the formats your stack already parses. Compact JWS and AKP JWK under the ML-DSA-87 and ML-DSA-65 algorithm names, and PKCS#8 seed-form keys.
  • A supply chain you can check. Reproducible builds verified in CI, with SLSA provenance and a CycloneDX SBOM on every release since 1.4.1. Apache-2.0, with no licence check and nothing that phones home.

The migration has dates.

  • NIST published FIPS 203, FIPS 204 and FIPS 205 in August 2024.
  • United States: Executive Order 14412, signed on 22 June 2026, moves federal high-value and high-impact systems to post-quantum key establishment by 31 December 2030 and to post-quantum signatures by 31 December 2031. OMB M-26-15 requires PQC-agile libraries for all new applications.
  • United Kingdom: the NCSC sets 2028, 2031 and 2035 as its migration milestones.

Each requirement has an export.

| The requirement | What answers it | |---|---| | Post-quantum key establishment by 31 Dec 2030, EO 14412 s.4(b)(ii) | mlKem1024.encapsulate and mlKem1024.decapsulate, ML-KEM-1024 (mlKem is the ML-KEM-768 set, for keys that already exist) | | Post-quantum signatures by 31 Dec 2031, EO 14412 s.4(b)(iii) | mlDsa87.sign and mlDsa87.verify, ML-DSA-87 | | "PQC-agile libraries for all new applications", OMB M-26-15 | mlKem1024 and mlDsa87 behind the same API as the Category 3 sets | | "API gateways and application workloads must be configured to issue and validate PQC-signed tokens", OMB M-26-15 | jws.signJws and jws.verifyJws: compact JWS under ML-DSA-87 or ML-DSA-65, algorithm pinned at the verifier | | "re-encrypting long-lived sensitive data using keys protected by PQC mechanisms", OMB M-26-15 | kxco-pq-vault | | Minimum elements for a cryptographic bill of materials, in CISA guidance due by 19 Mar 2027, EO 14412 s.5(d) | kxco-pq-scan --cbom: a CycloneDX 1.6 CBOM today, ready to check against those elements when CISA publishes them |

TLS has already moved, so this is the rest. A stock Node.js client on 22.23.3, 24.21.0 and 26.1.0 negotiates X25519MLKEM768, the hybrid of X25519 and ML-KEM-768, with no options set (measured 30 September 2026). What TLS never reaches is what your application signs, issues and stores, and that is this package. The walkthrough, with every block run against this package: The 2030 Post-Quantum Deadline in Code.

This is the primitive layer every other kxco-pq-* package builds on.

Conformance · Benchmarks · Migration · Threat model · Changelog · For institutions · kxco.ai


Install

npm install kxco-post-quantum

Requires Node.js 22.12+. CI tests every change on Node 22, 24 and 26. ESM-only.


Quick start

import { mlDsa87, mlKem1024, slhDsa, fingerprint, kidEquals } from 'kxco-post-quantum'

// ML-DSA-87: sign and verify
const { publicKey, secretKey } = mlDsa87.keypairFromMaster(masterSecret, 'signing-87-v1')
const sig = mlDsa87.sign(secretKey, 'hello')
const ok  = mlDsa87.verify(publicKey, 'hello', sig)  // true

// SLH-DSA-SHA2-192s: hash-based signatures (same API shape as mlDsa87)
const slh = slhDsa.keypairFromMaster(masterSecret, 'signing-slh-v1')
const slhSig = slhDsa.sign(slh.secretKey, 'hello')
const slhOk  = slhDsa.verify(slh.publicKey, 'hello', slhSig)  // true

// Key fingerprint
const kid = fingerprint(publicKey)  // e.g. '4a7c9e2f1b3d5680'
kidEquals(kid, kid)                 // true (constant-time)

// ML-KEM-1024: key encapsulation
const kemKeys = mlKem1024.keypairFromMaster(masterSecret, 'encryption-1024-v1')
const { ciphertext, sharedSecret } = mlKem1024.encapsulate(kemKeys.publicKey)
const recovered = mlKem1024.decapsulate(ciphertext, kemKeys.secretKey)
// sharedSecret and recovered are the same 32 bytes

masterSecret is a Node Buffer or typed array (Uint8Array) with at least 16 bytes of entropy (typically 32–64 bytes from an env var or KMS).

One label per key. The label is the domain separation: one master under one label yields the same seed bytes, whichever parameter set reads them. Give every parameter set and every purpose its own label, as above, and version it (-v1, -v2) so a rotation is a new label.

Category 5 parameter sets

mlDsa87 (ML-DSA-87) is the signature set for new keys. mlDsa (ML-DSA-65) has the same API, one security category lower, and stays for the keys that already exist, whose signatures keep verifying. mlKem1024 (ML-KEM-1024) has the same API as mlKem, one security category higher, and is the recommended set for new keys. mlKem (ML-KEM-768) keeps its name and meaning for the keys that already exist.

import { mlDsa87, mlKem1024 } from 'kxco-post-quantum'

const { publicKey, secretKey } = mlDsa87.keypairFromMaster(masterSecret, 'signing-87-v1')
const sig = mlDsa87.sign(secretKey, 'hello')      // 4627 bytes, 9254 hex chars
mlDsa87.verify(publicKey, 'hello', sig)           // true

| | Category 5 | Category 3 | |---|---|---| | Signatures | mlDsa87: pk 2592, sig 4627, for new keys | mlDsa: pk 1952, sig 3309, for existing keys | | Key encapsulation | mlKem1024: pk 1568, ct 1568 | mlKem: pk 1184, ct 1088 |

The two sets do not mix, deliberately. Default derivation info differs, so one master yields unrelated keys for each, and so does a distinct label of your own; a signature from one set does not verify under the other. Sizes are the migration cost, so check any fixed-width signature or key field before mixing sets in one system.

CNSA 2.0 names ML-DSA-87 and ML-KEM-1024, so moving a deployment to the CNSA 2.0 parameter sets is a change of import. KXCO does not claim CNSA 2.0 compliance. See CONFORMANCE.md.

Context strings (FIPS 204 / FIPS 205)

sign and verify take an optional context string, at most 255 bytes. A signature made under a context does not verify without it, or under a different one.

const sig = mlDsa87.sign(secretKey, 'hello', { context: 'kxco-nexus-v1' })

mlDsa87.verify(publicKey, 'hello', sig, { context: 'kxco-nexus-v1' })  // true
mlDsa87.verify(publicKey, 'hello', sig)                                // false
mlDsa87.verify(publicKey, 'hello', sig, { context: 'other-v1' })       // false

The parameter is optional and defaults to no context, so every existing call site is unaffected. An empty context is identical to omitting it. mlDsa and slhDsa take the same option.

Context separates at the signature level; keypairFromMaster(master, info) separates at the key level. They are complementary. Use a context when one key legitimately signs for several purposes and you need a signature from one purpose to be unusable in another. Use a distinct derived key when the purposes should not share a key at all.

Strings are encoded as UTF-8, so the 255-byte limit is bytes and not characters. Over-length or wrongly typed input throws (RangeError / TypeError) rather than returning false, because that is a caller bug and not a bad signature:

mlDsa87.sign(secretKey, 'hello', 'kxco-nexus-v1')  // throws TypeError
                                                   // (needs { context: ... })

That last case is worth guarding: without the throw it would silently sign with no context and produce a valid-looking signature carrying none of the intended separation.


For institutions

The cryptography is free under Apache-2.0, works offline and needs nothing from KXCO, now or in ten years. What KXCO sells is the part that has to be operated: an answer about the present.

| Service | What you get | |---|---| | Hosted key registry | Whether a key is active, revoked or rotated, answered at verification time | | Meta-transaction relay | KXCO validates your signed intent, pays the gas and submits it, so you never hold a token or run a node | | On-chain anchoring | A timestamp on Armature L1 that the chain itself has verified | | Live revocation | anchored+live verification, which confirms the signing key is still trusted now | | Support and SLA | Availability commitments, an escalation path and a named contact |

Priced in USD, per seat, per year. No tokens, no nodes and no wallets. The line between free and paid is set out in LICENCE-PRODUCT.md.

Talk to us: [email protected] · kxco.ai


API

mlDsa87: ML-DSA-87 signatures (NIST FIPS 204)

The signature set for new keys. Security Category 5.

| Export | Signature | Description | |---|---|---| | keypairFromMaster | (master, info?) → { publicKey, secretKey, seed } | Deterministic keypair via HKDF-SHA-512. info defaults to 'ml-dsa-87-v1'. seed is the 32 bytes the pair was expanded from. See seed. | | sign | (secretKey, message) → string | Signs a message. Returns a hex-encoded signature (9254 chars). | | verify | (publicKey, message, signature) → boolean | Verifies a signature given as hex or as raw bytes (Uint8Array or Buffer). Returns false on any failure. | | ml_dsa87 | raw primitive | The underlying @noble/post-quantum primitive, re-exported. |

publicKey is 2592 bytes. secretKey is 4896 bytes. message accepts Buffer, Uint8Array, or string.

mlDsa: ML-DSA-65 signatures (NIST FIPS 204)

The ML-DSA-65 namespace, kept for keys that already exist. Same API as mlDsa87, at Security Category 3. Its name and exports are unchanged, and every signature it has made keeps verifying.

| Export | Signature | Description | |---|---|---| | keypairFromMaster | (master, info?) → { publicKey, secretKey, seed } | Deterministic keypair via HKDF-SHA-512. info defaults to 'ml-dsa-65-v1'. seed is the 32 bytes the pair was expanded from. See seed. | | sign | (secretKey, message) → string | Signs a message. Returns a hex-encoded signature (6618 chars). | | verify | (publicKey, message, signature) → boolean | Verifies a signature given as hex or as raw bytes (Uint8Array or Buffer). Returns false on any failure. | | ml_dsa65 | raw primitive | The underlying @noble/post-quantum primitive, re-exported. |

publicKey is 1952 bytes. secretKey is 4032 bytes. message accepts Buffer, Uint8Array, or string.

slhDsa: SLH-DSA-SHA2-192s signatures (NIST FIPS 205)

Hash-based, stateless signatures. Security Category 3 (matching ML-DSA-65), with security resting only on the SHA-2 hash function: no lattice or number-theoretic assumptions. Use it as a conservative hedge alongside mlDsa87. Signatures are 16,224 bytes against 4,627 for ML-DSA-87 and 3,309 for ML-DSA-65, per FIPS 205 and FIPS 204, so ML-DSA stays the choice for high-volume signing.

| Export | Signature | Description | |---|---|---| | keypairFromMaster | (master, info?) → { publicKey, secretKey } | Deterministic keypair via HKDF-SHA-512. info defaults to 'slh-dsa-sha2-192s-v1'. | | sign | (secretKey, message) → string | Signs a message. Returns a hex-encoded signature (32448 chars). | | verify | (publicKey, message, signature) → boolean | Verifies a signature given as hex or as raw bytes (Uint8Array or Buffer). Returns false on any failure. | | slh_dsa_sha2_192s | raw primitive | The underlying @noble/post-quantum primitive, re-exported. |

publicKey is 48 bytes. secretKey is 96 bytes. message accepts Buffer, Uint8Array, or string.

mlKem: ML-KEM-768 key encapsulation (NIST FIPS 203)

| Export | Signature | Description | |---|---|---| | keypairFromMaster | (master, info?) → { publicKey, secretKey, seed } | Deterministic keypair via HKDF-SHA-512. info defaults to 'ml-kem-768-v1'. seed is the 64 bytes the pair was expanded from. | | encapsulate | (publicKey) → { ciphertext, sharedSecret } | Generates a shared secret and ciphertext to send to the key holder. | | decapsulate | (ciphertext, secretKey) → Buffer | Recovers the shared secret from a ciphertext. Returns 32 bytes. | | ml_kem768 | raw primitive | The underlying @noble/post-quantum primitive, re-exported. |

publicKey is 1184 bytes. ciphertext is 1088 bytes. sharedSecret is 32 bytes.

fingerprint(publicKey) → string

First 16 hex characters of SHA-256 of the public key. Stable for the lifetime of the key. Accepts raw bytes or a hex string.

kidEquals(a, b) → boolean

Constant-time comparison of two kid strings. Use this when comparing user-supplied input, in place of ===.

deriveSeed(master, info, length) → Buffer

HKDF-SHA-512 derivation. master must be at least 16 bytes. info is a required domain-separation string. Returns length bytes.

seed: seed-form keys (RFC 9964, LAMPS)

FIPS 203 and 204 expand a keypair from a short seed. The expanded private key this package returns is derived from that seed and does not contain it, so a seed cannot be recovered from an expanded key. keypairFromMaster therefore returns the seed it derived alongside the pair. That is additive, so callers that destructure { publicKey, secretKey } are unaffected.

Seed form is 32 bytes for ML-DSA and 64 for ML-KEM. It fits in a KMS secret, an HSM object or an env var, and it is the only private form an RFC 9964 AKP JWK accepts.

| Export | Signature | Description | |---|---|---| | SEED_ALGORITHMS | string[] | ML-DSA-65, ML-DSA-87, ML-KEM-768, ML-KEM-1024. SLH-DSA has no seed form. | | seedFromMaster | (alg, master, info?) → bytes | Same HKDF-SHA-512 derivation keypairFromMaster uses, so it reproduces keys already in production. | | keypairFromSeed | (alg, seed) → { publicKey, secretKey, seed } | Expands a seed. Byte-identical to what OpenSSL 3.5 derives from the same seed. | | exportJwk | (alg, { publicKey, seed? }, opts?) → jwk | RFC 9964 AKP JWK. priv carries the seed. | | importJwk | (jwk) → { alg, publicKey, secretKey?, seed? } | Rejects a JWK whose seed and pub disagree. | | exportSeedPkcs8 | (alg, seed) → bytes | PKCS#8 in LAMPS seed form, the [0] IMPLICIT CHOICE. | | importSeedPkcs8 | (der) → { alg, seed } | Refuses an expanded-form key rather than truncating it into a seed. |

The encodings are not read off the specification. test/seed.test.js asserts that the DER this package writes is byte-identical to what this machine's OpenSSL writes, for every parameter set, and skips with a reason where there is no native backend to compare against.

import { mlDsa87, seed } from 'kxco-post-quantum'

const key = mlDsa87.keypairFromMaster(process.env.KXCO_MASTER_KEY)
const jwk = seed.exportJwk('ML-DSA-87', key, { kid: fingerprint(key.publicKey) })
// { kty: 'AKP', alg: 'ML-DSA-87', pub: '...', priv: '<32-byte seed>', kid: '...' }

jws: compact JWS with the RFC 9964 algorithm names

Format only. A token signed here verifies in any process holding the public key, offline, with no configuration and no licence. RFC 9964 registered ML-DSA-87 and ML-DSA-65 as JWS algorithms so a post-quantum signature can travel the path an institution's gateway, IdP and partner verifier already parse.

| Export | Signature | Description | |---|---|---| | signJws | (payload, secretKey, opts?) → string | Compact JWS. Objects are JSON-serialised. Without opts.alg the key decides: an ML-DSA-65 secret key signs ML-DSA-65, and every other key ML-DSA-87, the default. | | verifyJws | (token, publicKey, opts?) → { valid, ... } | Fails closed. { alg } and { kid } pin what the header may declare. | | decodeJwsHeader | (token) → object \| null | Unauthenticated read, for choosing which key to fetch. |

The algorithm is resolved from an allowlist inside the module, never from the token, so a token cannot name its own verification routine. crit and b64 headers are refused rather than ignored, and the public key's length must match the algorithm the header declares.

The JWS algorithms are ML-DSA-87 and ML-DSA-65, the parameter sets sized for a request path.

backend() and isNative(alg)

Reports which implementation is doing the maths in this process: openssl with its version and parameter sets, or javascript with the reason the native backend is unavailable. For evidence bundles and support tickets. It reports, and the operator selects, as the next section shows.

webhook: hybrid HMAC + ML-DSA delivery signing

Low-level helpers for the KXCO hybrid webhook pattern: envelope, hmacHex, verifyHmac, pqSign, verifyPq, signDelivery, verifyDelivery. HMAC-SHA-256 gives symmetric verification with no library dependency; ML-DSA adds non-repudiation over the same ${timestamp}.${body} envelope. The full identity/credential surface lives in kxco-pq-sdk.

The key decides the PQ header form. An ML-DSA-87 key signs ml-dsa-87=<hex>, and an ML-DSA-65 key signs ml-dsa-65=<hex> over the same envelope. verifyPq and verifyDelivery accept only the form that matches the public key they are given: a header whose prefix names the other set fails, and an ML-DSA-87 key takes no bare-hex form.


Requiring the native backend

This package picks its implementation at import time: OpenSSL 3.5 where the runtime provides the FIPS 203/204/205 primitives, the JavaScript implementation otherwise. Both produce identical wire bytes, so falling back is the right default and nothing about a signature changes.

For a deployment under a control that says cryptography must execute inside a validated module, make the native backend a requirement:

import { requireNativeBackend } from 'kxco-post-quantum'

requireNativeBackend(['ML-DSA-87', 'ML-KEM-768'])

It throws ERR_KXCO_PQ_BACKEND if the JavaScript backend is live, or if the OpenSSL present cannot provide a parameter set you named. The error carries actual, missing and available so a failure says which, rather than only that.

Operators can enforce it without touching application code, which matters because the team under the control is usually not the team calling the library:

KXCO_PQ_REQUIRE_NATIVE=1

Set that and a process which has landed on the JavaScript backend fails at import, before its first signature rather than after.

It asserts that OpenSSL is doing the maths and removes the silent fallback. Pair it with the validated OpenSSL build your control names, and the control is enforced at import.

Pinning the implementation your certificate names

requireNativeBackend() only ever asserts OpenSSL. Both implementations are being taken to algorithm validation, so a deployment under a control that names a certificate has to be able to pin whichever one its certificate covers, and for some that is the JavaScript implementation.

KXCO_PQ_BACKEND=javascript    never use OpenSSL, even where it is present
KXCO_PQ_BACKEND=openssl       prefer OpenSSL, which is the default anyway
import { requireBackend } from 'kxco-post-quantum'

requireBackend('javascript')                            // the JS certificate
requireBackend('openssl', ['ML-DSA-87', 'ML-KEM-768'])  // the native one

A value that is neither throws at import, so a misspelled pin is caught before the first signature.

The environment selects, the function asserts, and they are deliberately kept apart: requireBackend('javascript') fails on an unpinned OpenSSL process rather than switching it. Application code cannot quietly change which implementation your evidence is about.

Nothing here changes what a signature looks like. The two produce identical wire bytes, which the interoperability matrix proves for every parameter set in both directions, and a signature made under the pin verifies on the other backend. What changes is which one computed it, and backend() always reports that truthfully, including when the answer is the result of a pin:

backend()
// { kind: 'javascript', library: '@noble/post-quantum', pinned: 'javascript',
//   reason: 'KXCO_PQ_BACKEND=javascript pins this process to the JavaScript backend' }

That reason keeps pinned and unavailable apart, so an evidence bundle records exactly which one applied.

kxco-post-quantum/fips: a self-contained module

A separate entry point for deployments that want the cryptography inside one fixed, self-testing module. It is designed against the FIPS 140-3 Level 1 software requirements. It is not a validated module: no laboratory has tested it and NIST has issued no certificate for it.

  • Fixed boundary. src/fips/ carries its own copy of the noble code and loads nothing from the rest of the package.
  • Approved random bit generator. HMAC_DRBG with SHA-512, seeded from /dev/urandom, supplies every random value.
  • Integrity test first. Every file is checked against a manifest signed with the ML-DSA-87 release key before any service loads.
  • Self-tests. The known answers come from NIST's own ACVP vectors, and a pairwise test runs on every generated key pair.
  • Error state. Any failure leaves every service refused until the process restarts.
import * as fips from 'kxco-post-quantum/fips'

if (fips.status().state !== 'operational') throw new Error(fips.status().reason)

const { publicKey, secretKey, seed } = fips.generateKeypair('ML-DSA-87')   // store `seed`; it is the key
const sig = fips.sign('ML-DSA-87', secretKey, 'message', { context: 'my-app' })
fips.verify('ML-DSA-87', publicKey, 'message', sig, { context: 'my-app' })  // true
fips.zeroize(secretKey, seed)

It offers ML-KEM-768 and ML-KEM-1024, and ML-DSA-65 and ML-DSA-87. It runs on Node.js, needs /dev/urandom, and has been tested on Linux. SECURITY-POLICY.md is the draft Security Policy, in NIST SP 800-140B structure.

The KXCO post-quantum family

This is the primitive layer: keys, signatures, encapsulation and fingerprints. Install it directly when you need ML-DSA or ML-KEM on their own, or pick the package that matches the job.

| You need to | Install | |---|---| | Put the whole stack in one install | kxco-pq | | Use ML-DSA, ML-KEM and SLH-DSA directly | kxco-post-quantum | | Keep signing keys on the HSM you already run | kxco-pq-hsm | | Sign a document or record anyone can verify offline | kxco-pq-attest | | Keep a tamper-evident audit trail | kxco-pq-audit | | Verify a signature in a browser, with no server | kxco-verify | | Issue institution identity credentials | kxco-pq-sdk | | Encrypt files and payloads to one or many recipients | kxco-pq-vault | | Encrypt Node streams and WebSockets | kxco-pq-tls | | Sign and verify webhooks | kxco-post-quantum-webhook | | Give an AI agent an identity a verified institution sponsors | kxco-pq-agent | | Have Armature L1 verify a signature in consensus | kxco-pq-chain | | Prove an envelope at three levels, offline to on-chain | kxco-pq-network | | Generate and rotate keys from a terminal | kxco-pq-cli | | Find quantum-vulnerable cryptography in a dependency tree | kxco-pq-scan | | Fail the build when code reaches past the wrapper | eslint-plugin-kxco-pq |


Security

The maths runs in OpenSSL 3.5 on Node 24 and later, and in @noble/post-quantum and @noble/hashes elsewhere. This package reimplements no NIST primitive. Every parameter set is held to NIST's own ACVP vectors and cross-checked against liboqs, Bouncy Castle and the Python reference implementations on both backends, per CONFORMANCE.md. The audit history of every upstream library is recorded in AUDIT.md.

To report a vulnerability: open a private security advisory or email [email protected]. Acknowledgement within 2 business days, triage decision within 5. Full policy, including safe harbour for good-faith research: https://kxco.ai/security.

License

Apache-2.0 © 2026 Knightsbridge Financial Ltd, trading as KXCO. See LICENSE and NOTICE.

Maintainers

Shayne Heffernan and John Heffernan, KXCO by Knightsbridge

Verifying a release

Every claim on this page is checkable without asking us. The evidence bundle for the current release sits at a permanent, unauthenticated URL:

# the full evidence bundle for the current release
curl -sLO https://github.com/KnightsbridgeAIQ/kxco-post-quantum/releases/latest/download/evidence-node24.x.zip

# or just the manifest: every file digest, and which backend produced the results
curl -sL  https://github.com/KnightsbridgeAIQ/kxco-post-quantum/releases/latest/download/manifest-node24.x.json

# licence and provenance, straight from the registry
npm view kxco-post-quantum license          # Apache-2.0
npm audit signatures --json                 # assert invalid:0 and missing:0

Every release asset is signed with ML-DSA-87 by this package's own signing path, and carries a SLSA provenance file recording the workflow that built it.

manifest-node24.x.json          the bundle's manifest
evidence-node24.x.zip           the bundle
evidence-node24.x.zip.sig       ML-DSA-87 signature over the zip, hex
evidence.intoto.jsonl           SLSA provenance
release-signing-key-87.pub.hex  the ML-DSA-87 public key, also committed to this repository
release-signing-key.pub.hex     the ML-DSA-65 key that signed releases before 1.9.0
import { readFileSync } from 'node:fs'
import { mlDsa87 } from 'kxco-post-quantum'

const pub = Buffer.from(readFileSync('release-signing-key-87.pub.hex', 'utf8').trim(), 'hex')
const sig = readFileSync('evidence-node24.x.zip.sig', 'utf8').trim()
mlDsa87.verify(pub, readFileSync('evidence-node24.x.zip'), sig)   // true

Compare the public key with the copy committed to this repository, so the key and the artefact are checked against two independent sources.