kxco-post-quantum 1.7.2 → 1.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,50 +1,33 @@
1
1
  # kxco-post-quantum
2
2
 
3
- Post-quantum cryptography primitives for the KXCO stack.
3
+ **NIST post-quantum signatures and key exchange for Node.js and the browser, proven against NIST's own test vectors.**
4
4
 
5
- [![npm](https://img.shields.io/npm/v/kxco-post-quantum)](https://www.npmjs.com/package/kxco-post-quantum)
5
+ [![npm](https://img.shields.io/npm/v/kxco-post-quantum?label=npm&color=b0964f)](https://www.npmjs.com/package/kxco-post-quantum)
6
+ [![downloads](https://img.shields.io/npm/dm/kxco-post-quantum?label=downloads&color=b0964f)](https://www.npmjs.com/package/kxco-post-quantum)
7
+ [![NIST ACVP](https://img.shields.io/badge/NIST_ACVP-1,793_passed,_0_failed-2ea44f)](./CONFORMANCE.md)
8
+ [![npm provenance](https://img.shields.io/badge/npm-provenance-2ea44f)](https://www.npmjs.com/package/kxco-post-quantum)
9
+ [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/KnightsbridgeAIQ/kxco-post-quantum/badge)](https://securityscorecards.dev/viewer/?uri=github.com/KnightsbridgeAIQ/kxco-post-quantum)
6
10
  [![CI](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/actions/workflows/ci.yml/badge.svg)](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/actions/workflows/ci.yml)
7
11
  [![conformance](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/actions/workflows/conformance.yml/badge.svg)](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/actions/workflows/conformance.yml)
8
12
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](./LICENSE)
9
13
 
10
- ML-DSA-65 (FIPS 204) and SLH-DSA-SHA2-192s (FIPS 205) signatures, ML-KEM-768 (FIPS 203) key encapsulation, and key fingerprinting utilities. Category 5 sets ML-DSA-87 and ML-KEM-1024 are also available. All other `kxco-pq-*` packages depend on this one.
14
+ - **All three NIST standards.** ML-KEM-768 (FIPS 203), ML-DSA-65 (FIPS 204) and SLH-DSA-SHA2-192s (FIPS 205), all at NIST Category 3.
15
+ - **The CNSA 2.0 parameter sets ship.** ML-DSA-87 and ML-KEM-1024 at Category 5, with the same API as the defaults.
16
+ - **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](./CONFORMANCE.md).
17
+ - **Interoperable by test.** 225 checks against liboqs, Bouncy Castle and the Python reference implementations, in both directions, 0 failed. See [CONFORMANCE.md](./CONFORMANCE.md).
18
+ - **Native speed on Node 24.** The maths runs in OpenSSL 3.5 on Node 24 and later, and in JavaScript on Node 20, Node 22 and in browsers, with identical bytes on the wire.
19
+ - **Speaks the formats your stack already parses.** Compact JWS and AKP JWK under the `ML-DSA-65` and `ML-DSA-87` algorithm names, and PKCS#8 seed-form keys.
20
+ - **A supply chain you can check.** Reproducible builds, SLSA provenance and a CycloneDX SBOM on every release. Apache-2.0, with no licence check and nothing that phones home.
11
21
 
12
- **On Node 24 and later the primitives run in OpenSSL 3.5**, not in JavaScript. Older Node and browsers use [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum). The two are interchangeable on the wire, which is checked rather than assumed: the interoperability matrix runs in full against both, and every report records which one produced it.
22
+ **The migration has dates.**
13
23
 
14
- **For an independent assessor.** Every claim below is checkable without asking
15
- us, and the machine-readable bundle behind them is a permanent unauthenticated
16
- URL, not an expiring CI artifact:
24
+ - **NIST** published [FIPS 203](https://csrc.nist.gov/pubs/fips/203/final), [FIPS 204](https://csrc.nist.gov/pubs/fips/204/final) and [FIPS 205](https://csrc.nist.gov/pubs/fips/205/final) in August 2024.
25
+ - **United States:** [Executive Order 14412](https://www.federalregister.gov/documents/2026/06/25/2026-12909/securing-the-nation-against-advanced-cryptographic-attacks), 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](https://www.whitehouse.gov/wp-content/uploads/2026/06/M-26-15-Execution-of-the-Migration-to-Post-Quantum-Cryptography.pdf) requires PQC-agile libraries for all new applications.
26
+ - **United Kingdom:** the [NCSC](https://www.ncsc.gov.uk/guidance/pqc-migration-timelines) sets 2028, 2031 and 2035 as its migration milestones.
17
27
 
18
- ```bash
19
- # the full evidence bundle for the current release
20
- curl -sLO https://github.com/KnightsbridgeAIQ/kxco-post-quantum/releases/latest/download/evidence-node24.x.zip
21
-
22
- # or just the manifest: every file digest, and which backend produced the results
23
- curl -sL https://github.com/KnightsbridgeAIQ/kxco-post-quantum/releases/latest/download/manifest-node24.x.json
24
-
25
- # licence and provenance, straight from the registry
26
- npm view kxco-post-quantum license # Apache-2.0
27
- npm audit signatures --json # assert invalid:0 and missing:0
28
- ```
28
+ This is the primitive layer every other `kxco-pq-*` package builds on.
29
29
 
30
- Facts that are commonly recorded wrong for this package, with the one-line
31
- check for each: the licence is **Apache-2.0**, not commercial; the SLH-DSA
32
- parameter set is **SLH-DSA-SHA2-192s**, a real FIPS 205 name, not
33
- `SLH-DSA-128s`; ML-DSA-65 and ML-KEM-768 are **NIST Category 3**, and
34
- ML-DSA-87 and ML-KEM-1024, also shipped, are Category 5; the implementation
35
- languages are **JavaScript and C** (OpenSSL 3.5 on Node 24+).
36
-
37
- **Evidence, not adjectives:**
38
-
39
- - [CONFORMANCE.md](./CONFORMANCE.md): NIST ACVP vectors for FIPS 203/204/205: **2,103 vectors, 1,793 passed, 0 failed, 310 skipped**, where every skip is this library refusing a pre-hash weaker than the parameter set and is listed individually with its reason. CONFORMANCE.md says a skip is not a pass, so the headline says so too. Of those, **1,551 are in the downloadable evidence bundle**, measured from the published v1.6.3 assets: 855 in `02-conformance-acvp.json`, 624 in `02b-conformance-acvp-fips205.json` and a 72-vector sample of SLH-DSA signature generation in `02d-conformance-acvp-fips205-siggen.json`. Signature generation is sampled rather than shipped whole because it signs in seconds per operation; the full set is reproduced on demand with `node conformance/run-acvp.mjs --set SLH-DSA-sigGen-FIPS205`. The bundle names that gap rather than leaving it to be noticed. Plus a cross-implementation interop matrix against liboqs, Bouncy Castle and two pure-Python implementations (225 checks, 0 failed, both directions, with negative controls), run against both backends. Reproducible: `npm run conformance:acvp`, `npm run conformance:interop`.
40
- - [BENCHMARKS.md](./BENCHMARKS.md): per-algorithm latency at p95/p99 on both backends and on x86-64 and arm64, plus memory. Two figures worth designing around: ML-DSA signing keeps a rejection-sampling tail on either backend (5.1x median-to-p99 in JavaScript, 3.5x on OpenSSL), and SLH-DSA-SHA2-192s signs in seconds rather than milliseconds (4.3 s and 1.7 s).
41
- - [THREAT-MODEL.md](./THREAT-MODEL.md): what this defends against and what it does not. Read the side-channel section before deciding where a signing key lives.
42
- - [MIGRATION.md](./MIGRATION.md): moving an RSA or ECDSA system across, and moving between versions of this package.
43
- - [SECURITY.md](./SECURITY.md): reporting, release integrity, and the dependency policy.
44
- - [AGILITY.md](./AGILITY.md): what has to change when the algorithm changes. The replacement plan, the mechanisms that exist today, the transition peers can follow, and the four kinds of agility this package does not give you.
45
- - [BOUNDARY.md](./BOUNDARY.md): which cryptography this package performs, which it depends on, and which it merely offers to a caller. Release signing is ML-DSA-65; the transport that delivers the release is classical TLS, and that is stated rather than folded into the claim.
46
- - [LIFECYCLE.md](./LIFECYCLE.md): supported versions, the runtime ceiling, and the one blocking supplier dependency with its mitigations. Read the roadmap beside a maturity claim, not after it.
47
- - **Every release is reproducible and attested.** The published tarball rebuilds bit-for-bit from its own tag, verified in CI on every run, and each release carries a SLSA provenance attestation plus a CycloneDX SBOM at a permanent unauthenticated URL. A provenance attestation says a build happened in CI; the reproducible build says the artefact is the source. They are different claims and both are checkable without asking us for anything.
30
+ [Conformance](./CONFORMANCE.md) · [Benchmarks](./BENCHMARKS.md) · [Migration](./MIGRATION.md) · [Threat model](./THREAT-MODEL.md) · [Changelog](./CHANGELOG.md) · [For institutions](#for-institutions) · [kxco.ai](https://kxco.ai)
48
31
 
49
32
  ---
50
33
 
@@ -63,12 +46,12 @@ Requires Node.js 20.19+. ESM-only.
63
46
  ```js
64
47
  import { mlDsa, mlKem, slhDsa, fingerprint, kidEquals } from 'kxco-post-quantum'
65
48
 
66
- // ML-DSA-65 — sign and verify
49
+ // ML-DSA-65: sign and verify
67
50
  const { publicKey, secretKey } = mlDsa.keypairFromMaster(masterSecret, 'signing-v1')
68
51
  const sig = mlDsa.sign(secretKey, 'hello')
69
52
  const ok = mlDsa.verify(publicKey, 'hello', sig) // true
70
53
 
71
- // SLH-DSA-SHA2-192s — hash-based signatures (same API shape as mlDsa)
54
+ // SLH-DSA-SHA2-192s: hash-based signatures (same API shape as mlDsa)
72
55
  const slh = slhDsa.keypairFromMaster(masterSecret, 'signing-v1')
73
56
  const slhSig = slhDsa.sign(slh.secretKey, 'hello')
74
57
  const slhOk = slhDsa.verify(slh.publicKey, 'hello', slhSig) // true
@@ -77,14 +60,14 @@ const slhOk = slhDsa.verify(slh.publicKey, 'hello', slhSig) // true
77
60
  const kid = fingerprint(publicKey) // e.g. '4a7c9e2f1b3d5680'
78
61
  kidEquals(kid, kid) // true (constant-time)
79
62
 
80
- // ML-KEM-768 — key encapsulation
63
+ // ML-KEM-768: key encapsulation
81
64
  const kemKeys = mlKem.keypairFromMaster(masterSecret, 'encryption-v1')
82
65
  const { ciphertext, sharedSecret } = mlKem.encapsulate(kemKeys.publicKey)
83
66
  const recovered = mlKem.decapsulate(ciphertext, kemKeys.secretKey)
84
67
  // sharedSecret and recovered are the same 32 bytes
85
68
  ```
86
69
 
87
- `masterSecret` is a `Buffer` or `Uint8Array` with at least 16 bytes of entropy (typically 32–64 bytes from an env var or KMS).
70
+ `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).
88
71
 
89
72
  ### Category 5 parameter sets
90
73
 
@@ -103,17 +86,17 @@ mlDsa87.verify(publicKey, 'hello', sig) // true
103
86
 
104
87
  | | Category 3 (default) | Category 5 |
105
88
  |---|---|---|
106
- | Signatures | `mlDsa` — pk 1952, sig 3309 | `mlDsa87` — pk 2592, sig 4627 |
107
- | Key encapsulation | `mlKem` — pk 1184, ct 1088 | `mlKem1024` — pk 1568, ct 1568 |
89
+ | Signatures | `mlDsa`: pk 1952, sig 3309 | `mlDsa87`: pk 2592, sig 4627 |
90
+ | Key encapsulation | `mlKem`: pk 1184, ct 1088 | `mlKem1024`: pk 1568, ct 1568 |
108
91
 
109
92
  The two sets do not mix, deliberately. Default derivation info differs, so one
110
93
  master yields unrelated keys for each; and a signature from one set does not
111
94
  verify under the other. Sizes are the migration cost, so check any fixed-width
112
95
  signature or key field before mixing sets in one system.
113
96
 
114
- **CNSA 2.0 names ML-DSA-87 and ML-KEM-1024, and supporting them is not a CNSA
115
- 2.0 compliance claim.** Compliance is a property of a deployment, not of an
116
- available function. See [CONFORMANCE.md](./CONFORMANCE.md).
97
+ **CNSA 2.0 names ML-DSA-87 and ML-KEM-1024**, so moving a deployment to the
98
+ CNSA 2.0 parameter sets is a change of import. See
99
+ [CONFORMANCE.md](./CONFORMANCE.md).
117
100
 
118
101
  ### Context strings (FIPS 204 / FIPS 205)
119
102
 
@@ -142,7 +125,7 @@ should not share a key at all.
142
125
  Strings are encoded as UTF-8, so the 255-byte limit is bytes and not
143
126
  characters. Over-length or wrongly typed input throws (`RangeError` /
144
127
  `TypeError`) rather than returning `false`, because that is a caller bug and not
145
- a failed verification:
128
+ a bad signature:
146
129
 
147
130
  ```js
148
131
  mlDsa.sign(secretKey, 'hello', 'kxco-nexus-v1') // throws TypeError
@@ -155,22 +138,43 @@ separation.
155
138
 
156
139
  ---
157
140
 
141
+ ## For institutions
142
+
143
+ The cryptography is free under Apache-2.0, works offline and needs nothing from
144
+ KXCO, now or in ten years. What KXCO sells is the part that has to be operated:
145
+ an answer about the present.
146
+
147
+ | Service | What you get |
148
+ |---|---|
149
+ | Hosted key registry | Whether a key is active, revoked or rotated, answered at verification time |
150
+ | Meta-transaction relay | KXCO validates your signed intent, pays the gas and submits it, so you never hold a token or run a node |
151
+ | On-chain anchoring | A timestamp on Armature L1 that the chain itself has verified |
152
+ | Live revocation | `anchored+live` verification, which confirms the signing key is still trusted now |
153
+ | Support and SLA | Availability commitments, an escalation path and a named contact |
154
+
155
+ Priced in USD, per seat, per year. No tokens, no nodes and no wallets. The line
156
+ between free and paid is set out in [LICENCE-PRODUCT.md](./LICENCE-PRODUCT.md).
157
+
158
+ **Talk to us: [admin@kxco.ai](mailto:admin@kxco.ai)** · [kxco.ai](https://kxco.ai)
159
+
160
+ ---
161
+
158
162
  ## API
159
163
 
160
- ### `mlDsa` — ML-DSA-65 (NIST FIPS 204)
164
+ ### `mlDsa`: ML-DSA-65 signatures (NIST FIPS 204)
161
165
 
162
166
  | Export | Signature | Description |
163
167
  |---|---|---|
164
- | `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`](#seed--seed-form-keys-rfc-9964-lamps). |
168
+ | `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`](#seed-seed-form-keys-rfc-9964-lamps). |
165
169
  | `sign` | `(secretKey, message) → string` | Signs a message. Returns a hex-encoded signature (6618 chars). |
166
170
  | `verify` | `(publicKey, message, sigHex) → boolean` | Verifies a hex-encoded signature. Returns `false` on any failure. |
167
171
  | `ml_dsa65` | raw primitive | The underlying `@noble/post-quantum` primitive, re-exported. |
168
172
 
169
173
  `publicKey` is 1952 bytes. `secretKey` is 4032 bytes. `message` accepts `Buffer`, `Uint8Array`, or `string`.
170
174
 
171
- ### `slhDsa` — SLH-DSA-SHA2-192s (NIST FIPS 205)
175
+ ### `slhDsa`: SLH-DSA-SHA2-192s signatures (NIST FIPS 205)
172
176
 
173
- Hash-based, stateless signatures. Security Category 3 (matching ML-DSA-65), but security rests only on the SHA-2 hash function — no lattice or number-theoretic assumptions. Use this as a conservative hedge alongside `mlDsa`. Tradeoff: signatures are ~5× larger (16224 vs 3309 bytes) and signing is slower.
177
+ 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 `mlDsa`. Signatures are 16,224 bytes against 3,309 for ML-DSA-65, per [FIPS 205](https://csrc.nist.gov/pubs/fips/205/final) and [FIPS 204](https://csrc.nist.gov/pubs/fips/204/final), so `mlDsa` stays the default for high-volume signing.
174
178
 
175
179
  | Export | Signature | Description |
176
180
  |---|---|---|
@@ -181,7 +185,7 @@ Hash-based, stateless signatures. Security Category 3 (matching ML-DSA-65), but
181
185
 
182
186
  `publicKey` is 48 bytes. `secretKey` is 96 bytes. `message` accepts `Buffer`, `Uint8Array`, or `string`.
183
187
 
184
- ### `mlKem` — ML-KEM-768 (NIST FIPS 203)
188
+ ### `mlKem`: ML-KEM-768 key encapsulation (NIST FIPS 203)
185
189
 
186
190
  | Export | Signature | Description |
187
191
  |---|---|---|
@@ -198,18 +202,18 @@ First 16 hex characters of SHA-256 of the public key. Stable for the lifetime of
198
202
 
199
203
  ### `kidEquals(a, b)` → `boolean`
200
204
 
201
- Constant-time comparison of two kid strings. Use this when comparing user-supplied input — not `===`.
205
+ Constant-time comparison of two kid strings. Use this when comparing user-supplied input, in place of `===`.
202
206
 
203
207
  ### `deriveSeed(master, info, length)` → `Buffer`
204
208
 
205
209
  HKDF-SHA-512 derivation. `master` must be at least 16 bytes. `info` is a required domain-separation string. Returns `length` bytes.
206
210
 
207
- ### `seed` — seed-form keys (RFC 9964, LAMPS)
211
+ ### `seed`: seed-form keys (RFC 9964, LAMPS)
208
212
 
209
213
  FIPS 203 and 204 expand a keypair from a short seed. The expanded private key
210
214
  this package returns is derived from that seed and does not contain it, so a
211
215
  seed cannot be recovered from an expanded key. `keypairFromMaster` therefore
212
- returns the seed it derived alongside the pair — additive, so callers that
216
+ returns the seed it derived alongside the pair. That is additive, so callers that
213
217
  destructure `{ publicKey, secretKey }` are unaffected.
214
218
 
215
219
  Seed form is 32 bytes for ML-DSA and 64 for ML-KEM. It fits in a KMS secret, an
@@ -239,7 +243,7 @@ const jwk = seed.exportJwk('ML-DSA-65', key, { kid: fingerprint(key.publicKey) }
239
243
  // { kty: 'AKP', alg: 'ML-DSA-65', pub: '...', priv: '<32-byte seed>', kid: '...' }
240
244
  ```
241
245
 
242
- ### `jws` — compact JWS with the RFC 9964 algorithm names
246
+ ### `jws`: compact JWS with the RFC 9964 algorithm names
243
247
 
244
248
  Format only. A token signed here verifies in any process holding the public
245
249
  key, offline, with no configuration and no licence. RFC 9964 registered
@@ -258,17 +262,17 @@ token, so a token cannot name its own verification routine. `crit` and `b64`
258
262
  headers are refused rather than ignored, and the public key's length must match
259
263
  the algorithm the header declares.
260
264
 
261
- There is no SLH-DSA option here: FIPS 205 signing takes on the order of a
262
- second and a half, which does not belong on a request path.
265
+ The JWS algorithms are ML-DSA-65 and ML-DSA-87, the parameter sets sized for a
266
+ request path.
263
267
 
264
268
  ### `backend()` and `isNative(alg)`
265
269
 
266
- Reports which implementation is doing the maths in this process — `openssl`
270
+ Reports which implementation is doing the maths in this process: `openssl`
267
271
  with its version and parameter sets, or `javascript` with the reason the native
268
- backend is unavailable. For evidence bundles and support tickets. It reports;
269
- there is deliberately no way to switch backend from here.
272
+ backend is unavailable. For evidence bundles and support tickets. It reports,
273
+ and the operator selects, as the next section shows.
270
274
 
271
- ### `webhook` — hybrid HMAC + ML-DSA-65 delivery signing
275
+ ### `webhook`: hybrid HMAC + ML-DSA-65 delivery signing
272
276
 
273
277
  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-65 adds non-repudiation over the same `${timestamp}.${body}` envelope. The full identity/credential surface lives in `kxco-pq-sdk`.
274
278
 
@@ -281,9 +285,8 @@ runtime provides the FIPS 203/204/205 primitives, the JavaScript implementation
281
285
  otherwise. Both produce identical wire bytes, so falling back is the right
282
286
  default and nothing about a signature changes.
283
287
 
284
- It is the wrong default in one situation: a deployment under a control that says
285
- cryptography must execute inside a validated module. There, a silent fallback
286
- means the control is not in force and nothing says so.
288
+ For a deployment under a control that says cryptography must execute inside a
289
+ validated module, make the native backend a requirement:
287
290
 
288
291
  ```js
289
292
  import { requireNativeBackend } from 'kxco-post-quantum'
@@ -306,37 +309,82 @@ KXCO_PQ_REQUIRE_NATIVE=1
306
309
  Set that and a process which has landed on the JavaScript backend fails at
307
310
  import, before its first signature rather than after.
308
311
 
309
- **What this does and does not claim.** It asserts that OpenSSL is doing the
310
- maths. Whether that OpenSSL is a FIPS-validated module is a property of your
311
- build, not of this package, and no library can see it from the inside. What it
312
- removes is the silent fallback, which is the part this package is responsible
313
- for. It is an assertion, never a switch: it cannot change which backend runs,
314
- because a flag that changed which implementation signed would change what your
315
- evidence means.
312
+ **It asserts that OpenSSL is doing the maths** and removes the silent fallback.
313
+ Pair it with the validated OpenSSL build your control names, and the control is
314
+ enforced at import.
315
+
316
+ ### Pinning the implementation your certificate names
317
+
318
+ `requireNativeBackend()` only ever asserts OpenSSL. Both implementations are
319
+ being taken to algorithm validation, so a deployment under a control that names
320
+ a certificate has to be able to pin whichever one its certificate covers, and
321
+ for some that is the JavaScript implementation.
316
322
 
317
- ## Where this fits
323
+ ```
324
+ KXCO_PQ_BACKEND=javascript never use OpenSSL, even where it is present
325
+ KXCO_PQ_BACKEND=openssl prefer OpenSSL, which is the default anyway
326
+ ```
318
327
 
319
- This is the primitive layer, and it stays that: keys, signatures, encapsulation
320
- and fingerprints, with nothing else in the way. Everything above it builds here.
328
+ ```js
329
+ import { requireBackend } from 'kxco-post-quantum'
330
+
331
+ requireBackend('javascript') // the JS certificate
332
+ requireBackend('openssl', ['ML-DSA-65', 'ML-KEM-768']) // the native one
333
+ ```
321
334
 
322
- - [`kxco-pq-sdk`](https://www.npmjs.com/package/kxco-pq-sdk) for identity credentials and verifiable claims
323
- - [`kxco-pq-chain`](https://www.npmjs.com/package/kxco-pq-chain) to put a signature on Armature L1, where the chain verifies it in consensus
324
- - [`kxco-pq-hsm`](https://www.npmjs.com/package/kxco-pq-hsm) to hold the key in hardware
335
+ A value that is neither throws at import, so a misspelled pin is caught before
336
+ the first signature.
325
337
 
326
- ## Part of the KXCO stack
338
+ The environment selects, the function asserts, and they are deliberately kept
339
+ apart: `requireBackend('javascript')` fails on an unpinned OpenSSL process
340
+ rather than switching it. Application code cannot quietly change which
341
+ implementation your evidence is about.
327
342
 
328
- `kxco-post-quantum` is the primitive layer. Everything else builds on it:
343
+ Nothing here changes what a signature looks like. The two produce identical
344
+ wire bytes, which the interoperability matrix proves for every parameter set in
345
+ both directions, and a signature made under the pin verifies on the other
346
+ backend. What changes is which one computed it, and `backend()` always reports
347
+ that truthfully, including when the answer is the result of a pin:
329
348
 
330
- - **`kxco-pq-sdk`** — identity credentials, webhook signing, verifiable claims
331
- - Other `kxco-pq-*` packages — domain-specific integrations
349
+ ```js
350
+ backend()
351
+ // { kind: 'javascript', library: '@noble/post-quantum', pinned: 'javascript',
352
+ // reason: 'KXCO_PQ_BACKEND=javascript pins this process to the JavaScript backend' }
353
+ ```
332
354
 
333
- Install this package directly when you need ML-DSA or ML-KEM without the rest of the identity stack.
355
+ That `reason` keeps pinned and unavailable apart, so an evidence bundle records
356
+ exactly which one applied.
357
+
358
+ ## The KXCO post-quantum family
359
+
360
+ This is the primitive layer: keys, signatures, encapsulation and fingerprints.
361
+ Install it directly when you need ML-DSA or ML-KEM on their own, or pick the
362
+ package that matches the job.
363
+
364
+ | You need to | Install |
365
+ |---|---|
366
+ | Put the whole stack in one install | [`kxco-pq`](https://www.npmjs.com/package/kxco-pq) |
367
+ | Use ML-DSA, ML-KEM and SLH-DSA directly | [`kxco-post-quantum`](https://www.npmjs.com/package/kxco-post-quantum) |
368
+ | Keep signing keys on the HSM you already run | [`kxco-pq-hsm`](https://www.npmjs.com/package/kxco-pq-hsm) |
369
+ | Sign a document or record anyone can verify offline | [`kxco-pq-attest`](https://www.npmjs.com/package/kxco-pq-attest) |
370
+ | Keep a tamper-evident audit trail | [`kxco-pq-audit`](https://www.npmjs.com/package/kxco-pq-audit) |
371
+ | Verify a signature in a browser, with no server | [`kxco-verify`](https://www.npmjs.com/package/kxco-verify) |
372
+ | Issue institution identity credentials | [`kxco-pq-sdk`](https://www.npmjs.com/package/kxco-pq-sdk) |
373
+ | Encrypt files and payloads to one or many recipients | [`kxco-pq-vault`](https://www.npmjs.com/package/kxco-pq-vault) |
374
+ | Encrypt Node streams and WebSockets | [`kxco-pq-tls`](https://www.npmjs.com/package/kxco-pq-tls) |
375
+ | Sign and verify webhooks | [`kxco-post-quantum-webhook`](https://www.npmjs.com/package/kxco-post-quantum-webhook) |
376
+ | Give an AI agent an identity a verified institution sponsors | [`kxco-pq-agent`](https://www.npmjs.com/package/kxco-pq-agent) |
377
+ | Have Armature L1 verify a signature in consensus | [`kxco-pq-chain`](https://www.npmjs.com/package/kxco-pq-chain) |
378
+ | Prove an envelope at three levels, offline to on-chain | [`kxco-pq-network`](https://www.npmjs.com/package/kxco-pq-network) |
379
+ | Generate and rotate keys from a terminal | [`kxco-pq-cli`](https://www.npmjs.com/package/kxco-pq-cli) |
380
+ | Find quantum-vulnerable cryptography in a dependency tree | [`kxco-pq-scan`](https://www.npmjs.com/package/kxco-pq-scan) |
381
+ | Fail the build when code reaches past the wrapper | [`eslint-plugin-kxco-pq`](https://www.npmjs.com/package/eslint-plugin-kxco-pq) |
334
382
 
335
383
  ---
336
384
 
337
385
  ## Security
338
386
 
339
- Cryptographic operations delegate entirely to [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum) and [`@noble/hashes`](https://github.com/paulmillr/noble-hashes) — this package does not reimplement any NIST primitive. `@noble/hashes` falls under Cure53's 2023 audit of the `@noble` ecosystem (`ciphers`, `curves`, `hashes`); `@noble/post-quantum` was **not** in that audit's scope and has been self-audited by its maintainer. See [AUDIT.md](./AUDIT.md) for the full posture.
387
+ The maths runs in OpenSSL 3.5 on Node 24 and later, and in [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum) and [`@noble/hashes`](https://github.com/paulmillr/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](./CONFORMANCE.md). The audit history of every upstream library is recorded in [AUDIT.md](./AUDIT.md).
340
388
 
341
389
  To report a vulnerability: [open a private security advisory](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/security/advisories/new) or email **john@knightsbridgelaw.com**. Acknowledgement within 2 business days, triage decision within 5. Full policy, including safe harbour for good-faith research: <https://kxco.ai/security>.
342
390
 
@@ -346,10 +394,25 @@ Apache-2.0. See [LICENSE](./LICENSE).
346
394
 
347
395
  ## Maintainers
348
396
 
349
- Shayne Heffernan and John Heffernan — [KXCO by Knightsbridge](https://kxco.ai)
397
+ Shayne Heffernan and John Heffernan, [KXCO by Knightsbridge](https://kxco.ai)
350
398
 
351
399
  ## Verifying a release
352
400
 
401
+ Every claim on this page is checkable without asking us. The evidence bundle
402
+ for the current release sits at a permanent, unauthenticated URL:
403
+
404
+ ```bash
405
+ # the full evidence bundle for the current release
406
+ curl -sLO https://github.com/KnightsbridgeAIQ/kxco-post-quantum/releases/latest/download/evidence-node24.x.zip
407
+
408
+ # or just the manifest: every file digest, and which backend produced the results
409
+ curl -sL https://github.com/KnightsbridgeAIQ/kxco-post-quantum/releases/latest/download/manifest-node24.x.json
410
+
411
+ # licence and provenance, straight from the registry
412
+ npm view kxco-post-quantum license # Apache-2.0
413
+ npm audit signatures --json # assert invalid:0 and missing:0
414
+ ```
415
+
353
416
  Every release asset is signed with ML-DSA-65 by this package's own signing path,
354
417
  and carries a SLSA provenance file recording the workflow that built it.
355
418
 
@@ -370,6 +433,5 @@ const sig = readFileSync('evidence-node24.x.zip.sig', 'utf8').trim()
370
433
  mlDsa.verify(pub, readFileSync('evidence-node24.x.zip'), sig) // true
371
434
  ```
372
435
 
373
- Compare the public key against the copy in this repository before trusting a
374
- signature: a key served alongside the artefact it signs proves only that the
375
- same party produced both.
436
+ Compare the public key with the copy committed to this repository, so the key
437
+ and the artefact are checked against two independent sources.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kxco-post-quantum",
3
- "version": "1.7.2",
4
- "description": "ML-DSA-65, ML-KEM-768 and SLH-DSA-SHA2-192s with key fingerprinting. OpenSSL 3.5 primitives on Node 24+, JavaScript elsewhere. 2,103 NIST ACVP vectors: 1,793 passed, 0 failed, 310 skipped. 225 interop checks, 0 failed. Reproducible builds and provenance.",
3
+ "version": "1.7.5",
4
+ "description": "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.",
5
5
  "keywords": [
6
6
  "post-quantum",
7
7
  "pqc",
@@ -12,6 +12,7 @@
12
12
  "dilithium",
13
13
  "kyber",
14
14
  "nist",
15
+ "acvp",
15
16
  "fips-203",
16
17
  "fips-204",
17
18
  "fips-205",
@@ -27,7 +28,17 @@
27
28
  "non-repudiation",
28
29
  "ml-dsa-87",
29
30
  "ml-kem-1024",
30
- "category-5"
31
+ "category-5",
32
+ "quantum-safe",
33
+ "pqc-migration",
34
+ "cnsa-2.0",
35
+ "crypto-agility",
36
+ "harvest-now-decrypt-later",
37
+ "digital-signature",
38
+ "key-encapsulation",
39
+ "jws",
40
+ "jwk",
41
+ "openssl"
31
42
  ],
32
43
  "license": "Apache-2.0",
33
44
  "author": "Shayne Heffernan and John Heffernan",
@@ -43,7 +54,7 @@
43
54
  "funding": "https://kxco.ai",
44
55
  "repository": {
45
56
  "type": "git",
46
- "url": "https://github.com/KnightsbridgeAIQ/kxco-post-quantum.git"
57
+ "url": "git+https://github.com/KnightsbridgeAIQ/kxco-post-quantum.git"
47
58
  },
48
59
  "bugs": {
49
60
  "url": "https://github.com/KnightsbridgeAIQ/kxco-post-quantum/issues"
@@ -61,51 +72,63 @@
61
72
  "exports": {
62
73
  ".": {
63
74
  "types": "./src/index.d.ts",
64
- "import": "./src/index.js"
75
+ "import": "./src/index.js",
76
+ "default": "./src/index.js"
65
77
  },
66
78
  "./ml-dsa": {
67
79
  "types": "./src/ml-dsa.d.ts",
68
- "import": "./src/ml-dsa.js"
80
+ "import": "./src/ml-dsa.js",
81
+ "default": "./src/ml-dsa.js"
69
82
  },
70
83
  "./ml-dsa-87": {
71
84
  "types": "./src/ml-dsa-87.d.ts",
72
- "import": "./src/ml-dsa-87.js"
85
+ "import": "./src/ml-dsa-87.js",
86
+ "default": "./src/ml-dsa-87.js"
73
87
  },
74
88
  "./ml-kem": {
75
89
  "types": "./src/ml-kem.d.ts",
76
- "import": "./src/ml-kem.js"
90
+ "import": "./src/ml-kem.js",
91
+ "default": "./src/ml-kem.js"
77
92
  },
78
93
  "./ml-kem-1024": {
79
94
  "types": "./src/ml-kem-1024.d.ts",
80
- "import": "./src/ml-kem-1024.js"
95
+ "import": "./src/ml-kem-1024.js",
96
+ "default": "./src/ml-kem-1024.js"
81
97
  },
82
98
  "./slh-dsa": {
83
99
  "types": "./src/slh-dsa.d.ts",
84
- "import": "./src/slh-dsa.js"
100
+ "import": "./src/slh-dsa.js",
101
+ "default": "./src/slh-dsa.js"
85
102
  },
86
103
  "./derive": {
87
104
  "types": "./src/derive.d.ts",
88
- "import": "./src/derive.js"
105
+ "import": "./src/derive.js",
106
+ "default": "./src/derive.js"
89
107
  },
90
108
  "./webhook": {
91
109
  "types": "./src/webhook.d.ts",
92
- "import": "./src/webhook.js"
110
+ "import": "./src/webhook.js",
111
+ "default": "./src/webhook.js"
93
112
  },
94
113
  "./kid": {
95
114
  "types": "./src/kid.d.ts",
96
- "import": "./src/kid.js"
115
+ "import": "./src/kid.js",
116
+ "default": "./src/kid.js"
97
117
  },
98
118
  "./seed": {
99
119
  "types": "./src/seed.d.ts",
100
- "import": "./src/seed.js"
120
+ "import": "./src/seed.js",
121
+ "default": "./src/seed.js"
101
122
  },
102
123
  "./jws": {
103
124
  "types": "./src/jws.d.ts",
104
- "import": "./src/jws.js"
125
+ "import": "./src/jws.js",
126
+ "default": "./src/jws.js"
105
127
  },
106
128
  "./backend": {
107
129
  "types": "./src/backend.d.ts",
108
- "import": "./src/backend.js"
130
+ "import": "./src/backend.js",
131
+ "default": "./src/backend.js"
109
132
  }
110
133
  },
111
134
  "files": [
@@ -114,11 +137,14 @@
114
137
  "BOUNDARY.md",
115
138
  "CHANGELOG.md",
116
139
  "CONFORMANCE.md",
140
+ "CRYPTO-INVENTORY.md",
117
141
  "DEPENDENCIES.md",
142
+ "HNDL.md",
118
143
  "LICENCE-PRODUCT.md",
119
144
  "LICENSE",
120
145
  "LIFECYCLE.md",
121
146
  "MIGRATION.md",
147
+ "PQCMM.md",
122
148
  "README.md",
123
149
  "SECURITY.md",
124
150
  "THREAT-MODEL.md",