@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/hybrid.js CHANGED
@@ -83,7 +83,7 @@ import { abool, afunction, asciiToBytes, bytesToNumberBE, bytesToNumberLE, conca
83
83
  import { expand, extract } from '@noble/hashes/hkdf.js';
84
84
  import { sha256 } from '@noble/hashes/sha2.js';
85
85
  import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
86
- import { abytes, ahash, anumber } from '@noble/hashes/utils.js';
86
+ import { abytes, ahash, anumber, isBytes } from '@noble/hashes/utils.js';
87
87
  import { ml_kem1024, ml_kem768 } from "./ml-kem.js";
88
88
  import { aobject, astring, cleanBytes, copyBytes, randomBytes, splitCoder, validateSigOpts, validateVerOpts, } from "./utils.js";
89
89
  const validateKEM = (kem, title) => {
@@ -148,6 +148,12 @@ function ecKeygen(curve, allowZeroKey = false) {
148
148
  /**
149
149
  * Wraps an ECDH-capable curve as a KEM.
150
150
  * Shared secrets stay in the wrapped curve's raw ECDH byte format with no built-in KDF.
151
+ *
152
+ * SECURITY: this is a low-level component adapter, not a standalone IND-CCA-secure KEM. It does
153
+ * not bind the encapsulation or recipient public key into the secret, so distinct accepted point
154
+ * encodings can produce the same output. Use it only inside a construction whose specified
155
+ * combiner binds those values, or use a standardized DHKEM with labeled extract-and-expand.
156
+ *
151
157
  * On SEC 1 / Weierstrass curves, that means the compressed shared-point body without the
152
158
  * 1-byte `0x02` / `0x03` prefix.
153
159
  * The X25519 path also leaves RFC 7748's optional all-zero shared-secret check to callers.
@@ -163,12 +169,12 @@ function ecKeygen(curve, allowZeroKey = false) {
163
169
  * Wrap an ECDH-capable curve as a generic KEM.
164
170
  * ```ts
165
171
  * import { x25519 } from '@noble/curves/ed25519.js';
166
- * import { ecdhKem } from '@noble/post-quantum/hybrid.js';
167
- * const kem = ecdhKem(x25519);
172
+ * import { _ecdhKem } from '@noble/post-quantum/hybrid.js';
173
+ * const kem = _ecdhKem(x25519);
168
174
  * const publicKeyLen = kem.lengths.publicKey;
169
175
  * ```
170
176
  */
171
- export function ecdhKem(curve, allowZeroKey = false) {
177
+ export function _ecdhKem(curve, allowZeroKey = false) {
172
178
  const kg = ecKeygen(curve, allowZeroKey);
173
179
  if (!curve.getSharedSecret)
174
180
  throw new Error('wrong curve'); // ed25519 doesn't have one!
@@ -233,7 +239,7 @@ export function ecSigner(curve, allowZeroKey = false) {
233
239
  keygen: kg.keygen,
234
240
  getPublicKey: kg.getPublicKey,
235
241
  sign: (message, secretKey, opts = {}) => {
236
- validateSigOpts(opts);
242
+ opts = validateSigOpts(opts);
237
243
  // This generic wrapper intentionally keeps the Signer contract to message + key only.
238
244
  // Backend-specific knobs like ECDSA extraEntropy or Ed25519ctx context cannot be forwarded
239
245
  // uniformly through combineSigners(), so callers that need them must use the curve directly.
@@ -248,20 +254,28 @@ export function ecSigner(curve, allowZeroKey = false) {
248
254
  * generic opts and lets wrapped-curve malformed-input errors escape unchanged.
249
255
  */
250
256
  verify: (signature, message, publicKey, opts = {}) => {
251
- validateVerOpts(opts);
257
+ opts = validateVerOpts(opts);
252
258
  if (opts.context !== undefined)
253
259
  throw new Error('ecSigner does not support context; use the underlying curve directly');
254
260
  return curve.verify(signature, message, publicKey);
255
261
  },
256
262
  };
257
263
  }
264
+ function positiveLength(value, title) {
265
+ const length = anumber(value, title);
266
+ if (length === 0)
267
+ throw new RangeError(`"${title}" expected integer greater than 0, got 0`);
268
+ return length;
269
+ }
258
270
  function splitLengths(lst, name) {
259
271
  // Preserve caller order exactly; raw numeric fields still decode as splitCoder() subarray views.
260
- return splitCoder(name, ...lst.map((i) => {
272
+ const coder = splitCoder(name, ...lst.map((i) => {
261
273
  if (typeof i.lengths[name] !== 'number')
262
274
  throw new Error('wrong length: ' + name);
263
- return i.lengths[name];
275
+ return positiveLength(i.lengths[name], name);
264
276
  }));
277
+ positiveLength(coder.bytesLen, name);
278
+ return coder;
265
279
  }
266
280
  // It is XOF for most cases, but can be more complex!
267
281
  /**
@@ -290,11 +304,9 @@ expandSeed_, ...ck_) {
290
304
  const seedCoder = splitLengths(ck, 'seed');
291
305
  const pkCoder = splitLengths(ck, 'publicKey');
292
306
  // Allows to use identity functions for combiner/expandSeed
293
- if (realSeedLen === undefined)
294
- realSeedLen = seedCoder.bytesLen;
295
- anumber(realSeedLen);
307
+ const rootSeedLen = positiveLength(realSeedLen === undefined ? seedCoder.bytesLen : realSeedLen, 'realSeedLen');
296
308
  function expandDecapsulationKey(seed) {
297
- abytes(seed, realSeedLen);
309
+ abytes(seed, rootSeedLen);
298
310
  const expandedRaw = expandSeed(seed, seedCoder.bytesLen);
299
311
  // Identity/subarray expanders can hand back caller-owned seed storage. Detach those outputs so
300
312
  // later cleanup can wipe the expanded schedule without mutating the caller's root seed bytes.
@@ -332,7 +344,7 @@ expandSeed_, ...ck_) {
332
344
  const keygen = (seed) => {
333
345
  // Detach the root: the exported secretKey must not alias caller-owned seed bytes, so later
334
346
  // caller mutation of the seed cannot silently change the secret key (and vice versa).
335
- const root = seed === undefined ? randomBytes(realSeedLen) : copyBytes(seed);
347
+ const root = seed === undefined ? randomBytes(rootSeedLen) : copyBytes(seed);
336
348
  let res;
337
349
  try {
338
350
  const { publicKey: pk, secretKey } = expandDecapsulationKey(root);
@@ -355,7 +367,7 @@ expandSeed_, ...ck_) {
355
367
  }
356
368
  };
357
369
  return {
358
- info: { lengths: { seed: realSeedLen, publicKey: pkCoder.bytesLen, secretKey: realSeedLen } },
370
+ info: { lengths: { seed: rootSeedLen, publicKey: pkCoder.bytesLen, secretKey: rootSeedLen } },
359
371
  // Composite secret keys are root seeds, so public-key derivation reruns key expansion from
360
372
  // that seed instead of decoding a packed child-secret-key structure.
361
373
  getPublicKey: (secretKey) => {
@@ -366,18 +378,24 @@ expandSeed_, ...ck_) {
366
378
  },
367
379
  keygen,
368
380
  expandDecapsulationKey,
369
- realSeedLen,
381
+ realSeedLen: rootSeedLen,
370
382
  };
371
383
  }
372
384
  // This generic function that combines multiple KEMs into single one
373
385
  /**
374
386
  * Combines multiple KEMs into one composite KEM.
375
- * @param realSeedLen - Input seed length expected by `expandSeed`.
376
- * @param realMsgLen - Shared-secret length returned by `combiner`.
387
+ * @param realSeedLen - Positive input seed length expected by `expandSeed`, or `undefined` to use
388
+ * the sum of component seed lengths. Callers remain responsible for choosing a security-appropriate
389
+ * size.
390
+ * @param realMsgLen - Positive shared-secret length returned by `combiner`, or `undefined` to use
391
+ * the sum of component message lengths.
377
392
  * @param expandSeed - Seed expander used to derive per-KEM seeds.
378
393
  * @param combiner - Combines the per-KEM outputs into one shared secret.
379
- * @param kems - KEM implementations to combine.
394
+ * @param kems - At least one KEM implementation. A construction advertised as hybrid normally
395
+ * supplies two or more.
380
396
  * @returns Composite KEM.
397
+ * @throws On wrong argument types. {@link TypeError}
398
+ * @throws If there are no components or any required length resolves to zero. {@link RangeError}
381
399
  * @example
382
400
  * Combine multiple KEMs into one composite KEM.
383
401
  * ```ts
@@ -399,30 +417,52 @@ export function combineKEMS(realSeedLen, // how much bytes expandSeed expects
399
417
  realMsgLen, // how much bytes combiner returns
400
418
  expandSeed, combiner, ...kems) {
401
419
  if (realSeedLen !== undefined)
402
- anumber(realSeedLen, 'realSeedLen');
420
+ positiveLength(realSeedLen, 'realSeedLen');
403
421
  if (realMsgLen !== undefined)
404
- anumber(realMsgLen, 'realMsgLen');
422
+ positiveLength(realMsgLen, 'realMsgLen');
405
423
  if (typeof expandSeed !== 'function')
406
424
  throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
407
425
  if (typeof combiner !== 'function')
408
426
  throw new TypeError('"combiner" expected function, got type=' + typeof combiner);
409
427
  const rawCombiner = combiner;
410
428
  const rawKems = kems;
429
+ if (rawKems.length === 0)
430
+ throw new RangeError('combineKEMS requires at least one KEM');
411
431
  for (let i = 0; i < rawKems.length; i++)
412
432
  validateKEM(rawKems[i], `kems[${i}]`);
413
433
  const keys = combineKeys(realSeedLen, expandSeed, ...rawKems);
414
434
  const ctCoder = splitLengths(rawKems, 'cipherText');
415
435
  const pkCoder = splitLengths(rawKems, 'publicKey');
416
436
  const msgCoder = splitLengths(rawKems, 'msg');
417
- if (realMsgLen === undefined)
418
- realMsgLen = msgCoder.bytesLen;
419
- anumber(realMsgLen, 'realMsgLen');
437
+ const sharedSecretLen = positiveLength(realMsgLen === undefined ? msgCoder.bytesLen : realMsgLen, 'realMsgLen');
420
438
  const lengths = Object.freeze({
421
439
  ...keys.info.lengths,
422
- msg: realMsgLen,
440
+ msg: sharedSecretLen,
423
441
  msgRand: msgCoder.bytesLen,
424
442
  cipherText: ctCoder.bytesLen,
425
443
  });
444
+ const combine = (publicKeys, cipherTexts, sharedSecrets) => {
445
+ const combined = rawCombiner(publicKeys, cipherTexts, sharedSecrets);
446
+ try {
447
+ return copyBytes(abytes(combined, sharedSecretLen, 'sharedSecret'));
448
+ }
449
+ catch (error) {
450
+ if (isBytes(combined)) {
451
+ // A combiner may return any callback argument. Public keys during encapsulation and
452
+ // ciphertexts during decapsulation are views into caller-owned inputs, so wipe an invalid
453
+ // byte result only when its range does not overlap either public argument vector. Child
454
+ // shared-secret aliases are already wiped by the operation's outer finally block.
455
+ const overlaps = (value) => combined.buffer === value.buffer &&
456
+ combined.byteOffset < value.byteOffset + value.byteLength &&
457
+ value.byteOffset < combined.byteOffset + combined.byteLength;
458
+ const aliasesPublicInput = publicKeys.some(overlaps) ||
459
+ cipherTexts.some(overlaps);
460
+ if (!aliasesPublicInput)
461
+ cleanBytes(combined);
462
+ }
463
+ throw error;
464
+ }
465
+ };
426
466
  return Object.freeze({
427
467
  lengths,
428
468
  getPublicKey: keys.getPublicKey,
@@ -438,11 +478,14 @@ expandSeed, combiner, ...kems) {
438
478
  sharedSecret.push(enc.sharedSecret);
439
479
  cipherText.push(enc.cipherText);
440
480
  }
481
+ // Validate and detach public ciphertexts before deriving a final secret from them. This
482
+ // also ensures a malformed child cannot make us allocate and then strand a combined key.
483
+ const encodedCipherText = ctCoder.encode(cipherText);
441
484
  return {
442
485
  // Detach the combiner result before cleanup: a caller-provided combiner may alias one of
443
486
  // the child sharedSecret buffers, and those child buffers are zeroized immediately below.
444
- sharedSecret: copyBytes(rawCombiner(pks, cipherText, sharedSecret)),
445
- cipherText: ctCoder.encode(cipherText),
487
+ sharedSecret: combine(pks, cipherText, sharedSecret),
488
+ cipherText: encodedCipherText,
446
489
  };
447
490
  }
448
491
  finally {
@@ -454,11 +497,16 @@ expandSeed, combiner, ...kems) {
454
497
  decapsulate(ct, seed) {
455
498
  const cts = ctCoder.decode(ct);
456
499
  const { publicKey, secretKey } = keys.expandDecapsulationKey(seed);
457
- const sharedSecret = rawKems.map((i, j) => i.decapsulate(cts[j], secretKey[j]));
500
+ const sharedSecret = [];
458
501
  try {
502
+ // Child decapsulate() is inside the try: it can throw on an attacker-supplied ciphertext
503
+ // (e.g. a low-order X25519 point), and by then the expanded child secret keys — plus any
504
+ // child shared secrets already produced — are live and must still be wiped.
505
+ for (let i = 0; i < rawKems.length; i++)
506
+ sharedSecret.push(rawKems[i].decapsulate(cts[i], secretKey[i]));
459
507
  // Detach the decapsulation result before cleanup: the combiner may hand back one of the
460
508
  // child shared-secret buffers, and those temporary buffers are zeroized below.
461
- return copyBytes(rawCombiner(publicKey, cts, sharedSecret));
509
+ return combine(publicKey, cts, sharedSecret);
462
510
  }
463
511
  finally {
464
512
  // Decapsulation only needs the expanded child secret keys and child shared secrets for this
@@ -472,10 +520,15 @@ expandSeed, combiner, ...kems) {
472
520
  // realSeedLen: how much bytes expandSeed expects.
473
521
  /**
474
522
  * Combines multiple signers into one composite signer.
475
- * @param realSeedLen - Input seed length expected by `expandSeed`.
523
+ * @param realSeedLen - Positive input seed length expected by `expandSeed`, or `undefined` to use
524
+ * the sum of component seed lengths. Callers remain responsible for choosing a security-appropriate
525
+ * size.
476
526
  * @param expandSeed - Seed expander used to derive per-signer seeds.
477
- * @param signers - Signers to combine.
527
+ * @param signers - At least one signer. A construction advertised as hybrid normally supplies two
528
+ * or more.
478
529
  * @returns Composite signer.
530
+ * @throws On wrong argument types. {@link TypeError}
531
+ * @throws If there are no components or any required length resolves to zero. {@link RangeError}
479
532
  * @example
480
533
  * Combine multiple signers into one composite signer.
481
534
  * ```ts
@@ -492,10 +545,12 @@ expandSeed, combiner, ...kems) {
492
545
  */
493
546
  export function combineSigners(realSeedLen, expandSeed, ...signers) {
494
547
  if (realSeedLen !== undefined)
495
- anumber(realSeedLen, 'realSeedLen');
548
+ positiveLength(realSeedLen, 'realSeedLen');
496
549
  if (typeof expandSeed !== 'function')
497
550
  throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
498
551
  const rawSigners = signers;
552
+ if (rawSigners.length === 0)
553
+ throw new RangeError('combineSigners requires at least one signer');
499
554
  for (let i = 0; i < rawSigners.length; i++)
500
555
  validateSigner(rawSigners[i], `signers[${i}]`);
501
556
  const keys = combineKeys(realSeedLen, expandSeed, ...rawSigners);
@@ -506,7 +561,7 @@ export function combineSigners(realSeedLen, expandSeed, ...signers) {
506
561
  getPublicKey: keys.getPublicKey,
507
562
  keygen: keys.keygen,
508
563
  sign(message, seed, opts = {}) {
509
- validateSigOpts(opts);
564
+ opts = validateSigOpts(opts);
510
565
  // This generic wrapper intentionally keeps the composite signer contract to message + root
511
566
  // seed only. Per-signer opts like context or extraEntropy cannot be preserved uniformly
512
567
  // across mixed backends, so callers that need them must use the underlying signer directly.
@@ -530,7 +585,7 @@ export function combineSigners(realSeedLen, expandSeed, ...signers) {
530
585
  * does any failing child verify. Throws on unsupported generic opts or malformed publicKey.
531
586
  */
532
587
  verify: (signature, message, publicKey, opts = {}) => {
533
- validateVerOpts(opts);
588
+ opts = validateVerOpts(opts);
534
589
  if (opts.context !== undefined)
535
590
  throw new Error('combineSigners does not support context; use the underlying signer directly');
536
591
  // Malformed signature *length* is a verification failure, not a thrown type error —
@@ -563,14 +618,16 @@ export function combineSigners(realSeedLen, expandSeed, ...signers) {
563
618
  * @param xof - XOF used for seed expansion.
564
619
  * @param kdf - Hash used for the final combiner.
565
620
  * @returns Hybrid KEM.
621
+ * @throws On wrong argument types. {@link TypeError}
622
+ * @throws On wrong argument ranges or values. {@link RangeError}
566
623
  * @example
567
624
  * Build a QSF hybrid KEM preset from a PQ KEM and an elliptic-curve KEM.
568
625
  * ```ts
569
626
  * import { p256 } from '@noble/curves/nist.js';
570
627
  * import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
571
- * import { QSF, ecdhKem } from '@noble/post-quantum/hybrid.js';
628
+ * import { QSF, _ecdhKem } from '@noble/post-quantum/hybrid.js';
572
629
  * import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
573
- * const kem = QSF('example', ml_kem768, ecdhKem(p256, true), shake256, sha3_256);
630
+ * const kem = QSF('example', ml_kem768, _ecdhKem(p256, true), shake256, sha3_256);
574
631
  * const publicKeyLen = kem.lengths.publicKey;
575
632
  * ```
576
633
  */
@@ -587,9 +644,9 @@ export function QSF(label, pqc, curveKEM, xof, kdf) {
587
644
  return combineKEMS(32, kdf.outputLen, expandSeedXof(xof), (pk, ct, ss) => kdf(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))), pqc, curveKEM);
588
645
  }
589
646
  /** QSF preset combining ML-KEM-768 with P-256. */
590
- export const QSF_ml_kem768_p256 = /* @__PURE__ */ (() => QSF('QSF-KEM(ML-KEM-768,P-256)-XOF(SHAKE256)-KDF(SHA3-256)', ml_kem768, ecdhKem(p256, true), shake256, sha3_256))();
647
+ export const QSF_ml_kem768_p256 = /* @__PURE__ */ (() => QSF('QSF-KEM(ML-KEM-768,P-256)-XOF(SHAKE256)-KDF(SHA3-256)', ml_kem768, _ecdhKem(p256, true), shake256, sha3_256))();
591
648
  /** QSF preset combining ML-KEM-1024 with P-384. */
592
- export const QSF_ml_kem1024_p384 = /* @__PURE__ */ (() => QSF('QSF-KEM(ML-KEM-1024,P-384)-XOF(SHAKE256)-KDF(SHA3-256)', ml_kem1024, ecdhKem(p384, true), shake256, sha3_256))();
649
+ export const QSF_ml_kem1024_p384 = /* @__PURE__ */ (() => QSF('QSF-KEM(ML-KEM-1024,P-384)-XOF(SHAKE256)-KDF(SHA3-256)', ml_kem1024, _ecdhKem(p384, true), shake256, sha3_256))();
593
650
  /**
594
651
  * Builds the "KitchenSink" hybrid KEM combiner.
595
652
  * The current builder always derives a fixed 32-byte output,
@@ -604,15 +661,17 @@ export const QSF_ml_kem1024_p384 = /* @__PURE__ */ (() => QSF('QSF-KEM(ML-KEM-10
604
661
  * @param xof - XOF used for seed expansion.
605
662
  * @param hash - Hash used for HKDF extraction and expansion.
606
663
  * @returns Hybrid KEM.
664
+ * @throws On wrong argument types. {@link TypeError}
665
+ * @throws On wrong argument ranges or values. {@link RangeError}
607
666
  * @example
608
667
  * Build the "KitchenSink" hybrid KEM combiner.
609
668
  * ```ts
610
669
  * import { sha256 } from '@noble/hashes/sha2.js';
611
670
  * import { shake256 } from '@noble/hashes/sha3.js';
612
- * import { createKitchenSink, ecdhKem } from '@noble/post-quantum/hybrid.js';
671
+ * import { createKitchenSink, _ecdhKem } from '@noble/post-quantum/hybrid.js';
613
672
  * import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
614
673
  * import { x25519 } from '@noble/curves/ed25519.js';
615
- * const kem = createKitchenSink('example', ml_kem768, ecdhKem(x25519), shake256, sha256);
674
+ * const kem = createKitchenSink('example', ml_kem768, _ecdhKem(x25519), shake256, sha256);
616
675
  * const publicKeyLen = kem.lengths.publicKey;
617
676
  * ```
618
677
  */
@@ -637,9 +696,9 @@ export function createKitchenSink(label, pqc, curveKEM, xof, hash) {
637
696
  return res;
638
697
  }, pqc, curveKEM);
639
698
  }
640
- // Internal alias only: this stays exactly `ecdhKem(x25519)`
699
+ // Internal alias only: this stays exactly `_ecdhKem(x25519)`
641
700
  // and inherits that wrapper's mutation/oracle behavior.
642
- const x25519kem = /* @__PURE__ */ ecdhKem(x25519);
701
+ const x25519kem = /* @__PURE__ */ _ecdhKem(x25519);
643
702
  /** KitchenSink preset combining ML-KEM-768 with X25519.
644
703
  * Caller randomness splits into 32 ML-KEM coins plus a 32-byte X25519 ephemeral-secret seed.
645
704
  */
package/index.js 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, XWing,
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/ml-dsa.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { CHash } from '@noble/hashes/utils.js';
2
- import { type CryptoKeys, type Signer, type SigOpts, type TArg, type TRet, type VerOpts } from './utils.ts';
2
+ import { type CryptoKeys, type Signer, type SigOpts, type TArg, type TRet } from './utils.ts';
3
3
  /** Internal ML-DSA options. */
4
4
  export type DSAInternalOpts = {
5
5
  /**
@@ -12,8 +12,8 @@ export type DSAInternalOpts = {
12
12
  /** ML-DSA signer surface with access to the internal message formatting mode. */
13
13
  export type DSAInternal = CryptoKeys & {
14
14
  lengths: Signer['lengths'];
15
- sign: (msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>, opts?: TArg<SigOpts & DSAInternalOpts>) => TRet<Uint8Array>;
16
- verify: (sig: TArg<Uint8Array>, msg: TArg<Uint8Array>, pubKey: TArg<Uint8Array>, opts?: TArg<VerOpts & DSAInternalOpts>) => boolean;
15
+ sign: (msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>, opts?: TArg<Omit<SigOpts, 'context'> & DSAInternalOpts>) => TRet<Uint8Array>;
16
+ verify: (sig: TArg<Uint8Array>, msg: TArg<Uint8Array>, pubKey: TArg<Uint8Array>, opts?: TArg<DSAInternalOpts>) => boolean;
17
17
  };
18
18
  /** Public ML-DSA signer surface. */
19
19
  export type DSA = Signer & {
package/ml-dsa.js CHANGED
@@ -11,11 +11,26 @@
11
11
  import { abool } from '@noble/curves/utils.js';
12
12
  import { shake256 } from '@noble/hashes/sha3.js';
13
13
  import { genCrystals, XOF128, XOF256 } from "./_crystals.js";
14
- import { abytes, checkHash, cleanBytes, equalBytes, getMessage, getMessagePrehash, randomBytes, splitCoder, validateOpts, validateSigOpts, validateVerOpts, vecCoder, } from "./utils.js";
15
- function validateInternalOpts(opts) {
16
- validateOpts(opts);
17
- if (opts.externalMu !== undefined)
18
- abool(opts.externalMu, 'opts.externalMu');
14
+ import { abytes, checkHash, cleanBytes, equalBytes, getMessage, getMessagePrehash, randomBytes, splitCoder, validateSigOpts, validateVerOpts, checkOptKeys, vecCoder, } from "./utils.js";
15
+ /**
16
+ * Keys each internal surface accepts.
17
+ *
18
+ * `context` is deliberately absent from both. The internal functions never read it: the
19
+ * public wrappers consume it when they format `M'` and must not pass it down, because a
20
+ * key that is accepted and then not acted on is the same silent downgrade this validation
21
+ * exists to prevent. `externalMu` is the mirror case, existing here and rejected above.
22
+ * `extraEntropy` is signing-only, so verification does not take it either.
23
+ */
24
+ const INTERNAL_SIG_OPT_KEYS = /* @__PURE__ */ Object.freeze([
25
+ 'extraEntropy',
26
+ 'externalMu',
27
+ ]);
28
+ const INTERNAL_VER_OPT_KEYS = /* @__PURE__ */ Object.freeze(['externalMu']);
29
+ function validateInternalOpts(opts, allowed) {
30
+ const normalized = checkOptKeys(opts, allowed);
31
+ if (normalized.externalMu !== undefined)
32
+ abool(normalized.externalMu, 'opts.externalMu');
33
+ return normalized;
19
34
  }
20
35
  // Constants
21
36
  // FIPS 204 fixes ML-DSA over R = Z[X]/(X^256 + 1), so every polynomial has 256 coefficients.
@@ -423,8 +438,8 @@ function getDilithium(opts_) {
423
438
  },
424
439
  // NOTE: random is optional.
425
440
  sign: (msg, secretKey, opts = {}) => {
426
- validateSigOpts(opts);
427
- validateInternalOpts(opts);
441
+ opts = validateSigOpts(opts, INTERNAL_SIG_OPT_KEYS);
442
+ opts = validateInternalOpts(opts, INTERNAL_SIG_OPT_KEYS);
428
443
  const { extraEntropy: random, externalMu = false } = opts;
429
444
  // FIPS 204 external-mu mode expects the 64-byte message representative µ = H(tr || M).
430
445
  if (externalMu)
@@ -516,8 +531,13 @@ function getDilithium(opts_) {
516
531
  const cs1 = s1.map((i) => MultiplyNTTs(i, cHat));
517
532
  for (let i = 0; i < L; i++) {
518
533
  polyAdd(crystals.NTT.decode(cs1[i]), y[i]); // z ← y + ⟨⟨cs1⟩⟩
519
- if (polyChknorm(cs1[i], GAMMA1 - BETA))
534
+ if (polyChknorm(cs1[i], GAMMA1 - BETA)) {
535
+ // Rejected. Wipe this iteration's secret-derived buffers before retrying; the
536
+ // accepted path wipes the same set, and only the persistent key material (s1, s2,
537
+ // t0, A, rhoprime) is kept for the next iteration and cleaned at the very end.
538
+ cleanBytes(cTilde, cs1, cHat, w1, w, z, y);
520
539
  continue main_loop; // ||z||∞ ≥ γ1 − β
540
+ }
521
541
  }
522
542
  // cs1 is now z (▷ Signer’s response)
523
543
  let cnt = 0;
@@ -525,19 +545,25 @@ function getDilithium(opts_) {
525
545
  for (let i = 0; i < K; i++) {
526
546
  const cs2 = crystals.NTT.decode(MultiplyNTTs(s2[i], cHat)); // ⟨⟨cs2⟩⟩ ← NTT−1(cˆ◦ sˆ2)
527
547
  const r0 = polySub(w[i], cs2).map(LowBits); // r0 ← LowBits(w − ⟨⟨cs2⟩⟩)
528
- if (polyChknorm(r0, GAMMA2 - BETA))
548
+ if (polyChknorm(r0, GAMMA2 - BETA)) {
549
+ cleanBytes(cTilde, cs1, cHat, w1, w, z, y, h, cs2, r0);
529
550
  continue main_loop; // ||r0||∞ ≥ γ2 − β
551
+ }
530
552
  const ct0 = crystals.NTT.decode(MultiplyNTTs(t0[i], cHat)); // ⟨⟨ct0⟩⟩ ← NTT−1(cˆ◦ tˆ0)
531
- if (polyChknorm(ct0, GAMMA2))
553
+ if (polyChknorm(ct0, GAMMA2)) {
554
+ cleanBytes(cTilde, cs1, cHat, w1, w, z, y, h, cs2, r0, ct0);
532
555
  continue main_loop;
556
+ }
533
557
  polyAdd(r0, ct0);
534
558
  // ▷ Signer’s hint
535
559
  const hint = polyMakeHint(r0, w1[i]); // h ← MakeHint(−⟨⟨ct0⟩⟩, w− ⟨⟨cs2⟩⟩ + ⟨⟨ct0⟩⟩)
536
560
  h.push(hint.v);
537
561
  cnt += hint.cnt;
538
562
  }
539
- if (cnt > OMEGA)
563
+ if (cnt > OMEGA) {
564
+ cleanBytes(cTilde, cs1, cHat, w1, w, z, y, h);
540
565
  continue; // the number of 1’s in h is greater than ω
566
+ }
541
567
  x256.clean();
542
568
  const res = sigCoder.encode([cTilde, cs1, h]); // σ ← sigEncode(c˜, z mod±q, h)
543
569
  // rho, _K, tr is subarray of secretKey, cannot clean.
@@ -553,7 +579,7 @@ function getDilithium(opts_) {
553
579
  throw new Error('Unreachable code path reached, report this error');
554
580
  },
555
581
  verify: (sig, msg, publicKey, opts = {}) => {
556
- validateInternalOpts(opts);
582
+ opts = validateInternalOpts(opts, INTERNAL_VER_OPT_KEYS);
557
583
  const { externalMu = false } = opts;
558
584
  // FIPS 204 external-mu mode expects the 64-byte message representative µ = H(tr || M).
559
585
  if (externalMu)
@@ -622,16 +648,21 @@ function getDilithium(opts_) {
622
648
  lengths: internal.lengths,
623
649
  getPublicKey: internal.getPublicKey,
624
650
  sign: (msg, secretKey, opts = {}) => {
625
- validateSigOpts(opts);
651
+ opts = validateSigOpts(opts);
626
652
  const M = getMessage(msg, opts.context);
627
- const res = internal.sign(M, secretKey, opts);
653
+ // `context` is consumed by getMessage() above; forwarding it would make the internal
654
+ // surface accept a key it never reads.
655
+ const res = internal.sign(M, secretKey, {
656
+ extraEntropy: opts.extraEntropy,
657
+ externalMu: false,
658
+ });
628
659
  cleanBytes(M);
629
660
  return res;
630
661
  },
631
662
  verify: (sig, msg, publicKey, opts = {}) => {
632
- validateVerOpts(opts);
663
+ opts = validateVerOpts(opts);
633
664
  abytes(sig, undefined, 'signature');
634
- return internal.verify(sig, getMessage(msg, opts.context), publicKey);
665
+ return internal.verify(sig, getMessage(msg, opts.context), publicKey, { externalMu: false });
635
666
  },
636
667
  prehash: (hash) => {
637
668
  checkHash(hash, securityLevel);
@@ -643,16 +674,22 @@ function getDilithium(opts_) {
643
674
  keygen: internal.keygen,
644
675
  getPublicKey: internal.getPublicKey,
645
676
  sign: (msg, secretKey, opts = {}) => {
646
- validateSigOpts(opts);
677
+ opts = validateSigOpts(opts);
647
678
  const M = getMessagePrehash(rawHash, msg, opts.context);
648
- const res = internal.sign(M, secretKey, opts);
679
+ // As above: getMessagePrehash() consumes `context`, so it must not travel further.
680
+ const res = internal.sign(M, secretKey, {
681
+ extraEntropy: opts.extraEntropy,
682
+ externalMu: false,
683
+ });
649
684
  cleanBytes(M);
650
685
  return res;
651
686
  },
652
687
  verify: (sig, msg, publicKey, opts = {}) => {
653
- validateVerOpts(opts);
688
+ opts = validateVerOpts(opts);
654
689
  abytes(sig, undefined, 'signature');
655
- return internal.verify(sig, getMessagePrehash(rawHash, msg, opts.context), publicKey);
690
+ return internal.verify(sig, getMessagePrehash(rawHash, msg, opts.context), publicKey, {
691
+ externalMu: false,
692
+ });
656
693
  },
657
694
  });
658
695
  },