kxco-post-quantum 1.3.0 → 1.4.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/SECURITY.md CHANGED
@@ -1,41 +1,63 @@
1
- # Security Policy
2
-
3
- ## Reporting a vulnerability
4
- Email **john@knightsbridgelaw.com**. Do not open public issues for security reports.
5
- PGP key available on request. We credit reporters in `CHANGELOG.md` unless they
6
- request otherwise.
7
-
8
- Acknowledgement within **2 business days**. Triage decision within **5 business days**.
9
-
10
- Full policy: <https://kxco.ai/security>
11
-
12
- ## Safe harbour
13
- If you make a good-faith effort to comply with this policy, we will treat your
14
- research as authorised, and we will not pursue or support legal action against
15
- you. Good faith means: do not access, modify, exfiltrate, or destroy data that
16
- is not yours; use test accounts where possible; do not degrade service for
17
- others; and stop and report as soon as you have established that a
18
- vulnerability exists. This cannot bind third parties, and it does not cover
19
- extortion, data sale, or public disclosure ahead of the window below.
20
-
21
- ## Scope
22
- In scope:
23
- - Cryptographic correctness of the wrappers in this package
24
- - Constant-time guarantees on signature/HMAC comparison
25
- - Replay-window enforcement in `webhook.verify`
26
- - HKDF domain separation in `derive`
27
- - Kid fingerprint collision behaviour
28
-
29
- Out of scope (report upstream to https://github.com/paulmillr/noble-post-quantum):
30
- - Bugs in the underlying ML-DSA-65, ML-KEM-768, SLH-DSA-SHA2-192s, or HKDF primitives
31
-
32
- ## Algorithms used
33
- - ML-DSA-65 — NIST FIPS 204 (lattice signatures)
34
- - ML-KEM-768 — NIST FIPS 203 (key encapsulation)
35
- - SLH-DSA-SHA2-192s — NIST FIPS 205 (hash-based signatures)
36
- - HMAC-SHA-256
37
- - HKDF-SHA-512 (RFC 5869)
38
-
39
- ## Disclosure
40
- We follow coordinated disclosure with a 90-day default window.
41
- For actively-exploited issues we ship a patch release within 48 hours.
1
+ # Security Policy
2
+
3
+ ## Reporting a vulnerability
4
+ Email **john@knightsbridgelaw.com**. Do not open public issues for security reports.
5
+ PGP key available on request. We credit reporters in `CHANGELOG.md` unless they
6
+ request otherwise.
7
+
8
+ Acknowledgement within **2 business days**. Triage decision within **5 business days**.
9
+
10
+ Full policy: <https://kxco.ai/security>
11
+
12
+ ## Safe harbour
13
+ If you make a good-faith effort to comply with this policy, we will treat your
14
+ research as authorised, and we will not pursue or support legal action against
15
+ you. Good faith means: do not access, modify, exfiltrate, or destroy data that
16
+ is not yours; use test accounts where possible; do not degrade service for
17
+ others; and stop and report as soon as you have established that a
18
+ vulnerability exists. This cannot bind third parties, and it does not cover
19
+ extortion, data sale, or public disclosure ahead of the window below.
20
+
21
+ ## Scope
22
+ In scope:
23
+ - Cryptographic correctness of the wrappers in this package
24
+ - Constant-time guarantees on signature/HMAC comparison
25
+ - Replay-window enforcement in `webhook.verify`
26
+ - HKDF domain separation in `derive`
27
+ - Kid fingerprint collision behaviour
28
+
29
+ Out of scope (report upstream to https://github.com/paulmillr/noble-post-quantum):
30
+ - Bugs in the underlying ML-DSA-65, ML-KEM-768, SLH-DSA-SHA2-192s, or HKDF primitives
31
+
32
+ ## Algorithms used
33
+ - ML-DSA-65 — NIST FIPS 204 (lattice signatures)
34
+ - ML-KEM-768 — NIST FIPS 203 (key encapsulation)
35
+ - SLH-DSA-SHA2-192s — NIST FIPS 205 (hash-based signatures)
36
+ - HMAC-SHA-256
37
+ - HKDF-SHA-512 (RFC 5869)
38
+
39
+ ## Dependency policy
40
+ Both runtime dependencies are pinned to an exact version, never a range:
41
+ `@noble/post-quantum` and `@noble/hashes`. A range would let the code that runs
42
+ the cryptography change without a release of this package, which is not a
43
+ property we are willing to give up for convenience. Every GitHub Action in our
44
+ workflows is pinned by 40-character commit SHA for the same reason.
45
+
46
+ Updates are proposed, never automatic. Dependabot opens pull requests weekly for
47
+ both npm and GitHub Actions, and Dependabot security updates are enabled at the
48
+ repository level so an advisory does not wait for the Monday run. The
49
+ configuration is in [.github/dependabot.yml](.github/dependabot.yml).
50
+
51
+ A dependency bump is not merged on the strength of a green test run alone. A
52
+ change to `@noble/post-quantum` is a change to the primitives themselves, so it
53
+ is gated on the full conformance evidence regenerating clean: the NIST ACVP
54
+ vectors and the cross-implementation interoperability matrix, both described in
55
+ [CONFORMANCE.md](CONFORMANCE.md).
56
+
57
+ Each release publishes a CycloneDX SBOM as a GitHub Release asset, at
58
+ `releases/download/<tag>/sbom.cyclonedx.json`, generated from the tree that was
59
+ actually installed for that build.
60
+
61
+ ## Disclosure
62
+ We follow coordinated disclosure with a 90-day default window.
63
+ For actively-exploited issues we ship a patch release within 48 hours.
@@ -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,106 +1,126 @@
1
- {
2
- "name": "kxco-post-quantum",
3
- "version": "1.3.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
- ],
29
- "license": "Apache-2.0",
30
- "author": "Shayne Heffernan and John Heffernan",
31
- "contributors": [
32
- {
33
- "name": "Shayne Heffernan"
34
- },
35
- {
36
- "name": "John Heffernan"
37
- }
38
- ],
39
- "homepage": "https://kxco.ai",
40
- "funding": "https://kxco.ai",
41
- "repository": {
42
- "type": "git",
43
- "url": "https://github.com/KnightsbridgeAIQ/kxco-post-quantum.git"
44
- },
45
- "bugs": {
46
- "url": "https://github.com/KnightsbridgeAIQ/kxco-post-quantum/issues"
47
- },
48
- "type": "module",
49
- "sideEffects": false,
50
- "main": "./src/index.js",
51
- "types": "./src/index.d.ts",
52
- "exports": {
53
- ".": {
54
- "types": "./src/index.d.ts",
55
- "import": "./src/index.js"
56
- },
57
- "./ml-dsa": {
58
- "types": "./src/ml-dsa.d.ts",
59
- "import": "./src/ml-dsa.js"
60
- },
61
- "./ml-kem": {
62
- "types": "./src/ml-kem.d.ts",
63
- "import": "./src/ml-kem.js"
64
- },
65
- "./slh-dsa": {
66
- "types": "./src/slh-dsa.d.ts",
67
- "import": "./src/slh-dsa.js"
68
- },
69
- "./derive": {
70
- "types": "./src/derive.d.ts",
71
- "import": "./src/derive.js"
72
- },
73
- "./webhook": {
74
- "types": "./src/webhook.d.ts",
75
- "import": "./src/webhook.js"
76
- },
77
- "./kid": {
78
- "types": "./src/kid.d.ts",
79
- "import": "./src/kid.js"
80
- }
81
- },
82
- "files": [
83
- "src",
84
- "README.md",
85
- "LICENSE",
86
- "SECURITY.md",
87
- "CHANGELOG.md"
88
- ],
89
- "engines": {
90
- "node": ">=20.19"
91
- },
92
- "dependencies": {
93
- "@noble/hashes": "2.3.0",
94
- "@noble/post-quantum": "0.7.0"
95
- },
96
- "scripts": {
97
- "test": "node --test test/basic.test.js && node --test test/context.test.js && node --test test/browser-smoke.test.js && node test/run-vectors.js",
98
- "test:vectors": "node test/run-vectors.js",
99
- "generate:vectors": "node test/generate-vectors.js > test/vectors.json",
100
- "bench": "node bench/bench.js"
101
- },
102
- "publishConfig": {
103
- "provenance": true,
104
- "access": "public"
105
- }
106
- }
1
+ {
2
+ "name": "kxco-post-quantum",
3
+ "version": "1.4.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
+ "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
+ "BENCHMARKS.md",
99
+ "THREAT-MODEL.md",
100
+ "MIGRATION.md",
101
+ "SECURITY.md",
102
+ "CHANGELOG.md"
103
+ ],
104
+ "engines": {
105
+ "node": ">=20.19"
106
+ },
107
+ "dependencies": {
108
+ "@noble/hashes": "2.3.0",
109
+ "@noble/post-quantum": "0.7.0"
110
+ },
111
+ "scripts": {
112
+ "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",
113
+ "test:vectors": "node test/run-vectors.js",
114
+ "generate:vectors": "node test/generate-vectors.js > test/vectors.json",
115
+ "bench": "node bench/bench.js",
116
+ "conformance:fetch": "node conformance/fetch-vectors.mjs",
117
+ "conformance:acvp": "node conformance/run-acvp.mjs --json conformance/results/acvp.json",
118
+ "conformance:interop": "node conformance/interop/run-interop.mjs --json conformance/results/interop.json",
119
+ "sbom": "npm sbom --sbom-format cyclonedx --sbom-type library",
120
+ "bench:primitives": "node --expose-gc bench/primitives.mjs --iterations 100 --json bench/results/primitives.json"
121
+ },
122
+ "publishConfig": {
123
+ "provenance": true,
124
+ "access": "public"
125
+ }
126
+ }
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