@noble/post-quantum 0.7.0 → 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 +90 -16
- package/_crystals.js +1 -1
- package/falcon.d.ts +1 -1
- package/falcon.js +121 -62
- package/hybrid.d.ts +33 -12
- package/hybrid.js +100 -41
- package/index.js +1 -1
- package/ml-dsa.d.ts +3 -3
- package/ml-dsa.js +57 -20
- package/ml-kem.js +85 -34
- package/package.json +15 -11
- package/slh-dsa.js +34 -11
- package/src/_crystals.ts +1 -1
- package/src/falcon.ts +127 -70
- package/src/hybrid.ts +108 -39
- package/src/index.ts +1 -1
- package/src/ml-dsa.ts +70 -25
- package/src/ml-kem.ts +76 -35
- package/src/slh-dsa.ts +44 -13
- package/src/utils.ts +115 -10
- package/src/webcrypto.ts +322 -0
- package/utils.d.ts +36 -2
- package/utils.js +105 -12
- package/webcrypto.d.ts +91 -0
- package/webcrypto.js +213 -0
package/src/hybrid.ts
CHANGED
|
@@ -91,7 +91,7 @@ import {
|
|
|
91
91
|
import { expand, extract } from '@noble/hashes/hkdf.js';
|
|
92
92
|
import { sha256 } from '@noble/hashes/sha2.js';
|
|
93
93
|
import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
|
|
94
|
-
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';
|
|
95
95
|
import { ml_kem1024, ml_kem768 } from './ml-kem.ts';
|
|
96
96
|
import {
|
|
97
97
|
aobject,
|
|
@@ -181,6 +181,12 @@ function ecKeygen(curve: CurveAll, allowZeroKey: boolean = false) {
|
|
|
181
181
|
/**
|
|
182
182
|
* Wraps an ECDH-capable curve as a KEM.
|
|
183
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
|
+
*
|
|
184
190
|
* On SEC 1 / Weierstrass curves, that means the compressed shared-point body without the
|
|
185
191
|
* 1-byte `0x02` / `0x03` prefix.
|
|
186
192
|
* The X25519 path also leaves RFC 7748's optional all-zero shared-secret check to callers.
|
|
@@ -196,12 +202,12 @@ function ecKeygen(curve: CurveAll, allowZeroKey: boolean = false) {
|
|
|
196
202
|
* Wrap an ECDH-capable curve as a generic KEM.
|
|
197
203
|
* ```ts
|
|
198
204
|
* import { x25519 } from '@noble/curves/ed25519.js';
|
|
199
|
-
* import {
|
|
200
|
-
* const kem =
|
|
205
|
+
* import { _ecdhKem } from '@noble/post-quantum/hybrid.js';
|
|
206
|
+
* const kem = _ecdhKem(x25519);
|
|
201
207
|
* const publicKeyLen = kem.lengths.publicKey;
|
|
202
208
|
* ```
|
|
203
209
|
*/
|
|
204
|
-
export function
|
|
210
|
+
export function _ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): TRet<KEM> {
|
|
205
211
|
const kg = ecKeygen(curve, allowZeroKey);
|
|
206
212
|
if (!curve.getSharedSecret) throw new Error('wrong curve'); // ed25519 doesn't have one!
|
|
207
213
|
// Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
|
|
@@ -266,7 +272,7 @@ export function ecSigner(curve: CurveSign, allowZeroKey: boolean = false): TRet<
|
|
|
266
272
|
keygen: kg.keygen,
|
|
267
273
|
getPublicKey: kg.getPublicKey,
|
|
268
274
|
sign: (message, secretKey, opts = {}) => {
|
|
269
|
-
validateSigOpts(opts);
|
|
275
|
+
opts = validateSigOpts(opts);
|
|
270
276
|
// This generic wrapper intentionally keeps the Signer contract to message + key only.
|
|
271
277
|
// Backend-specific knobs like ECDSA extraEntropy or Ed25519ctx context cannot be forwarded
|
|
272
278
|
// uniformly through combineSigners(), so callers that need them must use the curve directly.
|
|
@@ -283,7 +289,7 @@ export function ecSigner(curve: CurveSign, allowZeroKey: boolean = false): TRet<
|
|
|
283
289
|
* generic opts and lets wrapped-curve malformed-input errors escape unchanged.
|
|
284
290
|
*/
|
|
285
291
|
verify: (signature, message, publicKey, opts = {}) => {
|
|
286
|
-
validateVerOpts(opts);
|
|
292
|
+
opts = validateVerOpts(opts);
|
|
287
293
|
if (opts.context !== undefined)
|
|
288
294
|
throw new Error('ecSigner does not support context; use the underlying curve directly');
|
|
289
295
|
return curve.verify(signature, message, publicKey);
|
|
@@ -291,18 +297,26 @@ export function ecSigner(curve: CurveSign, allowZeroKey: boolean = false): TRet<
|
|
|
291
297
|
};
|
|
292
298
|
}
|
|
293
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
|
+
|
|
294
306
|
function splitLengths<K extends string, T extends { lengths: Partial<Record<K, number>> }>(
|
|
295
307
|
lst: T[],
|
|
296
308
|
name: K
|
|
297
309
|
) {
|
|
298
310
|
// Preserve caller order exactly; raw numeric fields still decode as splitCoder() subarray views.
|
|
299
|
-
|
|
311
|
+
const coder = splitCoder(
|
|
300
312
|
name,
|
|
301
313
|
...lst.map((i) => {
|
|
302
314
|
if (typeof i.lengths[name] !== 'number') throw new Error('wrong length: ' + name);
|
|
303
|
-
return i.lengths[name];
|
|
315
|
+
return positiveLength(i.lengths[name], name);
|
|
304
316
|
})
|
|
305
317
|
);
|
|
318
|
+
positiveLength(coder.bytesLen, name);
|
|
319
|
+
return coder;
|
|
306
320
|
}
|
|
307
321
|
|
|
308
322
|
/** Seed-expansion callback used by the hybrid combiners. */
|
|
@@ -348,13 +362,15 @@ function combineKeys(
|
|
|
348
362
|
const seedCoder = splitLengths(ck, 'seed');
|
|
349
363
|
const pkCoder = splitLengths(ck, 'publicKey');
|
|
350
364
|
// Allows to use identity functions for combiner/expandSeed
|
|
351
|
-
|
|
352
|
-
|
|
365
|
+
const rootSeedLen = positiveLength(
|
|
366
|
+
realSeedLen === undefined ? seedCoder.bytesLen : realSeedLen,
|
|
367
|
+
'realSeedLen'
|
|
368
|
+
);
|
|
353
369
|
function expandDecapsulationKey(seed: TArg<Uint8Array>): TRet<{
|
|
354
370
|
secretKey: Uint8Array[];
|
|
355
371
|
publicKey: Uint8Array[];
|
|
356
372
|
}> {
|
|
357
|
-
abytes(seed,
|
|
373
|
+
abytes(seed, rootSeedLen);
|
|
358
374
|
const expandedRaw = expandSeed(seed, seedCoder.bytesLen);
|
|
359
375
|
// Identity/subarray expanders can hand back caller-owned seed storage. Detach those outputs so
|
|
360
376
|
// later cleanup can wipe the expanded schedule without mutating the caller's root seed bytes.
|
|
@@ -392,7 +408,7 @@ function combineKeys(
|
|
|
392
408
|
const keygen = (seed?: TArg<Uint8Array>) => {
|
|
393
409
|
// Detach the root: the exported secretKey must not alias caller-owned seed bytes, so later
|
|
394
410
|
// caller mutation of the seed cannot silently change the secret key (and vice versa).
|
|
395
|
-
const root = seed === undefined ? randomBytes(
|
|
411
|
+
const root = seed === undefined ? randomBytes(rootSeedLen) : copyBytes(seed);
|
|
396
412
|
let res;
|
|
397
413
|
try {
|
|
398
414
|
const { publicKey: pk, secretKey } = expandDecapsulationKey(root);
|
|
@@ -412,7 +428,7 @@ function combineKeys(
|
|
|
412
428
|
}
|
|
413
429
|
};
|
|
414
430
|
return {
|
|
415
|
-
info: { lengths: { seed:
|
|
431
|
+
info: { lengths: { seed: rootSeedLen, publicKey: pkCoder.bytesLen, secretKey: rootSeedLen } },
|
|
416
432
|
// Composite secret keys are root seeds, so public-key derivation reruns key expansion from
|
|
417
433
|
// that seed instead of decoding a packed child-secret-key structure.
|
|
418
434
|
getPublicKey: (secretKey: TArg<Uint8Array>) => {
|
|
@@ -423,19 +439,25 @@ function combineKeys(
|
|
|
423
439
|
},
|
|
424
440
|
keygen,
|
|
425
441
|
expandDecapsulationKey,
|
|
426
|
-
realSeedLen,
|
|
442
|
+
realSeedLen: rootSeedLen,
|
|
427
443
|
};
|
|
428
444
|
}
|
|
429
445
|
|
|
430
446
|
// This generic function that combines multiple KEMs into single one
|
|
431
447
|
/**
|
|
432
448
|
* Combines multiple KEMs into one composite KEM.
|
|
433
|
-
* @param realSeedLen -
|
|
434
|
-
*
|
|
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.
|
|
435
454
|
* @param expandSeed - Seed expander used to derive per-KEM seeds.
|
|
436
455
|
* @param combiner - Combines the per-KEM outputs into one shared secret.
|
|
437
|
-
* @param kems - KEM
|
|
456
|
+
* @param kems - At least one KEM implementation. A construction advertised as hybrid normally
|
|
457
|
+
* supplies two or more.
|
|
438
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}
|
|
439
461
|
* @example
|
|
440
462
|
* Combine multiple KEMs into one composite KEM.
|
|
441
463
|
* ```ts
|
|
@@ -460,27 +482,56 @@ export function combineKEMS(
|
|
|
460
482
|
combiner: TArg<Combiner>,
|
|
461
483
|
...kems: TArg<KEM[]>
|
|
462
484
|
): TRet<KEM> {
|
|
463
|
-
if (realSeedLen !== undefined)
|
|
464
|
-
if (realMsgLen !== undefined)
|
|
485
|
+
if (realSeedLen !== undefined) positiveLength(realSeedLen, 'realSeedLen');
|
|
486
|
+
if (realMsgLen !== undefined) positiveLength(realMsgLen, 'realMsgLen');
|
|
465
487
|
if (typeof expandSeed !== 'function')
|
|
466
488
|
throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
|
|
467
489
|
if (typeof combiner !== 'function')
|
|
468
490
|
throw new TypeError('"combiner" expected function, got type=' + typeof combiner);
|
|
469
491
|
const rawCombiner = combiner as Combiner;
|
|
470
492
|
const rawKems = kems as KEM[];
|
|
493
|
+
if (rawKems.length === 0) throw new RangeError('combineKEMS requires at least one KEM');
|
|
471
494
|
for (let i = 0; i < rawKems.length; i++) validateKEM(rawKems[i], `kems[${i}]`);
|
|
472
495
|
const keys = combineKeys(realSeedLen, expandSeed, ...rawKems);
|
|
473
496
|
const ctCoder = splitLengths(rawKems, 'cipherText');
|
|
474
497
|
const pkCoder = splitLengths(rawKems, 'publicKey');
|
|
475
498
|
const msgCoder = splitLengths(rawKems, 'msg');
|
|
476
|
-
|
|
477
|
-
|
|
499
|
+
const sharedSecretLen = positiveLength(
|
|
500
|
+
realMsgLen === undefined ? msgCoder.bytesLen : realMsgLen,
|
|
501
|
+
'realMsgLen'
|
|
502
|
+
);
|
|
478
503
|
const lengths = Object.freeze({
|
|
479
504
|
...keys.info.lengths,
|
|
480
|
-
msg:
|
|
505
|
+
msg: sharedSecretLen,
|
|
481
506
|
msgRand: msgCoder.bytesLen,
|
|
482
507
|
cipherText: ctCoder.bytesLen,
|
|
483
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
|
+
};
|
|
484
535
|
return Object.freeze({
|
|
485
536
|
lengths,
|
|
486
537
|
getPublicKey: keys.getPublicKey,
|
|
@@ -499,11 +550,14 @@ export function combineKEMS(
|
|
|
499
550
|
sharedSecret.push(enc.sharedSecret);
|
|
500
551
|
cipherText.push(enc.cipherText);
|
|
501
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>;
|
|
502
556
|
return {
|
|
503
557
|
// Detach the combiner result before cleanup: a caller-provided combiner may alias one of
|
|
504
558
|
// the child sharedSecret buffers, and those child buffers are zeroized immediately below.
|
|
505
|
-
sharedSecret:
|
|
506
|
-
cipherText:
|
|
559
|
+
sharedSecret: combine(pks, cipherText, sharedSecret),
|
|
560
|
+
cipherText: encodedCipherText,
|
|
507
561
|
};
|
|
508
562
|
} finally {
|
|
509
563
|
// Child encapsulation or combiner failures can happen after some components already
|
|
@@ -514,11 +568,16 @@ export function combineKEMS(
|
|
|
514
568
|
decapsulate(ct: TArg<Uint8Array>, seed: TArg<Uint8Array>) {
|
|
515
569
|
const cts = ctCoder.decode(ct);
|
|
516
570
|
const { publicKey, secretKey } = keys.expandDecapsulationKey(seed);
|
|
517
|
-
const sharedSecret
|
|
571
|
+
const sharedSecret: Uint8Array[] = [];
|
|
518
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]));
|
|
519
578
|
// Detach the decapsulation result before cleanup: the combiner may hand back one of the
|
|
520
579
|
// child shared-secret buffers, and those temporary buffers are zeroized below.
|
|
521
|
-
return
|
|
580
|
+
return combine(publicKey, cts, sharedSecret);
|
|
522
581
|
} finally {
|
|
523
582
|
// Decapsulation only needs the expanded child secret keys and child shared secrets for this
|
|
524
583
|
// call; keep the caller/root seed intact, but wipe all derived material even on errors.
|
|
@@ -531,10 +590,15 @@ export function combineKEMS(
|
|
|
531
590
|
// realSeedLen: how much bytes expandSeed expects.
|
|
532
591
|
/**
|
|
533
592
|
* Combines multiple signers into one composite signer.
|
|
534
|
-
* @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.
|
|
535
596
|
* @param expandSeed - Seed expander used to derive per-signer seeds.
|
|
536
|
-
* @param signers -
|
|
597
|
+
* @param signers - At least one signer. A construction advertised as hybrid normally supplies two
|
|
598
|
+
* or more.
|
|
537
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}
|
|
538
602
|
* @example
|
|
539
603
|
* Combine multiple signers into one composite signer.
|
|
540
604
|
* ```ts
|
|
@@ -554,10 +618,11 @@ export function combineSigners(
|
|
|
554
618
|
expandSeed: TArg<ExpandSeed>,
|
|
555
619
|
...signers: TArg<Signer[]>
|
|
556
620
|
): TRet<Signer> {
|
|
557
|
-
if (realSeedLen !== undefined)
|
|
621
|
+
if (realSeedLen !== undefined) positiveLength(realSeedLen, 'realSeedLen');
|
|
558
622
|
if (typeof expandSeed !== 'function')
|
|
559
623
|
throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
|
|
560
624
|
const rawSigners = signers as Signer[];
|
|
625
|
+
if (rawSigners.length === 0) throw new RangeError('combineSigners requires at least one signer');
|
|
561
626
|
for (let i = 0; i < rawSigners.length; i++) validateSigner(rawSigners[i], `signers[${i}]`);
|
|
562
627
|
const keys = combineKeys(realSeedLen, expandSeed, ...rawSigners);
|
|
563
628
|
const sigCoder = splitLengths(rawSigners, 'signature');
|
|
@@ -567,7 +632,7 @@ export function combineSigners(
|
|
|
567
632
|
getPublicKey: keys.getPublicKey,
|
|
568
633
|
keygen: keys.keygen,
|
|
569
634
|
sign(message, seed, opts = {}) {
|
|
570
|
-
validateSigOpts(opts);
|
|
635
|
+
opts = validateSigOpts(opts);
|
|
571
636
|
// This generic wrapper intentionally keeps the composite signer contract to message + root
|
|
572
637
|
// seed only. Per-signer opts like context or extraEntropy cannot be preserved uniformly
|
|
573
638
|
// across mixed backends, so callers that need them must use the underlying signer directly.
|
|
@@ -594,7 +659,7 @@ export function combineSigners(
|
|
|
594
659
|
* does any failing child verify. Throws on unsupported generic opts or malformed publicKey.
|
|
595
660
|
*/
|
|
596
661
|
verify: (signature, message, publicKey, opts = {}) => {
|
|
597
|
-
validateVerOpts(opts);
|
|
662
|
+
opts = validateVerOpts(opts);
|
|
598
663
|
if (opts.context !== undefined)
|
|
599
664
|
throw new Error(
|
|
600
665
|
'combineSigners does not support context; use the underlying signer directly'
|
|
@@ -628,14 +693,16 @@ export function combineSigners(
|
|
|
628
693
|
* @param xof - XOF used for seed expansion.
|
|
629
694
|
* @param kdf - Hash used for the final combiner.
|
|
630
695
|
* @returns Hybrid KEM.
|
|
696
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
697
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
631
698
|
* @example
|
|
632
699
|
* Build a QSF hybrid KEM preset from a PQ KEM and an elliptic-curve KEM.
|
|
633
700
|
* ```ts
|
|
634
701
|
* import { p256 } from '@noble/curves/nist.js';
|
|
635
702
|
* import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
|
|
636
|
-
* import { QSF,
|
|
703
|
+
* import { QSF, _ecdhKem } from '@noble/post-quantum/hybrid.js';
|
|
637
704
|
* import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
|
|
638
|
-
* const kem = QSF('example', ml_kem768,
|
|
705
|
+
* const kem = QSF('example', ml_kem768, _ecdhKem(p256, true), shake256, sha3_256);
|
|
639
706
|
* const publicKeyLen = kem.lengths.publicKey;
|
|
640
707
|
* ```
|
|
641
708
|
*/
|
|
@@ -671,7 +738,7 @@ export const QSF_ml_kem768_p256: TRet<KEM> = /* @__PURE__ */ (() =>
|
|
|
671
738
|
QSF(
|
|
672
739
|
'QSF-KEM(ML-KEM-768,P-256)-XOF(SHAKE256)-KDF(SHA3-256)',
|
|
673
740
|
ml_kem768,
|
|
674
|
-
|
|
741
|
+
_ecdhKem(p256, true),
|
|
675
742
|
shake256,
|
|
676
743
|
sha3_256
|
|
677
744
|
))();
|
|
@@ -680,7 +747,7 @@ export const QSF_ml_kem1024_p384: TRet<KEM> = /* @__PURE__ */ (() =>
|
|
|
680
747
|
QSF(
|
|
681
748
|
'QSF-KEM(ML-KEM-1024,P-384)-XOF(SHAKE256)-KDF(SHA3-256)',
|
|
682
749
|
ml_kem1024,
|
|
683
|
-
|
|
750
|
+
_ecdhKem(p384, true),
|
|
684
751
|
shake256,
|
|
685
752
|
sha3_256
|
|
686
753
|
))();
|
|
@@ -699,15 +766,17 @@ export const QSF_ml_kem1024_p384: TRet<KEM> = /* @__PURE__ */ (() =>
|
|
|
699
766
|
* @param xof - XOF used for seed expansion.
|
|
700
767
|
* @param hash - Hash used for HKDF extraction and expansion.
|
|
701
768
|
* @returns Hybrid KEM.
|
|
769
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
770
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
702
771
|
* @example
|
|
703
772
|
* Build the "KitchenSink" hybrid KEM combiner.
|
|
704
773
|
* ```ts
|
|
705
774
|
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
706
775
|
* import { shake256 } from '@noble/hashes/sha3.js';
|
|
707
|
-
* import { createKitchenSink,
|
|
776
|
+
* import { createKitchenSink, _ecdhKem } from '@noble/post-quantum/hybrid.js';
|
|
708
777
|
* import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
|
|
709
778
|
* import { x25519 } from '@noble/curves/ed25519.js';
|
|
710
|
-
* const kem = createKitchenSink('example', ml_kem768,
|
|
779
|
+
* const kem = createKitchenSink('example', ml_kem768, _ecdhKem(x25519), shake256, sha256);
|
|
711
780
|
* const publicKeyLen = kem.lengths.publicKey;
|
|
712
781
|
* ```
|
|
713
782
|
*/
|
|
@@ -750,9 +819,9 @@ export function createKitchenSink(
|
|
|
750
819
|
);
|
|
751
820
|
}
|
|
752
821
|
|
|
753
|
-
// Internal alias only: this stays exactly `
|
|
822
|
+
// Internal alias only: this stays exactly `_ecdhKem(x25519)`
|
|
754
823
|
// and inherits that wrapper's mutation/oracle behavior.
|
|
755
|
-
const x25519kem = /* @__PURE__ */
|
|
824
|
+
const x25519kem = /* @__PURE__ */ _ecdhKem(x25519);
|
|
756
825
|
/** KitchenSink preset combining ML-KEM-768 with X25519.
|
|
757
826
|
* Caller randomness splits into 32 ML-KEM coins plus a 32-byte X25519 ephemeral-secret seed.
|
|
758
827
|
*/
|
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
|
```
|
package/src/ml-dsa.ts
CHANGED
|
@@ -27,9 +27,9 @@ import {
|
|
|
27
27
|
splitCoder,
|
|
28
28
|
type TArg,
|
|
29
29
|
type TRet,
|
|
30
|
-
validateOpts,
|
|
31
30
|
validateSigOpts,
|
|
32
31
|
validateVerOpts,
|
|
32
|
+
checkOptKeys,
|
|
33
33
|
vecCoder,
|
|
34
34
|
type VerOpts,
|
|
35
35
|
} from './utils.ts';
|
|
@@ -43,9 +43,28 @@ export type DSAInternalOpts = {
|
|
|
43
43
|
*/
|
|
44
44
|
externalMu?: boolean;
|
|
45
45
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Keys each internal surface accepts.
|
|
48
|
+
*
|
|
49
|
+
* `context` is deliberately absent from both. The internal functions never read it: the
|
|
50
|
+
* public wrappers consume it when they format `M'` and must not pass it down, because a
|
|
51
|
+
* key that is accepted and then not acted on is the same silent downgrade this validation
|
|
52
|
+
* exists to prevent. `externalMu` is the mirror case, existing here and rejected above.
|
|
53
|
+
* `extraEntropy` is signing-only, so verification does not take it either.
|
|
54
|
+
*/
|
|
55
|
+
const INTERNAL_SIG_OPT_KEYS = /* @__PURE__ */ Object.freeze([
|
|
56
|
+
'extraEntropy',
|
|
57
|
+
'externalMu',
|
|
58
|
+
] as const);
|
|
59
|
+
const INTERNAL_VER_OPT_KEYS = /* @__PURE__ */ Object.freeze(['externalMu'] as const);
|
|
60
|
+
|
|
61
|
+
function validateInternalOpts<T extends TArg<DSAInternalOpts>>(
|
|
62
|
+
opts: T,
|
|
63
|
+
allowed: readonly string[]
|
|
64
|
+
): T {
|
|
65
|
+
const normalized = checkOptKeys(opts, allowed);
|
|
66
|
+
if (normalized.externalMu !== undefined) abool(normalized.externalMu, 'opts.externalMu');
|
|
67
|
+
return normalized;
|
|
49
68
|
}
|
|
50
69
|
|
|
51
70
|
/** ML-DSA signer surface with access to the internal message formatting mode. */
|
|
@@ -54,13 +73,13 @@ export type DSAInternal = CryptoKeys & {
|
|
|
54
73
|
sign: (
|
|
55
74
|
msg: TArg<Uint8Array>,
|
|
56
75
|
secretKey: TArg<Uint8Array>,
|
|
57
|
-
opts?: TArg<SigOpts & DSAInternalOpts>
|
|
76
|
+
opts?: TArg<Omit<SigOpts, 'context'> & DSAInternalOpts>
|
|
58
77
|
) => TRet<Uint8Array>;
|
|
59
78
|
verify: (
|
|
60
79
|
sig: TArg<Uint8Array>,
|
|
61
80
|
msg: TArg<Uint8Array>,
|
|
62
81
|
pubKey: TArg<Uint8Array>,
|
|
63
|
-
opts?: TArg<
|
|
82
|
+
opts?: TArg<DSAInternalOpts>
|
|
64
83
|
) => boolean;
|
|
65
84
|
};
|
|
66
85
|
/** Public ML-DSA signer surface. */
|
|
@@ -212,7 +231,7 @@ function RejNTTPoly(xof_: TArg<XofGet>): TRet<Poly> {
|
|
|
212
231
|
// Samples a polynomial ∈ Tq. xof() must return byte lengths divisible by 3.
|
|
213
232
|
const r = newPoly(N);
|
|
214
233
|
// NOTE: we can represent 3xu24 as 4xu32, but it doesn't improve perf :(
|
|
215
|
-
for (let j = 0; j < N;
|
|
234
|
+
for (let j = 0; j < N;) {
|
|
216
235
|
const b = xof();
|
|
217
236
|
if (b.length % 3) throw new Error('RejNTTPoly: unaligned block');
|
|
218
237
|
for (let i = 0; j < N && i <= b.length - 3; i += 3) {
|
|
@@ -374,7 +393,7 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
|
|
|
374
393
|
const xof = xof_ as XofGet;
|
|
375
394
|
// Samples an element a ∈ Rq with coeffcients in [−η, η] computed via rejection sampling from ρ.
|
|
376
395
|
const r: Poly = newPoly(N);
|
|
377
|
-
for (let j = 0; j < N;
|
|
396
|
+
for (let j = 0; j < N;) {
|
|
378
397
|
const b = xof();
|
|
379
398
|
for (let i = 0; j < N && i < b.length; i += 1) {
|
|
380
399
|
// half byte. Should be superfast with vector instructions. But very slow with js :(
|
|
@@ -398,7 +417,7 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
|
|
|
398
417
|
const masks = buf.slice(0, 8);
|
|
399
418
|
for (let i = N - TAU, pos = 8, maskPos = 0, maskBit = 0; i < N; i++) {
|
|
400
419
|
let b = i + 1;
|
|
401
|
-
for (; b > i;
|
|
420
|
+
for (; b > i;) {
|
|
402
421
|
b = buf[pos++];
|
|
403
422
|
if (pos < shake256.blockLen) continue;
|
|
404
423
|
s.xofInto(buf);
|
|
@@ -539,8 +558,8 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
|
|
|
539
558
|
secretKey: TArg<Uint8Array>,
|
|
540
559
|
opts: TArg<SigOpts & DSAInternalOpts> = {}
|
|
541
560
|
): TRet<Uint8Array> => {
|
|
542
|
-
validateSigOpts(opts);
|
|
543
|
-
validateInternalOpts(opts);
|
|
561
|
+
opts = validateSigOpts(opts, INTERNAL_SIG_OPT_KEYS);
|
|
562
|
+
opts = validateInternalOpts(opts, INTERNAL_SIG_OPT_KEYS);
|
|
544
563
|
const { extraEntropy: random, externalMu = false } = opts;
|
|
545
564
|
// FIPS 204 external-mu mode expects the 64-byte message representative µ = H(tr || M).
|
|
546
565
|
if (externalMu) abytes(msg, CRH_BYTES, 'mu');
|
|
@@ -599,7 +618,7 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
|
|
|
599
618
|
abytes(rhoprime, CRH_BYTES);
|
|
600
619
|
const x256 = XOF256(rhoprime, ZCoder.bytesLen);
|
|
601
620
|
// Rejection sampling loop
|
|
602
|
-
main_loop: for (let kappa = 0; ;
|
|
621
|
+
main_loop: for (let kappa = 0; ;) {
|
|
603
622
|
const y = [];
|
|
604
623
|
// y ← ExpandMask(ρ , κ)
|
|
605
624
|
for (let i = 0; i < L; i++, kappa++)
|
|
@@ -627,7 +646,13 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
|
|
|
627
646
|
const cs1 = s1.map((i) => MultiplyNTTs(i, cHat));
|
|
628
647
|
for (let i = 0; i < L; i++) {
|
|
629
648
|
polyAdd(crystals.NTT.decode(cs1[i]), y[i]); // z ← y + ⟨⟨cs1⟩⟩
|
|
630
|
-
if (polyChknorm(cs1[i], GAMMA1 - BETA))
|
|
649
|
+
if (polyChknorm(cs1[i], GAMMA1 - BETA)) {
|
|
650
|
+
// Rejected. Wipe this iteration's secret-derived buffers before retrying; the
|
|
651
|
+
// accepted path wipes the same set, and only the persistent key material (s1, s2,
|
|
652
|
+
// t0, A, rhoprime) is kept for the next iteration and cleaned at the very end.
|
|
653
|
+
cleanBytes(cTilde, cs1, cHat, w1, w, z, y);
|
|
654
|
+
continue main_loop; // ||z||∞ ≥ γ1 − β
|
|
655
|
+
}
|
|
631
656
|
}
|
|
632
657
|
// cs1 is now z (▷ Signer’s response)
|
|
633
658
|
let cnt = 0;
|
|
@@ -635,16 +660,25 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
|
|
|
635
660
|
for (let i = 0; i < K; i++) {
|
|
636
661
|
const cs2 = crystals.NTT.decode(MultiplyNTTs(s2[i], cHat)); // ⟨⟨cs2⟩⟩ ← NTT−1(cˆ◦ sˆ2)
|
|
637
662
|
const r0 = polySub(w[i], cs2).map(LowBits); // r0 ← LowBits(w − ⟨⟨cs2⟩⟩)
|
|
638
|
-
if (polyChknorm(r0, GAMMA2 - BETA))
|
|
663
|
+
if (polyChknorm(r0, GAMMA2 - BETA)) {
|
|
664
|
+
cleanBytes(cTilde, cs1, cHat, w1, w, z, y, h, cs2, r0);
|
|
665
|
+
continue main_loop; // ||r0||∞ ≥ γ2 − β
|
|
666
|
+
}
|
|
639
667
|
const ct0 = crystals.NTT.decode(MultiplyNTTs(t0[i], cHat)); // ⟨⟨ct0⟩⟩ ← NTT−1(cˆ◦ tˆ0)
|
|
640
|
-
if (polyChknorm(ct0, GAMMA2))
|
|
668
|
+
if (polyChknorm(ct0, GAMMA2)) {
|
|
669
|
+
cleanBytes(cTilde, cs1, cHat, w1, w, z, y, h, cs2, r0, ct0);
|
|
670
|
+
continue main_loop;
|
|
671
|
+
}
|
|
641
672
|
polyAdd(r0, ct0);
|
|
642
673
|
// ▷ Signer’s hint
|
|
643
674
|
const hint = polyMakeHint(r0, w1[i]); // h ← MakeHint(−⟨⟨ct0⟩⟩, w− ⟨⟨cs2⟩⟩ + ⟨⟨ct0⟩⟩)
|
|
644
675
|
h.push(hint.v);
|
|
645
676
|
cnt += hint.cnt;
|
|
646
677
|
}
|
|
647
|
-
if (cnt > OMEGA)
|
|
678
|
+
if (cnt > OMEGA) {
|
|
679
|
+
cleanBytes(cTilde, cs1, cHat, w1, w, z, y, h);
|
|
680
|
+
continue; // the number of 1’s in h is greater than ω
|
|
681
|
+
}
|
|
648
682
|
x256.clean();
|
|
649
683
|
const res = sigCoder.encode([cTilde, cs1, h]); // σ ← sigEncode(c˜, z mod±q, h)
|
|
650
684
|
// rho, _K, tr is subarray of secretKey, cannot clean.
|
|
@@ -664,7 +698,7 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
|
|
|
664
698
|
publicKey: TArg<Uint8Array>,
|
|
665
699
|
opts: TArg<DSAInternalOpts> = {}
|
|
666
700
|
) => {
|
|
667
|
-
validateInternalOpts(opts);
|
|
701
|
+
opts = validateInternalOpts(opts, INTERNAL_VER_OPT_KEYS);
|
|
668
702
|
const { externalMu = false } = opts;
|
|
669
703
|
// FIPS 204 external-mu mode expects the 64-byte message representative µ = H(tr || M).
|
|
670
704
|
if (externalMu) abytes(msg, CRH_BYTES, 'mu');
|
|
@@ -729,9 +763,14 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
|
|
|
729
763
|
secretKey: TArg<Uint8Array>,
|
|
730
764
|
opts: TArg<SigOpts> = {}
|
|
731
765
|
): TRet<Uint8Array> => {
|
|
732
|
-
validateSigOpts(opts);
|
|
766
|
+
opts = validateSigOpts(opts);
|
|
733
767
|
const M = getMessage(msg, opts.context);
|
|
734
|
-
|
|
768
|
+
// `context` is consumed by getMessage() above; forwarding it would make the internal
|
|
769
|
+
// surface accept a key it never reads.
|
|
770
|
+
const res = internal.sign(M, secretKey, {
|
|
771
|
+
extraEntropy: opts.extraEntropy,
|
|
772
|
+
externalMu: false,
|
|
773
|
+
});
|
|
735
774
|
cleanBytes(M);
|
|
736
775
|
return res as TRet<Uint8Array>;
|
|
737
776
|
},
|
|
@@ -741,9 +780,9 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
|
|
|
741
780
|
publicKey: TArg<Uint8Array>,
|
|
742
781
|
opts: TArg<VerOpts> = {}
|
|
743
782
|
) => {
|
|
744
|
-
validateVerOpts(opts);
|
|
783
|
+
opts = validateVerOpts(opts);
|
|
745
784
|
abytes(sig, undefined, 'signature');
|
|
746
|
-
return internal.verify(sig, getMessage(msg, opts.context), publicKey);
|
|
785
|
+
return internal.verify(sig, getMessage(msg, opts.context), publicKey, { externalMu: false });
|
|
747
786
|
},
|
|
748
787
|
prehash: (hash: TArg<CHash>): TRet<Signer> => {
|
|
749
788
|
checkHash(hash as CHash, securityLevel);
|
|
@@ -759,9 +798,13 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
|
|
|
759
798
|
secretKey: TArg<Uint8Array>,
|
|
760
799
|
opts: TArg<SigOpts> = {}
|
|
761
800
|
): TRet<Uint8Array> => {
|
|
762
|
-
validateSigOpts(opts);
|
|
801
|
+
opts = validateSigOpts(opts);
|
|
763
802
|
const M = getMessagePrehash(rawHash, msg, opts.context);
|
|
764
|
-
|
|
803
|
+
// As above: getMessagePrehash() consumes `context`, so it must not travel further.
|
|
804
|
+
const res = internal.sign(M, secretKey, {
|
|
805
|
+
extraEntropy: opts.extraEntropy,
|
|
806
|
+
externalMu: false,
|
|
807
|
+
});
|
|
765
808
|
cleanBytes(M);
|
|
766
809
|
return res as TRet<Uint8Array>;
|
|
767
810
|
},
|
|
@@ -771,9 +814,11 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
|
|
|
771
814
|
publicKey: TArg<Uint8Array>,
|
|
772
815
|
opts: TArg<VerOpts> = {}
|
|
773
816
|
) => {
|
|
774
|
-
validateVerOpts(opts);
|
|
817
|
+
opts = validateVerOpts(opts);
|
|
775
818
|
abytes(sig, undefined, 'signature');
|
|
776
|
-
return internal.verify(sig, getMessagePrehash(rawHash, msg, opts.context), publicKey
|
|
819
|
+
return internal.verify(sig, getMessagePrehash(rawHash, msg, opts.context), publicKey, {
|
|
820
|
+
externalMu: false,
|
|
821
|
+
});
|
|
777
822
|
},
|
|
778
823
|
});
|
|
779
824
|
},
|