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 +45 -0
- package/README.md +66 -0
- package/package.json +2 -2
- package/src/backend.js +73 -0
- package/src/index.js +1 -1
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.
|
|
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'
|