kxco-post-quantum 1.4.0 → 1.4.1

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/BENCHMARKS.md ADDED
@@ -0,0 +1,124 @@
1
+ # Performance
2
+
3
+ Per-algorithm latency and memory, reported as tail latency rather than a mean.
4
+
5
+ A mean hides the tail, and the tail is what a request budget has to absorb. For
6
+ ML-DSA that distinction is not cosmetic: signing uses rejection sampling, so it
7
+ loops until the candidate signature falls in range, and the slow tail is inherent
8
+ to the algorithm rather than measurement noise. Anyone sizing a timeout from a
9
+ mean will size it wrong.
10
+
11
+ Reproduce with:
12
+
13
+ ```
14
+ node --expose-gc bench/primitives.mjs --iterations 100 --json bench/results/primitives.json
15
+ ```
16
+
17
+ Every parameter set the package can reach is measured, not only the five it wraps
18
+ in its own helpers.
19
+
20
+ ## Signatures
21
+
22
+ Milliseconds. 100 iterations except where the `n` column says otherwise.
23
+
24
+ | Algorithm | Operation | n | median | p95 | p99 | ops/s |
25
+ |---|---|---:|---:|---:|---:|---:|
26
+ | ML-DSA-44 | keygen | 100 | 1.695 | 2.847 | 3.317 | 545 |
27
+ | ML-DSA-44 | sign | 100 | 8.880 | 24.104 | 30.193 | 91 |
28
+ | ML-DSA-44 | verify | 100 | 1.554 | 3.000 | 3.650 | 552 |
29
+ | ML-DSA-65 | keygen | 100 | 3.394 | 9.058 | 11.699 | 238 |
30
+ | ML-DSA-65 | sign | 100 | 11.308 | 36.752 | 51.084 | 64 |
31
+ | ML-DSA-65 | verify | 100 | 3.456 | 5.032 | 5.553 | 288 |
32
+ | ML-DSA-87 | keygen | 100 | 5.226 | 8.270 | 9.635 | 194 |
33
+ | ML-DSA-87 | sign | 100 | 13.148 | 33.932 | 42.381 | 62 |
34
+ | ML-DSA-87 | verify | 100 | 4.877 | 11.119 | 21.760 | 167 |
35
+ | SLH-DSA-SHA2-128f | sign | 20 | 151.319 | 194.915 | 205.881 | 7 |
36
+ | SLH-DSA-SHA2-128f | verify | 20 | 6.412 | 10.672 | 30.298 | 121 |
37
+ | SLH-DSA-SHA2-192s | sign | 3 | 6788.860 | 7229.209 | 7229.209 | 0 |
38
+ | SLH-DSA-SHA2-192s | verify | 10 | 4.997 | 62.287 | 62.287 | 89 |
39
+ | SLH-DSA-SHAKE-256f | sign | 5 | 3333.195 | 3786.271 | 3786.271 | 0 |
40
+ | SLH-DSA-SHAKE-256f | verify | 10 | 72.219 | 86.779 | 86.779 | 14 |
41
+
42
+ **Two numbers to design around.**
43
+
44
+ **ML-DSA signing has a long tail.** ML-DSA-65 signs in 11 ms at the median and
45
+ 51 ms at p99, a factor of 4.5. Rejection sampling means an unlucky signature does
46
+ several more rounds. Size request budgets from p99, not the median.
47
+
48
+ **SLH-DSA-SHA2-192s signs in about 6.8 seconds.** That is the set `slhDsa` wraps,
49
+ and it is not a per-request operation. It suits infrequent, high-value signatures
50
+ such as firmware or root attestations. Verification is cheap, around 5 ms, so an
51
+ SLH-DSA signature is expensive to make and cheap to check. The `f` variants trade
52
+ signature size for signing speed: SHA2-128f signs in 151 ms.
53
+
54
+ ## Key encapsulation
55
+
56
+ | Algorithm | Operation | n | median | p95 | p99 | ops/s |
57
+ |---|---|---:|---:|---:|---:|---:|
58
+ | ML-KEM-512 | keygen | 100 | 0.338 | 0.969 | 1.277 | 2222 |
59
+ | ML-KEM-512 | encapsulate | 100 | 0.512 | 1.043 | 1.617 | 1720 |
60
+ | ML-KEM-512 | decapsulate | 100 | 0.845 | 2.133 | 2.561 | 965 |
61
+ | ML-KEM-768 | keygen | 100 | 0.645 | 1.357 | 1.757 | 1344 |
62
+ | ML-KEM-768 | encapsulate | 100 | 0.822 | 2.050 | 5.067 | 971 |
63
+ | ML-KEM-768 | decapsulate | 100 | 1.292 | 2.750 | 4.048 | 674 |
64
+ | ML-KEM-1024 | keygen | 100 | 1.062 | 2.043 | 2.776 | 860 |
65
+ | ML-KEM-1024 | encapsulate | 100 | 1.066 | 1.902 | 10.844 | 641 |
66
+ | ML-KEM-1024 | decapsulate | 100 | 0.917 | 1.813 | 2.169 | 918 |
67
+
68
+ ML-KEM is sub-millisecond at the median across all three sets. Moving from
69
+ Category 3 to Category 5 costs well under a millisecond per operation, so the
70
+ migration cost of ML-KEM-1024 is its 1568-byte keys and ciphertexts, not its
71
+ speed. See [MIGRATION.md](MIGRATION.md).
72
+
73
+ ## Wrapper overhead
74
+
75
+ The package helpers derive keys from a master secret through HKDF, which the raw
76
+ primitives do not. That is the only overhead they add:
77
+
78
+ | Helper | median | raw keygen median | HKDF cost |
79
+ |---|---:|---:|---:|
80
+ | `mlDsa.keypairFromMaster` | 3.945 | 3.394 | ~0.55 ms |
81
+ | `mlKem.keypairFromMaster` | 1.129 | 0.645 | ~0.48 ms |
82
+
83
+ Signing and verification go straight through, so they carry no wrapper cost
84
+ beyond hex encoding.
85
+
86
+ ## Memory
87
+
88
+ Heap growth per operation, in bytes, from `process.memoryUsage().heapUsed` across
89
+ a batch:
90
+
91
+ | Algorithm | keygen | sign or encapsulate |
92
+ |---|---:|---:|
93
+ | ML-KEM-768 | ~11.7 kB | ~13.0 kB |
94
+ | ML-KEM-1024 | ~20.6 kB | ~6.1 kB |
95
+ | ML-DSA-65 | ~12.9 kB | ~21.0 kB |
96
+ | ML-DSA-87 | ~3.3 kB | ~11.4 kB |
97
+ | SLH-DSA-SHA2-128f | ~143 kB | |
98
+ | SLH-DSA-SHA2-192s | ~192 kB | |
99
+ | SLH-DSA-SHAKE-256f | ~864 kB | |
100
+
101
+ SLH-DSA allocates one to three orders of magnitude more than the lattice schemes,
102
+ which is consistent with building a hypertree per key. The lattice figures are
103
+ tens of kilobytes and are not a constraint at these rates.
104
+
105
+ **Read these as orders of magnitude, not exact allocations.** Garbage collection
106
+ can run mid-batch, so a figure smaller than a sibling's does not reliably mean
107
+ less allocation. ML-DSA-87 keygen reading lower than ML-DSA-65 is an artefact of
108
+ collection timing, not evidence that the larger parameter set allocates less.
109
+
110
+ ## What these figures are not
111
+
112
+ - **One machine, one run.** Node v26.1.0 on win32-x64. Absolute numbers move with
113
+ hardware and runtime; the ratios between operations are the portable part.
114
+ - **Not a cross-vendor comparison.** Nothing here was measured against another
115
+ implementation, so it says nothing about how this compares to a native or
116
+ hardware-backed stack. It will be slower than both.
117
+ - **Reduced samples where marked.** SLH-DSA slow variants take 3 to 20 samples
118
+ rather than 100, because 100 signatures at 6.8 seconds each is not a benchmark,
119
+ it is an afternoon. Where n is small, p95 and p99 collapse onto the maximum and
120
+ are reported that way rather than dressed up.
121
+ - **Not a side-channel measurement.** Timing here is throughput, gathered without
122
+ any attempt to detect secret-dependent variation, and it must not be read as
123
+ evidence about constant-time behaviour. See [THREAT-MODEL.md](THREAT-MODEL.md),
124
+ which states plainly that no such property is claimed.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.1
4
+
5
+ Evidence and documentation only. **No `src/` module changed**, no export was
6
+ added or removed, and the dependency set is unchanged, so no call site can
7
+ behave differently than it did on 1.4.0.
8
+
9
+ **liboqs is now a third interop implementation.** The cross-implementation
10
+ matrix ran against two peers and now runs against three: liboqs 0.16.0 (C),
11
+ Bouncy Castle 1.85.2 (Java) and dilithium-py 1.4.0 / kyber-py 1.2.0 (Python).
12
+ **225 checks passed, 0 failed, 42 not applicable, across 38 rows**, up from
13
+ 156/0/10 across 24. The previous figures are reproduced exactly when the new
14
+ peer is excluded, so nothing about the existing evidence moved.
15
+
16
+ SLH-DSA had one peer and now has two. liboqs runs from a container built from
17
+ source at a tag pinned in `peers-lock.json`, and CI builds it, so the version
18
+ tested is the version named.
19
+
20
+ A peer that cannot do something now records as not-applicable rather than as a
21
+ disagreement. The 42 not-applicable are itemised in CONFORMANCE.md: ten from our
22
+ own hedged signing, thirty-two from two liboqs API limits.
23
+
24
+ **AUDIT.md corrections.** The reviewer checklist told anyone doing due diligence
25
+ to fetch an endpoint that returns 500. Verification now goes to Armature L1 over
26
+ public JSON-RPC, with the calldata layout documented and a named article page
27
+ for the other half of the join. Both documented commands were run verbatim
28
+ against production before publishing.
29
+
30
+ Two false claims in the same file are withdrawn. It said we pin
31
+ `@noble/post-quantum@0.6.1`, "the exact version covered by the maintainer's own
32
+ self-audit"; we ship `0.7.0`, the self-audit covers `0.6.1`, and **the version we
33
+ ship is covered by no audit at all**. It also described the dependency as
34
+ "itself audited", contradicting its own section 1.
35
+
36
+ **Supply chain.** Each release now publishes its CycloneDX SBOM as a GitHub
37
+ Release asset at `releases/download/<tag>/sbom.cyclonedx.json`, a permanent
38
+ unauthenticated URL. It was previously generated but retained only as an
39
+ expiring Actions artifact, which is not a published SBOM. The dependency policy
40
+ is now stated in SECURITY.md rather than living only in `dependabot.yml`.
41
+
3
42
  ## 1.4.0
4
43
 
5
44
  Adds the Security Category 5 parameter sets, and publishes conformance and
package/CONFORMANCE.md CHANGED
@@ -5,7 +5,7 @@ Two claims, each with a harness in this repository that anyone can run:
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
- consume theirs.** Evidenced against Bouncy Castle (Java) and
8
+ consume theirs.** Evidenced against liboqs (C), Bouncy Castle (Java) and
9
9
  dilithium-py / kyber-py (Python), in both directions.
10
10
 
11
11
  The second claim is the one that matters in deployment and the one that vector
@@ -93,18 +93,26 @@ Peers, both pinned in
93
93
 
94
94
  | Peer | Implementation | Language | Covers |
95
95
  |---|---|---|---|
96
+ | `liboqs` | `liboqs` 0.16.0 with binding 0.16.0, built from source | C | ML-DSA, ML-KEM, SLH-DSA |
96
97
  | `bouncycastle` | `org.bouncycastle:bcprov-jdk18on:1.85.2`, SHA-256 pinned | Java | ML-DSA, ML-KEM, SLH-DSA |
97
98
  | `python` | `dilithium-py==1.4.0`, `kyber-py==1.2.0` | Python | ML-DSA, ML-KEM |
98
99
 
99
- SLH-DSA is covered by Bouncy Castle only, because no maintained pure-Python
100
- implementation was available to pin. That is a narrower base than the other two
101
- families and is stated rather than averaged away.
100
+ SLH-DSA is covered by liboqs and Bouncy Castle, not by the Python pair, because
101
+ no maintained pure-Python implementation was available to pin. That is a
102
+ narrower base than the other two families and is stated rather than averaged
103
+ away.
102
104
 
103
- Neither shares code with this package's backend. Bouncy Castle is a widely
104
- deployed independent implementation; the Python pair are independent
105
+ None of the three shares code with this package's backend. liboqs is the
106
+ reference C implementation the wider ecosystem tests against; Bouncy Castle is a
107
+ widely deployed independent implementation; the Python pair are independent
105
108
  spec-derived implementations.
106
109
 
107
- **Result: 156 checks passed, 0 failed, 10 not applicable, across 24 rows.**
110
+ liboqs needs a C toolchain, so its peer runs in a container built from
111
+ [conformance/interop/peers/liboqs.Dockerfile](conformance/interop/peers/liboqs.Dockerfile)
112
+ rather than requiring every contributor to install one. If the image is absent
113
+ the peer reports unavailable, which is not a failure but is also not evidence.
114
+
115
+ **Result: 225 checks passed, 0 failed, 42 not applicable, across 38 rows.**
108
116
 
109
117
  Per row, in both directions:
110
118
 
@@ -130,17 +138,40 @@ Each parameter set this package publishes a helper for is run twice: once
130
138
  through that helper (`wrapper`) and once through the primitive (`backend`), so
131
139
  the evidence covers the published API and not only its dependency.
132
140
 
133
- ### The ten not-applicable checks
134
-
135
- `bytes` on the five `wrapper` rows, in each of two context modes. This package's
136
- `sign` is hedged: it draws fresh randomness per signature, which FIPS 204 permits
137
- and recommends, so its output is deliberately not reproducible. Byte equality is
138
- asserted on the `backend` rows for the same parameter sets, so determinism is
139
- still evidenced everywhere it is meaningful. Reasoning is in
140
- [THREAT-MODEL.md](THREAT-MODEL.md).
141
-
142
- The five wrapper rows are the five parameter sets this package publishes helpers
143
- for: ML-DSA-65, ML-DSA-87, ML-KEM-768, ML-KEM-1024 and SLH-DSA-SHA2-192s.
141
+ ### The forty-two not-applicable checks
142
+
143
+ A check is recorded as not applicable when a peer says it cannot do something,
144
+ never when a peer disagrees with us. A peer that cannot honour a request answers
145
+ `unsupported` and the matrix records N/A; any other error is a failure and is
146
+ counted as one.
147
+
148
+ **Ten from hedged signing.** This package's `sign` is hedged: it draws fresh
149
+ randomness per signature, which FIPS 204 permits and recommends, so its output is
150
+ deliberately not reproducible. Byte equality is asserted on the `backend` rows
151
+ for the same parameter sets, so determinism is still evidenced everywhere it is
152
+ meaningful. Reasoning is in [THREAT-MODEL.md](THREAT-MODEL.md).
153
+
154
+ They are the `bytes` check on each signature `wrapper` row, in each of two
155
+ context modes: three such rows against Bouncy Castle (ML-DSA-65, ML-DSA-87,
156
+ SLH-DSA-SHA2-192s) and two against the Python pair, which has no SLH-DSA. The
157
+ KEM wrapper rows carry no `bytes` check, because a KEM produces a fresh
158
+ ciphertext by design and byte equality is not defined for it.
159
+
160
+ **Thirty-two from two liboqs API limits**, both properties of that library
161
+ rather than of this one:
162
+
163
+ - `keys` on all fourteen liboqs rows. liboqs exposes no seed-derived keygen, so
164
+ it cannot rebuild a key from a FIPS 203 / FIPS 204 seed the way the other
165
+ peers do. It is addressed by encoded secret key instead, which still tests
166
+ something real: the private key encodings are themselves standardised, so a
167
+ key of ours that failed to load into liboqs and produce interoperable output
168
+ would be a genuine defect.
169
+ - `bytes` on all eighteen liboqs signature checks. liboqs signs hedged and
170
+ exposes no deterministic mode through its Python binding, so byte equality
171
+ against it is not defined. Bouncy Castle and the Python pair both cover it.
172
+
173
+ Everything else is exercised against liboqs, in both directions, including FIPS
174
+ 204 context strings and both negative controls.
144
175
 
145
176
  ---
146
177
 
package/README.md CHANGED
@@ -11,10 +11,11 @@ ML-DSA-65 (FIPS 204) and SLH-DSA-SHA2-192s (FIPS 205) signatures, ML-KEM-768 (FI
11
11
 
12
12
  **Evidence, not adjectives:**
13
13
 
14
- - [CONFORMANCE.md](./CONFORMANCE.md) — NIST ACVP vectors for FIPS 203/204/205, and a cross-implementation interop matrix against Bouncy Castle and two pure-Python implementations. 134 interop checks, both directions, with negative controls. Reproducible: `npm run conformance:acvp`, `npm run conformance:interop`.
15
- - [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.
16
- - [MIGRATION.md](./MIGRATION.md) — moving an RSA or ECDSA system across, and moving between versions of this package.
17
- - [SECURITY.md](./SECURITY.md) — reporting, and release integrity.
14
+ - [CONFORMANCE.md](./CONFORMANCE.md): NIST ACVP vectors for FIPS 203/204/205 (2,103 tests, 0 failed), and a cross-implementation interop matrix against liboqs, Bouncy Castle and two pure-Python implementations (225 checks, 0 failed, both directions, with negative controls). Reproducible: `npm run conformance:acvp`, `npm run conformance:interop`.
15
+ - [BENCHMARKS.md](./BENCHMARKS.md): per-algorithm latency at p95/p99, plus memory. Two figures worth designing around: ML-DSA signing has a 4.5x tail between median and p99, and SLH-DSA-SHA2-192s signs in about 6.8 seconds.
16
+ - [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.
17
+ - [MIGRATION.md](./MIGRATION.md): moving an RSA or ECDSA system across, and moving between versions of this package.
18
+ - [SECURITY.md](./SECURITY.md): reporting, and release integrity.
18
19
 
19
20
  ---
20
21
 
package/SECURITY.md CHANGED
@@ -36,6 +36,28 @@ Out of scope (report upstream to https://github.com/paulmillr/noble-post-quantum
36
36
  - HMAC-SHA-256
37
37
  - HKDF-SHA-512 (RFC 5869)
38
38
 
39
+ ## Dependency policy
40
+ Both runtime dependencies are pinned to an exact version, never a range:
41
+ `@noble/post-quantum` and `@noble/hashes`. A range would let the code that runs
42
+ the cryptography change without a release of this package, which is not a
43
+ property we are willing to give up for convenience. Every GitHub Action in our
44
+ workflows is pinned by 40-character commit SHA for the same reason.
45
+
46
+ Updates are proposed, never automatic. Dependabot opens pull requests weekly for
47
+ both npm and GitHub Actions, and Dependabot security updates are enabled at the
48
+ repository level so an advisory does not wait for the Monday run. The
49
+ configuration is in [.github/dependabot.yml](.github/dependabot.yml).
50
+
51
+ A dependency bump is not merged on the strength of a green test run alone. A
52
+ change to `@noble/post-quantum` is a change to the primitives themselves, so it
53
+ is gated on the full conformance evidence regenerating clean: the NIST ACVP
54
+ vectors and the cross-implementation interoperability matrix, both described in
55
+ [CONFORMANCE.md](CONFORMANCE.md).
56
+
57
+ Each release publishes a CycloneDX SBOM as a GitHub Release asset, at
58
+ `releases/download/<tag>/sbom.cyclonedx.json`, generated from the tree that was
59
+ actually installed for that build.
60
+
39
61
  ## Disclosure
40
62
  We follow coordinated disclosure with a 90-day default window.
41
63
  For actively-exploited issues we ship a patch release within 48 hours.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kxco-post-quantum",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "ML-DSA-65, ML-KEM-768 and SLH-DSA-SHA2-192s primitives with key fingerprinting. The base layer for all kxco-pq-* packages.",
5
5
  "keywords": [
6
6
  "post-quantum",
@@ -95,6 +95,7 @@
95
95
  "README.md",
96
96
  "LICENSE",
97
97
  "CONFORMANCE.md",
98
+ "BENCHMARKS.md",
98
99
  "THREAT-MODEL.md",
99
100
  "MIGRATION.md",
100
101
  "SECURITY.md",
@@ -115,7 +116,8 @@
115
116
  "conformance:fetch": "node conformance/fetch-vectors.mjs",
116
117
  "conformance:acvp": "node conformance/run-acvp.mjs --json conformance/results/acvp.json",
117
118
  "conformance:interop": "node conformance/interop/run-interop.mjs --json conformance/results/interop.json",
118
- "sbom": "npm sbom --sbom-format cyclonedx --sbom-type library"
119
+ "sbom": "npm sbom --sbom-format cyclonedx --sbom-type library",
120
+ "bench:primitives": "node --expose-gc bench/primitives.mjs --iterations 100 --json bench/results/primitives.json"
119
121
  },
120
122
  "publishConfig": {
121
123
  "provenance": true,