kxco-post-quantum 1.5.3 → 1.6.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,105 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.6.0
4
+
5
+ Additive. No existing export changes shape in a way a caller can observe, no
6
+ wire format moves, and the pinned vectors still match bit-for-bit.
7
+
8
+ **Seed-form keys.** FIPS 203 and 204 expand a keypair from a short seed, and
9
+ the expanded private key does not contain the seed it came from. So seed export
10
+ had to start at derivation: `keypairFromMaster` now also returns the seed it
11
+ already derived. Callers destructuring `{ publicKey, secretKey }` see no
12
+ difference. `seed.seedFromMaster` reproduces exactly the same bytes, which is
13
+ what makes seed export possible for keys already in production.
14
+
15
+ New `seed` namespace: `keypairFromSeed`, `seedFromMaster`, `exportJwk`,
16
+ `importJwk`, `exportSeedPkcs8`, `importSeedPkcs8`, for ML-DSA-65, ML-DSA-87,
17
+ ML-KEM-768 and ML-KEM-1024. JWKs are RFC 9964 `kty: "AKP"`, where `priv` is the
18
+ seed; PKCS#8 is the LAMPS seed form, the `[0] IMPLICIT OCTET STRING` CHOICE.
19
+ Both load directly into OpenSSL 3.5 and Node 24+. The existing expanded-key
20
+ export is untouched and old keys verify unchanged.
21
+
22
+ None of that is read off the specification. `test/seed.test.js` asserts the DER
23
+ this package writes is byte-identical to what OpenSSL writes, for every
24
+ parameter set, and that a seed expands to the same public key under both
25
+ backends. Where there is no native backend those tests skip with a reason
26
+ rather than passing quietly, because a green suite on a runtime without OpenSSL
27
+ is not evidence about OpenSSL.
28
+
29
+ An expanded secret key is refused where a seed belongs, and an expanded-form
30
+ PKCS#8 key is refused rather than truncated. Both would otherwise silently
31
+ produce a different keypair: the first 32 bytes of an expanded ML-DSA key are
32
+ not its seed.
33
+
34
+ **Compact JWS.** New `jws` namespace: `signJws`, `verifyJws`,
35
+ `decodeJwsHeader`, using the RFC 9964 algorithm names `ML-DSA-65` and
36
+ `ML-DSA-87`. Format only — no network, no configuration, no licence, and a
37
+ token stays verifiable offline for as long as the holder keeps the public key.
38
+ The algorithm is resolved from an allowlist inside the module rather than from
39
+ the token, so a token cannot name its own verification routine; `crit` and `b64`
40
+ are refused rather than ignored; and the key length must match the declared
41
+ algorithm, so a mixed-up key reports itself instead of looking like a bad
42
+ signature. SLH-DSA is deliberately not offered: FIPS 205 signing takes about a
43
+ second and a half and does not belong on a request path.
44
+
45
+ **A second correction to AUDIT.md.** The v1.1.2 correction fixed a false claim
46
+ that `@noble/post-quantum` was Cure53-audited, and in doing so introduced a
47
+ different inaccuracy: it described a single "Cure53 2023 NDS-01" audit covering
48
+ `@noble/ciphers`, `@noble/curves` and `@noble/hashes`. There is no such
49
+ engagement. Those are three separate audits at three different dates, and this
50
+ repository's own `audit/dependency-review.json` — generated by
51
+ `audit/run-audit.mjs`, not written by hand — recorded them correctly the whole
52
+ time:
53
+
54
+ | Package | Audited by |
55
+ |---|---|
56
+ | `@noble/post-quantum` | maintainer-audited, v0.6.1, Apr 2026 |
57
+ | `@noble/hashes` | Cure53, Jan 2022, v1.0.0 (Ethereum Foundation with Nomic Labs) |
58
+ | `@noble/curves` | Trail of Bits Feb 2023; Kudelski Sep 2023; Cure53 Sep 2024 |
59
+ | `@noble/ciphers` | Cure53, Sep 2024, v1.0.0 (OpenSats) |
60
+
61
+ The load-bearing claim never changed and was correct throughout:
62
+ **`@noble/post-quantum` has never been audited by a third party.** What was
63
+ wrong was the supporting detail, and the wrong wording had been copied into
64
+ sibling repositories before it was caught. AUDIT.md now matches the generated
65
+ record, and the v1.1.2 changelog entry is left as it was written — a released
66
+ entry is a record of what was said at the time, not a place to rewrite it.
67
+
68
+ **An evidence bundle.** `npm run evidence` writes `dist/evidence/`: versions,
69
+ the commit, the toolchain, which backend did the maths, the ACVP and interop
70
+ conformance output, the test suite output, a CycloneDX SBOM, the registry
71
+ signature check, and copies of AUDIT.md, THREAT-MODEL.md, SECURITY.md,
72
+ CONFORMANCE.md, DEPENDENCIES.md and LICENCE-PRODUCT.md.
73
+
74
+ **It is not an audit and it says so three times** — in the manifest's
75
+ `disclaimer` field, in the bundle README, and in the copied AUDIT.md. A CI step
76
+ asserts both disclaimers are still present, because the one sentence that must
77
+ never be edited out of an evidence bundle is the one saying it is not an audit.
78
+
79
+ A step that fails is recorded as a failure in the manifest rather than aborting
80
+ the build: a bundle missing a section with no explanation is worse than one
81
+ that says what did not run and why. Optional steps (peers needing a JVM, a
82
+ registry check needing a token) do not fail the build; required ones do.
83
+
84
+ The `evidence` workflow runs on Node 22 and 24 — the first has no OpenSSL 3.5
85
+ FIPS primitives and so exercises the JavaScript backend, the second exercises
86
+ OpenSSL. Shipping only one bundle would let a reader draw a conclusion about
87
+ code that never ran. Push builds subsample and record that they did; scheduled
88
+ and release builds do a full pass.
89
+
90
+ New `LICENCE-PRODUCT.md` states what is free (the maths, permanently, and
91
+ chain-agnostic) and what is paid (the hosted registry, the relay, anchoring,
92
+ live revocation, SLA), that the `KXCO Verified` mark needs a contract, and that
93
+ no third party has assessed this code and a customer may re-run the published
94
+ vectors without asking.
95
+
96
+ **`backend()` and `isNative(alg)`.** The package chooses its backend by probing
97
+ the runtime, so the only honest way to state which one a deployment used is to
98
+ ask it. These report; neither switches. The ACVP conformance report now records
99
+ `backendUnderTest` explicitly, saying that it drives the JavaScript primitives
100
+ directly and that the OpenSSL backend is evidenced by the interop matrix
101
+ instead. The same pass count previously described either implementation.
102
+
3
103
  ## 1.5.3
4
104
 
5
105
  Documentation and metadata. No code change.
package/CONFORMANCE.md CHANGED
@@ -1,12 +1,15 @@
1
1
  # Conformance and interoperability evidence
2
2
 
3
- Two claims, each with a harness in this repository that anyone can run:
3
+ Three claims, each with a harness in this repository that anyone can run:
4
4
 
5
5
  1. **This package computes what FIPS 203, 204 and 205 say it should.** Evidenced
6
6
  against NIST's own ACVP test vectors.
7
7
  2. **Independent implementations can consume what it produces, and it can
8
8
  consume theirs.** Evidenced against liboqs (C), Bouncy Castle (Java) and
9
9
  dilithium-py / kyber-py (Python), in both directions.
10
+ 3. **The PKI artefacts standard tooling issues validate here.** Evidenced by
11
+ having OpenSSL 3.5 issue ML-DSA certificates and signed messages, and
12
+ verifying them from this package with its own DER parsing.
10
13
 
11
14
  The second claim is the one that matters in deployment and the one that vector
12
15
  files cannot make. Passing NIST's vectors proves agreement with NIST. It does
@@ -19,9 +22,10 @@ Everything below is reproducible:
19
22
  npm run conformance:fetch # pinned NIST vectors, digest-checked
20
23
  npm run conformance:acvp # claim 1
21
24
  npm run conformance:interop # claim 2
25
+ npm run conformance:protocol # claim 3
22
26
  ```
23
27
 
24
- Both harnesses run in CI on every push and in full weekly. See
28
+ All three harnesses run in CI on every push and in full weekly. See
25
29
  [.github/workflows/conformance.yml](.github/workflows/conformance.yml).
26
30
 
27
31
  ---
@@ -243,6 +247,72 @@ keys, both carrying these identifiers, which is what makes the two backends
243
247
  interchangeable with each other and with any X.509 or CMS consumer that speaks
244
248
  the same encodings.
245
249
 
250
+ ## 5. Protocol artefacts: X.509 and CMS
251
+
252
+ Claims 1 and 2 are about primitives and encodings. Neither answers the question a
253
+ PKI team asks first, which is whether a certificate issued by the tooling they
254
+ already run will validate.
255
+
256
+ So OpenSSL issues and this package verifies. Nothing in the chain is ours on both
257
+ sides, and the artefacts are generated fresh on every run rather than committed,
258
+ because a fixture keeps passing after an encoding has drifted.
259
+
260
+ | Issuer | Version | Base image |
261
+ |---|---|---|
262
+ | OpenSSL | 3.5.8, asserted at image build time | `alpine:3.22` |
263
+
264
+ Pinned as `openssl` in
265
+ [conformance/interop/peers-lock.json](conformance/interop/peers-lock.json). The
266
+ exact build string is recorded in every report rather than the family, because
267
+ "OpenSSL 3.5" names a line and not the binary that signed the bytes.
268
+
269
+ ### X.509
270
+
271
+ A self-signed certificate per parameter set. Verification walks the DER to the
272
+ `tbsCertificate`, takes it including its own tag and length octets, which is what
273
+ RFC 5280 actually signs, and checks the `signature` BIT STRING over it with the
274
+ public key read out of a separately supplied SubjectPublicKeyInfo.
275
+
276
+ ### CMS SignedData
277
+
278
+ A signed message per parameter set, `-nodetach`, SHA-256 digest. ML-DSA carries no
279
+ default digest in OpenSSL 3.5, so `-md` has to be named explicitly; without it
280
+ `CMS_add1_signer` fails with "no default digest". That is a property of the CMS
281
+ layer rather than of the key.
282
+
283
+ The signature in a SignedData does not cover the message. It covers the DER
284
+ encoding of the signed attributes, re-tagged from the `[0] IMPLICIT` they travel
285
+ in to the `SET OF` they are signed as (RFC 5652 s5.4). Verifying it therefore
286
+ takes four checks that only mean something together:
287
+
288
+ | Check | What it establishes |
289
+ |---|---|
290
+ | `signatureVerified` | the signature is valid over the signed attributes |
291
+ | `digestBindsMessage` | the `messageDigest` attribute is the digest of the content |
292
+ | `contentTypeBound` | the signed `contentType` matches `eContentType` (RFC 5652 s5.3) |
293
+ | `contentMatches` | the embedded content is the message that was submitted |
294
+
295
+ The first alone would hold equally for a signature over somebody else's message.
296
+
297
+ ### Controls
298
+
299
+ Every row carries OpenSSL's own verdict on the artefact, so a disagreement is
300
+ attributable rather than assumed, and a tamper control that flips one bit of the
301
+ signed body and requires the verification to fail. The controls are not
302
+ decoration: the first version of the certificate parser sliced the wrong byte
303
+ range and returned false for everything, which passed the tamper control and
304
+ failed the positive check. Without the pair, a verifier that always returned true
305
+ and one that always returned false would both have looked green.
306
+
307
+ `algorithmOid` additionally checks that the SignerInfo names the OID in section 4
308
+ for the set it claims. An implementation that verifies the bytes but disagrees
309
+ about which algorithm they belong to still fails to interoperate.
310
+
311
+ ### Result
312
+
313
+ 33 checks across 6 rows, all passing, none not applicable. Reports are published
314
+ as CI artefacts per Node version.
315
+
246
316
  ## What this evidence does not cover
247
317
 
248
318
  Stated plainly, because a conformance report that only lists what passed is
@@ -264,9 +334,11 @@ marketing.
264
334
  these modules change. The accurate sentences are "NIST FIPS 203/204/205
265
335
  conformant" and "supports ML-DSA-87 and ML-KEM-1024". Anything stronger,
266
336
  including "CNSA 2.0 ready", would be an overclaim.
267
- - **Protocol-level interop is not covered here.** Key and signature bytes
268
- interoperate. Certificate and message encodings, X.509, CMS, COSE, JOSE and
269
- TLS group negotiation are separate surfaces and no claim is made about them.
337
+ - **Protocol coverage is X.509 and CMS, for the ML-DSA sets.** Section 5 covers
338
+ certificates and CMS SignedData against OpenSSL 3.5 for ML-DSA-44/65/87. COSE,
339
+ JOSE and TLS group negotiation are separate surfaces, and ML-KEM and SLH-DSA
340
+ are covered at the key and signature layer rather than in certificates,
341
+ because OpenSSL 3.5 issues neither.
270
342
  - **Self-administered.** Every number above was produced by harnesses in this
271
343
  repository, run by us. That is why they are reproducible and why the pins,
272
344
  digests and negative controls are there. It is not third-party attestation and
@@ -0,0 +1,137 @@
1
+ # Dependency audit
2
+
3
+ Four production dependencies. No development dependencies. Every one of them
4
+ reviewed by a person, and every mechanical fact in that review re-checked by a
5
+ harness that fails when reality moves away from it.
6
+
7
+ ```
8
+ npm run audit:deps
9
+ ```
10
+
11
+ - Curated review: [audit/dependency-review.json](audit/dependency-review.json)
12
+ - Harness: [audit/run-audit.mjs](audit/run-audit.mjs)
13
+ - Report: `audit/results/dependencies.json`, published as a CI artefact
14
+
15
+ Current result: **26 checks passed, 0 failed, 0 skipped.**
16
+
17
+ ---
18
+
19
+ ## The tree
20
+
21
+ | Package | Version | Licence | Pinning | On the code path | Independently reviewed by |
22
+ |---|---|---|---|---|---|
23
+ | `@noble/post-quantum` | 0.7.0 | MIT | exact | yes | maintainer self-audit at 0.6.1, Apr 2026 |
24
+ | `@noble/hashes` | 2.3.0 | MIT | exact | yes | Cure53, Jan 2022 |
25
+ | `@noble/curves` | 2.3.0 | MIT | transitive | yes | Trail of Bits, Feb 2023; Kudelski Security, Sep 2023; Cure53, Sep 2024 |
26
+ | `@noble/ciphers` | 2.3.0 | MIT | transitive | no | Cure53, Sep 2024 |
27
+
28
+ One maintainer publishes all four, and all four carry a verified npm registry
29
+ signature and a verified SLSA provenance attestation, with nothing invalid and
30
+ nothing missing across the installed tree. Nothing in the tree runs code at
31
+ install time, and `npm audit` reports no advisory at any severity.
32
+
33
+ ## Why each one is there
34
+
35
+ **`@noble/post-quantum`** supplies the FIPS 203, 204 and 205 primitives. This
36
+ package is the layer above: parameter-set selection, FIPS 204 context strings,
37
+ key derivation, fingerprinting, webhook signing and the OpenSSL backend. It does
38
+ not reimplement the arithmetic.
39
+
40
+ It is also the one package in the tree with no independent review, and that is
41
+ the reason [conformance/](CONFORMANCE.md) exists. Every parameter set is checked
42
+ against NIST's own ACVP vectors, cross-checked against liboqs, Bouncy Castle and
43
+ dilithium-py / kyber-py in both directions, and checked against certificates and
44
+ signed messages that OpenSSL 3.5 issued. The primitives are evidenced here rather
45
+ than accepted on the dependency's word, and that evidence is reproducible on any
46
+ machine.
47
+
48
+ The pin is exact and deliberately so. Version 0.7.1 regressed nine NIST SLH-DSA
49
+ verification vectors that 0.7.0 passes. It shipped in this package's 1.5.1, was
50
+ reverted in 1.5.2, is deprecated on npm and is blocked in dependabot. A caret
51
+ range would have taken that regression automatically, which is why the harness
52
+ fails the build on any range in `dependencies`.
53
+
54
+ **`@noble/hashes`** supplies SHA-256, SHA-512, SHAKE-256 and HKDF, for key
55
+ derivation from a master secret, public-key fingerprinting and the hashing FIPS
56
+ 205 requires. It is declared directly rather than inherited, so the version in
57
+ use is visible in this package's own manifest instead of resolved out of a
58
+ transitive range. The 2022 Cure53 scope covered everything except `blake3`,
59
+ `sha3-addons`, `sha1` and `argon2`; none of those four are used here.
60
+
61
+ **`@noble/curves`** is not named anywhere in this package's source. It arrives
62
+ through `@noble/post-quantum`, and it is on the hot path: `ml-dsa.js` takes
63
+ `abool` from `@noble/curves/utils.js`, and `_crystals.js`, which both ML-DSA and
64
+ ML-KEM are built on, takes `FFTCore` and `reverseBits` from
65
+ `@noble/curves/abstract/fft.js`. So every signature and every encapsulation this
66
+ package performs runs through it.
67
+
68
+ That was established by the reachability walk, not by reading manifests. It is
69
+ also the most heavily reviewed package in the tree: three independent firms
70
+ across four years.
71
+
72
+ **`@noble/ciphers`** arrives the same way and is the one package in the tree that
73
+ nothing here reaches. It is present in `node_modules` and absent from every code
74
+ path from every published entry point.
75
+
76
+ ## What the harness checks
77
+
78
+ The review is a human document. The harness re-derives each of its mechanical
79
+ claims from the lockfile, the registry and the installed tree, and exits non-zero
80
+ on any disagreement:
81
+
82
+ | Check | Source of truth |
83
+ |---|---|
84
+ | every dependency in the tree appears in the review | `package-lock.json` |
85
+ | the review names nothing that has left the tree | `package-lock.json` |
86
+ | the tree is within the declared ceiling of four | `package-lock.json` |
87
+ | every direct dependency is pinned to an exact version | `package.json` |
88
+ | no development dependencies | `package.json` |
89
+ | each package is at the reviewed version | `package-lock.json` |
90
+ | each licence is on the allowed list | `package-lock.json` |
91
+ | no package declares an install script | lockfile flag and every installed manifest |
92
+ | reachability matches the review, in both directions | static import walk |
93
+ | no known advisories | `npm audit` |
94
+ | no invalid registry signature or attestation anywhere | `npm audit signatures --json` |
95
+ | no missing registry signature or attestation anywhere | `npm audit signatures --json` |
96
+
97
+ The reachability check is bidirectional, and that matters. A package the review
98
+ calls unused becoming reachable fails the build, and so does a package the review
99
+ calls used becoming unreachable. The first version of the review recorded
100
+ `@noble/curves` as unused; the harness rejected it, and the entry above is what
101
+ replaced it.
102
+
103
+ Adding or bumping a dependency fails this harness until somebody updates the
104
+ review. That is the mechanism by which the document stays true, rather than being
105
+ accurate on the day it was written.
106
+
107
+ ## How reachability is derived
108
+
109
+ A breadth-first walk from all nine entry points in the `exports` map, reading
110
+ every import, export-from, dynamic `import()` and `require()` specifier out of
111
+ the source and resolving each with `import.meta.resolve` from the importing file.
112
+ Using Node's own resolver means subpath exports, export maps and the `#native`
113
+ conditional import resolve the way Node resolves them at run time.
114
+
115
+ Both branches of `#native` are walked, because the Node backend and the browser
116
+ stub both live in `src/`. The reported set is therefore the union of the Node and
117
+ browser paths, which can only over-report what is reachable. Current figures: 33
118
+ files walked, three packages reached, and one Node builtin, `node:crypto`, used
119
+ by the OpenSSL backend.
120
+
121
+ ## Supply-chain posture in one paragraph
122
+
123
+ Four packages, one publisher, all MIT, all exactly locked by integrity hash, all
124
+ carrying registry signatures and SLSA provenance with nothing invalid or missing,
125
+ none able to execute at install time, none carrying an advisory, three of four independently reviewed by named
126
+ firms, and the fourth evidenced directly against NIST's vectors and three
127
+ independent implementations in this repository. The whole tree is small enough
128
+ that "all dependencies audited" is a statement someone can check in an afternoon,
129
+ which is the only reason it is worth making.
130
+
131
+ ---
132
+
133
+ ## Correcting this document
134
+
135
+ Every figure comes from `npm run audit:deps`. If one does not reproduce on your
136
+ machine, that is a defect worth reporting through [SECURITY.md](SECURITY.md);
137
+ include the generated `audit/results/dependencies.json` from your run.
@@ -0,0 +1,90 @@
1
+ # Licensing: what is free, what is paid
2
+
3
+ Two different things share the KXCO name, and confusing them wastes everyone's time. This document draws the line.
4
+
5
+ ---
6
+
7
+ ## Free, and permanently so: the maths
8
+
9
+ Apache-2.0. Use it commercially, fork it, ship it inside a product, never speak to us.
10
+
11
+ | Package | What it does |
12
+ |---|---|
13
+ | [`kxco-post-quantum`](https://www.npmjs.com/package/kxco-post-quantum) | ML-DSA-65, ML-KEM-768, SLH-DSA, seed-form keys, compact JWS, deterministic derivation, kid fingerprints |
14
+ | [`kxco-verify`](https://www.npmjs.com/package/kxco-verify) | Verify a site's deploy attestation, in a terminal or a browser |
15
+
16
+ These are **chain-agnostic**. There is no chain id, no relay URL and no licence check anywhere in their source. They do not phone home. Signing and verification work with the network unplugged and keep working if KXCO stops existing, which is the only sense in which a signature is worth anything ten years from now.
17
+
18
+ The other Apache-2.0 packages — `kxco-pq-attest`, `kxco-pq-audit`, `kxco-pq-hsm`, `kxco-pq-tls`, `kxco-pq-vault`, `kxco-post-quantum-webhook`, `kxco-pq-sdk`, `kxco-pq-agent`, `kxco-pq-chain`, `kxco-pq-network` — are also Apache-2.0 source. **The licence covers the code, not the service.** Reading and modifying the source is free. Calling the KXCO-operated registry and relay is not.
19
+
20
+ ---
21
+
22
+ ## Paid: the service
23
+
24
+ What you buy is not cryptography. Cryptography is free, and we could not charge for it honestly if we wanted to.
25
+
26
+ What you buy is **an answer about the present**.
27
+
28
+ A signature made by a key that was revoked an hour ago is still a perfectly valid signature. No amount of offline mathematics can tell you it was revoked. Only something that knows what happened after the signature was made can, and that something has to be operated, kept available, and answerable when it is wrong.
29
+
30
+ | Paid | Why it cannot be free |
31
+ |---|---|
32
+ | **Hosted key registry** (`chain.kxco.ai/kids/:kid`) | Answers whether a key is active, revoked or rotated. Someone has to run it and stand behind the answer |
33
+ | **Meta-transaction relay** (`relay.kxco.ai`) | KXCO validates your signed intent, **pays the gas in ARMR**, and submits the transaction. You never hold a token or run a node |
34
+ | **On-chain anchoring** | Writes to Armature L1, chain 1111111 |
35
+ | **Live revocation** (`anchored+live`) | The registry lookup at verification time |
36
+ | **Support and SLA** | Availability commitments, an escalation path, a name to call |
37
+
38
+ Priced **in USD, per seat**. You do not need to hold ARMR, run a node, configure an RPC endpoint or touch a wallet. If a vendor tells you post-quantum identity requires you to buy a token, that is a different product from this one.
39
+
40
+ See [`SALES-SKU.md`](https://github.com/KnightsbridgeAIQ/kxco-pq-network/blob/main/SALES-SKU.md) in `kxco-pq-network` for the seat definitions.
41
+
42
+ ---
43
+
44
+ ## Which mode you are in
45
+
46
+ | Mode | Network at verify time | Licence | What it proves |
47
+ |---|---|---|---|
48
+ | `signature` | none | no | The holder of this key signed this |
49
+ | `anchored` | none | no | …and it was written to Armature L1 |
50
+ | `anchored+live` | yes, fails closed | **yes** | …and that key is still trusted **now** |
51
+
52
+ `signature` and `anchored` will not start requiring a licence. Envelopes issued today verify in those modes forever, with no KXCO server in the path. That is a commitment about the format, not a pricing tier that may move.
53
+
54
+ Writes to the hosted relay require a licence, and always did in substance — this is now enforced in the client rather than only at the server, so a misconfigured service fails at boot instead of at a customer's first transaction.
55
+
56
+ ---
57
+
58
+ ## Trademark
59
+
60
+ The name **KXCO** and the mark **"KXCO Verified"** are not covered by the Apache-2.0 licence.
61
+
62
+ You may fork the code, ship it, and say so. You may not describe your deployment as "KXCO Verified", or use the mark on a badge, a certificate, a report or a sales page, without a written agreement with us. The mark is meant to tell a reader that KXCO stands behind a specific claim, and it is worthless the moment anyone can apply it to themselves.
63
+
64
+ Describing your product as "built on kxco-post-quantum" is fine and accurate. Describing it as "KXCO Verified" is not, unless it is.
65
+
66
+ ---
67
+
68
+ ## Assurance
69
+
70
+ The cryptography is NIST-standardised and the conformance is published, pinned and reproducible.
71
+
72
+ | | |
73
+ |---|---|
74
+ | Algorithms | ML-DSA-65 (FIPS 204), ML-KEM-768 (FIPS 203), SLH-DSA (FIPS 205) |
75
+ | Backend | OpenSSL 3.5 where the runtime provides it, `@noble/post-quantum` elsewhere. Identical on the wire, checked in both directions |
76
+ | Conformance | 2,103 NIST ACVP vectors across FIPS 203, 204 and 205. Vectors pinned by digest |
77
+ | Interoperability | 225 checks against OpenSSL 3.5, liboqs, Bouncy Castle and dilithium-py/kyber-py |
78
+ | Supply chain | SLSA provenance attestation on every release; CycloneDX SBOM; reproducible build |
79
+ | Evidence bundle | `npm run evidence` regenerates all of it from source, on your machine |
80
+
81
+ **You do not have to take any of it on our word.** Every figure above comes from a command you can run yourself against the same commit, and the vectors are NIST's, not ours. A customer may re-run them at any time without asking us and without telling us. If your result differs from ours we want to know: `john@knightsbridgelaw.com`, acknowledged within 2 business days.
82
+
83
+ Dependency audit history — which upstream libraries were reviewed, by whom, and when — is recorded in [`AUDIT.md`](AUDIT.md), kept current by a generated dependency review rather than by hand.
84
+
85
+ ---
86
+
87
+ ## Support
88
+
89
+ Commercial terms, seat pricing and SLA: **hello@kxco.ai**
90
+ Security and vulnerability reports: **john@knightsbridgelaw.com**, or a private advisory on the relevant repository. Full policy, including safe harbour for good-faith research: <https://kxco.ai/security>
package/README.md CHANGED
@@ -135,7 +135,7 @@ separation.
135
135
 
136
136
  | Export | Signature | Description |
137
137
  |---|---|---|
138
- | `keypairFromMaster` | `(master, info?) → { publicKey, secretKey }` | Deterministic keypair via HKDF-SHA-512. `info` defaults to `'ml-dsa-65-v1'`. |
138
+ | `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). |
139
139
  | `sign` | `(secretKey, message) → string` | Signs a message. Returns a hex-encoded signature (6618 chars). |
140
140
  | `verify` | `(publicKey, message, sigHex) → boolean` | Verifies a hex-encoded signature. Returns `false` on any failure. |
141
141
  | `ml_dsa65` | raw primitive | The underlying `@noble/post-quantum` primitive, re-exported. |
@@ -159,7 +159,7 @@ Hash-based, stateless signatures. Security Category 3 (matching ML-DSA-65), but
159
159
 
160
160
  | Export | Signature | Description |
161
161
  |---|---|---|
162
- | `keypairFromMaster` | `(master, info?) → { publicKey, secretKey }` | Deterministic keypair via HKDF-SHA-512. `info` defaults to `'ml-kem-768-v1'`. |
162
+ | `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. |
163
163
  | `encapsulate` | `(publicKey) → { ciphertext, sharedSecret }` | Generates a shared secret and ciphertext to send to the key holder. |
164
164
  | `decapsulate` | `(ciphertext, secretKey) → Buffer` | Recovers the shared secret from a ciphertext. Returns 32 bytes. |
165
165
  | `ml_kem768` | raw primitive | The underlying `@noble/post-quantum` primitive, re-exported. |
@@ -178,6 +178,70 @@ Constant-time comparison of two kid strings. Use this when comparing user-suppli
178
178
 
179
179
  HKDF-SHA-512 derivation. `master` must be at least 16 bytes. `info` is a required domain-separation string. Returns `length` bytes.
180
180
 
181
+ ### `seed` — seed-form keys (RFC 9964, LAMPS)
182
+
183
+ FIPS 203 and 204 expand a keypair from a short seed. The expanded private key
184
+ this package returns is derived from that seed and does not contain it, so a
185
+ seed cannot be recovered from an expanded key. `keypairFromMaster` therefore
186
+ returns the seed it derived alongside the pair — additive, so callers that
187
+ destructure `{ publicKey, secretKey }` are unaffected.
188
+
189
+ Seed form is 32 bytes for ML-DSA and 64 for ML-KEM. It fits in a KMS secret, an
190
+ HSM object or an env var, and it is the only private form an RFC 9964 AKP JWK
191
+ accepts.
192
+
193
+ | Export | Signature | Description |
194
+ |---|---|---|
195
+ | `SEED_ALGORITHMS` | `string[]` | `ML-DSA-65`, `ML-DSA-87`, `ML-KEM-768`, `ML-KEM-1024`. SLH-DSA has no seed form. |
196
+ | `seedFromMaster` | `(alg, master, info?) → bytes` | Same HKDF-SHA-512 derivation `keypairFromMaster` uses, so it reproduces keys already in production. |
197
+ | `keypairFromSeed` | `(alg, seed) → { publicKey, secretKey, seed }` | Expands a seed. Byte-identical to what OpenSSL 3.5 derives from the same seed. |
198
+ | `exportJwk` | `(alg, { publicKey, seed? }, opts?) → jwk` | RFC 9964 AKP JWK. `priv` carries the seed. |
199
+ | `importJwk` | `(jwk) → { alg, publicKey, secretKey?, seed? }` | Rejects a JWK whose seed and `pub` disagree. |
200
+ | `exportSeedPkcs8` | `(alg, seed) → bytes` | PKCS#8 in LAMPS seed form, the `[0] IMPLICIT` CHOICE. |
201
+ | `importSeedPkcs8` | `(der) → { alg, seed }` | Refuses an expanded-form key rather than truncating it into a seed. |
202
+
203
+ The encodings are not read off the specification. `test/seed.test.js` asserts
204
+ that the DER this package writes is byte-identical to what this machine's
205
+ OpenSSL writes, for every parameter set, and skips with a reason where there is
206
+ no native backend to compare against.
207
+
208
+ ```js
209
+ import { mlDsa, seed } from 'kxco-post-quantum'
210
+
211
+ const key = mlDsa.keypairFromMaster(process.env.KXCO_MASTER_KEY)
212
+ const jwk = seed.exportJwk('ML-DSA-65', key, { kid: fingerprint(key.publicKey) })
213
+ // { kty: 'AKP', alg: 'ML-DSA-65', pub: '...', priv: '<32-byte seed>', kid: '...' }
214
+ ```
215
+
216
+ ### `jws` — compact JWS with the RFC 9964 algorithm names
217
+
218
+ Format only. A token signed here verifies in any process holding the public
219
+ key, offline, with no configuration and no licence. RFC 9964 registered
220
+ `ML-DSA-65` and `ML-DSA-87` as JWS algorithms so a post-quantum signature can
221
+ travel the path an institution's gateway, IdP and partner verifier already
222
+ parse.
223
+
224
+ | Export | Signature | Description |
225
+ |---|---|---|
226
+ | `signJws` | `(payload, secretKey, opts?) → string` | Compact JWS. Objects are JSON-serialised. `opts.alg` defaults to `ML-DSA-65`. |
227
+ | `verifyJws` | `(token, publicKey, opts?) → { valid, ... }` | Fails closed. `{ alg }` and `{ kid }` pin what the header may declare. |
228
+ | `decodeJwsHeader` | `(token) → object \| null` | Unauthenticated read, for choosing which key to fetch. |
229
+
230
+ The algorithm is resolved from an allowlist inside the module, never from the
231
+ token, so a token cannot name its own verification routine. `crit` and `b64`
232
+ headers are refused rather than ignored, and the public key's length must match
233
+ the algorithm the header declares.
234
+
235
+ There is no SLH-DSA option here: FIPS 205 signing takes on the order of a
236
+ second and a half, which does not belong on a request path.
237
+
238
+ ### `backend()` and `isNative(alg)`
239
+
240
+ Reports which implementation is doing the maths in this process — `openssl`
241
+ with its version and parameter sets, or `javascript` with the reason the native
242
+ backend is unavailable. For evidence bundles and support tickets. It reports;
243
+ there is deliberately no way to switch backend from here.
244
+
181
245
  ### `webhook` — hybrid HMAC + ML-DSA-65 delivery signing
182
246
 
183
247
  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`.
@@ -189,7 +253,6 @@ Low-level helpers for the KXCO hybrid webhook pattern: `envelope`, `hmacHex`, `v
189
253
  - No identity credentials or verifiable claims (those are in `kxco-pq-sdk`)
190
254
  - No relay, transport, or network layer
191
255
  - No key storage or KMS integration
192
- - No FIPS 140-3 module validation (the algorithms are FIPS-standardised; the module is not validated)
193
256
 
194
257
  ---
195
258
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kxco-post-quantum",
3
- "version": "1.5.3",
3
+ "version": "1.6.0",
4
4
  "description": "ML-DSA-65, ML-KEM-768 and SLH-DSA-SHA2-192s with key fingerprinting. Runs in OpenSSL 3.5 on Node 24+, JavaScript elsewhere. 2,103 NIST ACVP vectors and 225 cross-implementation interop checks, 0 failed. Reproducible builds, SLSA provenance, published SBOM. The base layer for all kxco-pq-* packages.",
5
5
  "keywords": [
6
6
  "post-quantum",
@@ -94,37 +94,55 @@
94
94
  "./kid": {
95
95
  "types": "./src/kid.d.ts",
96
96
  "import": "./src/kid.js"
97
+ },
98
+ "./seed": {
99
+ "types": "./src/seed.d.ts",
100
+ "import": "./src/seed.js"
101
+ },
102
+ "./jws": {
103
+ "types": "./src/jws.d.ts",
104
+ "import": "./src/jws.js"
105
+ },
106
+ "./backend": {
107
+ "types": "./src/backend.d.ts",
108
+ "import": "./src/backend.js"
97
109
  }
98
110
  },
99
111
  "files": [
100
- "src",
101
- "README.md",
102
- "LICENSE",
103
- "CONFORMANCE.md",
104
112
  "BENCHMARKS.md",
105
- "THREAT-MODEL.md",
113
+ "CHANGELOG.md",
114
+ "CONFORMANCE.md",
115
+ "DEPENDENCIES.md",
116
+ "LICENCE-PRODUCT.md",
117
+ "LICENSE",
106
118
  "MIGRATION.md",
119
+ "README.md",
107
120
  "SECURITY.md",
108
- "CHANGELOG.md"
121
+ "THREAT-MODEL.md",
122
+ "src"
109
123
  ],
110
124
  "engines": {
111
125
  "node": ">=20.19"
112
126
  },
113
127
  "dependencies": {
114
- "@noble/hashes": "2.3.0",
128
+ "@noble/hashes": "2.4.0",
115
129
  "@noble/post-quantum": "0.7.0"
116
130
  },
117
131
  "scripts": {
118
- "test": "node --test test/basic.test.js && node --test test/context.test.js && node --test test/category5.test.js && node --test test/edge-cases.test.js && node --test test/browser-smoke.test.js && node test/run-vectors.js",
132
+ "test": "node --test test/basic.test.js && node --test test/context.test.js && node --test test/category5.test.js && node --test test/edge-cases.test.js && node --test test/seed.test.js && node --test test/browser-smoke.test.js && node test/run-vectors.js",
119
133
  "test:vectors": "node test/run-vectors.js",
120
134
  "generate:vectors": "node test/generate-vectors.js > test/vectors.json",
121
135
  "bench": "node bench/bench.js",
122
136
  "conformance:fetch": "node conformance/fetch-vectors.mjs",
123
137
  "conformance:acvp": "node conformance/run-acvp.mjs --json conformance/results/acvp.json",
124
138
  "conformance:interop": "node conformance/interop/run-interop.mjs --json conformance/results/interop.json",
139
+ "conformance:protocol": "node conformance/protocol/run-protocol.mjs --json conformance/results/protocol.json",
140
+ "audit:deps": "node audit/run-audit.mjs --json audit/results/dependencies.json",
125
141
  "sbom": "npm sbom --sbom-format cyclonedx --sbom-type library",
126
142
  "bench:timing": "node --expose-gc bench/timing.mjs --iterations 20000 --json bench/results/timing.json",
127
- "bench:primitives": "node --expose-gc bench/primitives.mjs --iterations 100 --json bench/results/primitives.json"
143
+ "bench:primitives": "node --expose-gc bench/primitives.mjs --iterations 100 --json bench/results/primitives.json",
144
+ "evidence": "node scripts/build-evidence.mjs",
145
+ "evidence:full": "node scripts/build-evidence.mjs --full"
128
146
  },
129
147
  "publishConfig": {
130
148
  "provenance": true,
@@ -0,0 +1,16 @@
1
+ export interface BackendReport {
2
+ kind: 'openssl' | 'javascript'
3
+ library: string
4
+ /** OpenSSL version, on the native backend only. */
5
+ openssl?: string
6
+ /** Parameter sets the native backend can express. */
7
+ parameterSets?: string[]
8
+ /** Why the native backend is unavailable, on the JavaScript backend only. */
9
+ reason?: string
10
+ }
11
+
12
+ /** Describe the backend doing the maths in this process. Reports; never switches. */
13
+ export function backend(): BackendReport
14
+
15
+ /** Whether a parameter set runs natively here, e.g. isNative('ML-DSA-65'). */
16
+ export function isNative(alg: string): boolean