@noble/post-quantum 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/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,
@@ -92,6 +94,8 @@ import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
92
94
  import { abytes, ahash, anumber, 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) {
@@ -177,6 +204,11 @@ function ecKeygen(curve: CurveAll, allowZeroKey: boolean = false) {
177
204
  export function ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): TRet<KEM> {
178
205
  const kg = ecKeygen(curve, allowZeroKey);
179
206
  if (!curve.getSharedSecret) throw new Error('wrong curve'); // ed25519 doesn't have one!
207
+ // Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
208
+ const decapsulate = (cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>) => {
209
+ const res = curve.getSharedSecret(secretKey, cipherText);
210
+ return (curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res) as TRet<Uint8Array>;
211
+ };
180
212
  return {
181
213
  lengths: { ...kg.lengths, msg: kg.lengths.seed, cipherText: kg.lengths.publicKey },
182
214
  keygen: kg.keygen,
@@ -190,8 +222,8 @@ export function ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): TRet<K
190
222
  const seed = copyBytes(rand);
191
223
  let ek: Uint8Array | undefined = undefined;
192
224
  try {
193
- ek = this.keygen(seed).secretKey;
194
- const sharedSecret = this.decapsulate(publicKey, ek);
225
+ ek = kg.keygen(seed).secretKey;
226
+ const sharedSecret = decapsulate(publicKey, ek);
195
227
  const cipherText = curve.getPublicKey(ek) as TRet<Uint8Array>;
196
228
  return { sharedSecret, cipherText };
197
229
  } finally {
@@ -201,10 +233,7 @@ export function ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): TRet<K
201
233
  if (ek) cleanBytes(ek);
202
234
  }
203
235
  },
204
- decapsulate(cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>) {
205
- const res = curve.getSharedSecret(secretKey, cipherText);
206
- return (curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res) as TRet<Uint8Array>;
207
- },
236
+ decapsulate,
208
237
  };
209
238
  }
210
239
 
@@ -359,25 +388,40 @@ function combineKeys(
359
388
  if (!ok) cleanBytes(secretKey);
360
389
  }
361
390
  }
362
- return {
363
- info: { lengths: { seed: realSeedLen, publicKey: pkCoder.bytesLen, secretKey: realSeedLen } },
364
- getPublicKey(secretKey: TArg<Uint8Array>) {
365
- // Composite secret keys are root seeds, so public-key derivation reruns key expansion from
366
- // that seed instead of decoding a packed child-secret-key structure.
367
- return this.keygen(secretKey).publicKey as TRet<Uint8Array>;
368
- },
369
- keygen(seed: TArg<Uint8Array> = randomBytes(realSeedLen)) {
370
- const { publicKey: pk, secretKey } = expandDecapsulationKey(seed);
391
+ // Standalone (not a method) so getPublicKey / destructured usage never depends on `this`.
392
+ const keygen = (seed?: TArg<Uint8Array>) => {
393
+ // Detach the root: the exported secretKey must not alias caller-owned seed bytes, so later
394
+ // caller mutation of the seed cannot silently change the secret key (and vice versa).
395
+ const root = seed === undefined ? randomBytes(realSeedLen!) : copyBytes(seed);
396
+ let res;
397
+ try {
398
+ const { publicKey: pk, secretKey } = expandDecapsulationKey(root);
371
399
  try {
372
- const publicKey = pkCoder.encode(pk) as TRet<Uint8Array>;
373
- return { secretKey: seed as TRet<Uint8Array>, publicKey };
400
+ res = {
401
+ secretKey: root as TRet<Uint8Array>,
402
+ publicKey: pkCoder.encode(pk) as TRet<Uint8Array>,
403
+ };
374
404
  } finally {
375
- cleanBytes(pk);
376
- // The exported secretKey is the caller/root seed itself; child secret keys are internal
405
+ // The exported secretKey is the (detached) root seed; child secret keys are internal
377
406
  // expansion outputs that are cleaned whether encoding succeeds or throws.
378
- cleanBytes(secretKey);
407
+ cleanBytes(pk, secretKey);
379
408
  }
409
+ return res;
410
+ } finally {
411
+ if (!res) cleanBytes(root);
412
+ }
413
+ };
414
+ return {
415
+ info: { lengths: { seed: realSeedLen, publicKey: pkCoder.bytesLen, secretKey: realSeedLen } },
416
+ // Composite secret keys are root seeds, so public-key derivation reruns key expansion from
417
+ // that seed instead of decoding a packed child-secret-key structure.
418
+ getPublicKey: (secretKey: TArg<Uint8Array>) => {
419
+ const keys = keygen(secretKey);
420
+ // keygen detaches its exported root; getPublicKey discards that half of the result.
421
+ cleanBytes(keys.secretKey);
422
+ return keys.publicKey as TRet<Uint8Array>;
380
423
  },
424
+ keygen,
381
425
  expandDecapsulationKey,
382
426
  realSeedLen,
383
427
  };
@@ -416,14 +460,21 @@ export function combineKEMS(
416
460
  combiner: TArg<Combiner>,
417
461
  ...kems: TArg<KEM[]>
418
462
  ): TRet<KEM> {
463
+ if (realSeedLen !== undefined) anumber(realSeedLen, 'realSeedLen');
464
+ if (realMsgLen !== undefined) anumber(realMsgLen, 'realMsgLen');
465
+ if (typeof expandSeed !== 'function')
466
+ throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
467
+ if (typeof combiner !== 'function')
468
+ throw new TypeError('"combiner" expected function, got type=' + typeof combiner);
419
469
  const rawCombiner = combiner as Combiner;
420
470
  const rawKems = kems as KEM[];
471
+ for (let i = 0; i < rawKems.length; i++) validateKEM(rawKems[i], `kems[${i}]`);
421
472
  const keys = combineKeys(realSeedLen, expandSeed, ...rawKems);
422
473
  const ctCoder = splitLengths(rawKems, 'cipherText');
423
474
  const pkCoder = splitLengths(rawKems, 'publicKey');
424
475
  const msgCoder = splitLengths(rawKems, 'msg');
425
476
  if (realMsgLen === undefined) realMsgLen = msgCoder.bytesLen;
426
- anumber(realMsgLen);
477
+ anumber(realMsgLen, 'realMsgLen');
427
478
  const lengths = Object.freeze({
428
479
  ...keys.info.lengths,
429
480
  msg: realMsgLen,
@@ -491,7 +542,11 @@ export function combineKEMS(
491
542
  * import { combineSigners, expandSeedXof } from '@noble/post-quantum/hybrid.js';
492
543
  * import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
493
544
  * const hybrid = combineSigners(32, expandSeedXof(shake256), ml_dsa44, ml_dsa44);
494
- * const { publicKey } = hybrid.keygen();
545
+ * const seed = new Uint8Array(hybrid.lengths.seed!).fill(1);
546
+ * const { secretKey, publicKey } = hybrid.keygen(seed);
547
+ * const msg = new TextEncoder().encode('hello noble');
548
+ * const sig = hybrid.sign(msg, secretKey);
549
+ * const isValid = hybrid.verify(sig, msg, publicKey);
495
550
  * ```
496
551
  */
497
552
  export function combineSigners(
@@ -499,7 +554,11 @@ export function combineSigners(
499
554
  expandSeed: TArg<ExpandSeed>,
500
555
  ...signers: TArg<Signer[]>
501
556
  ): TRet<Signer> {
557
+ if (realSeedLen !== undefined) anumber(realSeedLen, 'realSeedLen');
558
+ if (typeof expandSeed !== 'function')
559
+ throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
502
560
  const rawSigners = signers as Signer[];
561
+ for (let i = 0; i < rawSigners.length; i++) validateSigner(rawSigners[i], `signers[${i}]`);
503
562
  const keys = combineKeys(realSeedLen, expandSeed, ...rawSigners);
504
563
  const sigCoder = splitLengths(rawSigners, 'signature');
505
564
  const pkCoder = splitLengths(rawSigners, 'publicKey');
@@ -531,8 +590,8 @@ export function combineSigners(
531
590
  }
532
591
  },
533
592
  /** Verify one combined signature.
534
- * Returns `false` when the aggregate signature/publicKey decode succeeds but any child verify
535
- * check fails. Throws on unsupported generic opts or malformed aggregate encodings.
593
+ * Wrong-length aggregate signatures return `false` (matching ml-dsa / slh-dsa behavior), as
594
+ * does any failing child verify. Throws on unsupported generic opts or malformed publicKey.
536
595
  */
537
596
  verify: (signature, message, publicKey, opts = {}) => {
538
597
  validateVerOpts(opts);
@@ -540,7 +599,13 @@ export function combineSigners(
540
599
  throw new Error(
541
600
  'combineSigners does not support context; use the underlying signer directly'
542
601
  );
602
+ // Malformed signature *length* is a verification failure, not a thrown type error —
603
+ // consistent with ml-dsa / slh-dsa. Must run before sigCoder.decode, which throws.
604
+ // Preserve TypeError for non-byte API arguments before treating byte lengths as invalid.
605
+ abytes(signature, undefined, 'signature');
606
+ // A signature failure must not hide malformed aggregate public-key bytes.
543
607
  const pks = pkCoder.decode(publicKey);
608
+ if (signature.length !== sigCoder.bytesLen) return false;
544
609
  const sigs = sigCoder.decode(signature);
545
610
  for (let i = 0; i < rawSigners.length; i++) {
546
611
  if (!rawSigners[i].verify(sigs[i], message, pks[i])) return false;
@@ -581,7 +646,14 @@ export function QSF(
581
646
  xof: TArg<XOF>,
582
647
  kdf: CHash
583
648
  ): TRet<KEM> {
649
+ astring(label, 'label');
650
+ validateKEM(pqc, 'pqc');
651
+ validateKEM(curveKEM, 'curveKEM');
652
+ if (typeof xof !== 'function' || typeof (xof as any).create !== 'function')
653
+ throw new TypeError('"xof" expected hash function, got type=' + typeof xof);
584
654
  ahash(xof);
655
+ if (typeof kdf !== 'function' || typeof (kdf as any).create !== 'function')
656
+ throw new TypeError('"kdf" expected hash function, got type=' + typeof kdf);
585
657
  ahash(kdf);
586
658
  return combineKEMS(
587
659
  32,
@@ -646,7 +718,14 @@ export function createKitchenSink(
646
718
  xof: TArg<XOF>,
647
719
  hash: CHash
648
720
  ): TRet<KEM> {
721
+ astring(label, 'label');
722
+ validateKEM(pqc, 'pqc');
723
+ validateKEM(curveKEM, 'curveKEM');
724
+ if (typeof xof !== 'function' || typeof (xof as any).create !== 'function')
725
+ throw new TypeError('"xof" expected hash function, got type=' + typeof xof);
649
726
  ahash(xof);
727
+ if (typeof hash !== 'function' || typeof (hash as any).create !== 'function')
728
+ throw new TypeError('"hash" expected hash function, got type=' + typeof hash);
650
729
  ahash(hash);
651
730
  return combineKEMS(
652
731
  32,
@@ -734,6 +813,11 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
734
813
  }>;
735
814
  }
736
815
 
816
+ // Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
817
+ const decapsulate = (cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>) => {
818
+ const full = curve.getSharedSecret(secretKey, cipherText);
819
+ return full.subarray(1) as TRet<Uint8Array>;
820
+ };
737
821
  return {
738
822
  lengths: {
739
823
  secretKey: scalarLen,
@@ -754,7 +838,7 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
754
838
  let ek: Uint8Array | undefined = undefined;
755
839
  try {
756
840
  ek = rejectionSampling(rand).secretKey;
757
- const sharedSecret = this.decapsulate(publicKey, ek);
841
+ const sharedSecret = decapsulate(publicKey, ek);
758
842
  const cipherText = curve.getPublicKey(ek, false) as TRet<Uint8Array>;
759
843
  return { sharedSecret, cipherText };
760
844
  } finally {
@@ -763,10 +847,7 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
763
847
  if (ek) cleanBytes(ek);
764
848
  }
765
849
  },
766
- decapsulate(cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>) {
767
- const full = curve.getSharedSecret(secretKey, cipherText);
768
- return full.subarray(1) as TRet<Uint8Array>;
769
- },
850
+ decapsulate,
770
851
  };
771
852
  }
772
853
 
@@ -795,10 +876,11 @@ function concreteHybridKem(
795
876
  32,
796
877
  (seed: TArg<Uint8Array>): TRet<Uint8Array> => {
797
878
  abytes(seed, 32);
798
- const expanded = shake256(seed, { dkLen: totalSeedLen });
799
- const mlkemSeed = expanded.subarray(0, mlkemSeedLen);
800
- const curveSeed = expanded.subarray(mlkemSeedLen, totalSeedLen);
801
- return concatBytes(mlkemSeed, curveSeed) as TRet<Uint8Array>;
879
+ // One SHAKE256 stream split by the seed coder as mlkemSeed (64) || curveSeed (nseed).
880
+ // Returned directly: the previous concatBytes of two adjacent subarrays produced an
881
+ // identical copy while leaving this original buffer unwiped; expandDecapsulationKey
882
+ // wipes the returned buffer after the child seeds are copied out.
883
+ return shake256(seed, { dkLen: totalSeedLen }) as TRet<Uint8Array>;
802
884
  },
803
885
  (pk: TArg<Uint8Array[]>, ct: TArg<Uint8Array[]>, ss: TArg<Uint8Array[]>) =>
804
886
  sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))),
@@ -814,20 +896,3 @@ export const ml_kem768_p256: TRet<KEM> = /* @__PURE__ */ (() =>
814
896
  /** P-384 + ML-KEM-1024 hybrid preset. */
815
897
  export const ml_kem1024_p384: TRet<KEM> = /* @__PURE__ */ (() =>
816
898
  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/ml-dsa.ts CHANGED
@@ -64,7 +64,16 @@ export type DSAInternal = CryptoKeys & {
64
64
  ) => boolean;
65
65
  };
66
66
  /** Public ML-DSA signer surface. */
67
- export type DSA = Signer & { internal: TRet<DSAInternal> };
67
+ export type DSA = Signer & {
68
+ internal: TRet<DSAInternal>;
69
+ securityLevel: number;
70
+ /**
71
+ * HashML-DSA (FIPS 204 §5.4) variant which signs a pre-hashed message.
72
+ * @param hash - Approved hash, checked against the parameter set security level.
73
+ * @returns Signer which pre-hashes `msg` before formatting `M'`.
74
+ */
75
+ prehash: (hash: TArg<CHash>) => TRet<Signer>;
76
+ };
68
77
 
69
78
  // Constants
70
79
  // FIPS 204 fixes ML-DSA over R = Z[X]/(X^256 + 1), so every polynomial has 256 coefficients.
@@ -154,6 +163,8 @@ const polyCoder = (d: number, compress: IdNum = id, verify: IdNum = id) =>
154
163
  });
155
164
 
156
165
  // Mutates `a` in place; callers must pass same-length polynomials.
166
+ // NOTE: conditional-reduction variants (as in ml-kem) were measured performance-neutral here —
167
+ // int32 `%` with 23-bit Q is already cheap — so the simpler mod() form is kept for audit.
157
168
  const polyAdd = (a_: TArg<Poly>, b_: TArg<Poly>): TRet<Poly> => {
158
169
  const a = a_ as Poly;
159
170
  const b = b_ as Poly;
@@ -275,13 +286,16 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
275
286
  return res0;
276
287
  };
277
288
 
289
+ // m = (q-1)/(2γ2): 44 for ML-DSA-44, 16 for 65/87. Hoisted out of UseHint, which runs
290
+ // per coefficient during verification.
291
+ const HINT_M = Math.floor((Q - 1) / (2 * GAMMA2));
278
292
  const UseHint = (h: number, r: number) => {
279
293
  // Returns the high bits of r adjusted according to hint h
280
- const m = Math.floor((Q - 1) / (2 * GAMMA2));
281
294
  const { r1, r0 } = decompose(r);
282
295
  // 3: if h = 1 and r0 > 0 return (r1 + 1) mod m
283
296
  // 4: if h = 1 and r0 ≤ 0 return (r1 − 1) mod m
284
- if (h === 1) return r0 > 0 ? crystals.mod(r1 + 1, m) | 0 : crystals.mod(r1 - 1, m) | 0;
297
+ if (h === 1)
298
+ return r0 > 0 ? crystals.mod(r1 + 1, HINT_M) | 0 : crystals.mod(r1 - 1, HINT_M) | 0;
285
299
  return r1 | 0;
286
300
  };
287
301
  const Power2Round = (r: number) => {
@@ -527,11 +541,32 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
527
541
  ): TRet<Uint8Array> => {
528
542
  validateSigOpts(opts);
529
543
  validateInternalOpts(opts);
530
- let { extraEntropy: random, externalMu = false } = opts;
544
+ const { extraEntropy: random, externalMu = false } = opts;
545
+ // FIPS 204 external-mu mode expects the 64-byte message representative µ = H(tr || M).
546
+ if (externalMu) abytes(msg, CRH_BYTES, 'mu');
547
+ // Prepare entropy before touching decoded secrets: randomBytes() may throw, and an RNG
548
+ // failure must not leave expanded secret-polynomial copies behind.
549
+ const ownRnd = random === false || random === undefined;
550
+ const rnd =
551
+ random === false
552
+ ? new Uint8Array(32)
553
+ : random === undefined
554
+ ? randomBytes(signRandBytes)
555
+ : (random as Uint8Array);
556
+ abytes(rnd, 32, 'extraEntropy');
531
557
  // This part can be pre-cached per secretKey, but there is only minor performance improvement,
532
558
  // since we re-use a lot of variables to computation.
533
559
  // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
534
- const [rho, _K, tr, s1, s2, t0] = secretCoder.decode(secretKey);
560
+ const decoded = (() => {
561
+ try {
562
+ return secretCoder.decode(secretKey);
563
+ } catch (error) {
564
+ // A malformed key must not strand entropy owned by the library.
565
+ if (ownRnd) cleanBytes(rnd);
566
+ throw error;
567
+ }
568
+ })();
569
+ const [rho, _K, tr, s1, s2, t0] = decoded;
535
570
  // Cache matrix to avoid re-compute later
536
571
  const A: Poly[][] = []; // A ← ExpandA(ρ)
537
572
  const xof = XOF128(rho);
@@ -553,21 +588,14 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
553
588
  // ▷ Compute message representative µ
554
589
  shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest();
555
590
 
556
- // Compute private random seed
557
- const rnd =
558
- random === false
559
- ? new Uint8Array(32)
560
- : random === undefined
561
- ? randomBytes(signRandBytes)
562
- : random;
563
- abytes(rnd, 32, 'extraEntropy');
564
591
  const rhoprime = shake256
565
592
  .create({ dkLen: CRH_BYTES })
566
593
  .update(_K)
567
594
  .update(rnd)
568
595
  .update(mu)
569
596
  .digest(); // ρ′← H(K||rnd||µ, 512)
570
-
597
+ // Only wipe entropy we generated; caller-provided extraEntropy stays caller-owned.
598
+ if (ownRnd) cleanBytes(rnd);
571
599
  abytes(rhoprime, CRH_BYTES);
572
600
  const x256 = XOF256(rhoprime, ZCoder.bytesLen);
573
601
  // Rejection sampling loop
@@ -638,6 +666,8 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
638
666
  ) => {
639
667
  validateInternalOpts(opts);
640
668
  const { externalMu = false } = opts;
669
+ // FIPS 204 external-mu mode expects the 64-byte message representative µ = H(tr || M).
670
+ if (externalMu) abytes(msg, CRH_BYTES, 'mu');
641
671
  // ML-DSA.Verify(pk, M, σ): Verifes a signature σ for a message M.
642
672
  const [rho, t1] = publicCoder.decode(publicKey); // (ρ, t1) ← pkDecode(pk)
643
673
  const tr = shake256(publicKey, { dkLen: TR_BYTES }); // 6: tr ← H(BytesToBits(pk), 512)
@@ -712,10 +742,12 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
712
742
  opts: TArg<VerOpts> = {}
713
743
  ) => {
714
744
  validateVerOpts(opts);
745
+ abytes(sig, undefined, 'signature');
715
746
  return internal.verify(sig, getMessage(msg, opts.context), publicKey);
716
747
  },
717
- prehash: (hash: CHash) => {
718
- checkHash(hash, securityLevel);
748
+ prehash: (hash: TArg<CHash>): TRet<Signer> => {
749
+ checkHash(hash as CHash, securityLevel);
750
+ const rawHash = hash as CHash;
719
751
  return Object.freeze({
720
752
  info: Object.freeze({ type: 'hashml-dsa' }),
721
753
  securityLevel: securityLevel,
@@ -728,7 +760,7 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
728
760
  opts: TArg<SigOpts> = {}
729
761
  ): TRet<Uint8Array> => {
730
762
  validateSigOpts(opts);
731
- const M = getMessagePrehash(hash, msg, opts.context);
763
+ const M = getMessagePrehash(rawHash, msg, opts.context);
732
764
  const res = internal.sign(M, secretKey, opts);
733
765
  cleanBytes(M);
734
766
  return res as TRet<Uint8Array>;
@@ -740,14 +772,34 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
740
772
  opts: TArg<VerOpts> = {}
741
773
  ) => {
742
774
  validateVerOpts(opts);
743
- return internal.verify(sig, getMessagePrehash(hash, msg, opts.context), publicKey);
775
+ abytes(sig, undefined, 'signature');
776
+ return internal.verify(sig, getMessagePrehash(rawHash, msg, opts.context), publicKey);
744
777
  },
745
778
  });
746
779
  },
747
780
  });
748
781
  }
749
782
 
750
- /** ML-DSA-44 for 128-bit security level. Not recommended after 2030, as per ASD. */
783
+ /**
784
+ * ML-DSA-44 for 128-bit security level. Not recommended after 2030, as per ASD.
785
+ * @example
786
+ * Generate deterministic ML-DSA-44 keys, sign one message, and verify the signature.
787
+ * ```ts
788
+ * import { sha256 } from '@noble/hashes/sha2.js';
789
+ * import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
790
+ * const seed = new Uint8Array(ml_dsa44.lengths.seed!);
791
+ * const { secretKey, publicKey } = ml_dsa44.keygen(seed);
792
+ * const msg = new TextEncoder().encode('hello noble');
793
+ * const sig = ml_dsa44.sign(msg, secretKey);
794
+ * const isValid = ml_dsa44.verify(sig, msg, publicKey);
795
+ * const recovered = ml_dsa44.getPublicKey(secretKey);
796
+ * const context = new Uint8Array([1, 2, 3]);
797
+ * const prehash = ml_dsa44.prehash(sha256);
798
+ * const preSig = prehash.sign(msg, secretKey, { context });
799
+ * const preValid = prehash.verify(preSig, msg, publicKey, { context });
800
+ * const internalSig = ml_dsa44.internal.sign(msg, secretKey);
801
+ * ```
802
+ */
751
803
  export const ml_dsa44: TRet<DSA> = /* @__PURE__ */ (() =>
752
804
  getDilithium({
753
805
  ...PARAMS[2],