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 +100 -0
- package/CONFORMANCE.md +77 -5
- package/DEPENDENCIES.md +137 -0
- package/LICENCE-PRODUCT.md +90 -0
- package/README.md +66 -3
- package/package.json +28 -10
- package/src/backend.d.ts +16 -0
- package/src/backend.js +48 -0
- package/src/index.d.ts +6 -0
- package/src/index.js +18 -2
- package/src/jws.d.ts +74 -0
- package/src/jws.js +249 -0
- package/src/ml-dsa-87.d.ts +8 -0
- package/src/ml-dsa-87.js +5 -0
- package/src/ml-dsa.d.ts +8 -0
- package/src/ml-dsa.js +5 -0
- package/src/ml-kem-1024.d.ts +8 -0
- package/src/ml-kem-1024.js +5 -0
- package/src/ml-kem.d.ts +8 -0
- package/src/ml-kem.js +5 -0
- package/src/seed.d.ts +91 -0
- package/src/seed.js +394 -0
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
|
-
|
|
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
|
-
|
|
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
|
|
268
|
-
|
|
269
|
-
TLS group negotiation are separate surfaces and
|
|
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
|
package/DEPENDENCIES.md
ADDED
|
@@ -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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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.
|
|
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,
|
package/src/backend.d.ts
ADDED
|
@@ -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
|