@noble/post-quantum 0.6.1 → 0.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/README.md +191 -104
- package/_crystals.d.ts +8 -3
- package/_crystals.js +38 -10
- package/falcon.d.ts +1 -2
- package/falcon.js +202 -115
- package/hybrid.d.ts +38 -28
- package/hybrid.js +215 -86
- package/index.d.ts +0 -1
- package/index.js +1 -2
- package/ml-dsa.d.ts +31 -5
- package/ml-dsa.js +118 -34
- package/ml-kem.d.ts +45 -4
- package/ml-kem.js +206 -64
- package/package.json +16 -20
- package/slh-dsa.d.ts +23 -3
- package/slh-dsa.js +127 -60
- package/src/_crystals.ts +45 -11
- package/src/falcon.ts +206 -121
- package/src/hybrid.ts +217 -83
- package/src/index.ts +1 -1
- package/src/ml-dsa.ts +140 -43
- package/src/ml-kem.ts +239 -66
- package/src/slh-dsa.ts +149 -69
- package/src/utils.ts +186 -25
- package/src/webcrypto.ts +322 -0
- package/utils.d.ts +54 -4
- package/utils.js +167 -28
- package/webcrypto.d.ts +91 -0
- package/webcrypto.js +213 -0
- package/_crystals.d.ts.map +0 -1
- package/_crystals.js.map +0 -1
- package/falcon.d.ts.map +0 -1
- package/falcon.js.map +0 -1
- package/hybrid.d.ts.map +0 -1
- package/hybrid.js.map +0 -1
- package/index.d.ts.map +0 -1
- package/index.js.map +0 -1
- package/ml-dsa.d.ts.map +0 -1
- package/ml-dsa.js.map +0 -1
- package/ml-kem.d.ts.map +0 -1
- package/ml-kem.js.map +0 -1
- package/slh-dsa.d.ts.map +0 -1
- package/slh-dsa.js.map +0 -1
- package/utils.d.ts.map +0 -1
- package/utils.js.map +0 -1
package/src/hybrid.ts
CHANGED
|
@@ -80,6 +80,8 @@ import { type ECDSA } from '@noble/curves/abstract/weierstrass.js';
|
|
|
80
80
|
import { x25519 } from '@noble/curves/ed25519.js';
|
|
81
81
|
import { p256, p384 } from '@noble/curves/nist.js';
|
|
82
82
|
import {
|
|
83
|
+
abool,
|
|
84
|
+
afunction,
|
|
83
85
|
asciiToBytes,
|
|
84
86
|
bytesToNumberBE,
|
|
85
87
|
bytesToNumberLE,
|
|
@@ -89,9 +91,11 @@ import {
|
|
|
89
91
|
import { expand, extract } from '@noble/hashes/hkdf.js';
|
|
90
92
|
import { sha256 } from '@noble/hashes/sha2.js';
|
|
91
93
|
import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
|
|
92
|
-
import { abytes, ahash, anumber, type CHash, type CHashXOF } from '@noble/hashes/utils.js';
|
|
94
|
+
import { abytes, ahash, anumber, isBytes, type CHash, type CHashXOF } from '@noble/hashes/utils.js';
|
|
93
95
|
import { ml_kem1024, ml_kem768 } from './ml-kem.ts';
|
|
94
96
|
import {
|
|
97
|
+
aobject,
|
|
98
|
+
astring,
|
|
95
99
|
cleanBytes,
|
|
96
100
|
copyBytes,
|
|
97
101
|
randomBytes,
|
|
@@ -108,9 +112,32 @@ import {
|
|
|
108
112
|
type CurveAll = ECDSA | EdDSA | MontgomeryECDH;
|
|
109
113
|
type CurveECDH = ECDSA | MontgomeryECDH;
|
|
110
114
|
type CurveSign = ECDSA | EdDSA;
|
|
115
|
+
const validateKEM = (kem: TArg<KEM>, title: string): TRet<KEM> => {
|
|
116
|
+
const k = aobject<KEM>(kem, title);
|
|
117
|
+
aobject(k.lengths, `${title}.lengths`);
|
|
118
|
+
afunction(k.keygen, `${title}.keygen`);
|
|
119
|
+
afunction(k.getPublicKey, `${title}.getPublicKey`);
|
|
120
|
+
afunction(k.encapsulate, `${title}.encapsulate`);
|
|
121
|
+
afunction(k.decapsulate, `${title}.decapsulate`);
|
|
122
|
+
return k as TRet<KEM>;
|
|
123
|
+
};
|
|
124
|
+
const validateSigner = (signer: TArg<Signer>, title: string): TRet<Signer> => {
|
|
125
|
+
const s = aobject<Signer>(signer, title);
|
|
126
|
+
aobject(s.lengths, `${title}.lengths`);
|
|
127
|
+
afunction(s.keygen, `${title}.keygen`);
|
|
128
|
+
afunction(s.getPublicKey, `${title}.getPublicKey`);
|
|
129
|
+
afunction(s.sign, `${title}.sign`);
|
|
130
|
+
afunction(s.verify, `${title}.verify`);
|
|
131
|
+
return s as TRet<Signer>;
|
|
132
|
+
};
|
|
111
133
|
|
|
112
134
|
// Can re-use if decide to signatures support, on other hand getSecretKey is specific and ugly
|
|
113
135
|
function ecKeygen(curve: CurveAll, allowZeroKey: boolean = false) {
|
|
136
|
+
const c = aobject<CurveAll>(curve, 'curve');
|
|
137
|
+
aobject(c.lengths, 'curve.lengths');
|
|
138
|
+
afunction(c.keygen, 'curve.keygen');
|
|
139
|
+
afunction(c.getPublicKey, 'curve.getPublicKey');
|
|
140
|
+
abool(allowZeroKey, 'allowZeroKey');
|
|
114
141
|
const lengths = curve.lengths;
|
|
115
142
|
let keygen = curve.keygen;
|
|
116
143
|
if (allowZeroKey) {
|
|
@@ -154,6 +181,12 @@ function ecKeygen(curve: CurveAll, allowZeroKey: boolean = false) {
|
|
|
154
181
|
/**
|
|
155
182
|
* Wraps an ECDH-capable curve as a KEM.
|
|
156
183
|
* Shared secrets stay in the wrapped curve's raw ECDH byte format with no built-in KDF.
|
|
184
|
+
*
|
|
185
|
+
* SECURITY: this is a low-level component adapter, not a standalone IND-CCA-secure KEM. It does
|
|
186
|
+
* not bind the encapsulation or recipient public key into the secret, so distinct accepted point
|
|
187
|
+
* encodings can produce the same output. Use it only inside a construction whose specified
|
|
188
|
+
* combiner binds those values, or use a standardized DHKEM with labeled extract-and-expand.
|
|
189
|
+
*
|
|
157
190
|
* On SEC 1 / Weierstrass curves, that means the compressed shared-point body without the
|
|
158
191
|
* 1-byte `0x02` / `0x03` prefix.
|
|
159
192
|
* The X25519 path also leaves RFC 7748's optional all-zero shared-secret check to callers.
|
|
@@ -169,14 +202,19 @@ function ecKeygen(curve: CurveAll, allowZeroKey: boolean = false) {
|
|
|
169
202
|
* Wrap an ECDH-capable curve as a generic KEM.
|
|
170
203
|
* ```ts
|
|
171
204
|
* import { x25519 } from '@noble/curves/ed25519.js';
|
|
172
|
-
* import {
|
|
173
|
-
* const kem =
|
|
205
|
+
* import { _ecdhKem } from '@noble/post-quantum/hybrid.js';
|
|
206
|
+
* const kem = _ecdhKem(x25519);
|
|
174
207
|
* const publicKeyLen = kem.lengths.publicKey;
|
|
175
208
|
* ```
|
|
176
209
|
*/
|
|
177
|
-
export function
|
|
210
|
+
export function _ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): TRet<KEM> {
|
|
178
211
|
const kg = ecKeygen(curve, allowZeroKey);
|
|
179
212
|
if (!curve.getSharedSecret) throw new Error('wrong curve'); // ed25519 doesn't have one!
|
|
213
|
+
// Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
|
|
214
|
+
const decapsulate = (cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>) => {
|
|
215
|
+
const res = curve.getSharedSecret(secretKey, cipherText);
|
|
216
|
+
return (curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res) as TRet<Uint8Array>;
|
|
217
|
+
};
|
|
180
218
|
return {
|
|
181
219
|
lengths: { ...kg.lengths, msg: kg.lengths.seed, cipherText: kg.lengths.publicKey },
|
|
182
220
|
keygen: kg.keygen,
|
|
@@ -190,8 +228,8 @@ export function ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): TRet<K
|
|
|
190
228
|
const seed = copyBytes(rand);
|
|
191
229
|
let ek: Uint8Array | undefined = undefined;
|
|
192
230
|
try {
|
|
193
|
-
ek =
|
|
194
|
-
const sharedSecret =
|
|
231
|
+
ek = kg.keygen(seed).secretKey;
|
|
232
|
+
const sharedSecret = decapsulate(publicKey, ek);
|
|
195
233
|
const cipherText = curve.getPublicKey(ek) as TRet<Uint8Array>;
|
|
196
234
|
return { sharedSecret, cipherText };
|
|
197
235
|
} finally {
|
|
@@ -201,10 +239,7 @@ export function ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): TRet<K
|
|
|
201
239
|
if (ek) cleanBytes(ek);
|
|
202
240
|
}
|
|
203
241
|
},
|
|
204
|
-
decapsulate
|
|
205
|
-
const res = curve.getSharedSecret(secretKey, cipherText);
|
|
206
|
-
return (curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res) as TRet<Uint8Array>;
|
|
207
|
-
},
|
|
242
|
+
decapsulate,
|
|
208
243
|
};
|
|
209
244
|
}
|
|
210
245
|
|
|
@@ -237,7 +272,7 @@ export function ecSigner(curve: CurveSign, allowZeroKey: boolean = false): TRet<
|
|
|
237
272
|
keygen: kg.keygen,
|
|
238
273
|
getPublicKey: kg.getPublicKey,
|
|
239
274
|
sign: (message, secretKey, opts = {}) => {
|
|
240
|
-
validateSigOpts(opts);
|
|
275
|
+
opts = validateSigOpts(opts);
|
|
241
276
|
// This generic wrapper intentionally keeps the Signer contract to message + key only.
|
|
242
277
|
// Backend-specific knobs like ECDSA extraEntropy or Ed25519ctx context cannot be forwarded
|
|
243
278
|
// uniformly through combineSigners(), so callers that need them must use the curve directly.
|
|
@@ -254,7 +289,7 @@ export function ecSigner(curve: CurveSign, allowZeroKey: boolean = false): TRet<
|
|
|
254
289
|
* generic opts and lets wrapped-curve malformed-input errors escape unchanged.
|
|
255
290
|
*/
|
|
256
291
|
verify: (signature, message, publicKey, opts = {}) => {
|
|
257
|
-
validateVerOpts(opts);
|
|
292
|
+
opts = validateVerOpts(opts);
|
|
258
293
|
if (opts.context !== undefined)
|
|
259
294
|
throw new Error('ecSigner does not support context; use the underlying curve directly');
|
|
260
295
|
return curve.verify(signature, message, publicKey);
|
|
@@ -262,18 +297,26 @@ export function ecSigner(curve: CurveSign, allowZeroKey: boolean = false): TRet<
|
|
|
262
297
|
};
|
|
263
298
|
}
|
|
264
299
|
|
|
300
|
+
function positiveLength(value: number, title: string): number {
|
|
301
|
+
const length = anumber(value, title);
|
|
302
|
+
if (length === 0) throw new RangeError(`"${title}" expected integer greater than 0, got 0`);
|
|
303
|
+
return length;
|
|
304
|
+
}
|
|
305
|
+
|
|
265
306
|
function splitLengths<K extends string, T extends { lengths: Partial<Record<K, number>> }>(
|
|
266
307
|
lst: T[],
|
|
267
308
|
name: K
|
|
268
309
|
) {
|
|
269
310
|
// Preserve caller order exactly; raw numeric fields still decode as splitCoder() subarray views.
|
|
270
|
-
|
|
311
|
+
const coder = splitCoder(
|
|
271
312
|
name,
|
|
272
313
|
...lst.map((i) => {
|
|
273
314
|
if (typeof i.lengths[name] !== 'number') throw new Error('wrong length: ' + name);
|
|
274
|
-
return i.lengths[name];
|
|
315
|
+
return positiveLength(i.lengths[name], name);
|
|
275
316
|
})
|
|
276
317
|
);
|
|
318
|
+
positiveLength(coder.bytesLen, name);
|
|
319
|
+
return coder;
|
|
277
320
|
}
|
|
278
321
|
|
|
279
322
|
/** Seed-expansion callback used by the hybrid combiners. */
|
|
@@ -319,13 +362,15 @@ function combineKeys(
|
|
|
319
362
|
const seedCoder = splitLengths(ck, 'seed');
|
|
320
363
|
const pkCoder = splitLengths(ck, 'publicKey');
|
|
321
364
|
// Allows to use identity functions for combiner/expandSeed
|
|
322
|
-
|
|
323
|
-
|
|
365
|
+
const rootSeedLen = positiveLength(
|
|
366
|
+
realSeedLen === undefined ? seedCoder.bytesLen : realSeedLen,
|
|
367
|
+
'realSeedLen'
|
|
368
|
+
);
|
|
324
369
|
function expandDecapsulationKey(seed: TArg<Uint8Array>): TRet<{
|
|
325
370
|
secretKey: Uint8Array[];
|
|
326
371
|
publicKey: Uint8Array[];
|
|
327
372
|
}> {
|
|
328
|
-
abytes(seed,
|
|
373
|
+
abytes(seed, rootSeedLen);
|
|
329
374
|
const expandedRaw = expandSeed(seed, seedCoder.bytesLen);
|
|
330
375
|
// Identity/subarray expanders can hand back caller-owned seed storage. Detach those outputs so
|
|
331
376
|
// later cleanup can wipe the expanded schedule without mutating the caller's root seed bytes.
|
|
@@ -359,39 +404,60 @@ function combineKeys(
|
|
|
359
404
|
if (!ok) cleanBytes(secretKey);
|
|
360
405
|
}
|
|
361
406
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const { publicKey: pk, secretKey } = expandDecapsulationKey(seed);
|
|
407
|
+
// Standalone (not a method) so getPublicKey / destructured usage never depends on `this`.
|
|
408
|
+
const keygen = (seed?: TArg<Uint8Array>) => {
|
|
409
|
+
// Detach the root: the exported secretKey must not alias caller-owned seed bytes, so later
|
|
410
|
+
// caller mutation of the seed cannot silently change the secret key (and vice versa).
|
|
411
|
+
const root = seed === undefined ? randomBytes(rootSeedLen) : copyBytes(seed);
|
|
412
|
+
let res;
|
|
413
|
+
try {
|
|
414
|
+
const { publicKey: pk, secretKey } = expandDecapsulationKey(root);
|
|
371
415
|
try {
|
|
372
|
-
|
|
373
|
-
|
|
416
|
+
res = {
|
|
417
|
+
secretKey: root as TRet<Uint8Array>,
|
|
418
|
+
publicKey: pkCoder.encode(pk) as TRet<Uint8Array>,
|
|
419
|
+
};
|
|
374
420
|
} finally {
|
|
375
|
-
|
|
376
|
-
// The exported secretKey is the caller/root seed itself; child secret keys are internal
|
|
421
|
+
// The exported secretKey is the (detached) root seed; child secret keys are internal
|
|
377
422
|
// expansion outputs that are cleaned whether encoding succeeds or throws.
|
|
378
|
-
cleanBytes(secretKey);
|
|
423
|
+
cleanBytes(pk, secretKey);
|
|
379
424
|
}
|
|
425
|
+
return res;
|
|
426
|
+
} finally {
|
|
427
|
+
if (!res) cleanBytes(root);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
return {
|
|
431
|
+
info: { lengths: { seed: rootSeedLen, publicKey: pkCoder.bytesLen, secretKey: rootSeedLen } },
|
|
432
|
+
// Composite secret keys are root seeds, so public-key derivation reruns key expansion from
|
|
433
|
+
// that seed instead of decoding a packed child-secret-key structure.
|
|
434
|
+
getPublicKey: (secretKey: TArg<Uint8Array>) => {
|
|
435
|
+
const keys = keygen(secretKey);
|
|
436
|
+
// keygen detaches its exported root; getPublicKey discards that half of the result.
|
|
437
|
+
cleanBytes(keys.secretKey);
|
|
438
|
+
return keys.publicKey as TRet<Uint8Array>;
|
|
380
439
|
},
|
|
440
|
+
keygen,
|
|
381
441
|
expandDecapsulationKey,
|
|
382
|
-
realSeedLen,
|
|
442
|
+
realSeedLen: rootSeedLen,
|
|
383
443
|
};
|
|
384
444
|
}
|
|
385
445
|
|
|
386
446
|
// This generic function that combines multiple KEMs into single one
|
|
387
447
|
/**
|
|
388
448
|
* Combines multiple KEMs into one composite KEM.
|
|
389
|
-
* @param realSeedLen -
|
|
390
|
-
*
|
|
449
|
+
* @param realSeedLen - Positive input seed length expected by `expandSeed`, or `undefined` to use
|
|
450
|
+
* the sum of component seed lengths. Callers remain responsible for choosing a security-appropriate
|
|
451
|
+
* size.
|
|
452
|
+
* @param realMsgLen - Positive shared-secret length returned by `combiner`, or `undefined` to use
|
|
453
|
+
* the sum of component message lengths.
|
|
391
454
|
* @param expandSeed - Seed expander used to derive per-KEM seeds.
|
|
392
455
|
* @param combiner - Combines the per-KEM outputs into one shared secret.
|
|
393
|
-
* @param kems - KEM
|
|
456
|
+
* @param kems - At least one KEM implementation. A construction advertised as hybrid normally
|
|
457
|
+
* supplies two or more.
|
|
394
458
|
* @returns Composite KEM.
|
|
459
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
460
|
+
* @throws If there are no components or any required length resolves to zero. {@link RangeError}
|
|
395
461
|
* @example
|
|
396
462
|
* Combine multiple KEMs into one composite KEM.
|
|
397
463
|
* ```ts
|
|
@@ -416,20 +482,56 @@ export function combineKEMS(
|
|
|
416
482
|
combiner: TArg<Combiner>,
|
|
417
483
|
...kems: TArg<KEM[]>
|
|
418
484
|
): TRet<KEM> {
|
|
485
|
+
if (realSeedLen !== undefined) positiveLength(realSeedLen, 'realSeedLen');
|
|
486
|
+
if (realMsgLen !== undefined) positiveLength(realMsgLen, 'realMsgLen');
|
|
487
|
+
if (typeof expandSeed !== 'function')
|
|
488
|
+
throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
|
|
489
|
+
if (typeof combiner !== 'function')
|
|
490
|
+
throw new TypeError('"combiner" expected function, got type=' + typeof combiner);
|
|
419
491
|
const rawCombiner = combiner as Combiner;
|
|
420
492
|
const rawKems = kems as KEM[];
|
|
493
|
+
if (rawKems.length === 0) throw new RangeError('combineKEMS requires at least one KEM');
|
|
494
|
+
for (let i = 0; i < rawKems.length; i++) validateKEM(rawKems[i], `kems[${i}]`);
|
|
421
495
|
const keys = combineKeys(realSeedLen, expandSeed, ...rawKems);
|
|
422
496
|
const ctCoder = splitLengths(rawKems, 'cipherText');
|
|
423
497
|
const pkCoder = splitLengths(rawKems, 'publicKey');
|
|
424
498
|
const msgCoder = splitLengths(rawKems, 'msg');
|
|
425
|
-
|
|
426
|
-
|
|
499
|
+
const sharedSecretLen = positiveLength(
|
|
500
|
+
realMsgLen === undefined ? msgCoder.bytesLen : realMsgLen,
|
|
501
|
+
'realMsgLen'
|
|
502
|
+
);
|
|
427
503
|
const lengths = Object.freeze({
|
|
428
504
|
...keys.info.lengths,
|
|
429
|
-
msg:
|
|
505
|
+
msg: sharedSecretLen,
|
|
430
506
|
msgRand: msgCoder.bytesLen,
|
|
431
507
|
cipherText: ctCoder.bytesLen,
|
|
432
508
|
});
|
|
509
|
+
const combine = (
|
|
510
|
+
publicKeys: TArg<Uint8Array[]>,
|
|
511
|
+
cipherTexts: TArg<Uint8Array[]>,
|
|
512
|
+
sharedSecrets: TArg<Uint8Array[]>
|
|
513
|
+
): TRet<Uint8Array> => {
|
|
514
|
+
const combined = rawCombiner(publicKeys, cipherTexts, sharedSecrets);
|
|
515
|
+
try {
|
|
516
|
+
return copyBytes(abytes(combined, sharedSecretLen, 'sharedSecret'));
|
|
517
|
+
} catch (error) {
|
|
518
|
+
if (isBytes(combined)) {
|
|
519
|
+
// A combiner may return any callback argument. Public keys during encapsulation and
|
|
520
|
+
// ciphertexts during decapsulation are views into caller-owned inputs, so wipe an invalid
|
|
521
|
+
// byte result only when its range does not overlap either public argument vector. Child
|
|
522
|
+
// shared-secret aliases are already wiped by the operation's outer finally block.
|
|
523
|
+
const overlaps = (value: TArg<Uint8Array>) =>
|
|
524
|
+
combined.buffer === value.buffer &&
|
|
525
|
+
combined.byteOffset < value.byteOffset + value.byteLength &&
|
|
526
|
+
value.byteOffset < combined.byteOffset + combined.byteLength;
|
|
527
|
+
const aliasesPublicInput =
|
|
528
|
+
(publicKeys as Uint8Array[]).some(overlaps) ||
|
|
529
|
+
(cipherTexts as Uint8Array[]).some(overlaps);
|
|
530
|
+
if (!aliasesPublicInput) cleanBytes(combined);
|
|
531
|
+
}
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
433
535
|
return Object.freeze({
|
|
434
536
|
lengths,
|
|
435
537
|
getPublicKey: keys.getPublicKey,
|
|
@@ -448,11 +550,14 @@ export function combineKEMS(
|
|
|
448
550
|
sharedSecret.push(enc.sharedSecret);
|
|
449
551
|
cipherText.push(enc.cipherText);
|
|
450
552
|
}
|
|
553
|
+
// Validate and detach public ciphertexts before deriving a final secret from them. This
|
|
554
|
+
// also ensures a malformed child cannot make us allocate and then strand a combined key.
|
|
555
|
+
const encodedCipherText = ctCoder.encode(cipherText) as TRet<Uint8Array>;
|
|
451
556
|
return {
|
|
452
557
|
// Detach the combiner result before cleanup: a caller-provided combiner may alias one of
|
|
453
558
|
// the child sharedSecret buffers, and those child buffers are zeroized immediately below.
|
|
454
|
-
sharedSecret:
|
|
455
|
-
cipherText:
|
|
559
|
+
sharedSecret: combine(pks, cipherText, sharedSecret),
|
|
560
|
+
cipherText: encodedCipherText,
|
|
456
561
|
};
|
|
457
562
|
} finally {
|
|
458
563
|
// Child encapsulation or combiner failures can happen after some components already
|
|
@@ -463,11 +568,16 @@ export function combineKEMS(
|
|
|
463
568
|
decapsulate(ct: TArg<Uint8Array>, seed: TArg<Uint8Array>) {
|
|
464
569
|
const cts = ctCoder.decode(ct);
|
|
465
570
|
const { publicKey, secretKey } = keys.expandDecapsulationKey(seed);
|
|
466
|
-
const sharedSecret
|
|
571
|
+
const sharedSecret: Uint8Array[] = [];
|
|
467
572
|
try {
|
|
573
|
+
// Child decapsulate() is inside the try: it can throw on an attacker-supplied ciphertext
|
|
574
|
+
// (e.g. a low-order X25519 point), and by then the expanded child secret keys — plus any
|
|
575
|
+
// child shared secrets already produced — are live and must still be wiped.
|
|
576
|
+
for (let i = 0; i < rawKems.length; i++)
|
|
577
|
+
sharedSecret.push(rawKems[i].decapsulate(cts[i], secretKey[i]));
|
|
468
578
|
// Detach the decapsulation result before cleanup: the combiner may hand back one of the
|
|
469
579
|
// child shared-secret buffers, and those temporary buffers are zeroized below.
|
|
470
|
-
return
|
|
580
|
+
return combine(publicKey, cts, sharedSecret);
|
|
471
581
|
} finally {
|
|
472
582
|
// Decapsulation only needs the expanded child secret keys and child shared secrets for this
|
|
473
583
|
// call; keep the caller/root seed intact, but wipe all derived material even on errors.
|
|
@@ -480,10 +590,15 @@ export function combineKEMS(
|
|
|
480
590
|
// realSeedLen: how much bytes expandSeed expects.
|
|
481
591
|
/**
|
|
482
592
|
* Combines multiple signers into one composite signer.
|
|
483
|
-
* @param realSeedLen -
|
|
593
|
+
* @param realSeedLen - Positive input seed length expected by `expandSeed`, or `undefined` to use
|
|
594
|
+
* the sum of component seed lengths. Callers remain responsible for choosing a security-appropriate
|
|
595
|
+
* size.
|
|
484
596
|
* @param expandSeed - Seed expander used to derive per-signer seeds.
|
|
485
|
-
* @param signers -
|
|
597
|
+
* @param signers - At least one signer. A construction advertised as hybrid normally supplies two
|
|
598
|
+
* or more.
|
|
486
599
|
* @returns Composite signer.
|
|
600
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
601
|
+
* @throws If there are no components or any required length resolves to zero. {@link RangeError}
|
|
487
602
|
* @example
|
|
488
603
|
* Combine multiple signers into one composite signer.
|
|
489
604
|
* ```ts
|
|
@@ -491,7 +606,11 @@ export function combineKEMS(
|
|
|
491
606
|
* import { combineSigners, expandSeedXof } from '@noble/post-quantum/hybrid.js';
|
|
492
607
|
* import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
|
|
493
608
|
* const hybrid = combineSigners(32, expandSeedXof(shake256), ml_dsa44, ml_dsa44);
|
|
494
|
-
* const
|
|
609
|
+
* const seed = new Uint8Array(hybrid.lengths.seed!).fill(1);
|
|
610
|
+
* const { secretKey, publicKey } = hybrid.keygen(seed);
|
|
611
|
+
* const msg = new TextEncoder().encode('hello noble');
|
|
612
|
+
* const sig = hybrid.sign(msg, secretKey);
|
|
613
|
+
* const isValid = hybrid.verify(sig, msg, publicKey);
|
|
495
614
|
* ```
|
|
496
615
|
*/
|
|
497
616
|
export function combineSigners(
|
|
@@ -499,7 +618,12 @@ export function combineSigners(
|
|
|
499
618
|
expandSeed: TArg<ExpandSeed>,
|
|
500
619
|
...signers: TArg<Signer[]>
|
|
501
620
|
): TRet<Signer> {
|
|
621
|
+
if (realSeedLen !== undefined) positiveLength(realSeedLen, 'realSeedLen');
|
|
622
|
+
if (typeof expandSeed !== 'function')
|
|
623
|
+
throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
|
|
502
624
|
const rawSigners = signers as Signer[];
|
|
625
|
+
if (rawSigners.length === 0) throw new RangeError('combineSigners requires at least one signer');
|
|
626
|
+
for (let i = 0; i < rawSigners.length; i++) validateSigner(rawSigners[i], `signers[${i}]`);
|
|
503
627
|
const keys = combineKeys(realSeedLen, expandSeed, ...rawSigners);
|
|
504
628
|
const sigCoder = splitLengths(rawSigners, 'signature');
|
|
505
629
|
const pkCoder = splitLengths(rawSigners, 'publicKey');
|
|
@@ -508,7 +632,7 @@ export function combineSigners(
|
|
|
508
632
|
getPublicKey: keys.getPublicKey,
|
|
509
633
|
keygen: keys.keygen,
|
|
510
634
|
sign(message, seed, opts = {}) {
|
|
511
|
-
validateSigOpts(opts);
|
|
635
|
+
opts = validateSigOpts(opts);
|
|
512
636
|
// This generic wrapper intentionally keeps the composite signer contract to message + root
|
|
513
637
|
// seed only. Per-signer opts like context or extraEntropy cannot be preserved uniformly
|
|
514
638
|
// across mixed backends, so callers that need them must use the underlying signer directly.
|
|
@@ -531,16 +655,22 @@ export function combineSigners(
|
|
|
531
655
|
}
|
|
532
656
|
},
|
|
533
657
|
/** Verify one combined signature.
|
|
534
|
-
*
|
|
535
|
-
*
|
|
658
|
+
* Wrong-length aggregate signatures return `false` (matching ml-dsa / slh-dsa behavior), as
|
|
659
|
+
* does any failing child verify. Throws on unsupported generic opts or malformed publicKey.
|
|
536
660
|
*/
|
|
537
661
|
verify: (signature, message, publicKey, opts = {}) => {
|
|
538
|
-
validateVerOpts(opts);
|
|
662
|
+
opts = validateVerOpts(opts);
|
|
539
663
|
if (opts.context !== undefined)
|
|
540
664
|
throw new Error(
|
|
541
665
|
'combineSigners does not support context; use the underlying signer directly'
|
|
542
666
|
);
|
|
667
|
+
// Malformed signature *length* is a verification failure, not a thrown type error —
|
|
668
|
+
// consistent with ml-dsa / slh-dsa. Must run before sigCoder.decode, which throws.
|
|
669
|
+
// Preserve TypeError for non-byte API arguments before treating byte lengths as invalid.
|
|
670
|
+
abytes(signature, undefined, 'signature');
|
|
671
|
+
// A signature failure must not hide malformed aggregate public-key bytes.
|
|
543
672
|
const pks = pkCoder.decode(publicKey);
|
|
673
|
+
if (signature.length !== sigCoder.bytesLen) return false;
|
|
544
674
|
const sigs = sigCoder.decode(signature);
|
|
545
675
|
for (let i = 0; i < rawSigners.length; i++) {
|
|
546
676
|
if (!rawSigners[i].verify(sigs[i], message, pks[i])) return false;
|
|
@@ -563,14 +693,16 @@ export function combineSigners(
|
|
|
563
693
|
* @param xof - XOF used for seed expansion.
|
|
564
694
|
* @param kdf - Hash used for the final combiner.
|
|
565
695
|
* @returns Hybrid KEM.
|
|
696
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
697
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
566
698
|
* @example
|
|
567
699
|
* Build a QSF hybrid KEM preset from a PQ KEM and an elliptic-curve KEM.
|
|
568
700
|
* ```ts
|
|
569
701
|
* import { p256 } from '@noble/curves/nist.js';
|
|
570
702
|
* import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
|
|
571
|
-
* import { QSF,
|
|
703
|
+
* import { QSF, _ecdhKem } from '@noble/post-quantum/hybrid.js';
|
|
572
704
|
* import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
|
|
573
|
-
* const kem = QSF('example', ml_kem768,
|
|
705
|
+
* const kem = QSF('example', ml_kem768, _ecdhKem(p256, true), shake256, sha3_256);
|
|
574
706
|
* const publicKeyLen = kem.lengths.publicKey;
|
|
575
707
|
* ```
|
|
576
708
|
*/
|
|
@@ -581,7 +713,14 @@ export function QSF(
|
|
|
581
713
|
xof: TArg<XOF>,
|
|
582
714
|
kdf: CHash
|
|
583
715
|
): TRet<KEM> {
|
|
716
|
+
astring(label, 'label');
|
|
717
|
+
validateKEM(pqc, 'pqc');
|
|
718
|
+
validateKEM(curveKEM, 'curveKEM');
|
|
719
|
+
if (typeof xof !== 'function' || typeof (xof as any).create !== 'function')
|
|
720
|
+
throw new TypeError('"xof" expected hash function, got type=' + typeof xof);
|
|
584
721
|
ahash(xof);
|
|
722
|
+
if (typeof kdf !== 'function' || typeof (kdf as any).create !== 'function')
|
|
723
|
+
throw new TypeError('"kdf" expected hash function, got type=' + typeof kdf);
|
|
585
724
|
ahash(kdf);
|
|
586
725
|
return combineKEMS(
|
|
587
726
|
32,
|
|
@@ -599,7 +738,7 @@ export const QSF_ml_kem768_p256: TRet<KEM> = /* @__PURE__ */ (() =>
|
|
|
599
738
|
QSF(
|
|
600
739
|
'QSF-KEM(ML-KEM-768,P-256)-XOF(SHAKE256)-KDF(SHA3-256)',
|
|
601
740
|
ml_kem768,
|
|
602
|
-
|
|
741
|
+
_ecdhKem(p256, true),
|
|
603
742
|
shake256,
|
|
604
743
|
sha3_256
|
|
605
744
|
))();
|
|
@@ -608,7 +747,7 @@ export const QSF_ml_kem1024_p384: TRet<KEM> = /* @__PURE__ */ (() =>
|
|
|
608
747
|
QSF(
|
|
609
748
|
'QSF-KEM(ML-KEM-1024,P-384)-XOF(SHAKE256)-KDF(SHA3-256)',
|
|
610
749
|
ml_kem1024,
|
|
611
|
-
|
|
750
|
+
_ecdhKem(p384, true),
|
|
612
751
|
shake256,
|
|
613
752
|
sha3_256
|
|
614
753
|
))();
|
|
@@ -627,15 +766,17 @@ export const QSF_ml_kem1024_p384: TRet<KEM> = /* @__PURE__ */ (() =>
|
|
|
627
766
|
* @param xof - XOF used for seed expansion.
|
|
628
767
|
* @param hash - Hash used for HKDF extraction and expansion.
|
|
629
768
|
* @returns Hybrid KEM.
|
|
769
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
770
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
630
771
|
* @example
|
|
631
772
|
* Build the "KitchenSink" hybrid KEM combiner.
|
|
632
773
|
* ```ts
|
|
633
774
|
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
634
775
|
* import { shake256 } from '@noble/hashes/sha3.js';
|
|
635
|
-
* import { createKitchenSink,
|
|
776
|
+
* import { createKitchenSink, _ecdhKem } from '@noble/post-quantum/hybrid.js';
|
|
636
777
|
* import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
|
|
637
778
|
* import { x25519 } from '@noble/curves/ed25519.js';
|
|
638
|
-
* const kem = createKitchenSink('example', ml_kem768,
|
|
779
|
+
* const kem = createKitchenSink('example', ml_kem768, _ecdhKem(x25519), shake256, sha256);
|
|
639
780
|
* const publicKeyLen = kem.lengths.publicKey;
|
|
640
781
|
* ```
|
|
641
782
|
*/
|
|
@@ -646,7 +787,14 @@ export function createKitchenSink(
|
|
|
646
787
|
xof: TArg<XOF>,
|
|
647
788
|
hash: CHash
|
|
648
789
|
): TRet<KEM> {
|
|
790
|
+
astring(label, 'label');
|
|
791
|
+
validateKEM(pqc, 'pqc');
|
|
792
|
+
validateKEM(curveKEM, 'curveKEM');
|
|
793
|
+
if (typeof xof !== 'function' || typeof (xof as any).create !== 'function')
|
|
794
|
+
throw new TypeError('"xof" expected hash function, got type=' + typeof xof);
|
|
649
795
|
ahash(xof);
|
|
796
|
+
if (typeof hash !== 'function' || typeof (hash as any).create !== 'function')
|
|
797
|
+
throw new TypeError('"hash" expected hash function, got type=' + typeof hash);
|
|
650
798
|
ahash(hash);
|
|
651
799
|
return combineKEMS(
|
|
652
800
|
32,
|
|
@@ -671,9 +819,9 @@ export function createKitchenSink(
|
|
|
671
819
|
);
|
|
672
820
|
}
|
|
673
821
|
|
|
674
|
-
// Internal alias only: this stays exactly `
|
|
822
|
+
// Internal alias only: this stays exactly `_ecdhKem(x25519)`
|
|
675
823
|
// and inherits that wrapper's mutation/oracle behavior.
|
|
676
|
-
const x25519kem = /* @__PURE__ */
|
|
824
|
+
const x25519kem = /* @__PURE__ */ _ecdhKem(x25519);
|
|
677
825
|
/** KitchenSink preset combining ML-KEM-768 with X25519.
|
|
678
826
|
* Caller randomness splits into 32 ML-KEM coins plus a 32-byte X25519 ephemeral-secret seed.
|
|
679
827
|
*/
|
|
@@ -734,6 +882,11 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
|
|
|
734
882
|
}>;
|
|
735
883
|
}
|
|
736
884
|
|
|
885
|
+
// Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
|
|
886
|
+
const decapsulate = (cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>) => {
|
|
887
|
+
const full = curve.getSharedSecret(secretKey, cipherText);
|
|
888
|
+
return full.subarray(1) as TRet<Uint8Array>;
|
|
889
|
+
};
|
|
737
890
|
return {
|
|
738
891
|
lengths: {
|
|
739
892
|
secretKey: scalarLen,
|
|
@@ -754,7 +907,7 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
|
|
|
754
907
|
let ek: Uint8Array | undefined = undefined;
|
|
755
908
|
try {
|
|
756
909
|
ek = rejectionSampling(rand).secretKey;
|
|
757
|
-
const sharedSecret =
|
|
910
|
+
const sharedSecret = decapsulate(publicKey, ek);
|
|
758
911
|
const cipherText = curve.getPublicKey(ek, false) as TRet<Uint8Array>;
|
|
759
912
|
return { sharedSecret, cipherText };
|
|
760
913
|
} finally {
|
|
@@ -763,10 +916,7 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
|
|
|
763
916
|
if (ek) cleanBytes(ek);
|
|
764
917
|
}
|
|
765
918
|
},
|
|
766
|
-
decapsulate
|
|
767
|
-
const full = curve.getSharedSecret(secretKey, cipherText);
|
|
768
|
-
return full.subarray(1) as TRet<Uint8Array>;
|
|
769
|
-
},
|
|
919
|
+
decapsulate,
|
|
770
920
|
};
|
|
771
921
|
}
|
|
772
922
|
|
|
@@ -795,10 +945,11 @@ function concreteHybridKem(
|
|
|
795
945
|
32,
|
|
796
946
|
(seed: TArg<Uint8Array>): TRet<Uint8Array> => {
|
|
797
947
|
abytes(seed, 32);
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
948
|
+
// One SHAKE256 stream split by the seed coder as mlkemSeed (64) || curveSeed (nseed).
|
|
949
|
+
// Returned directly: the previous concatBytes of two adjacent subarrays produced an
|
|
950
|
+
// identical copy while leaving this original buffer unwiped; expandDecapsulationKey
|
|
951
|
+
// wipes the returned buffer after the child seeds are copied out.
|
|
952
|
+
return shake256(seed, { dkLen: totalSeedLen }) as TRet<Uint8Array>;
|
|
802
953
|
},
|
|
803
954
|
(pk: TArg<Uint8Array[]>, ct: TArg<Uint8Array[]>, ss: TArg<Uint8Array[]>) =>
|
|
804
955
|
sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))),
|
|
@@ -814,20 +965,3 @@ export const ml_kem768_p256: TRet<KEM> = /* @__PURE__ */ (() =>
|
|
|
814
965
|
/** P-384 + ML-KEM-1024 hybrid preset. */
|
|
815
966
|
export const ml_kem1024_p384: TRet<KEM> = /* @__PURE__ */ (() =>
|
|
816
967
|
concreteHybridKem('MLKEM1024-P384', ml_kem1024, p384, 48))();
|
|
817
|
-
|
|
818
|
-
// Legacy aliases
|
|
819
|
-
/** Legacy alias for `ml_kem768_x25519`. */
|
|
820
|
-
export const XWing: TRet<KEM> = /* @__PURE__ */ (() => ml_kem768_x25519)();
|
|
821
|
-
/** Legacy alias for `ml_kem768_x25519`. */
|
|
822
|
-
export const MLKEM768X25519: TRet<KEM> = /* @__PURE__ */ (() => ml_kem768_x25519)();
|
|
823
|
-
/** Legacy alias for `ml_kem768_p256`. */
|
|
824
|
-
export const MLKEM768P256: TRet<KEM> = /* @__PURE__ */ (() => ml_kem768_p256)();
|
|
825
|
-
/** Legacy alias for `ml_kem1024_p384`. */
|
|
826
|
-
export const MLKEM1024P384: TRet<KEM> = /* @__PURE__ */ (() => ml_kem1024_p384)();
|
|
827
|
-
/** Legacy alias for `QSF_ml_kem768_p256`. */
|
|
828
|
-
export const QSFMLKEM768P256: TRet<KEM> = /* @__PURE__ */ (() => QSF_ml_kem768_p256)();
|
|
829
|
-
/** Legacy alias for `QSF_ml_kem1024_p384`. */
|
|
830
|
-
export const QSFMLKEM1024P384: TRet<KEM> = /* @__PURE__ */ (() => QSF_ml_kem1024_p384)();
|
|
831
|
-
/** Legacy alias for `KitchenSink_ml_kem768_x25519`. */
|
|
832
|
-
export const KitchenSinkMLKEM768X25519: TRet<KEM> = /* @__PURE__ */ (() =>
|
|
833
|
-
KitchenSink_ml_kem768_x25519)();
|
package/src/index.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from '@noble/post-quantum/falcon.js';
|
|
20
20
|
import {
|
|
21
21
|
ml_kem768_x25519, ml_kem768_p256, ml_kem1024_p384,
|
|
22
|
-
KitchenSink_ml_kem768_x25519,
|
|
22
|
+
KitchenSink_ml_kem768_x25519,
|
|
23
23
|
QSF_ml_kem768_p256, QSF_ml_kem1024_p384,
|
|
24
24
|
} from '@noble/post-quantum/hybrid.js';
|
|
25
25
|
```
|