kxco-post-quantum 1.6.3 → 1.7.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 +47 -0
- package/README.md +41 -1
- package/package.json +3 -3
- package/src/backend.js +73 -0
- package/src/index.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.0
|
|
4
|
+
|
|
5
|
+
Additive. No existing export changes shape, no wire format moves, and the
|
|
6
|
+
default behaviour of the package is unchanged.
|
|
7
|
+
|
|
8
|
+
**`requireNativeBackend([algorithms])`.** The package picks OpenSSL where the
|
|
9
|
+
runtime has the FIPS 203/204/205 primitives and the JavaScript implementation
|
|
10
|
+
otherwise. Both produce identical bytes, so the fallback is right nearly always.
|
|
11
|
+
It is wrong under a control that says cryptography must execute inside a
|
|
12
|
+
validated module: there the fallback means the control is not in force and
|
|
13
|
+
nothing says so. This throws `ERR_KXCO_PQ_BACKEND` instead, carrying `actual`,
|
|
14
|
+
`missing` and `available` so the failure says which requirement was not met.
|
|
15
|
+
|
|
16
|
+
**`KXCO_PQ_REQUIRE_NATIVE=1`.** The same requirement from the environment,
|
|
17
|
+
because the team under the control is usually not the team calling the library.
|
|
18
|
+
A process that has landed on the JavaScript backend then fails at import rather
|
|
19
|
+
than at its first signature.
|
|
20
|
+
|
|
21
|
+
It asserts, it does not switch. There is still no way to force a backend, for
|
|
22
|
+
the reason there never was: a flag that changed which implementation signed
|
|
23
|
+
would change what a customer's evidence means. This only refuses to continue on
|
|
24
|
+
the wrong one.
|
|
25
|
+
|
|
26
|
+
It also claims only what it can see. Whether the OpenSSL underneath is a
|
|
27
|
+
FIPS-validated module is a property of the operator's build; no library can
|
|
28
|
+
determine that from the inside, and this does not pretend to. It removes the
|
|
29
|
+
silent fallback, which is the part this package owns.
|
|
30
|
+
|
|
31
|
+
Tested on both backends without mocking: CI runs Node 20 and 22, which have no
|
|
32
|
+
native primitives and exercise the refusal, and Node 24, which exercises the
|
|
33
|
+
accept path.
|
|
34
|
+
|
|
35
|
+
## 1.6.4
|
|
36
|
+
|
|
37
|
+
Documentation. The npm description said "2,103 NIST ACVP vectors and 225
|
|
38
|
+
interop checks, 0 failed", which reads as 2,103 passing. The evidence records
|
|
39
|
+
2,103 tested, 1,793 passed, 0 failed, 310 skipped, and CONFORMANCE.md says a
|
|
40
|
+
skip is not a pass.
|
|
41
|
+
|
|
42
|
+
Twelve sibling READMEs were corrected for this yesterday and this package's own
|
|
43
|
+
README has been right for months. The description was the one place it
|
|
44
|
+
survived, and a description only changes when the package is published, so it
|
|
45
|
+
needed a release rather than a commit. It is the first thing anyone reads.
|
|
46
|
+
|
|
47
|
+
Also corrects the count of what the downloadable bundle contains, from 1,479
|
|
48
|
+
to the 1,551 actually present in the published v1.6.3 assets.
|
|
49
|
+
|
|
3
50
|
## 1.6.3
|
|
4
51
|
|
|
5
52
|
Release plumbing only. No source change.
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ languages are **JavaScript and C** (OpenSSL 3.5 on Node 24+).
|
|
|
36
36
|
|
|
37
37
|
**Evidence, not adjectives:**
|
|
38
38
|
|
|
39
|
-
- [CONFORMANCE.md](./CONFORMANCE.md): NIST ACVP vectors for FIPS 203/204/205: **2,103 vectors, 1,793 passed, 0 failed, 310 skipped**, where every skip is this library refusing a pre-hash weaker than the parameter set and is listed individually with its reason. CONFORMANCE.md says a skip is not a pass, so the headline says so too. Of those, **1,
|
|
39
|
+
- [CONFORMANCE.md](./CONFORMANCE.md): NIST ACVP vectors for FIPS 203/204/205: **2,103 vectors, 1,793 passed, 0 failed, 310 skipped**, where every skip is this library refusing a pre-hash weaker than the parameter set and is listed individually with its reason. CONFORMANCE.md says a skip is not a pass, so the headline says so too. Of those, **1,551 are in the downloadable evidence bundle**, measured from the published v1.6.3 assets: 855 in `02-conformance-acvp.json`, 624 in `02b-conformance-acvp-fips205.json` and a 72-vector sample of SLH-DSA signature generation in `02d-conformance-acvp-fips205-siggen.json`. Signature generation is sampled rather than shipped whole because it signs in seconds per operation; the full set is reproduced on demand with `node conformance/run-acvp.mjs --set SLH-DSA-sigGen-FIPS205`. The bundle names that gap rather than leaving it to be noticed. Plus 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`.
|
|
40
40
|
- [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).
|
|
41
41
|
- [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.
|
|
42
42
|
- [MIGRATION.md](./MIGRATION.md): moving an RSA or ECDSA system across, and moving between versions of this package.
|
|
@@ -271,6 +271,46 @@ Low-level helpers for the KXCO hybrid webhook pattern: `envelope`, `hmacHex`, `v
|
|
|
271
271
|
|
|
272
272
|
---
|
|
273
273
|
|
|
274
|
+
## Requiring the native backend
|
|
275
|
+
|
|
276
|
+
This package picks its implementation at import time: OpenSSL 3.5 where the
|
|
277
|
+
runtime provides the FIPS 203/204/205 primitives, the JavaScript implementation
|
|
278
|
+
otherwise. Both produce identical wire bytes, so falling back is the right
|
|
279
|
+
default and nothing about a signature changes.
|
|
280
|
+
|
|
281
|
+
It is the wrong default in one situation: a deployment under a control that says
|
|
282
|
+
cryptography must execute inside a validated module. There, a silent fallback
|
|
283
|
+
means the control is not in force and nothing says so.
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
import { requireNativeBackend } from 'kxco-post-quantum'
|
|
287
|
+
|
|
288
|
+
requireNativeBackend(['ML-DSA-65', 'ML-KEM-768'])
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
It throws `ERR_KXCO_PQ_BACKEND` if the JavaScript backend is live, or if the
|
|
292
|
+
OpenSSL present cannot provide a parameter set you named. The error carries
|
|
293
|
+
`actual`, `missing` and `available` so a failure says which, rather than only
|
|
294
|
+
that.
|
|
295
|
+
|
|
296
|
+
Operators can enforce it without touching application code, which matters
|
|
297
|
+
because the team under the control is usually not the team calling the library:
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
KXCO_PQ_REQUIRE_NATIVE=1
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Set that and a process which has landed on the JavaScript backend fails at
|
|
304
|
+
import, before its first signature rather than after.
|
|
305
|
+
|
|
306
|
+
**What this does and does not claim.** It asserts that OpenSSL is doing the
|
|
307
|
+
maths. Whether that OpenSSL is a FIPS-validated module is a property of your
|
|
308
|
+
build, not of this package, and no library can see it from the inside. What it
|
|
309
|
+
removes is the silent fallback, which is the part this package is responsible
|
|
310
|
+
for. It is an assertion, never a switch: it cannot change which backend runs,
|
|
311
|
+
because a flag that changed which implementation signed would change what your
|
|
312
|
+
evidence means.
|
|
313
|
+
|
|
274
314
|
## Where this fits
|
|
275
315
|
|
|
276
316
|
This is the primitive layer, and it stays that: keys, signatures, encapsulation
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kxco-post-quantum",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "ML-DSA-65, ML-KEM-768 and SLH-DSA-SHA2-192s with key fingerprinting. OpenSSL 3.5 primitives on Node 24+, JavaScript elsewhere. 2,103 NIST ACVP vectors
|
|
3
|
+
"version": "1.7.0",
|
|
4
|
+
"description": "ML-DSA-65, ML-KEM-768 and SLH-DSA-SHA2-192s with key fingerprinting. OpenSSL 3.5 primitives on Node 24+, JavaScript elsewhere. 2,103 NIST ACVP vectors: 1,793 passed, 0 failed, 310 skipped. 225 interop checks, 0 failed. Reproducible builds and provenance.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"post-quantum",
|
|
7
7
|
"pqc",
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
"@noble/post-quantum": "0.7.0"
|
|
130
130
|
},
|
|
131
131
|
"scripts": {
|
|
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",
|
|
132
|
+
"test": "node --test test/basic.test.js && node --test test/backend.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",
|
|
133
133
|
"test:vectors": "node test/run-vectors.js",
|
|
134
134
|
"generate:vectors": "node test/generate-vectors.js > test/vectors.json",
|
|
135
135
|
"bench": "node bench/bench.js",
|
package/src/backend.js
CHANGED
|
@@ -10,9 +10,31 @@
|
|
|
10
10
|
// backend from here: the two produce identical wire bytes, and a runtime flag
|
|
11
11
|
// that changed which one signed would be a flag that changes what a customer's
|
|
12
12
|
// evidence means.
|
|
13
|
+
//
|
|
14
|
+
// `requireNativeBackend` at the bottom is not an exception to that. It cannot
|
|
15
|
+
// change which backend runs. It refuses to let the process continue on the
|
|
16
|
+
// wrong one, which is a different thing, and the thing a deployment under a
|
|
17
|
+
// validated-module control actually needs.
|
|
13
18
|
|
|
14
19
|
import { native } from '#native'
|
|
15
20
|
|
|
21
|
+
// An operator control. A deployment under a validated-module requirement is
|
|
22
|
+
// usually not the same team as the one calling this library, so the
|
|
23
|
+
// requirement has to be enforceable from the environment rather than only from
|
|
24
|
+
// application code. Set KXCO_PQ_REQUIRE_NATIVE=1 and a process that has landed
|
|
25
|
+
// on the JavaScript backend fails at import, not at its first signature.
|
|
26
|
+
//
|
|
27
|
+
// Read defensively: this module is also loaded in browsers, where `process`
|
|
28
|
+
// does not exist.
|
|
29
|
+
const REQUIRE_NATIVE = (() => {
|
|
30
|
+
try {
|
|
31
|
+
const v = globalThis.process?.env?.KXCO_PQ_REQUIRE_NATIVE
|
|
32
|
+
return v === '1' || v === 'true'
|
|
33
|
+
} catch {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
})()
|
|
37
|
+
|
|
16
38
|
/**
|
|
17
39
|
* Describe the active backend.
|
|
18
40
|
*
|
|
@@ -46,3 +68,54 @@ export function backend() {
|
|
|
46
68
|
export function isNative(alg) {
|
|
47
69
|
return native !== null && native.supports(alg)
|
|
48
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Refuse to run unless the cryptography is executing in the native backend.
|
|
74
|
+
*
|
|
75
|
+
* This is an assertion, not a switch. It cannot change which implementation
|
|
76
|
+
* signs, for the reason given at the top of this file. What it does is stop a
|
|
77
|
+
* process that has silently landed on the JavaScript backend when the operator
|
|
78
|
+
* required otherwise, before it produces its first signature rather than after.
|
|
79
|
+
*
|
|
80
|
+
* The default behaviour of this package is to fall back, and that default is
|
|
81
|
+
* correct: the two backends produce identical wire bytes. It is wrong in one
|
|
82
|
+
* specific situation, which is a deployment under a control that says the
|
|
83
|
+
* cryptography must execute inside a validated module. There, a silent fallback
|
|
84
|
+
* means the control is not in force and nothing says so.
|
|
85
|
+
*
|
|
86
|
+
* Note what this does and does not claim. It asserts that OpenSSL is doing the
|
|
87
|
+
* maths. Whether that OpenSSL is a FIPS-validated module is a property of the
|
|
88
|
+
* operator's build, not of this package, and this function cannot see it. It
|
|
89
|
+
* removes the fallback, which is the part we can be responsible for.
|
|
90
|
+
*
|
|
91
|
+
* @param {string[]} [algorithms] — parameter sets that must run natively.
|
|
92
|
+
* Omit to require only that the native backend is present at all.
|
|
93
|
+
* @throws {Error} with `code: 'ERR_KXCO_PQ_BACKEND'` when the requirement fails.
|
|
94
|
+
*/
|
|
95
|
+
export function requireNativeBackend(algorithms) {
|
|
96
|
+
const b = backend()
|
|
97
|
+
if (b.kind !== 'openssl') {
|
|
98
|
+
throw backendError(
|
|
99
|
+
`the native backend is required and is not present: ${b.reason}`,
|
|
100
|
+
{ required: 'openssl', actual: b.kind, reason: b.reason },
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
const missing = (algorithms ?? []).filter((a) => !isNative(a))
|
|
104
|
+
if (missing.length) {
|
|
105
|
+
throw backendError(
|
|
106
|
+
`the native backend is required for ${missing.join(', ')}, ` +
|
|
107
|
+
`and this OpenSSL does not provide ${missing.length > 1 ? 'them' : 'it'}`,
|
|
108
|
+
{ required: 'openssl', actual: b.kind, missing, available: b.parameterSets },
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
return b
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function backendError(message, detail) {
|
|
115
|
+
const err = new Error(message)
|
|
116
|
+
err.code = 'ERR_KXCO_PQ_BACKEND'
|
|
117
|
+
Object.assign(err, detail)
|
|
118
|
+
return err
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (REQUIRE_NATIVE) requireNativeBackend()
|
package/src/index.js
CHANGED
|
@@ -37,4 +37,4 @@ export * as jws from './jws.js'
|
|
|
37
37
|
|
|
38
38
|
// Reports which backend is doing the maths in this process, for evidence
|
|
39
39
|
// bundles and support. It reports; it never switches.
|
|
40
|
-
export { backend, isNative } from './backend.js'
|
|
40
|
+
export { backend, isNative, requireNativeBackend } from './backend.js'
|