kxco-post-quantum 1.5.1 → 1.5.3
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 +18 -13
- package/CHANGELOG.md +60 -0
- package/CONFORMANCE.md +56 -0
- package/README.md +6 -3
- package/THREAT-MODEL.md +32 -4
- package/package.json +5 -4
package/BENCHMARKS.md
CHANGED
|
@@ -85,15 +85,19 @@ Milliseconds. 100 iterations except where the `n` column says otherwise.
|
|
|
85
85
|
|
|
86
86
|
**Two numbers to design around.**
|
|
87
87
|
|
|
88
|
-
**ML-DSA signing has a long tail.** ML-DSA-65 signs in
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
**ML-DSA signing has a long tail, on both backends.** ML-DSA-65 signs in 8.1 ms
|
|
89
|
+
at the median and 40.8 ms at p99 in JavaScript, a factor of 5.1. On OpenSSL it is
|
|
90
|
+
1.3 ms and 4.6 ms, a factor of 3.5. Rejection sampling means an unlucky signature
|
|
91
|
+
does several more rounds, and no backend removes that. Size request budgets from
|
|
92
|
+
p99, not the median, and from the p99 of the backend you will actually run.
|
|
93
|
+
|
|
94
|
+
**SLH-DSA-SHA2-192s signs in seconds, not milliseconds:** about 4.3 s in
|
|
95
|
+
JavaScript and 1.7 s on OpenSSL. That is the set `slhDsa` wraps, and on either
|
|
96
|
+
backend it is not a per-request operation. It suits infrequent, high-value
|
|
97
|
+
signatures such as firmware or root attestations. Verification is cheap, 7.5 ms
|
|
98
|
+
and 1.3 ms respectively, so an SLH-DSA signature is expensive to make and cheap
|
|
99
|
+
to check. The `f` variants trade signature size for signing speed: SHA2-128f
|
|
100
|
+
signs in 98 ms in JavaScript and 69 ms on OpenSSL.
|
|
97
101
|
|
|
98
102
|
## Key encapsulation
|
|
99
103
|
|
|
@@ -155,11 +159,12 @@ collection timing, not evidence that the larger parameter set allocates less.
|
|
|
155
159
|
|
|
156
160
|
- **One machine, one run.** Node v26.1.0 on win32-x64. Absolute numbers move with
|
|
157
161
|
hardware and runtime; the ratios between operations are the portable part.
|
|
158
|
-
- **Not a cross-vendor comparison.**
|
|
159
|
-
|
|
160
|
-
|
|
162
|
+
- **Not a cross-vendor comparison.** The two backends here are both ours to
|
|
163
|
+
ship, so the comparison above is between two paths through this package, not
|
|
164
|
+
between this package and someone else's. It says nothing about how either
|
|
165
|
+
compares to a hardware-backed stack.
|
|
161
166
|
- **Reduced samples where marked.** SLH-DSA slow variants take 3 to 20 samples
|
|
162
|
-
rather than 100, because 100 signatures at
|
|
167
|
+
rather than 100, because 100 signatures at seconds each is not a benchmark,
|
|
163
168
|
it is an afternoon. Where n is small, p95 and p99 collapse onto the maximum and
|
|
164
169
|
are reported that way rather than dressed up.
|
|
165
170
|
- **Not a side-channel measurement.** Timing here is throughput, gathered without
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.3
|
|
4
|
+
|
|
5
|
+
Documentation and metadata. No code change.
|
|
6
|
+
|
|
7
|
+
Corrects stale performance figures. The README and BENCHMARKS.md both quoted a
|
|
8
|
+
4.5x median-to-p99 tail for ML-DSA signing and about 6.8 seconds for
|
|
9
|
+
SLH-DSA-SHA2-192s. Measured now, across both backends: the tail is 5.1x in
|
|
10
|
+
JavaScript and 3.5x on OpenSSL, and SLH-DSA-SHA2-192s signs in 4.3 s and 1.7 s.
|
|
11
|
+
|
|
12
|
+
Says what the package does. The README still described it as a wrapper around
|
|
13
|
+
@noble/post-quantum without mentioning that on Node 24 and later the primitives
|
|
14
|
+
run in OpenSSL 3.5. The npm description, which is what appears in registry search
|
|
15
|
+
results, listed the algorithms and nothing else: not the 2,103 NIST ACVP vectors,
|
|
16
|
+
not the 225-check interoperability matrix, not the reproducible build or the
|
|
17
|
+
provenance attestation. Those are the parts that distinguish this from any other
|
|
18
|
+
post-quantum wrapper, and they were absent from the one line most readers see.
|
|
19
|
+
|
|
20
|
+
A dependabot ignore blocks @noble/post-quantum 0.7.1 specifically, so it is not
|
|
21
|
+
reproposed weekly. The ACVP job fails the build on it regardless; this stops the
|
|
22
|
+
pull request being opened. 0.7.2 and later will still be offered and should be
|
|
23
|
+
accepted once the vectors pass.
|
|
24
|
+
|
|
25
|
+
## 1.5.2
|
|
26
|
+
|
|
27
|
+
**Reverts `@noble/post-quantum` to 0.7.0. Upgrade from 1.5.1 immediately if you
|
|
28
|
+
verify SLH-DSA signatures.**
|
|
29
|
+
|
|
30
|
+
1.5.1 bumped the backend to 0.7.1. That version **fails nine NIST ACVP SLH-DSA
|
|
31
|
+
verification vectors that 0.7.0 passes**, returning `false` where the vectors
|
|
32
|
+
require `true`. In practice that means a valid SLH-DSA signature can be
|
|
33
|
+
rejected. It is a correctness and availability fault, not a security weakening:
|
|
34
|
+
nothing invalid is accepted. No other algorithm family is affected, and ML-DSA
|
|
35
|
+
and ML-KEM pass unchanged on both versions.
|
|
36
|
+
|
|
37
|
+
The failures are confined to the **internal** signature interface
|
|
38
|
+
(`SLH-DSA.Verify_internal`), not the pre-hash interface, and appear across both
|
|
39
|
+
SHA2 and SHAKE parameter sets: SHA2-128s, SHA2-192f, SHA2-256f, SHA2-256s,
|
|
40
|
+
SHAKE-128f, SHAKE-128s, SHAKE-256f, SHAKE-256s. Reproduce with:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
npm run conformance:fetch
|
|
44
|
+
node conformance/run-acvp.mjs --set SLH-DSA-sigVer-FIPS205 --max-per-group 3
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
0.7.0 returns 87 passed, 0 failed, 21 skipped. 0.7.1 returns 78 passed, 9
|
|
48
|
+
failed.
|
|
49
|
+
|
|
50
|
+
This has been reported upstream. Until it is resolved the pin stays at 0.7.0,
|
|
51
|
+
and the ACVP job will fail the build on any attempt to move it, which is the
|
|
52
|
+
behaviour we want.
|
|
53
|
+
|
|
54
|
+
**Why this is worth saying plainly rather than quietly reverting.** The
|
|
55
|
+
conformance evidence in this repository is not decoration. It caught a real
|
|
56
|
+
regression in a dependency within hours of that dependency's release, on a
|
|
57
|
+
version bump whose own release notes described only hardening. A test suite that
|
|
58
|
+
passes is not evidence; a test suite that fails when something breaks is. Ours
|
|
59
|
+
did.
|
|
60
|
+
|
|
61
|
+
1.5.1 is deprecated on npm.
|
|
62
|
+
|
|
3
63
|
## 1.5.1
|
|
4
64
|
|
|
5
65
|
Dependency bump: `@noble/post-quantum` 0.7.0 to **0.7.1**, published 2026-08-27.
|
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/README.md
CHANGED
|
@@ -7,15 +7,18 @@ Post-quantum cryptography primitives for the KXCO stack.
|
|
|
7
7
|
[](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/actions/workflows/conformance.yml)
|
|
8
8
|
[](./LICENSE)
|
|
9
9
|
|
|
10
|
-
ML-DSA-65 (FIPS 204) and SLH-DSA-SHA2-192s (FIPS 205) signatures, ML-KEM-768 (FIPS 203) key encapsulation, and key fingerprinting utilities. Category 5 sets ML-DSA-87 and ML-KEM-1024 are also available.
|
|
10
|
+
ML-DSA-65 (FIPS 204) and SLH-DSA-SHA2-192s (FIPS 205) signatures, ML-KEM-768 (FIPS 203) key encapsulation, and key fingerprinting utilities. Category 5 sets ML-DSA-87 and ML-KEM-1024 are also available. All other `kxco-pq-*` packages depend on this one.
|
|
11
|
+
|
|
12
|
+
**On Node 24 and later the primitives run in OpenSSL 3.5**, not in JavaScript. Older Node and browsers use [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum). The two are interchangeable on the wire, which is checked rather than assumed: the interoperability matrix runs in full against both, and every report records which one produced it.
|
|
11
13
|
|
|
12
14
|
**Evidence, not adjectives:**
|
|
13
15
|
|
|
14
16
|
- [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), run against both backends. 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
|
|
17
|
+
- [BENCHMARKS.md](./BENCHMARKS.md): per-algorithm latency at p95/p99 on both backends and on x86-64 and arm64, plus memory. Two figures worth designing around: ML-DSA signing keeps a rejection-sampling tail on either backend (5.1x median-to-p99 in JavaScript, 3.5x on OpenSSL), and SLH-DSA-SHA2-192s signs in seconds rather than milliseconds (4.3 s and 1.7 s).
|
|
16
18
|
- [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
19
|
- [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
|
|
20
|
+
- [SECURITY.md](./SECURITY.md): reporting, release integrity, and the dependency policy.
|
|
21
|
+
- **Every release is reproducible and attested.** The published tarball rebuilds bit-for-bit from its own tag, verified in CI on every run, and each release carries a SLSA provenance attestation plus a CycloneDX SBOM at a permanent unauthenticated URL. A provenance attestation says a build happened in CI; the reproducible build says the artefact is the source. They are different claims and both are checkable without asking us for anything.
|
|
19
22
|
|
|
20
23
|
---
|
|
21
24
|
|
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
|
|
89
|
-
timing measurements of
|
|
90
|
-
|
|
91
|
-
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kxco-post-quantum",
|
|
3
|
-
"version": "1.5.
|
|
4
|
-
"description": "ML-DSA-65, ML-KEM-768 and SLH-DSA-SHA2-192s
|
|
3
|
+
"version": "1.5.3",
|
|
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",
|
|
7
7
|
"pqc",
|
|
@@ -112,10 +112,10 @@
|
|
|
112
112
|
},
|
|
113
113
|
"dependencies": {
|
|
114
114
|
"@noble/hashes": "2.3.0",
|
|
115
|
-
"@noble/post-quantum": "0.7.
|
|
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": {
|