@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/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.
@@ -72,6 +87,8 @@ const polyCoder = (d, compress = id, verify = id) => crystals.bitsCoder(d, {
72
87
  decode: (i) => verify(compress(i)),
73
88
  });
74
89
  // Mutates `a` in place; callers must pass same-length polynomials.
90
+ // NOTE: conditional-reduction variants (as in ml-kem) were measured performance-neutral here —
91
+ // int32 `%` with 23-bit Q is already cheap — so the simpler mod() form is kept for audit.
75
92
  const polyAdd = (a_, b_) => {
76
93
  const a = a_;
77
94
  const b = b_;
@@ -179,14 +196,16 @@ function getDilithium(opts_) {
179
196
  // See dilithium-py README section "Optimising decomposition and making hints".
180
197
  return res0;
181
198
  };
199
+ // m = (q-1)/(2γ2): 44 for ML-DSA-44, 16 for 65/87. Hoisted out of UseHint, which runs
200
+ // per coefficient during verification.
201
+ const HINT_M = Math.floor((Q - 1) / (2 * GAMMA2));
182
202
  const UseHint = (h, r) => {
183
203
  // Returns the high bits of r adjusted according to hint h
184
- const m = Math.floor((Q - 1) / (2 * GAMMA2));
185
204
  const { r1, r0 } = decompose(r);
186
205
  // 3: if h = 1 and r0 > 0 return (r1 + 1) mod m
187
206
  // 4: if h = 1 and r0 ≤ 0 return (r1 − 1) mod m
188
207
  if (h === 1)
189
- return r0 > 0 ? crystals.mod(r1 + 1, m) | 0 : crystals.mod(r1 - 1, m) | 0;
208
+ return r0 > 0 ? crystals.mod(r1 + 1, HINT_M) | 0 : crystals.mod(r1 - 1, HINT_M) | 0;
190
209
  return r1 | 0;
191
210
  };
192
211
  const Power2Round = (r) => {
@@ -419,13 +438,36 @@ function getDilithium(opts_) {
419
438
  },
420
439
  // NOTE: random is optional.
421
440
  sign: (msg, secretKey, opts = {}) => {
422
- validateSigOpts(opts);
423
- validateInternalOpts(opts);
424
- let { extraEntropy: random, externalMu = false } = opts;
441
+ opts = validateSigOpts(opts, INTERNAL_SIG_OPT_KEYS);
442
+ opts = validateInternalOpts(opts, INTERNAL_SIG_OPT_KEYS);
443
+ const { extraEntropy: random, externalMu = false } = opts;
444
+ // FIPS 204 external-mu mode expects the 64-byte message representative µ = H(tr || M).
445
+ if (externalMu)
446
+ abytes(msg, CRH_BYTES, 'mu');
447
+ // Prepare entropy before touching decoded secrets: randomBytes() may throw, and an RNG
448
+ // failure must not leave expanded secret-polynomial copies behind.
449
+ const ownRnd = random === false || random === undefined;
450
+ const rnd = random === false
451
+ ? new Uint8Array(32)
452
+ : random === undefined
453
+ ? randomBytes(signRandBytes)
454
+ : random;
455
+ abytes(rnd, 32, 'extraEntropy');
425
456
  // This part can be pre-cached per secretKey, but there is only minor performance improvement,
426
457
  // since we re-use a lot of variables to computation.
427
458
  // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
428
- const [rho, _K, tr, s1, s2, t0] = secretCoder.decode(secretKey);
459
+ const decoded = (() => {
460
+ try {
461
+ return secretCoder.decode(secretKey);
462
+ }
463
+ catch (error) {
464
+ // A malformed key must not strand entropy owned by the library.
465
+ if (ownRnd)
466
+ cleanBytes(rnd);
467
+ throw error;
468
+ }
469
+ })();
470
+ const [rho, _K, tr, s1, s2, t0] = decoded;
429
471
  // Cache matrix to avoid re-compute later
430
472
  const A = []; // A ← ExpandA(ρ)
431
473
  const xof = XOF128(rho);
@@ -448,19 +490,15 @@ function getDilithium(opts_) {
448
490
  : // 6: µ ← H(tr||M, 512)
449
491
  // ▷ Compute message representative µ
450
492
  shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest();
451
- // Compute private random seed
452
- const rnd = random === false
453
- ? new Uint8Array(32)
454
- : random === undefined
455
- ? randomBytes(signRandBytes)
456
- : random;
457
- abytes(rnd, 32, 'extraEntropy');
458
493
  const rhoprime = shake256
459
494
  .create({ dkLen: CRH_BYTES })
460
495
  .update(_K)
461
496
  .update(rnd)
462
497
  .update(mu)
463
498
  .digest(); // ρ′← H(K||rnd||µ, 512)
499
+ // Only wipe entropy we generated; caller-provided extraEntropy stays caller-owned.
500
+ if (ownRnd)
501
+ cleanBytes(rnd);
464
502
  abytes(rhoprime, CRH_BYTES);
465
503
  const x256 = XOF256(rhoprime, ZCoder.bytesLen);
466
504
  // Rejection sampling loop
@@ -493,8 +531,13 @@ function getDilithium(opts_) {
493
531
  const cs1 = s1.map((i) => MultiplyNTTs(i, cHat));
494
532
  for (let i = 0; i < L; i++) {
495
533
  polyAdd(crystals.NTT.decode(cs1[i]), y[i]); // z ← y + ⟨⟨cs1⟩⟩
496
- 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);
497
539
  continue main_loop; // ||z||∞ ≥ γ1 − β
540
+ }
498
541
  }
499
542
  // cs1 is now z (▷ Signer’s response)
500
543
  let cnt = 0;
@@ -502,19 +545,25 @@ function getDilithium(opts_) {
502
545
  for (let i = 0; i < K; i++) {
503
546
  const cs2 = crystals.NTT.decode(MultiplyNTTs(s2[i], cHat)); // ⟨⟨cs2⟩⟩ ← NTT−1(cˆ◦ sˆ2)
504
547
  const r0 = polySub(w[i], cs2).map(LowBits); // r0 ← LowBits(w − ⟨⟨cs2⟩⟩)
505
- if (polyChknorm(r0, GAMMA2 - BETA))
548
+ if (polyChknorm(r0, GAMMA2 - BETA)) {
549
+ cleanBytes(cTilde, cs1, cHat, w1, w, z, y, h, cs2, r0);
506
550
  continue main_loop; // ||r0||∞ ≥ γ2 − β
551
+ }
507
552
  const ct0 = crystals.NTT.decode(MultiplyNTTs(t0[i], cHat)); // ⟨⟨ct0⟩⟩ ← NTT−1(cˆ◦ tˆ0)
508
- if (polyChknorm(ct0, GAMMA2))
553
+ if (polyChknorm(ct0, GAMMA2)) {
554
+ cleanBytes(cTilde, cs1, cHat, w1, w, z, y, h, cs2, r0, ct0);
509
555
  continue main_loop;
556
+ }
510
557
  polyAdd(r0, ct0);
511
558
  // ▷ Signer’s hint
512
559
  const hint = polyMakeHint(r0, w1[i]); // h ← MakeHint(−⟨⟨ct0⟩⟩, w− ⟨⟨cs2⟩⟩ + ⟨⟨ct0⟩⟩)
513
560
  h.push(hint.v);
514
561
  cnt += hint.cnt;
515
562
  }
516
- if (cnt > OMEGA)
563
+ if (cnt > OMEGA) {
564
+ cleanBytes(cTilde, cs1, cHat, w1, w, z, y, h);
517
565
  continue; // the number of 1’s in h is greater than ω
566
+ }
518
567
  x256.clean();
519
568
  const res = sigCoder.encode([cTilde, cs1, h]); // σ ← sigEncode(c˜, z mod±q, h)
520
569
  // rho, _K, tr is subarray of secretKey, cannot clean.
@@ -530,8 +579,11 @@ function getDilithium(opts_) {
530
579
  throw new Error('Unreachable code path reached, report this error');
531
580
  },
532
581
  verify: (sig, msg, publicKey, opts = {}) => {
533
- validateInternalOpts(opts);
582
+ opts = validateInternalOpts(opts, INTERNAL_VER_OPT_KEYS);
534
583
  const { externalMu = false } = opts;
584
+ // FIPS 204 external-mu mode expects the 64-byte message representative µ = H(tr || M).
585
+ if (externalMu)
586
+ abytes(msg, CRH_BYTES, 'mu');
535
587
  // ML-DSA.Verify(pk, M, σ): Verifes a signature σ for a message M.
536
588
  const [rho, t1] = publicCoder.decode(publicKey); // (ρ, t1) ← pkDecode(pk)
537
589
  const tr = shake256(publicKey, { dkLen: TR_BYTES }); // 6: tr ← H(BytesToBits(pk), 512)
@@ -596,18 +648,25 @@ function getDilithium(opts_) {
596
648
  lengths: internal.lengths,
597
649
  getPublicKey: internal.getPublicKey,
598
650
  sign: (msg, secretKey, opts = {}) => {
599
- validateSigOpts(opts);
651
+ opts = validateSigOpts(opts);
600
652
  const M = getMessage(msg, opts.context);
601
- 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
+ });
602
659
  cleanBytes(M);
603
660
  return res;
604
661
  },
605
662
  verify: (sig, msg, publicKey, opts = {}) => {
606
- validateVerOpts(opts);
607
- return internal.verify(sig, getMessage(msg, opts.context), publicKey);
663
+ opts = validateVerOpts(opts);
664
+ abytes(sig, undefined, 'signature');
665
+ return internal.verify(sig, getMessage(msg, opts.context), publicKey, { externalMu: false });
608
666
  },
609
667
  prehash: (hash) => {
610
668
  checkHash(hash, securityLevel);
669
+ const rawHash = hash;
611
670
  return Object.freeze({
612
671
  info: Object.freeze({ type: 'hashml-dsa' }),
613
672
  securityLevel: securityLevel,
@@ -615,21 +674,47 @@ function getDilithium(opts_) {
615
674
  keygen: internal.keygen,
616
675
  getPublicKey: internal.getPublicKey,
617
676
  sign: (msg, secretKey, opts = {}) => {
618
- validateSigOpts(opts);
619
- const M = getMessagePrehash(hash, msg, opts.context);
620
- const res = internal.sign(M, secretKey, opts);
677
+ opts = validateSigOpts(opts);
678
+ const M = getMessagePrehash(rawHash, msg, opts.context);
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
+ });
621
684
  cleanBytes(M);
622
685
  return res;
623
686
  },
624
687
  verify: (sig, msg, publicKey, opts = {}) => {
625
- validateVerOpts(opts);
626
- return internal.verify(sig, getMessagePrehash(hash, msg, opts.context), publicKey);
688
+ opts = validateVerOpts(opts);
689
+ abytes(sig, undefined, 'signature');
690
+ return internal.verify(sig, getMessagePrehash(rawHash, msg, opts.context), publicKey, {
691
+ externalMu: false,
692
+ });
627
693
  },
628
694
  });
629
695
  },
630
696
  });
631
697
  }
632
- /** ML-DSA-44 for 128-bit security level. Not recommended after 2030, as per ASD. */
698
+ /**
699
+ * ML-DSA-44 for 128-bit security level. Not recommended after 2030, as per ASD.
700
+ * @example
701
+ * Generate deterministic ML-DSA-44 keys, sign one message, and verify the signature.
702
+ * ```ts
703
+ * import { sha256 } from '@noble/hashes/sha2.js';
704
+ * import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
705
+ * const seed = new Uint8Array(ml_dsa44.lengths.seed!);
706
+ * const { secretKey, publicKey } = ml_dsa44.keygen(seed);
707
+ * const msg = new TextEncoder().encode('hello noble');
708
+ * const sig = ml_dsa44.sign(msg, secretKey);
709
+ * const isValid = ml_dsa44.verify(sig, msg, publicKey);
710
+ * const recovered = ml_dsa44.getPublicKey(secretKey);
711
+ * const context = new Uint8Array([1, 2, 3]);
712
+ * const prehash = ml_dsa44.prehash(sha256);
713
+ * const preSig = prehash.sign(msg, secretKey, { context });
714
+ * const preValid = prehash.verify(preSig, msg, publicKey, { context });
715
+ * const internalSig = ml_dsa44.internal.sign(msg, secretKey);
716
+ * ```
717
+ */
633
718
  export const ml_dsa44 = /* @__PURE__ */ (() => getDilithium({
634
719
  ...PARAMS[2],
635
720
  CRH_BYTES: 64,
@@ -659,4 +744,3 @@ export const ml_dsa87 = /* @__PURE__ */ (() => getDilithium({
659
744
  XOF256,
660
745
  securityLevel: 256,
661
746
  }))();
662
- //# sourceMappingURL=ml-dsa.js.map
package/ml-kem.d.ts CHANGED
@@ -26,20 +26,61 @@ export type KEMParam = {
26
26
  * not a generic security label.
27
27
  */
28
28
  export declare const PARAMS: Record<string, KEMParam>;
29
+ /**
30
+ * Prepared (pre-expanded) ML-KEM public key. Experimental prototype.
31
+ * Caches only public data: packed ek, the expanded matrix Â, decoded t̂ and H(ek). No secret
32
+ * material is retained between calls; secret keys passed to `decapsulate` are decoded and wiped
33
+ * per call, exactly like the one-shot API. `clean()` wipes the expanded Â/t̂ cache; the packed
34
+ * public key and H(ek) are public and are not wiped. The object must not be used afterwards.
35
+ */
36
+ export type KEMPrepared = {
37
+ /**
38
+ * Detached copy of the source public key. Treat as read-only while the prepared object is in use.
39
+ * Callers may wipe it after final use; any mutation invalidates subsequent operations.
40
+ */
41
+ publicKey: Uint8Array;
42
+ /** Same as `KEM.encapsulate`, minus per-call ek re-validation and  re-expansion. */
43
+ encapsulate: (msg?: Uint8Array) => {
44
+ cipherText: Uint8Array;
45
+ sharedSecret: Uint8Array;
46
+ };
47
+ /**
48
+ * Same as `KEM.decapsulate`; throws if `secretKey` does not embed this public key.
49
+ * The embedded-ek byte comparison plus stored-hash comparison is equivalent to the
50
+ * FIPS 203 §7.3 hash input check.
51
+ */
52
+ decapsulate: (cipherText: Uint8Array, secretKey: Uint8Array) => Uint8Array;
53
+ /** Wipe cached (public) data. */
54
+ clean: () => void;
55
+ };
56
+ /** KEM with prepared-key support. */
57
+ export type MLKEM = KEM & {
58
+ prepare: (publicKey: Uint8Array) => KEMPrepared;
59
+ };
29
60
  /**
30
61
  * ML-KEM-512: Table 2 row `k=2, η1=3, η2=2, du=10, dv=4`; Table 3 sizes `800/1632/768/32`.
31
62
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
63
+ * @example
64
+ * Generate deterministic ML-KEM-512 keys, encapsulate a shared secret, and decapsulate it.
65
+ * ```ts
66
+ * import { ml_kem512 } from '@noble/post-quantum/ml-kem.js';
67
+ * const seed = new Uint8Array(ml_kem512.lengths.seed!);
68
+ * const { secretKey, publicKey } = ml_kem512.keygen(seed);
69
+ * const msg = new Uint8Array(ml_kem512.lengths.msgRand!);
70
+ * const { cipherText, sharedSecret } = ml_kem512.encapsulate(publicKey, msg);
71
+ * const recovered = ml_kem512.decapsulate(cipherText, secretKey);
72
+ * const publicKey2 = ml_kem512.getPublicKey(secretKey);
73
+ * ```
32
74
  */
33
- export declare const ml_kem512: TRet<KEM>;
75
+ export declare const ml_kem512: TRet<MLKEM>;
34
76
  /**
35
77
  * ML-KEM-768: Table 2 row `k=3, η1=2, η2=2, du=10, dv=4`; Table 3 sizes `1184/2400/1088/32`.
36
78
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
37
79
  */
38
- export declare const ml_kem768: TRet<KEM>;
80
+ export declare const ml_kem768: TRet<MLKEM>;
39
81
  /**
40
82
  * ML-KEM-1024: Table 2 row `k=4, η1=2, η2=2, du=11, dv=5`; Table 3 sizes `1568/3168/1568/32`.
41
83
  * The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
42
84
  */
43
- export declare const ml_kem1024: TRet<KEM>;
85
+ export declare const ml_kem1024: TRet<MLKEM>;
44
86
  export declare const __tests: any;
45
- //# sourceMappingURL=ml-kem.d.ts.map