kxco-post-quantum 1.6.4 → 1.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.7.1
4
+
5
+ Release integrity. No source change to the library.
6
+
7
+ Release assets are now signed with ML-DSA-65 through this package's own signing
8
+ path, and each release carries a SLSA provenance file. Until now the npm tarball
9
+ had provenance and the GitHub release, where the evidence bundle is actually
10
+ downloaded from, had neither a signature nor a statement of where it was built.
11
+
12
+ The public key is committed as release-signing-key.pub.hex and published with
13
+ each release. The signing script refuses to run if the seed in CI stops deriving
14
+ that key, and verifies every signature immediately after producing it.
15
+
16
+ ## 1.7.0
17
+
18
+ Additive. No existing export changes shape, no wire format moves, and the
19
+ default behaviour of the package is unchanged.
20
+
21
+ **`requireNativeBackend([algorithms])`.** The package picks OpenSSL where the
22
+ runtime has the FIPS 203/204/205 primitives and the JavaScript implementation
23
+ otherwise. Both produce identical bytes, so the fallback is right nearly always.
24
+ It is wrong under a control that says cryptography must execute inside a
25
+ validated module: there the fallback means the control is not in force and
26
+ nothing says so. This throws `ERR_KXCO_PQ_BACKEND` instead, carrying `actual`,
27
+ `missing` and `available` so the failure says which requirement was not met.
28
+
29
+ **`KXCO_PQ_REQUIRE_NATIVE=1`.** The same requirement from the environment,
30
+ because the team under the control is usually not the team calling the library.
31
+ A process that has landed on the JavaScript backend then fails at import rather
32
+ than at its first signature.
33
+
34
+ It asserts, it does not switch. There is still no way to force a backend, for
35
+ the reason there never was: a flag that changed which implementation signed
36
+ would change what a customer's evidence means. This only refuses to continue on
37
+ the wrong one.
38
+
39
+ It also claims only what it can see. Whether the OpenSSL underneath is a
40
+ FIPS-validated module is a property of the operator's build; no library can
41
+ determine that from the inside, and this does not pretend to. It removes the
42
+ silent fallback, which is the part this package owns.
43
+
44
+ Tested on both backends without mocking: CI runs Node 20 and 22, which have no
45
+ native primitives and exercise the refusal, and Node 24, which exercises the
46
+ accept path.
47
+
3
48
  ## 1.6.4
4
49
 
5
50
  Documentation. The npm description said "2,103 NIST ACVP vectors and 225
package/README.md CHANGED
@@ -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
@@ -304,3 +344,29 @@ Apache-2.0. See [LICENSE](./LICENSE).
304
344
  ## Maintainers
305
345
 
306
346
  Shayne Heffernan and John Heffernan — [KXCO by Knightsbridge](https://kxco.ai)
347
+
348
+ ## Verifying a release
349
+
350
+ Every release asset is signed with ML-DSA-65 by this package's own signing path,
351
+ and carries a SLSA provenance file recording the workflow that built it.
352
+
353
+ ```
354
+ manifest-node24.x.json the bundle's manifest
355
+ evidence-node24.x.zip the bundle
356
+ evidence-node24.x.zip.sig ML-DSA-65 signature over the zip, hex
357
+ evidence.intoto.jsonl SLSA provenance
358
+ release-signing-key.pub.hex the public key, also committed to this repository
359
+ ```
360
+
361
+ ```js
362
+ import { readFileSync } from 'node:fs'
363
+ import { mlDsa } from 'kxco-post-quantum'
364
+
365
+ const pub = Buffer.from(readFileSync('release-signing-key.pub.hex', 'utf8').trim(), 'hex')
366
+ const sig = readFileSync('evidence-node24.x.zip.sig', 'utf8').trim()
367
+ mlDsa.verify(pub, readFileSync('evidence-node24.x.zip'), sig) // true
368
+ ```
369
+
370
+ Compare the public key against the copy in this repository before trusting a
371
+ signature: a key served alongside the artefact it signs proves only that the
372
+ same party produced both.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kxco-post-quantum",
3
- "version": "1.6.4",
3
+ "version": "1.7.1",
4
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",
@@ -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'