kxco-post-quantum 1.2.1 → 1.4.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 +374 -256
- package/CONFORMANCE.md +180 -0
- package/LICENSE +202 -202
- package/MIGRATION.md +143 -0
- package/README.md +215 -140
- package/SECURITY.md +41 -28
- package/THREAT-MODEL.md +194 -0
- package/package.json +124 -107
- package/src/_context.js +69 -0
- package/src/derive.d.ts +20 -20
- package/src/derive.js +47 -47
- package/src/index.d.ts +13 -8
- package/src/index.js +24 -17
- package/src/kid.d.ts +21 -21
- package/src/kid.js +53 -53
- package/src/ml-dsa-87.d.ts +81 -0
- package/src/ml-dsa-87.js +127 -0
- package/src/ml-dsa.d.ts +78 -45
- package/src/ml-dsa.js +102 -78
- package/src/ml-kem-1024.d.ts +59 -0
- package/src/ml-kem-1024.js +90 -0
- package/src/ml-kem.d.ts +49 -49
- package/src/ml-kem.js +61 -61
- package/src/slh-dsa.d.ts +72 -46
- package/src/slh-dsa.js +106 -88
- package/src/webhook.d.ts +119 -119
- package/src/webhook.js +135 -135
package/THREAT-MODEL.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Threat model
|
|
2
|
+
|
|
3
|
+
What this package defends against, what it does not, and where the boundary
|
|
4
|
+
falls. Read this before deciding where to run it.
|
|
5
|
+
|
|
6
|
+
The short version: this is a software library written in JavaScript. It gives
|
|
7
|
+
you correct, standards-conformant ML-KEM, ML-DSA and SLH-DSA. It does not give
|
|
8
|
+
you resistance to an attacker who can measure the machine while it runs. If your
|
|
9
|
+
threat model includes that attacker, the key needs to live somewhere else.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## What is being protected
|
|
14
|
+
|
|
15
|
+
| Asset | Where it lives | Consequence if lost |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| ML-DSA / SLH-DSA private key | Caller-owned bytes in process memory | Attacker can forge signatures indefinitely |
|
|
18
|
+
| ML-KEM decapsulation key | Caller-owned bytes in process memory | Attacker can recover past and future shared secrets |
|
|
19
|
+
| ML-KEM shared secret | Return value, caller-owned | Attacker can decrypt the session that used it |
|
|
20
|
+
| Master seed used with `deriveSeed` | Caller-owned | Attacker can regenerate every derived key |
|
|
21
|
+
|
|
22
|
+
The library holds no keys of its own, opens no sockets, reads no files and keeps
|
|
23
|
+
no state between calls. Everything above is caller-owned material passed in and
|
|
24
|
+
returned. That places most of the security boundary in the calling application,
|
|
25
|
+
not here.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Attackers in scope
|
|
30
|
+
|
|
31
|
+
**A remote attacker who can choose messages, signatures, ciphertexts and
|
|
32
|
+
context strings.** They submit arbitrary and malformed input across the network
|
|
33
|
+
and observe accept/reject and returned bytes. This is the attacker the library
|
|
34
|
+
is built to withstand.
|
|
35
|
+
|
|
36
|
+
- Forgery is resisted by the parameter sets themselves. The signature and
|
|
37
|
+
verification paths follow FIPS 204 and FIPS 205 including the context-string
|
|
38
|
+
binding, so a signature made under one context does not verify under another.
|
|
39
|
+
Cross-implementation evidence is in [CONFORMANCE.md](CONFORMANCE.md).
|
|
40
|
+
- Malformed input is rejected rather than misinterpreted. Key and ciphertext
|
|
41
|
+
lengths are checked, ML-KEM performs the FIPS 203 §7.2/§7.3 input checks, and
|
|
42
|
+
a corrupted ciphertext yields an unrelated shared secret through implicit
|
|
43
|
+
rejection instead of an error that would distinguish the failure.
|
|
44
|
+
- Signature malleability is not a defence the caller has to add: verification is
|
|
45
|
+
over the exact encoded signature.
|
|
46
|
+
|
|
47
|
+
**An attacker who tampers with data at rest or in transit.** Detecting this is
|
|
48
|
+
the library's purpose and it does so as well as the underlying parameter sets.
|
|
49
|
+
|
|
50
|
+
**A quantum adversary running Shor's algorithm.** The three algorithm families
|
|
51
|
+
here rest on module-lattice and hash-based problems with no known efficient
|
|
52
|
+
quantum attack, which is the reason to use them. This is a statement about the
|
|
53
|
+
current state of cryptanalysis, not a proof, and it is the same assumption every
|
|
54
|
+
FIPS 203/204/205 deployment makes.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Attackers out of scope
|
|
59
|
+
|
|
60
|
+
These are real attackers. They are excluded because this library genuinely does
|
|
61
|
+
not defend against them, and saying otherwise would be worse than saying nothing.
|
|
62
|
+
|
|
63
|
+
**An attacker who can measure execution on the same machine.** Timing, cache
|
|
64
|
+
occupancy, branch prediction, memory access patterns, power draw,
|
|
65
|
+
electromagnetic emission. Nothing here is hardened against any of it.
|
|
66
|
+
|
|
67
|
+
This is not an oversight that a future release closes. It follows from the
|
|
68
|
+
runtime. JavaScript exposes no control over instruction selection, branch
|
|
69
|
+
layout, memory placement or cache behaviour; the JIT may specialise a hot path
|
|
70
|
+
on the values flowing through it; the garbage collector may copy secret bytes to
|
|
71
|
+
places the caller cannot reach and cannot clear. Constant-time execution cannot
|
|
72
|
+
be established, let alone maintained across engine versions, from inside the
|
|
73
|
+
language. The backend states this about itself in plain terms: *"There is no
|
|
74
|
+
protection against side-channel attacks."*
|
|
75
|
+
|
|
76
|
+
Two consequences worth being blunt about:
|
|
77
|
+
|
|
78
|
+
- Do not run signing or decapsulation on hardware that also runs untrusted
|
|
79
|
+
code. Shared-tenancy compute where another tenant can co-schedule on your
|
|
80
|
+
physical core is exactly the setting this fails in.
|
|
81
|
+
- Do not run it where an adversary can attach measurement equipment. Smart
|
|
82
|
+
cards, payment terminals, anything physically in an attacker's hands.
|
|
83
|
+
|
|
84
|
+
**An attacker who can read process memory.** A core dump, a debugger, a heap
|
|
85
|
+
snapshot or an in-process code-execution bug exposes every key the process is
|
|
86
|
+
holding. Best-effort zeroization is documented below and it does not change this.
|
|
87
|
+
|
|
88
|
+
**An attacker who has already achieved code execution in your process.** They
|
|
89
|
+
can call the library themselves with the keys it was given.
|
|
90
|
+
|
|
91
|
+
**A weak or predictable random source.** Key generation and hedged signing draw
|
|
92
|
+
from the platform CSPRNG through the backend. On a host whose entropy source is
|
|
93
|
+
broken or replayed, for example a VM image cloned after boot, the keys are
|
|
94
|
+
predictable and no property here survives that.
|
|
95
|
+
|
|
96
|
+
**An attacker positioned in the supply chain.** Addressed separately, by
|
|
97
|
+
release provenance and pinning rather than by anything in the runtime. See
|
|
98
|
+
[SECURITY.md](SECURITY.md).
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Where the residual risk actually sits
|
|
103
|
+
|
|
104
|
+
If you accept the two boxes above, the risk that remains is concentrated in one
|
|
105
|
+
place: **a long-lived signing key held in the memory of a general-purpose
|
|
106
|
+
process on shared hardware.**
|
|
107
|
+
|
|
108
|
+
Mitigations in rough order of how much they buy:
|
|
109
|
+
|
|
110
|
+
1. **Keep the key out of the process.** An HSM or KMS that performs ML-DSA
|
|
111
|
+
internally removes the whole out-of-scope column for that key, because the
|
|
112
|
+
key never enters a JavaScript heap. This library then handles verification
|
|
113
|
+
only, which touches no secrets and is therefore unaffected by every
|
|
114
|
+
side-channel concern above.
|
|
115
|
+
2. **Isolate the signer.** A dedicated host or a VM with no untrusted
|
|
116
|
+
co-tenancy, running only the signing service, reachable through a narrow
|
|
117
|
+
authenticated interface. This does not make the code constant-time; it
|
|
118
|
+
removes the attacker who could exploit that.
|
|
119
|
+
3. **Prefer short-lived keys where the design allows it.** A key rotated
|
|
120
|
+
frequently limits what a successful measurement attack yields.
|
|
121
|
+
4. **Keep verification and signing on separate hosts.** Verification is the
|
|
122
|
+
operation exposed to hostile input, and it holds no secrets. Nothing is
|
|
123
|
+
gained by placing it next to the key.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Choices this library makes, and why
|
|
128
|
+
|
|
129
|
+
**Hedged signing by default.** `mlDsa.sign` and `slhDsa.sign` do not pass
|
|
130
|
+
`extraEntropy`, so the backend draws fresh randomness for each signature. FIPS
|
|
131
|
+
204 permits both, and hedged signing is the recommended default: it removes the
|
|
132
|
+
class of fault and differential attacks that recover a key from two signatures
|
|
133
|
+
produced over the same message with the same nonce. The cost is that signatures
|
|
134
|
+
are not reproducible, which the interop matrix reports rather than glosses over.
|
|
135
|
+
Callers who need reproducible output should use the backend primitives directly
|
|
136
|
+
and accept the trade.
|
|
137
|
+
|
|
138
|
+
**Pre-hash strength is enforced, and it is stricter than NIST's sample
|
|
139
|
+
vectors.** The backend refuses a HashML-DSA or HashSLH-DSA pre-hash whose
|
|
140
|
+
collision strength falls below the parameter set's security category, for
|
|
141
|
+
example SHA2-256 with ML-DSA-87. NIST's published vector files pair every
|
|
142
|
+
approved hash with every parameter set, so a run against them shows those
|
|
143
|
+
combinations as skipped. That is the intended behaviour, and the conformance
|
|
144
|
+
report counts them explicitly rather than hiding them in a pass total.
|
|
145
|
+
|
|
146
|
+
**Dependencies are exact-pinned, not range-pinned.** `@noble/post-quantum` and
|
|
147
|
+
`@noble/hashes` are pinned to single versions. A cryptographic backend that
|
|
148
|
+
floats within a semver range means the bytes you ship are not the bytes you
|
|
149
|
+
tested. The cost is manual review at each upgrade, which is the point.
|
|
150
|
+
|
|
151
|
+
**Context strings are supported and bounded.** FIPS 204 §5.2 folds a caller
|
|
152
|
+
context into the signed message, which is how a signature made for one purpose
|
|
153
|
+
is prevented from verifying for another. The 255-byte limit is enforced rather
|
|
154
|
+
than truncated, because silent truncation would merge two contexts a caller
|
|
155
|
+
meant to keep apart.
|
|
156
|
+
|
|
157
|
+
**Constant-time comparison is claimed; constant-time cryptography is not.**
|
|
158
|
+
`kidEquals` compares key fingerprints without an early return, so it does not
|
|
159
|
+
leak how many leading bytes matched. That is an achievable property in
|
|
160
|
+
JavaScript for a fixed-length byte comparison and it is asserted. It says
|
|
161
|
+
nothing about the algorithm implementations, where the property is not
|
|
162
|
+
achievable and is not claimed. Two different statements about two different
|
|
163
|
+
things; do not read the first as implying the second.
|
|
164
|
+
|
|
165
|
+
**Zeroization is best effort and is not a security control here.** Key bytes are
|
|
166
|
+
cleared where the library owns the buffer. In a garbage-collected runtime with
|
|
167
|
+
immutable strings and copying collection, no library can guarantee that no copy
|
|
168
|
+
survives. Treat memory disclosure as total key compromise regardless.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## What would change this assessment
|
|
173
|
+
|
|
174
|
+
Not a roadmap, and none of it is in the package today. Stated so the boundary is
|
|
175
|
+
falsifiable rather than permanent by assertion.
|
|
176
|
+
|
|
177
|
+
- A native or WebAssembly backend built from a constant-time implementation
|
|
178
|
+
would move the timing and cache attacker from out of scope to partly in
|
|
179
|
+
scope, with published measurements to show it. It would not cover power or
|
|
180
|
+
electromagnetic analysis.
|
|
181
|
+
- Published timing measurements under a statistical test such as dudect would
|
|
182
|
+
turn "no claim" into a measured bound. Absence of a detected leak is not
|
|
183
|
+
absence of a leak, and any such result would be reported that way.
|
|
184
|
+
- FIPS 140-3 validation of a module used underneath would change what can be
|
|
185
|
+
asserted about the boundary, and is not the same as the algorithm-level
|
|
186
|
+
conformance evidence in [CONFORMANCE.md](CONFORMANCE.md).
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Reporting
|
|
191
|
+
|
|
192
|
+
Security contact and disclosure timelines are in [SECURITY.md](SECURITY.md).
|
|
193
|
+
If you find that any statement in this document is wrong, that is a finding
|
|
194
|
+
worth reporting on its own.
|
package/package.json
CHANGED
|
@@ -1,107 +1,124 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "kxco-post-quantum",
|
|
3
|
-
"version": "1.
|
|
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
|
-
"keywords": [
|
|
6
|
-
"post-quantum",
|
|
7
|
-
"pqc",
|
|
8
|
-
"ml-dsa",
|
|
9
|
-
"ml-kem",
|
|
10
|
-
"slh-dsa",
|
|
11
|
-
"sphincs",
|
|
12
|
-
"dilithium",
|
|
13
|
-
"kyber",
|
|
14
|
-
"nist",
|
|
15
|
-
"fips-203",
|
|
16
|
-
"fips-204",
|
|
17
|
-
"fips-205",
|
|
18
|
-
"webhook-signing",
|
|
19
|
-
"quantum-resistant",
|
|
20
|
-
"armature",
|
|
21
|
-
"key-derivation",
|
|
22
|
-
"hkdf",
|
|
23
|
-
"deterministic-keys",
|
|
24
|
-
"fingerprint",
|
|
25
|
-
"replay-protection",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
},
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "kxco-post-quantum",
|
|
3
|
+
"version": "1.4.0",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"post-quantum",
|
|
7
|
+
"pqc",
|
|
8
|
+
"ml-dsa",
|
|
9
|
+
"ml-kem",
|
|
10
|
+
"slh-dsa",
|
|
11
|
+
"sphincs",
|
|
12
|
+
"dilithium",
|
|
13
|
+
"kyber",
|
|
14
|
+
"nist",
|
|
15
|
+
"fips-203",
|
|
16
|
+
"fips-204",
|
|
17
|
+
"fips-205",
|
|
18
|
+
"webhook-signing",
|
|
19
|
+
"quantum-resistant",
|
|
20
|
+
"armature",
|
|
21
|
+
"key-derivation",
|
|
22
|
+
"hkdf",
|
|
23
|
+
"deterministic-keys",
|
|
24
|
+
"fingerprint",
|
|
25
|
+
"replay-protection",
|
|
26
|
+
"hybrid-signing",
|
|
27
|
+
"non-repudiation",
|
|
28
|
+
"ml-dsa-87",
|
|
29
|
+
"ml-kem-1024",
|
|
30
|
+
"category-5"
|
|
31
|
+
],
|
|
32
|
+
"license": "Apache-2.0",
|
|
33
|
+
"author": "Shayne Heffernan and John Heffernan",
|
|
34
|
+
"contributors": [
|
|
35
|
+
{
|
|
36
|
+
"name": "Shayne Heffernan"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "John Heffernan"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"homepage": "https://kxco.ai",
|
|
43
|
+
"funding": "https://kxco.ai",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/KnightsbridgeAIQ/kxco-post-quantum.git"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/KnightsbridgeAIQ/kxco-post-quantum/issues"
|
|
50
|
+
},
|
|
51
|
+
"type": "module",
|
|
52
|
+
"sideEffects": false,
|
|
53
|
+
"main": "./src/index.js",
|
|
54
|
+
"types": "./src/index.d.ts",
|
|
55
|
+
"exports": {
|
|
56
|
+
".": {
|
|
57
|
+
"types": "./src/index.d.ts",
|
|
58
|
+
"import": "./src/index.js"
|
|
59
|
+
},
|
|
60
|
+
"./ml-dsa": {
|
|
61
|
+
"types": "./src/ml-dsa.d.ts",
|
|
62
|
+
"import": "./src/ml-dsa.js"
|
|
63
|
+
},
|
|
64
|
+
"./ml-dsa-87": {
|
|
65
|
+
"types": "./src/ml-dsa-87.d.ts",
|
|
66
|
+
"import": "./src/ml-dsa-87.js"
|
|
67
|
+
},
|
|
68
|
+
"./ml-kem": {
|
|
69
|
+
"types": "./src/ml-kem.d.ts",
|
|
70
|
+
"import": "./src/ml-kem.js"
|
|
71
|
+
},
|
|
72
|
+
"./ml-kem-1024": {
|
|
73
|
+
"types": "./src/ml-kem-1024.d.ts",
|
|
74
|
+
"import": "./src/ml-kem-1024.js"
|
|
75
|
+
},
|
|
76
|
+
"./slh-dsa": {
|
|
77
|
+
"types": "./src/slh-dsa.d.ts",
|
|
78
|
+
"import": "./src/slh-dsa.js"
|
|
79
|
+
},
|
|
80
|
+
"./derive": {
|
|
81
|
+
"types": "./src/derive.d.ts",
|
|
82
|
+
"import": "./src/derive.js"
|
|
83
|
+
},
|
|
84
|
+
"./webhook": {
|
|
85
|
+
"types": "./src/webhook.d.ts",
|
|
86
|
+
"import": "./src/webhook.js"
|
|
87
|
+
},
|
|
88
|
+
"./kid": {
|
|
89
|
+
"types": "./src/kid.d.ts",
|
|
90
|
+
"import": "./src/kid.js"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"files": [
|
|
94
|
+
"src",
|
|
95
|
+
"README.md",
|
|
96
|
+
"LICENSE",
|
|
97
|
+
"CONFORMANCE.md",
|
|
98
|
+
"THREAT-MODEL.md",
|
|
99
|
+
"MIGRATION.md",
|
|
100
|
+
"SECURITY.md",
|
|
101
|
+
"CHANGELOG.md"
|
|
102
|
+
],
|
|
103
|
+
"engines": {
|
|
104
|
+
"node": ">=20.19"
|
|
105
|
+
},
|
|
106
|
+
"dependencies": {
|
|
107
|
+
"@noble/hashes": "2.3.0",
|
|
108
|
+
"@noble/post-quantum": "0.7.0"
|
|
109
|
+
},
|
|
110
|
+
"scripts": {
|
|
111
|
+
"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",
|
|
112
|
+
"test:vectors": "node test/run-vectors.js",
|
|
113
|
+
"generate:vectors": "node test/generate-vectors.js > test/vectors.json",
|
|
114
|
+
"bench": "node bench/bench.js",
|
|
115
|
+
"conformance:fetch": "node conformance/fetch-vectors.mjs",
|
|
116
|
+
"conformance:acvp": "node conformance/run-acvp.mjs --json conformance/results/acvp.json",
|
|
117
|
+
"conformance:interop": "node conformance/interop/run-interop.mjs --json conformance/results/interop.json",
|
|
118
|
+
"sbom": "npm sbom --sbom-format cyclonedx --sbom-type library"
|
|
119
|
+
},
|
|
120
|
+
"publishConfig": {
|
|
121
|
+
"provenance": true,
|
|
122
|
+
"access": "public"
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/_context.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Internal: FIPS 204 / FIPS 205 signature context strings.
|
|
2
|
+
//
|
|
3
|
+
// Not part of the public exports map. Shared by ml-dsa.js and slh-dsa.js
|
|
4
|
+
// because a validator for security-relevant input should exist once, even
|
|
5
|
+
// though the small byte/hex helpers in those modules are duplicated.
|
|
6
|
+
//
|
|
7
|
+
// FIPS 204 section 5.2 (and FIPS 205 equivalently) allow an optional context
|
|
8
|
+
// string of at most 255 bytes, mixed into the message representative. It gives
|
|
9
|
+
// domain separation: a signature made under one context does not verify under
|
|
10
|
+
// another, or under no context at all.
|
|
11
|
+
//
|
|
12
|
+
// KXCO derives keys per domain via deriveSeed(master, info), which separates at
|
|
13
|
+
// the KEY level. Context separates at the SIGNATURE level, so one key can sign
|
|
14
|
+
// for several domains without a signature being replayable across them. The two
|
|
15
|
+
// are complementary, not alternatives.
|
|
16
|
+
|
|
17
|
+
const enc = new TextEncoder()
|
|
18
|
+
|
|
19
|
+
/** FIPS 204 section 5.2: |ctx| <= 255. */
|
|
20
|
+
export const MAX_CONTEXT_BYTES = 255
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Normalise the options bag into a context byte string, or undefined.
|
|
24
|
+
*
|
|
25
|
+
* Returns undefined for "no context", which makes the caller take the exact
|
|
26
|
+
* code path it took before this parameter existed. An empty context is
|
|
27
|
+
* cryptographically identical to no context, so it also returns undefined
|
|
28
|
+
* rather than passing a zero-length array down.
|
|
29
|
+
*
|
|
30
|
+
* THROWS on caller misuse (wrong type, over-length). This is deliberate and it
|
|
31
|
+
* differs from verify()'s usual fail-closed behaviour: a malformed context is a
|
|
32
|
+
* programming error, not a bad signature, and silently returning false would
|
|
33
|
+
* hide the bug behind an outcome that looks like a normal verification failure.
|
|
34
|
+
* No existing call site passes this argument, so nothing can regress.
|
|
35
|
+
*
|
|
36
|
+
* @param {{ context?: Uint8Array|Buffer|string }} [opts]
|
|
37
|
+
* @returns {Uint8Array|undefined}
|
|
38
|
+
*/
|
|
39
|
+
export function normalizeContext(opts) {
|
|
40
|
+
if (opts === undefined || opts === null) return undefined
|
|
41
|
+
|
|
42
|
+
if (typeof opts !== 'object' || Array.isArray(opts) || opts instanceof Uint8Array) {
|
|
43
|
+
throw new TypeError(
|
|
44
|
+
'expected an options object such as { context }, not a bare value',
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { context } = opts
|
|
49
|
+
if (context === undefined || context === null) return undefined
|
|
50
|
+
|
|
51
|
+
let bytes
|
|
52
|
+
if (context instanceof Uint8Array) {
|
|
53
|
+
bytes = context
|
|
54
|
+
} else if (typeof context === 'string') {
|
|
55
|
+
bytes = enc.encode(context)
|
|
56
|
+
} else {
|
|
57
|
+
throw new TypeError('context must be a Uint8Array or a string')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (bytes.length > MAX_CONTEXT_BYTES) {
|
|
61
|
+
throw new RangeError(
|
|
62
|
+
`context must be at most ${MAX_CONTEXT_BYTES} bytes (FIPS 204 section 5.2), got ${bytes.length}`,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Empty context is identical to no context under FIPS 204, so collapse it
|
|
67
|
+
// and keep the legacy call path byte-for-byte.
|
|
68
|
+
return bytes.length === 0 ? undefined : bytes
|
|
69
|
+
}
|
package/src/derive.d.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Derive a deterministic seed from a master secret + an info string,
|
|
5
|
-
* using HKDF-SHA-512 with an empty salt.
|
|
6
|
-
*
|
|
7
|
-
* Same `master + info + length` always produces the same seed.
|
|
8
|
-
*
|
|
9
|
-
* @param master — high-entropy input keying material (≥16 bytes)
|
|
10
|
-
* @param info — domain separation tag (eg. 'kxco-platform-ml-dsa-65-v1')
|
|
11
|
-
* @param length — output seed length in bytes (32 for ML-DSA, 64 for ML-KEM)
|
|
12
|
-
*
|
|
13
|
-
* @throws {Error} if `master` is shorter than 16 bytes
|
|
14
|
-
* @throws {Error} if `info` is empty or not a string
|
|
15
|
-
*/
|
|
16
|
-
export function deriveSeed(
|
|
17
|
-
master: Buffer | Uint8Array,
|
|
18
|
-
info: string,
|
|
19
|
-
length: number,
|
|
20
|
-
): Buffer
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Derive a deterministic seed from a master secret + an info string,
|
|
5
|
+
* using HKDF-SHA-512 with an empty salt.
|
|
6
|
+
*
|
|
7
|
+
* Same `master + info + length` always produces the same seed.
|
|
8
|
+
*
|
|
9
|
+
* @param master — high-entropy input keying material (≥16 bytes)
|
|
10
|
+
* @param info — domain separation tag (eg. 'kxco-platform-ml-dsa-65-v1')
|
|
11
|
+
* @param length — output seed length in bytes (32 for ML-DSA, 64 for ML-KEM)
|
|
12
|
+
*
|
|
13
|
+
* @throws {Error} if `master` is shorter than 16 bytes
|
|
14
|
+
* @throws {Error} if `info` is empty or not a string
|
|
15
|
+
*/
|
|
16
|
+
export function deriveSeed(
|
|
17
|
+
master: Buffer | Uint8Array,
|
|
18
|
+
info: string,
|
|
19
|
+
length: number,
|
|
20
|
+
): Buffer
|
package/src/derive.js
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
// Deterministic key derivation via HKDF-SHA-512.
|
|
2
|
-
//
|
|
3
|
-
// Same seed + same info = same keypair, every time. This is how the KXCO
|
|
4
|
-
// platform reproduces its signing identity across replicas without storing
|
|
5
|
-
// the private key in a database: the master key is the env var, the rest is
|
|
6
|
-
// pure derivation.
|
|
7
|
-
//
|
|
8
|
-
// Domain separation through `info` is critical — using the same master key
|
|
9
|
-
// for different purposes (signing vs encryption) MUST use distinct info
|
|
10
|
-
// strings or you create a cross-protocol attack surface.
|
|
11
|
-
//
|
|
12
|
-
// Isomorphic: uses @noble/hashes/hkdf which runs identically in Node 18+ and
|
|
13
|
-
// modern browsers. Returns Buffer when running on Node (for backwards
|
|
14
|
-
// compatibility with existing callers), Uint8Array in browsers.
|
|
15
|
-
|
|
16
|
-
import { hkdf } from '@noble/hashes/hkdf.js'
|
|
17
|
-
import { sha512 } from '@noble/hashes/sha2.js'
|
|
18
|
-
|
|
19
|
-
const HAS_BUFFER = typeof Buffer !== 'undefined'
|
|
20
|
-
const enc = new TextEncoder()
|
|
21
|
-
|
|
22
|
-
function toBytes(input) {
|
|
23
|
-
if (input instanceof Uint8Array) return input
|
|
24
|
-
if (typeof input === 'string') return enc.encode(input)
|
|
25
|
-
throw new Error('expected Uint8Array or string')
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Derive a deterministic seed from a master secret + an info string.
|
|
30
|
-
*
|
|
31
|
-
* @param {Buffer|Uint8Array|string} master — high-entropy keying material (>= 16 bytes)
|
|
32
|
-
* @param {string} info — domain separation tag
|
|
33
|
-
* @param {number} length — output seed length in bytes
|
|
34
|
-
* @returns {Buffer|Uint8Array}
|
|
35
|
-
*/
|
|
36
|
-
export function deriveSeed(master, info, length) {
|
|
37
|
-
const ikm = toBytes(master)
|
|
38
|
-
if (!ikm || ikm.length < 16) {
|
|
39
|
-
throw new Error('deriveSeed: master keying material must be at least 16 bytes')
|
|
40
|
-
}
|
|
41
|
-
if (!info || typeof info !== 'string') {
|
|
42
|
-
throw new Error('deriveSeed: info string is required for domain separation')
|
|
43
|
-
}
|
|
44
|
-
const salt = new Uint8Array(32) // 32 zero bytes — fine with high-entropy IKM
|
|
45
|
-
const out = hkdf(sha512, ikm, salt, enc.encode(info), length)
|
|
46
|
-
return HAS_BUFFER ? Buffer.from(out) : out
|
|
47
|
-
}
|
|
1
|
+
// Deterministic key derivation via HKDF-SHA-512.
|
|
2
|
+
//
|
|
3
|
+
// Same seed + same info = same keypair, every time. This is how the KXCO
|
|
4
|
+
// platform reproduces its signing identity across replicas without storing
|
|
5
|
+
// the private key in a database: the master key is the env var, the rest is
|
|
6
|
+
// pure derivation.
|
|
7
|
+
//
|
|
8
|
+
// Domain separation through `info` is critical — using the same master key
|
|
9
|
+
// for different purposes (signing vs encryption) MUST use distinct info
|
|
10
|
+
// strings or you create a cross-protocol attack surface.
|
|
11
|
+
//
|
|
12
|
+
// Isomorphic: uses @noble/hashes/hkdf which runs identically in Node 18+ and
|
|
13
|
+
// modern browsers. Returns Buffer when running on Node (for backwards
|
|
14
|
+
// compatibility with existing callers), Uint8Array in browsers.
|
|
15
|
+
|
|
16
|
+
import { hkdf } from '@noble/hashes/hkdf.js'
|
|
17
|
+
import { sha512 } from '@noble/hashes/sha2.js'
|
|
18
|
+
|
|
19
|
+
const HAS_BUFFER = typeof Buffer !== 'undefined'
|
|
20
|
+
const enc = new TextEncoder()
|
|
21
|
+
|
|
22
|
+
function toBytes(input) {
|
|
23
|
+
if (input instanceof Uint8Array) return input
|
|
24
|
+
if (typeof input === 'string') return enc.encode(input)
|
|
25
|
+
throw new Error('expected Uint8Array or string')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Derive a deterministic seed from a master secret + an info string.
|
|
30
|
+
*
|
|
31
|
+
* @param {Buffer|Uint8Array|string} master — high-entropy keying material (>= 16 bytes)
|
|
32
|
+
* @param {string} info — domain separation tag
|
|
33
|
+
* @param {number} length — output seed length in bytes
|
|
34
|
+
* @returns {Buffer|Uint8Array}
|
|
35
|
+
*/
|
|
36
|
+
export function deriveSeed(master, info, length) {
|
|
37
|
+
const ikm = toBytes(master)
|
|
38
|
+
if (!ikm || ikm.length < 16) {
|
|
39
|
+
throw new Error('deriveSeed: master keying material must be at least 16 bytes')
|
|
40
|
+
}
|
|
41
|
+
if (!info || typeof info !== 'string') {
|
|
42
|
+
throw new Error('deriveSeed: info string is required for domain separation')
|
|
43
|
+
}
|
|
44
|
+
const salt = new Uint8Array(32) // 32 zero bytes — fine with high-entropy IKM
|
|
45
|
+
const out = hkdf(sha512, ikm, salt, enc.encode(info), length)
|
|
46
|
+
return HAS_BUFFER ? Buffer.from(out) : out
|
|
47
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
|
|
3
|
-
export * as mlDsa from './ml-dsa.js'
|
|
4
|
-
export * as mlKem from './ml-kem.js'
|
|
5
|
-
export * as slhDsa from './slh-dsa.js'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export * as
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
export * as mlDsa from './ml-dsa.js'
|
|
4
|
+
export * as mlKem from './ml-kem.js'
|
|
5
|
+
export * as slhDsa from './slh-dsa.js'
|
|
6
|
+
|
|
7
|
+
/** ML-DSA-87 (Category 5). Not a CNSA 2.0 compliance claim; see CONFORMANCE.md. */
|
|
8
|
+
export * as mlDsa87 from './ml-dsa-87.js'
|
|
9
|
+
/** ML-KEM-1024 (Category 5). Not a CNSA 2.0 compliance claim; see CONFORMANCE.md. */
|
|
10
|
+
export * as mlKem1024 from './ml-kem-1024.js'
|
|
11
|
+
export * from './derive.js'
|
|
12
|
+
export * from './kid.js'
|
|
13
|
+
export * as webhook from './webhook.js'
|