kxco-post-quantum 1.5.0 → 1.5.2

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 CHANGED
@@ -15,7 +15,51 @@ node --expose-gc bench/primitives.mjs --iterations 100 --json bench/results/prim
15
15
  ```
16
16
 
17
17
  Every parameter set the package can reach is measured, not only the five it wraps
18
- in its own helpers.
18
+ in its own helpers. The OpenSSL rows appear only on a runtime that provides those
19
+ primitives, so run it on Node 24 or later to reproduce the comparison below and
20
+ on Node 22 for the JavaScript figures alone.
21
+
22
+ ## Two backends
23
+
24
+ Since 1.5.0 this package runs the FIPS primitives in OpenSSL 3.5 where the
25
+ runtime provides them (Node 24 and later) and in JavaScript everywhere else.
26
+ Those are different implementations, so one set of numbers cannot describe both.
27
+ The harness measures whichever are available and labels every row, and the
28
+ tables below the comparison are the JavaScript figures.
29
+
30
+ | Algorithm | Operation | OpenSSL p50 | JavaScript p50 | OpenSSL p99 | JavaScript p99 | faster by |
31
+ |---|---|---:|---:|---:|---:|---:|
32
+ | ML-DSA-44 | sign | 1.125 | 5.116 | 4.619 | 21.325 | 4.5x |
33
+ | ML-DSA-44 | verify | 0.241 | 1.373 | 0.725 | 2.451 | 5.7x |
34
+ | ML-DSA-65 | sign | 1.293 | 8.074 | 4.562 | 40.840 | 6.2x |
35
+ | ML-DSA-65 | verify | 0.288 | 2.249 | 0.652 | 3.803 | 7.8x |
36
+ | ML-DSA-87 | sign | 1.879 | 11.031 | 7.128 | 39.404 | 5.9x |
37
+ | ML-DSA-87 | verify | 0.613 | 3.363 | 1.831 | 5.843 | 5.5x |
38
+ | ML-KEM-1024 | encapsulate | 0.126 | 0.708 | 0.278 | 1.986 | 5.6x |
39
+ | ML-KEM-1024 | decapsulate | 0.230 | 0.919 | 0.339 | 1.630 | 4.0x |
40
+ | ML-KEM-512 | encapsulate | 0.079 | 0.378 | 0.212 | 1.008 | 4.8x |
41
+ | ML-KEM-512 | decapsulate | 0.236 | 0.461 | 0.380 | 1.498 | 2.0x |
42
+ | ML-KEM-768 | encapsulate | 0.148 | 0.558 | 0.256 | 1.288 | 3.8x |
43
+ | ML-KEM-768 | decapsulate | 0.183 | 0.874 | 0.523 | 5.378 | 4.8x |
44
+ | SLH-DSA-SHA2-128f | sign | 68.639 | 97.791 | 165.092 | 173.466 | 1.4x |
45
+ | SLH-DSA-SHA2-128f | verify | 3.459 | 6.082 | 4.303 | 27.278 | 1.8x |
46
+ | SLH-DSA-SHA2-192s | sign | 1710.954 | 4342.105 | 1938.773 | 4628.987 | 2.5x |
47
+ | SLH-DSA-SHA2-192s | verify | 1.263 | 7.457 | 2.405 | 40.920 | 5.9x |
48
+ | SLH-DSA-SHAKE-256f | sign | 161.358 | 2158.272 | 179.655 | 3510.503 | 13.4x |
49
+ | SLH-DSA-SHAKE-256f | verify | 5.599 | 66.177 | 6.774 | 98.654 | 11.8x |
50
+
51
+ The speedup column is the headline, but for a document about tail latency the
52
+ p99 columns matter more. ML-DSA-65 signing goes from a 40.8 ms p99 to 4.6 ms:
53
+ the rejection-sampling tail that the opening of this document warns about is
54
+ still there in the OpenSSL path, but it is roughly nine times tighter, so a
55
+ timeout sized off it can be far smaller.
56
+
57
+ SLH-DSA-SHAKE-256f is the largest single change, 2158 ms down to 161 ms for a
58
+ signature. SLH-DSA-SHA2-192s remains slow in absolute terms at 1.7 seconds and
59
+ no backend makes a hash-based signature cheap; it is 2.5x rather than fast.
60
+
61
+ Neither column is a side-channel claim. See
62
+ [THREAT-MODEL.md](THREAT-MODEL.md).
19
63
 
20
64
  ## Signatures
21
65
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,67 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.2
4
+
5
+ **Reverts `@noble/post-quantum` to 0.7.0. Upgrade from 1.5.1 immediately if you
6
+ verify SLH-DSA signatures.**
7
+
8
+ 1.5.1 bumped the backend to 0.7.1. That version **fails nine NIST ACVP SLH-DSA
9
+ verification vectors that 0.7.0 passes**, returning `false` where the vectors
10
+ require `true`. In practice that means a valid SLH-DSA signature can be
11
+ rejected. It is a correctness and availability fault, not a security weakening:
12
+ nothing invalid is accepted. No other algorithm family is affected, and ML-DSA
13
+ and ML-KEM pass unchanged on both versions.
14
+
15
+ The failures are confined to the **internal** signature interface
16
+ (`SLH-DSA.Verify_internal`), not the pre-hash interface, and appear across both
17
+ SHA2 and SHAKE parameter sets: SHA2-128s, SHA2-192f, SHA2-256f, SHA2-256s,
18
+ SHAKE-128f, SHAKE-128s, SHAKE-256f, SHAKE-256s. Reproduce with:
19
+
20
+ ```
21
+ npm run conformance:fetch
22
+ node conformance/run-acvp.mjs --set SLH-DSA-sigVer-FIPS205 --max-per-group 3
23
+ ```
24
+
25
+ 0.7.0 returns 87 passed, 0 failed, 21 skipped. 0.7.1 returns 78 passed, 9
26
+ failed.
27
+
28
+ This has been reported upstream. Until it is resolved the pin stays at 0.7.0,
29
+ and the ACVP job will fail the build on any attempt to move it, which is the
30
+ behaviour we want.
31
+
32
+ **Why this is worth saying plainly rather than quietly reverting.** The
33
+ conformance evidence in this repository is not decoration. It caught a real
34
+ regression in a dependency within hours of that dependency's release, on a
35
+ version bump whose own release notes described only hardening. A test suite that
36
+ passes is not evidence; a test suite that fails when something breaks is. Ours
37
+ did.
38
+
39
+ 1.5.1 is deprecated on npm.
40
+
41
+ ## 1.5.1
42
+
43
+ Dependency bump: `@noble/post-quantum` 0.7.0 to **0.7.1**, published 2026-08-27.
44
+ Exact pin as always, never a range.
45
+
46
+ Worth taking rather than waiting for the scheduled Dependabot run, because one of
47
+ its changes touches how this package calls it: 0.7.1 snapshots options on entry
48
+ so a caller's object cannot be mutated afterwards. Every FIPS 204 context string
49
+ this package passes goes in as an options object, so that hardening is directly
50
+ on our path.
51
+
52
+ It also adds a WebCrypto wrapper for ML-KEM upstream. Nothing here uses it yet,
53
+ but it is worth knowing that the backend is converging on the same thing this
54
+ package did in 1.5.0: use the platform's implementation where the platform has
55
+ one.
56
+
57
+ Verified before merging, on all three runtimes: 39 pinned vectors bit-for-bit and
58
+ 53 tests on Node 20 and 22 (the JavaScript path, where this bump actually
59
+ applies) and on Node 24+ (the OpenSSL path).
60
+
61
+ AUDIT.md carries the new pin and its integrity hash. The conservative bound is
62
+ unchanged and restated: the maintainer's self-audit covers 0.6.1, we ship 0.7.1,
63
+ so **the version we ship is covered by no audit at all**.
64
+
3
65
  ## 1.5.0
4
66
 
5
67
  **The FIPS primitives now run in OpenSSL where the runtime has them.** On Node 24
package/CONFORMANCE.md CHANGED
@@ -187,6 +187,62 @@ Everything else is exercised against liboqs, in both directions, including FIPS
187
187
 
188
188
  ---
189
189
 
190
+ ## 3. Edge cases
191
+
192
+ Passing vectors in the middle of the range says nothing about the ends. These are
193
+ run by `npm test` on every supported runtime, so they cover both backends:
194
+ OpenSSL on Node 24 and later, JavaScript on Node 20 and 22.
195
+
196
+ | Case | Asserted |
197
+ |---|---|
198
+ | Zero-length message | signs and verifies, and does **not** verify against a one-byte message |
199
+ | Empty vs absent context | both verify, and normalise identically |
200
+ | Context of exactly 255 bytes | accepted; one flipped byte in it fails verification |
201
+ | Context of 256 bytes | **throws**, rather than returning false |
202
+ | Malformed signature | empty, odd-length hex, non-hex, one byte short, one byte long, all zeroes, all ones: all return false, none throw |
203
+ | Wrong-size public key | empty, short, long, all zeroes: all return false, none throw |
204
+ | Signature under another key | false |
205
+ | One-megabyte message | verifies, and a flip in the **final** byte is detected |
206
+ | Corrupted ML-KEM ciphertext | returns an unrelated secret of the correct length, not an error |
207
+ | Repeated encapsulation | ciphertext and secret both differ |
208
+ | Key derivation | deterministic from the same master, different for a different info string |
209
+
210
+ Two of those deserve their reasoning stated, because the behaviour is a choice:
211
+
212
+ **A cryptographic failure returns false; caller misuse throws.** A wrong key or a
213
+ corrupted signature answers the question "is this valid" with no, and no is a
214
+ value. A 256-byte context is a bug in the calling code, and returning false there
215
+ would let a program that can never verify anything look like a program that is
216
+ merely receiving bad signatures.
217
+
218
+ **The one-megabyte case is not a size limit test.** It flips the last byte and
219
+ requires that verification fails. An implementation that hashed only a prefix
220
+ would pass every other case in this suite.
221
+
222
+ ## 4. Object identifiers
223
+
224
+ The NIST OIDs each parameter set is published under. These are not transcribed
225
+ from a registry: they are read back out of the SubjectPublicKeyInfo that OpenSSL
226
+ 3.5 produces for each algorithm, by walking the DER rather than scanning it, so
227
+ they are the identifiers this package actually interoperates on.
228
+
229
+ | Parameter set | OID |
230
+ |---|---|
231
+ | ML-DSA-44 | 2.16.840.1.101.3.4.3.17 |
232
+ | ML-DSA-65 | 2.16.840.1.101.3.4.3.18 |
233
+ | ML-DSA-87 | 2.16.840.1.101.3.4.3.19 |
234
+ | ML-KEM-512 | 2.16.840.1.101.3.4.4.1 |
235
+ | ML-KEM-768 | 2.16.840.1.101.3.4.4.2 |
236
+ | ML-KEM-1024 | 2.16.840.1.101.3.4.4.3 |
237
+ | SLH-DSA-SHA2-128f | 2.16.840.1.101.3.4.3.21 |
238
+ | SLH-DSA-SHA2-192s | 2.16.840.1.101.3.4.3.22 |
239
+ | SLH-DSA-SHAKE-256f | 2.16.840.1.101.3.4.3.31 |
240
+
241
+ Keys cross the boundary to OpenSSL as SPKI for public keys and PKCS8 for private
242
+ keys, both carrying these identifiers, which is what makes the two backends
243
+ interchangeable with each other and with any X.509 or CMS consumer that speaks
244
+ the same encodings.
245
+
190
246
  ## What this evidence does not cover
191
247
 
192
248
  Stated plainly, because a conformance report that only lists what passed is
package/THREAT-MODEL.md CHANGED
@@ -85,10 +85,13 @@ What that does and does not buy:
85
85
  - It **does** remove the structural impossibility. The reasoning above was that
86
86
  the property could not be established from inside the language at all. On the
87
87
  OpenSSL path it can, in principle, be established.
88
- - It **does not** amount to a constant-time claim from us. We have published no
89
- timing measurements of either backend, and OpenSSL's post-quantum
90
- implementations carry their own side-channel posture which is theirs to state,
91
- not ours to assert on their behalf.
88
+ - It **does not** amount to a constant-time claim from us. We have since
89
+ published timing measurements of both backends, reported at the end of this
90
+ document, and they detect nothing at their resolution. That is a bound, not a
91
+ clearance: a null result from a wall-clock test is weaker evidence than the
92
+ JavaScript backend's own statement that it does not defend against side
93
+ channels. OpenSSL's post-quantum side-channel posture is likewise theirs to
94
+ state, not ours to assert on their behalf.
92
95
  - It **does not** apply to Node 20 or 22, to browsers, or to any runtime without
93
96
  those primitives. Those keep the JavaScript backend and everything above
94
97
  applies to them unchanged.
@@ -207,6 +210,31 @@ falsifiable rather than permanent by assertion.
207
210
  - Published timing measurements under a statistical test such as dudect would
208
211
  turn "no claim" into a measured bound. Absence of a detected leak is not
209
212
  absence of a leak, and any such result would be reported that way.
213
+
214
+ **This one is now done, and the result is reported exactly that way.**
215
+ `bench/timing.mjs` runs a dudect-style Welch t-test on interleaved
216
+ fixed-versus-random secret key timings, and `npm run bench:timing`
217
+ reproduces it. At 3,000 iterations per class:
218
+
219
+ | Backend | ML-DSA-65 sign | ML-KEM-768 decapsulate |
220
+ |---|---:|---:|
221
+ | OpenSSL (Node 24+) | t = 0.027 | t = -0.93 |
222
+ | JavaScript (Node 22) | t = 0.454 | t = -0.658 |
223
+
224
+ dudect's convention treats \|t\| > 10 as a detected leak. Nothing here is
225
+ close to it.
226
+
227
+ **Do not read the second row as good news.** The JavaScript backend states
228
+ about itself that there is no protection against side-channel attacks, and
229
+ that statement is more authoritative than this measurement. A null result on
230
+ a path whose author says it leaks means the test is too coarse to see it, not
231
+ that it is absent: wall-clock timing at JavaScript resolution cannot resolve a
232
+ leak below its own noise floor, and it observes no cache, branch-prediction,
233
+ power or electromagnetic channel at all.
234
+
235
+ What the measurement is actually worth: it is a regression detector and a
236
+ published bound, not a clearance. If a future change introduced a leak large
237
+ enough to be visible at this resolution, this would catch it.
210
238
  - FIPS 140-3 validation of a module used underneath would change what can be
211
239
  asserted about the boundary, and is not the same as the algorithm-level
212
240
  conformance evidence in [CONFORMANCE.md](CONFORMANCE.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kxco-post-quantum",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
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",
@@ -115,7 +115,7 @@
115
115
  "@noble/post-quantum": "0.7.0"
116
116
  },
117
117
  "scripts": {
118
- "test": "node --test test/basic.test.js && node --test test/context.test.js && node --test test/category5.test.js && node --test test/browser-smoke.test.js && node test/run-vectors.js",
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",
119
119
  "test:vectors": "node test/run-vectors.js",
120
120
  "generate:vectors": "node test/generate-vectors.js > test/vectors.json",
121
121
  "bench": "node bench/bench.js",
@@ -123,6 +123,7 @@
123
123
  "conformance:acvp": "node conformance/run-acvp.mjs --json conformance/results/acvp.json",
124
124
  "conformance:interop": "node conformance/interop/run-interop.mjs --json conformance/results/interop.json",
125
125
  "sbom": "npm sbom --sbom-format cyclonedx --sbom-type library",
126
+ "bench:timing": "node --expose-gc bench/timing.mjs --iterations 20000 --json bench/results/timing.json",
126
127
  "bench:primitives": "node --expose-gc bench/primitives.mjs --iterations 100 --json bench/results/primitives.json"
127
128
  },
128
129
  "publishConfig": {