kxco-post-quantum 1.5.3 → 1.6.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/src/backend.js ADDED
@@ -0,0 +1,48 @@
1
+ // Which implementation is actually doing the maths, right now, on this box.
2
+ //
3
+ // The package picks its backend at import time by probing the runtime, not by
4
+ // reading a version number, so the only honest way to state which one a given
5
+ // deployment used is to ask it. This exists so that an evidence bundle, a
6
+ // support ticket or a customer's own conformance run can record the answer
7
+ // instead of inferring it from `process.version`.
8
+ //
9
+ // It reports; it does not switch. There is deliberately no way to force a
10
+ // backend from here: the two produce identical wire bytes, and a runtime flag
11
+ // that changed which one signed would be a flag that changes what a customer's
12
+ // evidence means.
13
+
14
+ import { native } from '#native'
15
+
16
+ /**
17
+ * Describe the active backend.
18
+ *
19
+ * @returns {{ kind: 'openssl'|'javascript', library: string, openssl?: string,
20
+ * parameterSets?: string[], reason?: string }}
21
+ */
22
+ export function backend() {
23
+ if (native === null) {
24
+ return {
25
+ kind: 'javascript',
26
+ library: '@noble/post-quantum',
27
+ reason: 'the runtime does not provide the FIPS 203/204/205 primitives',
28
+ }
29
+ }
30
+ return {
31
+ kind: 'openssl',
32
+ library: 'node:crypto',
33
+ openssl: native.openssl,
34
+ // Only the sets OpenSSL can express. Anything absent here still works, on
35
+ // the JavaScript backend, which is why this is a list rather than a flag.
36
+ parameterSets: native.algorithms(),
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Whether a given parameter set runs on the native backend in this process.
42
+ *
43
+ * @param {string} alg — e.g. 'ML-DSA-65'
44
+ * @returns {boolean}
45
+ */
46
+ export function isNative(alg) {
47
+ return native !== null && native.supports(alg)
48
+ }
package/src/index.d.ts CHANGED
@@ -11,3 +11,9 @@ export * as mlKem1024 from './ml-kem-1024.js'
11
11
  export * from './derive.js'
12
12
  export * from './kid.js'
13
13
  export * as webhook from './webhook.js'
14
+
15
+ /** Seed-form keys: RFC 9964 AKP JWKs and LAMPS seed-form PKCS#8. */
16
+ export * as seed from './seed.js'
17
+ /** Compact JWS using the RFC 9964 algorithm names. Format only, no network. */
18
+ export * as jws from './jws.js'
19
+ export { backend, isNative } from './backend.js'
package/src/index.js CHANGED
@@ -6,8 +6,13 @@
6
6
  // KnightsVault, KXCO Bank, KnightsBot, The Exchequer, and Armature L1.
7
7
  //
8
8
  // This package does NOT reimplement the NIST primitives. It wraps the
9
- // audited @noble/post-quantum reference implementation with the integration
10
- // patterns we have proven in production.
9
+ // @noble/post-quantum reference implementation with the integration patterns we
10
+ // have proven in production, and on Node 24+ it prefers the OpenSSL 3.5 backend.
11
+ //
12
+ // The primitives are evidenced here rather than taken on trust: every parameter
13
+ // set is checked against NIST's own ACVP vectors and cross-checked against
14
+ // liboqs, Bouncy Castle and dilithium-py/kyber-py in both directions. See
15
+ // CONFORMANCE.md, and audit/ for the dependency review.
11
16
 
12
17
  export * as mlDsa from './ml-dsa.js'
13
18
  export * as mlKem from './ml-kem.js'
@@ -22,3 +27,14 @@ export * as mlKem1024 from './ml-kem-1024.js'
22
27
  export * from './derive.js'
23
28
  export * from './kid.js'
24
29
  export * as webhook from './webhook.js'
30
+
31
+ // Seed-form keys (RFC 9964 AKP JWKs, LAMPS seed-form PKCS#8) and compact JWS
32
+ // with the RFC 9964 algorithm names. Both are format and derivation only: they
33
+ // contact nothing, need no licence, and a token or key they produce stays
34
+ // verifiable offline for as long as the holder keeps the public key.
35
+ export * as seed from './seed.js'
36
+ export * as jws from './jws.js'
37
+
38
+ // Reports which backend is doing the maths in this process, for evidence
39
+ // bundles and support. It reports; it never switches.
40
+ export { backend, isNative } from './backend.js'
package/src/jws.d.ts ADDED
@@ -0,0 +1,74 @@
1
+ /// <reference types="node" />
2
+
3
+ /** JWS `alg` values this module signs and verifies (RFC 9964 names). */
4
+ export type JwsAlgorithm = 'ML-DSA-65' | 'ML-DSA-87'
5
+
6
+ export const JWS_ALGORITHMS: JwsAlgorithm[]
7
+
8
+ export interface JwsHeader {
9
+ alg: JwsAlgorithm
10
+ typ?: string
11
+ kid?: string
12
+ [key: string]: unknown
13
+ }
14
+
15
+ export interface SignJwsOptions {
16
+ /** Defaults to 'ML-DSA-65'. */
17
+ alg?: JwsAlgorithm
18
+ /** Key identifier, e.g. the 16-hex `fingerprint()` of the public key. */
19
+ kid?: string
20
+ typ?: string
21
+ /** Extra protected header members. May not restate alg, kid or typ. */
22
+ header?: Record<string, unknown>
23
+ }
24
+
25
+ export interface VerifyJwsSuccess {
26
+ valid: true
27
+ header: JwsHeader
28
+ /** Raw payload bytes. */
29
+ payload: Uint8Array
30
+ /** The payload decoded as UTF-8. JSON payloads are not parsed for you. */
31
+ text: string
32
+ }
33
+
34
+ export interface VerifyJwsFailure {
35
+ valid: false
36
+ error: string
37
+ }
38
+
39
+ export type VerifyJwsResult = VerifyJwsSuccess | VerifyJwsFailure
40
+
41
+ /**
42
+ * Sign a payload into a compact JWS. Objects are JSON-serialised.
43
+ *
44
+ * @throws {TypeError} on a bad options object or payload type
45
+ * @throws {Error} on an unsupported alg, or a header member that restates
46
+ * alg, kid or typ
47
+ */
48
+ export function signJws(
49
+ payload: object | string | Uint8Array,
50
+ secretKey: Buffer | Uint8Array,
51
+ opts?: SignJwsOptions,
52
+ ): string
53
+
54
+ /**
55
+ * Verify a compact JWS.
56
+ *
57
+ * The algorithm is resolved from this module's allowlist using the header's
58
+ * `alg`; a token cannot name its own verification routine. Pass `{ alg }` to
59
+ * require a specific algorithm and `{ kid }` to require a specific key.
60
+ *
61
+ * Returns `{ valid: false, error }` for every failure. Throws only on caller
62
+ * misuse of `opts`.
63
+ */
64
+ export function verifyJws(
65
+ token: string,
66
+ publicKey: Buffer | Uint8Array,
67
+ opts?: { alg?: JwsAlgorithm; kid?: string },
68
+ ): VerifyJwsResult
69
+
70
+ /**
71
+ * Read a token's header without verifying it — for choosing which key to fetch
72
+ * from a `kid`. The header is unauthenticated until `verifyJws` returns valid.
73
+ */
74
+ export function decodeJwsHeader(token: string): JwsHeader | null
package/src/jws.js ADDED
@@ -0,0 +1,249 @@
1
+ // Compact JWS with ML-DSA, using the RFC 9964 algorithm names.
2
+ //
3
+ // Format only. Nothing here contacts a network, reads a chain, or needs a
4
+ // licence: a token signed by this module verifies in any process that holds
5
+ // the public key, forever, offline. The paid behaviour that decides whether a
6
+ // key is still ALLOWED to sign lives in kxco-pq-network and above, never here.
7
+ //
8
+ // Why a JWS at all when this package already has its own envelopes. Because an
9
+ // institution's existing stack already parses JWS. Their gateway, their IdP,
10
+ // their audit tooling and their partner's verifier all speak it, and RFC 9964
11
+ // registered "ML-DSA-44", "ML-DSA-65" and "ML-DSA-87" as JWS algorithms
12
+ // precisely so a post-quantum signature can travel that path unchanged. Giving
13
+ // them one is the difference between a migration and a rewrite.
14
+ //
15
+ // The header is the protected header and the whole of it is signed. There is
16
+ // no unprotected header, no JSON serialisation and no detached payload in this
17
+ // release: each is a place where a verifier can be talked into checking
18
+ // something other than what it thinks it is checking, and none of them is
19
+ // needed to carry a signature between two services.
20
+ //
21
+ // SLH-DSA is not offered here. FIPS 205 signing takes on the order of a second
22
+ // and a half per signature, which is not something to put on a request path.
23
+
24
+ import * as mlDsa from './ml-dsa.js'
25
+ import * as mlDsa87 from './ml-dsa-87.js'
26
+
27
+ const HAS_BUFFER = typeof Buffer !== 'undefined'
28
+ const enc = new TextEncoder()
29
+ const dec = new TextDecoder()
30
+
31
+ // The `alg` allowlist. A verifier resolves the implementation from THIS table
32
+ // and nowhere else, so a token cannot name its own verification routine — the
33
+ // alg-confusion failure that has broken JWT libraries repeatedly.
34
+ const ALGORITHMS = {
35
+ 'ML-DSA-65': { mod: mlDsa, publicKeyBytes: 1952, signatureBytes: 3309 },
36
+ 'ML-DSA-87': { mod: mlDsa87, publicKeyBytes: 2592, signatureBytes: 4627 },
37
+ }
38
+
39
+ /** JWS `alg` values this module will sign or verify. */
40
+ export const JWS_ALGORITHMS = Object.keys(ALGORITHMS)
41
+
42
+ const DEFAULT_ALG = 'ML-DSA-65'
43
+
44
+ function b64url(bytes) {
45
+ if (HAS_BUFFER) return Buffer.from(bytes).toString('base64url')
46
+ let binary = ''
47
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i])
48
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
49
+ }
50
+
51
+ function fromB64url(str) {
52
+ if (HAS_BUFFER) return new Uint8Array(Buffer.from(str, 'base64url'))
53
+ const padded = str.replace(/-/g, '+').replace(/_/g, '/')
54
+ const binary = atob(padded + '='.repeat((4 - (padded.length % 4)) % 4))
55
+ const out = new Uint8Array(binary.length)
56
+ for (let i = 0; i < out.length; i++) out[i] = binary.charCodeAt(i)
57
+ return out
58
+ }
59
+
60
+ // base64url is not a canonical encoding of arbitrary text: two different
61
+ // strings can decode to the same bytes if one carries padding or non-alphabet
62
+ // characters. A token whose segments re-encode differently from how they
63
+ // arrived is rejected, so a verifier and a downstream parser cannot be shown
64
+ // two different payloads for one signature.
65
+ function strictB64url(str, what) {
66
+ if (typeof str !== 'string' || str.length === 0 || !/^[A-Za-z0-9_-]+$/.test(str)) {
67
+ throw new Error(`malformed JWS: ${what} is not base64url`)
68
+ }
69
+ const bytes = fromB64url(str)
70
+ if (b64url(bytes) !== str) throw new Error(`malformed JWS: ${what} is not canonically encoded`)
71
+ return bytes
72
+ }
73
+
74
+ function bytesToHex(bytes) {
75
+ let s = ''
76
+ for (let i = 0; i < bytes.length; i++) s += bytes[i].toString(16).padStart(2, '0')
77
+ return s
78
+ }
79
+
80
+ function hexToBytes(hex) {
81
+ const b = new Uint8Array(hex.length / 2)
82
+ for (let i = 0; i < b.length; i++) b[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
83
+ return b
84
+ }
85
+
86
+ function payloadBytes(payload) {
87
+ if (payload instanceof Uint8Array) return payload
88
+ if (typeof payload === 'string') return enc.encode(payload)
89
+ if (payload === null || payload === undefined) {
90
+ throw new TypeError('payload is required')
91
+ }
92
+ if (typeof payload === 'object') return enc.encode(JSON.stringify(payload))
93
+ throw new TypeError('payload must be an object, a string or a Uint8Array')
94
+ }
95
+
96
+ /**
97
+ * Sign a payload into a compact JWS.
98
+ *
99
+ * @param {object|string|Uint8Array} payload — objects are JSON-serialised
100
+ * @param {Buffer|Uint8Array} secretKey
101
+ * @param {{ alg?: string, kid?: string, typ?: string, header?: object }} [opts]
102
+ * @returns {string} compact JWS: header.payload.signature
103
+ */
104
+ export function signJws(payload, secretKey, opts = {}) {
105
+ if (opts === null || typeof opts !== 'object') {
106
+ throw new TypeError('expected an options object such as { kid, alg }')
107
+ }
108
+ const alg = opts.alg ?? DEFAULT_ALG
109
+ const spec = ALGORITHMS[alg]
110
+ if (!spec) {
111
+ throw new Error(`unsupported JWS alg '${alg}' — this module signs ${JWS_ALGORITHMS.join(' and ')}`)
112
+ }
113
+
114
+ // Extra header members are allowed but may not restate or override the ones
115
+ // this module is responsible for, which would let a caller sign under one
116
+ // alg while the header advertises another.
117
+ const extra = opts.header ?? {}
118
+ for (const reserved of ['alg', 'kid', 'typ']) {
119
+ if (Object.hasOwn(extra, reserved)) {
120
+ throw new Error(`header.${reserved} is set through opts.${reserved}, not opts.header`)
121
+ }
122
+ }
123
+
124
+ const header = {
125
+ alg,
126
+ ...(opts.typ !== undefined ? { typ: opts.typ } : {}),
127
+ ...(opts.kid !== undefined ? { kid: opts.kid } : {}),
128
+ ...extra,
129
+ }
130
+
131
+ const protectedB64 = b64url(enc.encode(JSON.stringify(header)))
132
+ const payloadB64 = b64url(payloadBytes(payload))
133
+ const signingInput = enc.encode(`${protectedB64}.${payloadB64}`)
134
+
135
+ // Goes through the module's own sign(), so the OpenSSL backend is used
136
+ // wherever the runtime has it and the JavaScript one everywhere else. The
137
+ // wire bytes are identical either way.
138
+ const sigHex = spec.mod.sign(secretKey, signingInput)
139
+ return `${protectedB64}.${payloadB64}.${b64url(hexToBytes(sigHex))}`
140
+ }
141
+
142
+ /**
143
+ * Verify a compact JWS.
144
+ *
145
+ * Returns `{ valid: false, error }` for anything that fails, and throws only on
146
+ * caller misuse, matching how `mlDsa.verify` already behaves.
147
+ *
148
+ * The algorithm is resolved from this module's allowlist using the header's
149
+ * `alg`, and the public key's length must match what that algorithm expects.
150
+ * Pass `{ alg }` to require a specific one, and `{ kid }` to require the header
151
+ * to name a specific key.
152
+ *
153
+ * @param {string} token
154
+ * @param {Buffer|Uint8Array} publicKey
155
+ * @param {{ alg?: string, kid?: string }} [opts]
156
+ * @returns {{ valid: true, header: object, payload: Uint8Array, text: string }
157
+ * | { valid: false, error: string }}
158
+ */
159
+ export function verifyJws(token, publicKey, opts = {}) {
160
+ if (opts === null || typeof opts !== 'object') {
161
+ throw new TypeError('expected an options object such as { alg, kid }')
162
+ }
163
+ if (typeof token !== 'string') return { valid: false, error: 'token must be a string' }
164
+
165
+ const parts = token.split('.')
166
+ if (parts.length !== 3) {
167
+ return { valid: false, error: 'malformed JWS: expected three dot-separated segments' }
168
+ }
169
+ const [protectedB64, payloadB64, sigB64] = parts
170
+
171
+ let header, payload, signature
172
+ try {
173
+ header = JSON.parse(dec.decode(strictB64url(protectedB64, 'header')))
174
+ payload = strictB64url(payloadB64, 'payload')
175
+ signature = strictB64url(sigB64, 'signature')
176
+ } catch (err) {
177
+ return { valid: false, error: err.message }
178
+ }
179
+
180
+ if (!header || typeof header !== 'object' || Array.isArray(header)) {
181
+ return { valid: false, error: 'malformed JWS: header is not an object' }
182
+ }
183
+
184
+ const spec = ALGORITHMS[header.alg]
185
+ if (!spec) {
186
+ return { valid: false, error: `unsupported JWS alg '${header.alg}'` }
187
+ }
188
+ if (opts.alg !== undefined && header.alg !== opts.alg) {
189
+ return { valid: false, error: `alg mismatch: expected '${opts.alg}', token declares '${header.alg}'` }
190
+ }
191
+ if (opts.kid !== undefined && header.kid !== opts.kid) {
192
+ return { valid: false, error: `kid mismatch: expected '${opts.kid}', token declares '${header.kid ?? '(none)'}'` }
193
+ }
194
+
195
+ // RFC 7515 section 4.1.11: a verifier that does not understand every member
196
+ // named in `crit` must reject the token. This module understands none of the
197
+ // extensions that would be named there, so any `crit` at all is a rejection.
198
+ if (header.crit !== undefined) {
199
+ return { valid: false, error: 'JWS declares crit header parameters this verifier does not implement' }
200
+ }
201
+ // RFC 7797 unencoded payloads change what the signature covers. Not accepted.
202
+ if (header.b64 !== undefined) {
203
+ return { valid: false, error: 'JWS declares b64, which this verifier does not implement' }
204
+ }
205
+
206
+ // The key must be the size the declared algorithm uses. Without this a token
207
+ // could name ML-DSA-87 while being checked against an ML-DSA-65 key, and the
208
+ // failure would look like a bad signature rather than a mixed-up key.
209
+ const keyBytes = publicKey instanceof Uint8Array ? publicKey : new Uint8Array(publicKey)
210
+ if (keyBytes.length !== spec.publicKeyBytes) {
211
+ return {
212
+ valid: false,
213
+ error: `key is ${keyBytes.length} bytes, but ${header.alg} public keys are ${spec.publicKeyBytes}`,
214
+ }
215
+ }
216
+ if (signature.length !== spec.signatureBytes) {
217
+ return {
218
+ valid: false,
219
+ error: `signature is ${signature.length} bytes, but ${header.alg} signatures are ${spec.signatureBytes}`,
220
+ }
221
+ }
222
+
223
+ const signingInput = enc.encode(`${protectedB64}.${payloadB64}`)
224
+ const ok = spec.mod.verify(keyBytes, signingInput, bytesToHex(signature))
225
+ if (!ok) return { valid: false, error: 'signature invalid' }
226
+
227
+ return { valid: true, header, payload, text: dec.decode(payload) }
228
+ }
229
+
230
+ /**
231
+ * Read a token's header without verifying anything.
232
+ *
233
+ * For dispatch only — picking which public key to fetch from a `kid`. The
234
+ * header is unauthenticated until `verifyJws` returns valid, and nothing in it
235
+ * should be acted on before that.
236
+ *
237
+ * @param {string} token
238
+ * @returns {object|null} null if the token is not parseable
239
+ */
240
+ export function decodeJwsHeader(token) {
241
+ if (typeof token !== 'string') return null
242
+ const first = token.split('.')[0]
243
+ try {
244
+ const header = JSON.parse(dec.decode(strictB64url(first, 'header')))
245
+ return header && typeof header === 'object' && !Array.isArray(header) ? header : null
246
+ } catch {
247
+ return null
248
+ }
249
+ }
@@ -5,6 +5,14 @@ export interface MlDsa87Keypair {
5
5
  publicKey: Buffer
6
6
  /** 4896-byte secret key */
7
7
  secretKey: Buffer
8
+ /**
9
+ * The 32-byte seed this keypair was expanded from.
10
+ *
11
+ * An expanded secret key does not contain its seed, so this is the only
12
+ * point at which it can be captured. Pass it to `exportJwk` or
13
+ * `exportSeedPkcs8` for RFC 9964 / LAMPS seed-form storage.
14
+ */
15
+ seed: Buffer
8
16
  }
9
17
 
10
18
  /**
package/src/ml-dsa-87.js CHANGED
@@ -82,6 +82,11 @@ export function keypairFromMaster(master, info = 'ml-dsa-87-v1') {
82
82
  return {
83
83
  publicKey: wrap(k.publicKey),
84
84
  secretKey: wrap(k.secretKey),
85
+ // The seed this key was expanded from. Additive: callers destructuring
86
+ // { publicKey, secretKey } are unaffected. It is here because an expanded
87
+ // key does not contain its seed, so this is the only moment it can be
88
+ // captured, and seed form is what RFC 9964 JWKs and KMS custody take.
89
+ seed: wrap(seedU8),
85
90
  }
86
91
  }
87
92
 
package/src/ml-dsa.d.ts CHANGED
@@ -5,6 +5,14 @@ export interface MlDsaKeypair {
5
5
  publicKey: Buffer
6
6
  /** 4032-byte secret key */
7
7
  secretKey: Buffer
8
+ /**
9
+ * The 32-byte seed this keypair was expanded from.
10
+ *
11
+ * An expanded secret key does not contain its seed, so this is the only
12
+ * point at which it can be captured. Pass it to `exportJwk` or
13
+ * `exportSeedPkcs8` for RFC 9964 / LAMPS seed-form storage.
14
+ */
15
+ seed: Buffer
8
16
  }
9
17
 
10
18
  /**
package/src/ml-dsa.js CHANGED
@@ -62,6 +62,11 @@ export function keypairFromMaster(master, info = 'ml-dsa-65-v1') {
62
62
  return {
63
63
  publicKey: wrap(k.publicKey),
64
64
  secretKey: wrap(k.secretKey),
65
+ // The seed this key was expanded from. Additive: callers destructuring
66
+ // { publicKey, secretKey } are unaffected. It is here because an expanded
67
+ // key does not contain its seed, so this is the only moment it can be
68
+ // captured, and seed form is what RFC 9964 JWKs and KMS custody take.
69
+ seed: wrap(seedU8),
65
70
  }
66
71
  }
67
72
 
@@ -5,6 +5,14 @@ export interface MlKem1024Keypair {
5
5
  publicKey: Buffer
6
6
  /** 3168-byte secret key */
7
7
  secretKey: Buffer
8
+ /**
9
+ * The 64-byte seed this keypair was expanded from.
10
+ *
11
+ * An expanded secret key does not contain its seed, so this is the only
12
+ * point at which it can be captured. Pass it to `exportJwk` or
13
+ * `exportSeedPkcs8` for RFC 9964 / LAMPS seed-form storage.
14
+ */
15
+ seed: Buffer
8
16
  }
9
17
 
10
18
  export interface MlKem1024Encapsulation {
@@ -63,6 +63,11 @@ export function keypairFromMaster(master, info = 'ml-kem-1024-v1') {
63
63
  return {
64
64
  publicKey: wrap(k.publicKey),
65
65
  secretKey: wrap(k.secretKey),
66
+ // The seed this key was expanded from. Additive: callers destructuring
67
+ // { publicKey, secretKey } are unaffected. It is here because an expanded
68
+ // key does not contain its seed, so this is the only moment it can be
69
+ // captured, and seed form is what RFC 9964 JWKs and KMS custody take.
70
+ seed: wrap(seedU8),
66
71
  }
67
72
  }
68
73
 
package/src/ml-kem.d.ts CHANGED
@@ -5,6 +5,14 @@ export interface MlKemKeypair {
5
5
  publicKey: Buffer
6
6
  /** 2400-byte secret key */
7
7
  secretKey: Buffer
8
+ /**
9
+ * The 64-byte seed this keypair was expanded from.
10
+ *
11
+ * An expanded secret key does not contain its seed, so this is the only
12
+ * point at which it can be captured. Pass it to `exportJwk` or
13
+ * `exportSeedPkcs8` for RFC 9964 / LAMPS seed-form storage.
14
+ */
15
+ seed: Buffer
8
16
  }
9
17
 
10
18
  export interface MlKemEncapsulation {
package/src/ml-kem.js CHANGED
@@ -38,6 +38,11 @@ export function keypairFromMaster(master, info = 'ml-kem-768-v1') {
38
38
  return {
39
39
  publicKey: wrap(k.publicKey),
40
40
  secretKey: wrap(k.secretKey),
41
+ // The seed this key was expanded from. Additive: callers destructuring
42
+ // { publicKey, secretKey } are unaffected. It is here because an expanded
43
+ // key does not contain its seed, so this is the only moment it can be
44
+ // captured, and seed form is what RFC 9964 JWKs and KMS custody take.
45
+ seed: wrap(seedU8),
41
46
  }
42
47
  }
43
48
 
package/src/seed.d.ts ADDED
@@ -0,0 +1,91 @@
1
+ /// <reference types="node" />
2
+
3
+ /** Parameter sets that have a seed form. SLH-DSA does not and is absent. */
4
+ export type SeedAlgorithm = 'ML-DSA-65' | 'ML-DSA-87' | 'ML-KEM-768' | 'ML-KEM-1024'
5
+
6
+ export const SEED_ALGORITHMS: SeedAlgorithm[]
7
+
8
+ export interface SeededKeypair {
9
+ publicKey: Buffer | Uint8Array
10
+ secretKey: Buffer | Uint8Array
11
+ /** 32 bytes for ML-DSA, 64 for ML-KEM. */
12
+ seed: Buffer | Uint8Array
13
+ }
14
+
15
+ /** RFC 9964 algorithm-key-pair JWK. `priv` carries the SEED, not the expanded key. */
16
+ export interface AkpJwk {
17
+ kty: 'AKP'
18
+ alg: SeedAlgorithm
19
+ /** base64url public key */
20
+ pub: string
21
+ /** base64url seed, present only on private JWKs */
22
+ priv?: string
23
+ kid?: string
24
+ use?: string
25
+ }
26
+
27
+ /**
28
+ * Expand a seed into a keypair. Deterministic, and byte-identical to what
29
+ * OpenSSL 3.5 derives from the same seed.
30
+ *
31
+ * @throws {RangeError} if the seed is not the parameter set's seed length
32
+ */
33
+ export function keypairFromSeed(
34
+ alg: SeedAlgorithm,
35
+ seed: Buffer | Uint8Array,
36
+ ): SeededKeypair
37
+
38
+ /**
39
+ * Derive a parameter set's seed from a master secret, using the same
40
+ * HKDF-SHA-512 derivation `keypairFromMaster` uses. With the default `info`
41
+ * this reproduces exactly the key `keypairFromMaster` produces.
42
+ */
43
+ export function seedFromMaster(
44
+ alg: SeedAlgorithm,
45
+ master: Buffer | Uint8Array | string,
46
+ info?: string,
47
+ ): Buffer | Uint8Array
48
+
49
+ /**
50
+ * Export an RFC 9964 AKP JWK. Supply `seed` for a private JWK.
51
+ *
52
+ * An expanded secret key is rejected: RFC 9964 has no encoding for one.
53
+ *
54
+ * @throws {RangeError} on a wrong-length public key or seed
55
+ */
56
+ export function exportJwk(
57
+ alg: SeedAlgorithm,
58
+ key: { publicKey: Buffer | Uint8Array; seed?: Buffer | Uint8Array },
59
+ opts?: { kid?: string; use?: string },
60
+ ): AkpJwk
61
+
62
+ /**
63
+ * Import an RFC 9964 AKP JWK, expanding a private one back to a full keypair.
64
+ *
65
+ * @throws {Error} if the key derived from `priv` disagrees with `pub`
66
+ */
67
+ export function importJwk(jwk: AkpJwk | object): {
68
+ alg: SeedAlgorithm
69
+ publicKey: Buffer | Uint8Array
70
+ secretKey?: Buffer | Uint8Array
71
+ seed?: Buffer | Uint8Array
72
+ }
73
+
74
+ /**
75
+ * Export a seed as PKCS#8 in LAMPS seed form — the `[0] IMPLICIT OCTET STRING`
76
+ * CHOICE. Loads directly into OpenSSL 3.5 and Node 24+.
77
+ */
78
+ export function exportSeedPkcs8(
79
+ alg: SeedAlgorithm,
80
+ seed: Buffer | Uint8Array,
81
+ ): Buffer | Uint8Array
82
+
83
+ /**
84
+ * Read a seed-form PKCS#8 key back.
85
+ *
86
+ * @throws {Error} on an expanded-form key, which contains no seed to return
87
+ */
88
+ export function importSeedPkcs8(der: Buffer | Uint8Array): {
89
+ alg: SeedAlgorithm
90
+ seed: Buffer | Uint8Array
91
+ }