@noble/post-quantum 0.5.3 → 0.6.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/ml-dsa.ts CHANGED
@@ -32,13 +32,21 @@ import {
32
32
  type VerOpts,
33
33
  } from './utils.ts';
34
34
 
35
- export type DSAInternalOpts = { externalMu?: boolean };
35
+ /** Internal ML-DSA options. */
36
+ export type DSAInternalOpts = {
37
+ /**
38
+ * Whether `internal.sign` / `internal.verify` receive a caller-supplied 64-byte `mu`
39
+ * instead of the usual FIPS 204 formatted message `M'` / prehash-formatted message.
40
+ * validateInternalOpts() only checks this flag; callers still must supply the right input length.
41
+ */
42
+ externalMu?: boolean;
43
+ };
36
44
  function validateInternalOpts(opts: DSAInternalOpts) {
37
45
  validateOpts(opts);
38
46
  if (opts.externalMu !== undefined) abool(opts.externalMu, 'opts.externalMu');
39
47
  }
40
48
 
41
- /** Signer API, containing internal methods */
49
+ /** ML-DSA signer surface with access to the internal message formatting mode. */
42
50
  export type DSAInternal = CryptoKeys & {
43
51
  lengths: Signer['lengths'];
44
52
  sign: (msg: Uint8Array, secretKey: Uint8Array, opts?: SigOpts & DSAInternalOpts) => Uint8Array;
@@ -49,16 +57,22 @@ export type DSAInternal = CryptoKeys & {
49
57
  opts?: VerOpts & DSAInternalOpts
50
58
  ) => boolean;
51
59
  };
60
+ /** Public ML-DSA signer surface. */
52
61
  export type DSA = Signer & { internal: DSAInternal };
53
62
 
54
63
  // Constants
64
+ // FIPS 204 fixes ML-DSA over R = Z[X]/(X^256 + 1), so every polynomial has 256 coefficients.
55
65
  const N = 256;
56
66
  // 2**23 − 2**13 + 1, 23 bits: multiply will be 46. We have enough precision in JS to avoid bigints
57
67
  const Q = 8380417;
68
+ // FIPS 204 §2.5 / Table 1 fixes zeta = 1753 as the 512th root of unity used by ML-DSA's NTT.
58
69
  const ROOT_OF_UNITY = 1753;
59
70
  // f = 256**−1 mod q, pow(256, -1, q) = 8347681 (python3)
60
71
  const F = 8347681;
72
+ // FIPS 204 Table 1 / §7.4 fixes d = 13 dropped low bits for Power2Round on t.
61
73
  const D = 13;
74
+ // FIPS 204 Table 1 fixes gamma2 to (q-1)/88 for ML-DSA-44 and (q-1)/32 for ML-DSA-65/87;
75
+ // §7.4 then uses alpha = 2*gamma2 for Decompose / MakeHint / UseHint.
62
76
  // Dilithium is kinda parametrized over GAMMA2, but everything will break with any other value.
63
77
  const GAMMA2_1 = Math.floor((Q - 1) / 88) | 0;
64
78
  const GAMMA2_2 = Math.floor((Q - 1) / 32) | 0;
@@ -66,29 +80,45 @@ const GAMMA2_2 = Math.floor((Q - 1) / 32) | 0;
66
80
  type XofGet = ReturnType<ReturnType<XOF>['get']>;
67
81
 
68
82
  /** Various lattice params. */
83
+ /** Public ML-DSA parameter-set description. */
69
84
  export type DSAParam = {
85
+ /** Matrix row count. */
70
86
  K: number;
87
+ /** Matrix column count. */
71
88
  L: number;
89
+ /** Bit width used when rounding `t`. */
72
90
  D: number;
91
+ /** Bound used for the `y` sampling range. */
73
92
  GAMMA1: number;
93
+ /** Bound used during decomposition and hints. */
74
94
  GAMMA2: number;
95
+ /** Number of non-zero challenge coefficients. */
75
96
  TAU: number;
97
+ /** Centered-binomial noise parameter. */
76
98
  ETA: number;
99
+ /** Maximum number of hint bits in a signature. */
77
100
  OMEGA: number;
78
101
  };
79
102
  /** Internal params for different versions of ML-DSA */
80
103
  // prettier-ignore
81
- export const PARAMS: Record<string, DSAParam> = {
104
+ /** Built-in ML-DSA parameter presets keyed by security categories `2/3/5`
105
+ * for `ml_dsa44` / `ml_dsa65` / `ml_dsa87`.
106
+ * This is only the Table 1 subset used directly here: `BETA = TAU * ETA` is derived later,
107
+ * while `C_TILDE_BYTES`, `TR_BYTES`, `CRH_BYTES`, and `securityLevel` live in the preset wrappers.
108
+ */
109
+ export const PARAMS: Record<string, DSAParam> = /* @__PURE__ */ (() => ({
82
110
  2: { K: 4, L: 4, D, GAMMA1: 2 ** 17, GAMMA2: GAMMA2_1, TAU: 39, ETA: 2, OMEGA: 80 },
83
111
  3: { K: 6, L: 5, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 49, ETA: 4, OMEGA: 55 },
84
112
  5: { K: 8, L: 7, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 60, ETA: 2, OMEGA: 75 },
85
- } as const;
113
+ } as const))();
86
114
 
87
115
  // NOTE: there is a lot cases where negative numbers used (with smod instead of mod).
88
116
  type Poly = Int32Array;
89
117
  const newPoly = (n: number): Int32Array => new Int32Array(n);
90
118
 
91
- const { mod, smod, NTT, bitsCoder } = genCrystals({
119
+ // Shared CRYSTALS helper in the ML-DSA branch: non-Kyber mode, 8-bit bit-reversal,
120
+ // and Int32Array polys because ordinary-form coefficients can be negative / centered.
121
+ const crystals = /* @__PURE__ */ genCrystals({
92
122
  N,
93
123
  Q,
94
124
  F,
@@ -101,51 +131,59 @@ const { mod, smod, NTT, bitsCoder } = genCrystals({
101
131
  const id = <T>(n: T): T => n;
102
132
  type IdNum = (n: number) => number;
103
133
 
134
+ // compress()/verify() must be compatible in both directions:
135
+ // wrap the shared d-bit packer with the FIPS 204 SimpleBitPack / BitPack coefficient maps.
136
+ // malformed-input rejection only happens through the optional verify hook.
104
137
  const polyCoder = (d: number, compress: IdNum = id, verify: IdNum = id) =>
105
- bitsCoder(d, {
138
+ crystals.bitsCoder(d, {
106
139
  encode: (i: number) => compress(verify(i)),
107
140
  decode: (i: number) => verify(compress(i)),
108
141
  });
109
142
 
143
+ // Mutates `a` in place; callers must pass same-length polynomials.
110
144
  const polyAdd = (a: Poly, b: Poly) => {
111
- for (let i = 0; i < a.length; i++) a[i] = mod(a[i] + b[i]);
145
+ for (let i = 0; i < a.length; i++) a[i] = crystals.mod(a[i] + b[i]);
112
146
  return a;
113
147
  };
148
+ // Mutates `a` in place; callers must pass same-length polynomials.
114
149
  const polySub = (a: Poly, b: Poly): Poly => {
115
- for (let i = 0; i < a.length; i++) a[i] = mod(a[i] - b[i]);
150
+ for (let i = 0; i < a.length; i++) a[i] = crystals.mod(a[i] - b[i]);
116
151
  return a;
117
152
  };
118
153
 
154
+ // Mutates `p` in place and assumes it is a decoded `t1`-range polynomial.
119
155
  const polyShiftl = (p: Poly): Poly => {
120
156
  for (let i = 0; i < N; i++) p[i] <<= D;
121
157
  return p;
122
158
  };
123
159
 
124
160
  const polyChknorm = (p: Poly, B: number): boolean => {
125
- // Not very sure about this, but FIPS204 doesn't provide any function for that :(
126
- for (let i = 0; i < N; i++) if (Math.abs(smod(p[i])) >= B) return true;
161
+ // FIPS 204 Algorithms 7 and 8 express the same centered-norm check with explicit inequalities.
162
+ for (let i = 0; i < N; i++) if (Math.abs(crystals.smod(p[i])) >= B) return true;
127
163
  return false;
128
164
  };
129
165
 
166
+ // Both inputs must already be in NTT / `T_q` form.
130
167
  const MultiplyNTTs = (a: Poly, b: Poly): Poly => {
131
168
  // NOTE: we don't use montgomery reduction in code, since it requires 64 bit ints,
132
169
  // which is not available in JS. mod(a[i] * b[i]) is ok, since Q is 23 bit,
133
170
  // which means a[i] * b[i] is 46 bit, which is safe to use in JS. (number is 53 bits).
134
171
  // Barrett reduction is slower than mod :(
135
172
  const c = newPoly(N);
136
- for (let i = 0; i < a.length; i++) c[i] = mod(a[i] * b[i]);
173
+ for (let i = 0; i < a.length; i++) c[i] = crystals.mod(a[i] * b[i]);
137
174
  return c;
138
175
  };
139
176
 
140
177
  // Return poly in NTT representation
141
178
  function RejNTTPoly(xof: XofGet) {
142
- // Samples a polynomial ∈ Tq.
179
+ // Samples a polynomial ∈ Tq. xof() must return byte lengths divisible by 3.
143
180
  const r = newPoly(N);
144
181
  // NOTE: we can represent 3xu24 as 4xu32, but it doesn't improve perf :(
145
182
  for (let j = 0; j < N; ) {
146
183
  const b = xof();
147
184
  if (b.length % 3) throw new Error('RejNTTPoly: unaligned block');
148
185
  for (let i = 0; j < N && i <= b.length - 3; i += 3) {
186
+ // FIPS 204 Algorithm 14 clears the top bit of b2 before forming the 23-bit candidate.
149
187
  const t = (b[i + 0] | (b[i + 1] << 8) | (b[i + 2] << 16)) & 0x7fffff; // 3 bytes
150
188
  if (t < Q) r[j++] = t;
151
189
  }
@@ -169,6 +207,8 @@ type DilithiumOpts = {
169
207
  securityLevel: number;
170
208
  };
171
209
 
210
+ // Instantiate one ML-DSA parameter set from the Table 1 lattice constants plus the
211
+ // Table 2 byte lengths / hash-width choices used by the public wrappers below.
172
212
  function getDilithium(opts: DilithiumOpts) {
173
213
  const { K, L, GAMMA1, GAMMA2, TAU, ETA, OMEGA } = opts;
174
214
  const { CRH_BYTES, TR_BYTES, C_TILDE_BYTES, XOF128, XOF256, securityLevel } = opts;
@@ -180,8 +220,9 @@ function getDilithium(opts: DilithiumOpts) {
180
220
 
181
221
  const decompose = (r: number) => {
182
222
  // Decomposes r into (r1, r0) such that r ≡ r1(2γ2) + r0 mod q.
183
- const rPlus = mod(r);
184
- const r0 = smod(rPlus, 2 * GAMMA2) | 0;
223
+ const rPlus = crystals.mod(r);
224
+ const r0 = crystals.smod(rPlus, 2 * GAMMA2) | 0;
225
+ // FIPS 204 Algorithm 36 folds the top bucket `q-1` back to `(r1, r0) = (0, r0-1)`.
185
226
  if (rPlus - r0 === Q - 1) return { r1: 0 | 0, r0: (r0 - 1) | 0 };
186
227
  const r1 = Math.floor((rPlus - r0) / (2 * GAMMA2)) | 0;
187
228
  return { r1, r0 }; // r1 = HighBits, r0 = LowBits
@@ -191,6 +232,10 @@ function getDilithium(opts: DilithiumOpts) {
191
232
  const LowBits = (r: number) => decompose(r).r0;
192
233
  const MakeHint = (z: number, r: number) => {
193
234
  // Compute hint bit indicating whether adding z to r alters the high bits of r.
235
+ // FIPS 204 §6.2 also permits the Section 5.1 alternative from [6], which uses the
236
+ // transformed low-bits/high-bits state at this call site instead of Algorithm 39 literally.
237
+ // This optimized predicate only applies to those transformed Section 5.1 inputs; it is
238
+ // not a drop-in replacement for Algorithm 39 on arbitrary `(z, r)` pairs.
194
239
 
195
240
  // From dilithium code
196
241
  const res0 = z <= GAMMA2 || z > Q - GAMMA2 || (z === Q - GAMMA2 && r === 0) ? 0 : 1;
@@ -201,8 +246,9 @@ function getDilithium(opts: DilithiumOpts) {
201
246
  // But they return different results! However, decompose is same.
202
247
  // So, either there is a bug in Dilithium ref implementation or in FIPS204.
203
248
  // For now, lets use dilithium one, so test vectors can be passed.
204
- // See
205
- // https://github.com/GiacomoPope/dilithium-py?tab=readme-ov-file#optimising-decomposition-and-making-hints
249
+ // The round-3 Dilithium / ML-DSA code uses the same low-bits / high-bits convention after
250
+ // `r0 += ct0`.
251
+ // See dilithium-py README section "Optimising decomposition and making hints".
206
252
  return res0;
207
253
  };
208
254
 
@@ -212,13 +258,13 @@ function getDilithium(opts: DilithiumOpts) {
212
258
  const { r1, r0 } = decompose(r);
213
259
  // 3: if h = 1 and r0 > 0 return (r1 + 1) mod m
214
260
  // 4: if h = 1 and r0 ≤ 0 return (r1 − 1) mod m
215
- if (h === 1) return r0 > 0 ? mod(r1 + 1, m) | 0 : mod(r1 - 1, m) | 0;
261
+ if (h === 1) return r0 > 0 ? crystals.mod(r1 + 1, m) | 0 : crystals.mod(r1 - 1, m) | 0;
216
262
  return r1 | 0;
217
263
  };
218
264
  const Power2Round = (r: number) => {
219
265
  // Decomposes r into (r1, r0) such that r ≡ r1*(2**d) + r0 mod q.
220
- const rPlus = mod(r);
221
- const r0 = smod(rPlus, 2 ** D) | 0;
266
+ const rPlus = crystals.mod(r);
267
+ const r0 = crystals.smod(rPlus, 2 ** D) | 0;
222
268
  return { r1: Math.floor((rPlus - r0) / 2 ** D) | 0, r0 };
223
269
  };
224
270
 
@@ -263,7 +309,7 @@ function getDilithium(opts: DilithiumOpts) {
263
309
  const T0Coder = polyCoder(13, (i: number) => (1 << (D - 1)) - i);
264
310
  const T1Coder = polyCoder(10);
265
311
  // Requires smod. Need to fix!
266
- const ZCoder = polyCoder(GAMMA1 === 1 << 17 ? 18 : 20, (i: number) => smod(GAMMA1 - i));
312
+ const ZCoder = polyCoder(GAMMA1 === 1 << 17 ? 18 : 20, (i: number) => crystals.smod(GAMMA1 - i));
267
313
  const W1Coder = polyCoder(GAMMA2 === GAMMA2_1 ? 6 : 4);
268
314
  const W1Vec = vecCoder(W1Coder, K);
269
315
  // Main structures
@@ -283,7 +329,9 @@ function getDilithium(opts: DilithiumOpts) {
283
329
  ? (n: number) => (n < 15 ? 2 - (n % 5) : false)
284
330
  : (n: number) => (n < 9 ? 4 - n : false);
285
331
 
286
- // Return poly in NTT representation
332
+ // Return poly in ordinary representation.
333
+ // This helper returns ordinary-form `[-ETA, ETA]` coefficients for ExpandS; callers apply
334
+ // `NTT.encode()` later when needed.
287
335
  function RejBoundedPoly(xof: XofGet) {
288
336
  // Samples an element a ∈ Rq with coeffcients in [−η, η] computed via rejection sampling from ρ.
289
337
  const r: Poly = newPoly(N);
@@ -306,6 +354,8 @@ function getDilithium(opts: DilithiumOpts) {
306
354
  const s = shake256.create({}).update(seed);
307
355
  const buf = new Uint8Array(shake256.blockLen);
308
356
  s.xofInto(buf);
357
+ // FIPS 204 Algorithm 29 uses the first 8 squeezed bytes as the 64 sign bits `h`,
358
+ // then rejection-samples coefficient positions from the remaining XOF stream.
309
359
  const masks = buf.slice(0, 8);
310
360
  for (let i = N - TAU, pos = 8, maskPos = 0, maskBit = 0; i < N; i++) {
311
361
  let b = i + 1;
@@ -336,6 +386,8 @@ function getDilithium(opts: DilithiumOpts) {
336
386
  return { r0: res0, r1: res1 };
337
387
  };
338
388
  const polyUseHint = (u: Poly, h: Poly): Poly => {
389
+ // In-place on `u`: verification only needs the recovered high bits, so reuse the
390
+ // temporary `wApprox` buffer instead of allocating another polynomial.
339
391
  for (let i = 0; i < N; i++) u[i] = UseHint(h[i], u[i]);
340
392
  return u;
341
393
  };
@@ -381,7 +433,7 @@ function getDilithium(opts: DilithiumOpts) {
381
433
  const s2 = [];
382
434
  for (let i = L; i < L + K; i++)
383
435
  s2.push(RejBoundedPoly(xofPrime.get(i & 0xff, (i >> 8) & 0xff)));
384
- const s1Hat = s1.map((i) => NTT.encode(i.slice()));
436
+ const s1Hat = s1.map((i) => crystals.NTT.encode(i.slice()));
385
437
  const t0 = [];
386
438
  const t1 = [];
387
439
  const xof = XOF128(rho);
@@ -393,26 +445,30 @@ function getDilithium(opts: DilithiumOpts) {
393
445
  const aij = RejNTTPoly(xof.get(j, i)); // super slow!
394
446
  polyAdd(t, MultiplyNTTs(aij, s1Hat[j]));
395
447
  }
396
- NTT.decode(t);
448
+ crystals.NTT.decode(t);
397
449
  const { r0, r1 } = polyPowerRound(polyAdd(t, s2[i])); // (t1, t0) ← Power2Round(t, d)
398
450
  t0.push(r0);
399
451
  t1.push(r1);
400
452
  }
401
453
  const publicKey = publicCoder.encode([rho, t1]); // pk ← pkEncode(ρ, t1)
402
454
  const tr = shake256(publicKey, { dkLen: TR_BYTES }); // tr ← H(BytesToBits(pk), 512)
403
- const secretKey = secretCoder.encode([rho, K_, tr, s1, s2, t0]); // sk ← skEncode(ρ, K,tr, s1, s2, t0)
455
+ // sk ← skEncode(ρ, K,tr, s1, s2, t0)
456
+ const secretKey = secretCoder.encode([rho, K_, tr, s1, s2, t0]);
404
457
  xof.clean();
405
458
  xofPrime.clean();
406
459
  // STATS
407
- // Kyber512: { calls: 4, xofs: 12 }, Kyber768: { calls: 9, xofs: 27 }, Kyber1024: { calls: 16, xofs: 48 }
408
- // DSA44: { calls: 24, xofs: 24 }, DSA65: { calls: 41, xofs: 41 }, DSA87: { calls: 71, xofs: 71 }
460
+ // Kyber512: { calls: 4, xofs: 12 }, Kyber768: { calls: 9, xofs: 27 },
461
+ // Kyber1024: { calls: 16, xofs: 48 }
462
+ // DSA44: { calls: 24, xofs: 24 }, DSA65: { calls: 41, xofs: 41 },
463
+ // DSA87: { calls: 71, xofs: 71 }
409
464
  cleanBytes(rho, rhoPrime, K_, s1, s2, s1Hat, t, t0, t1, tr, seedDst);
410
465
  return { publicKey, secretKey };
411
466
  },
412
467
  getPublicKey: (secretKey: Uint8Array) => {
413
- const [rho, _K, _tr, s1, s2, _t0] = secretCoder.decode(secretKey); // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
468
+ // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
469
+ const [rho, _K, _tr, s1, s2, _t0] = secretCoder.decode(secretKey);
414
470
  const xof = XOF128(rho);
415
- const s1Hat = s1.map((p) => NTT.encode(p.slice()));
471
+ const s1Hat = s1.map((p) => crystals.NTT.encode(p.slice()));
416
472
  const t1: Poly[] = [];
417
473
  const tmp = newPoly(N);
418
474
  for (let i = 0; i < K; i++) {
@@ -421,7 +477,7 @@ function getDilithium(opts: DilithiumOpts) {
421
477
  const aij = RejNTTPoly(xof.get(j, i)); // A_ij in NTT
422
478
  polyAdd(tmp, MultiplyNTTs(aij, s1Hat[j])); // += A_ij * s1_j
423
479
  }
424
- NTT.decode(tmp); // NTT⁻¹
480
+ crystals.NTT.decode(tmp); // NTT⁻¹
425
481
  polyAdd(tmp, s2[i]); // t_i = A·s1 + s2
426
482
  const { r1 } = polyPowerRound(tmp); // r1 = t1, r0 ≈ t0
427
483
  t1.push(r1);
@@ -437,7 +493,8 @@ function getDilithium(opts: DilithiumOpts) {
437
493
  let { extraEntropy: random, externalMu = false } = opts;
438
494
  // This part can be pre-cached per secretKey, but there is only minor performance improvement,
439
495
  // since we re-use a lot of variables to computation.
440
- const [rho, _K, tr, s1, s2, t0] = secretCoder.decode(secretKey); // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
496
+ // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
497
+ const [rho, _K, tr, s1, s2, t0] = secretCoder.decode(secretKey);
441
498
  // Cache matrix to avoid re-compute later
442
499
  const A: Poly[][] = []; // A ← ExpandA(ρ)
443
500
  const xof = XOF128(rho);
@@ -447,15 +504,17 @@ function getDilithium(opts: DilithiumOpts) {
447
504
  A.push(pv);
448
505
  }
449
506
  xof.clean();
450
- for (let i = 0; i < L; i++) NTT.encode(s1[i]); // sˆ1 ← NTT(s1)
507
+ for (let i = 0; i < L; i++) crystals.NTT.encode(s1[i]); // sˆ1 ← NTT(s1)
451
508
  for (let i = 0; i < K; i++) {
452
- NTT.encode(s2[i]); // sˆ2 ← NTT(s2)
453
- NTT.encode(t0[i]); // tˆ0 ← NTT(t0)
509
+ crystals.NTT.encode(s2[i]); // sˆ2 ← NTT(s2)
510
+ crystals.NTT.encode(t0[i]); // tˆ0 ← NTT(t0)
454
511
  }
455
512
  // This part is per msg
456
513
  const mu = externalMu
457
514
  ? msg
458
- : shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest(); // 6: µ ← H(tr||M, 512) ▷ Compute message representative µ
515
+ : // 6: µ ← H(tr||M, 512)
516
+ // ▷ Compute message representative µ
517
+ shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest();
459
518
 
460
519
  // Compute private random seed
461
520
  const rnd =
@@ -480,13 +539,13 @@ function getDilithium(opts: DilithiumOpts) {
480
539
  // y ← ExpandMask(ρ , κ)
481
540
  for (let i = 0; i < L; i++, kappa++)
482
541
  y.push(ZCoder.decode(x256.get(kappa & 0xff, kappa >> 8)()));
483
- const z = y.map((i) => NTT.encode(i.slice()));
542
+ const z = y.map((i) => crystals.NTT.encode(i.slice()));
484
543
  const w = [];
485
544
  for (let i = 0; i < K; i++) {
486
545
  // w ← NTT−1(A ◦ NTT(y))
487
546
  const wi = newPoly(N);
488
547
  for (let j = 0; j < L; j++) polyAdd(wi, MultiplyNTTs(A[i][j], z[j]));
489
- NTT.decode(wi);
548
+ crystals.NTT.decode(wi);
490
549
  w.push(wi);
491
550
  }
492
551
  const w1 = w.map((j) => j.map(HighBits)); // w1 ← HighBits(w)
@@ -497,21 +556,22 @@ function getDilithium(opts: DilithiumOpts) {
497
556
  .update(W1Vec.encode(w1))
498
557
  .digest();
499
558
  // Verifer’s challenge
500
- const cHat = NTT.encode(SampleInBall(cTilde)); // c ← SampleInBall(c˜1); cˆ ← NTT(c)
559
+ // c ← SampleInBall(c˜1); cˆ ← NTT(c)
560
+ const cHat = crystals.NTT.encode(SampleInBall(cTilde));
501
561
  // ⟨⟨cs1⟩⟩ ← NTT−1(cˆ◦ sˆ1)
502
562
  const cs1 = s1.map((i) => MultiplyNTTs(i, cHat));
503
563
  for (let i = 0; i < L; i++) {
504
- polyAdd(NTT.decode(cs1[i]), y[i]); // z ← y + ⟨⟨cs1⟩⟩
564
+ polyAdd(crystals.NTT.decode(cs1[i]), y[i]); // z ← y + ⟨⟨cs1⟩⟩
505
565
  if (polyChknorm(cs1[i], GAMMA1 - BETA)) continue main_loop; // ||z||∞ ≥ γ1 − β
506
566
  }
507
567
  // cs1 is now z (▷ Signer’s response)
508
568
  let cnt = 0;
509
569
  const h = [];
510
570
  for (let i = 0; i < K; i++) {
511
- const cs2 = NTT.decode(MultiplyNTTs(s2[i], cHat)); // ⟨⟨cs2⟩⟩ ← NTT−1(cˆ◦ sˆ2)
571
+ const cs2 = crystals.NTT.decode(MultiplyNTTs(s2[i], cHat)); // ⟨⟨cs2⟩⟩ ← NTT−1(cˆ◦ sˆ2)
512
572
  const r0 = polySub(w[i], cs2).map(LowBits); // r0 ← LowBits(w − ⟨⟨cs2⟩⟩)
513
573
  if (polyChknorm(r0, GAMMA2 - BETA)) continue main_loop; // ||r0||∞ ≥ γ2 − β
514
- const ct0 = NTT.decode(MultiplyNTTs(t0[i], cHat)); // ⟨⟨ct0⟩⟩ ← NTT−1(cˆ◦ tˆ0)
574
+ const ct0 = crystals.NTT.decode(MultiplyNTTs(t0[i], cHat)); // ⟨⟨ct0⟩⟩ ← NTT−1(cˆ◦ tˆ0)
515
575
  if (polyChknorm(ct0, GAMMA2)) continue main_loop;
516
576
  polyAdd(r0, ct0);
517
577
  // ▷ Signer’s hint
@@ -523,7 +583,11 @@ function getDilithium(opts: DilithiumOpts) {
523
583
  x256.clean();
524
584
  const res = sigCoder.encode([cTilde, cs1, h]); // σ ← sigEncode(c˜, z mod±q, h)
525
585
  // rho, _K, tr is subarray of secretKey, cannot clean.
526
- cleanBytes(cTilde, cs1, h, cHat, w1, w, z, y, rhoprime, mu, s1, s2, t0, ...A);
586
+ cleanBytes(cTilde, cs1, h, cHat, w1, w, z, y, rhoprime, s1, s2, t0, ...A);
587
+ // `externalMu` hands ownership of `mu` to the caller,
588
+ // so only wipe the internally derived digest form here;
589
+ // zeroizing caller memory would break the caller's own reuse / verify path.
590
+ if (!externalMu) cleanBytes(mu);
527
591
  return res;
528
592
  }
529
593
  // @ts-ignore
@@ -542,27 +606,30 @@ function getDilithium(opts: DilithiumOpts) {
542
606
  const tr = shake256(publicKey, { dkLen: TR_BYTES }); // 6: tr ← H(BytesToBits(pk), 512)
543
607
 
544
608
  if (sig.length !== sigCoder.bytesLen) return false; // return false instead of exception
545
- const [cTilde, z, h] = sigCoder.decode(sig); // (c˜, z, h) ← sigDecode(σ), ▷ Signer’s commitment hash c ˜, response z and hint
609
+ // (c˜, z, h) ← sigDecode(σ)
610
+ // ▷ Signer’s commitment hash c ˜, response z and hint
611
+ const [cTilde, z, h] = sigCoder.decode(sig);
546
612
  if (h === false) return false; // if h = ⊥ then return false
547
613
  for (let i = 0; i < L; i++) if (polyChknorm(z[i], GAMMA1 - BETA)) return false;
548
614
  const mu = externalMu
549
615
  ? msg
550
- : shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest(); // 7: µ ← H(tr||M, 512)
616
+ : // 7: µ ← H(tr||M, 512)
617
+ shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest();
551
618
  // Compute verifer’s challenge from c˜
552
- const c = NTT.encode(SampleInBall(cTilde)); // c ← SampleInBall(c˜1)
619
+ const c = crystals.NTT.encode(SampleInBall(cTilde)); // c ← SampleInBall(c˜1)
553
620
  const zNtt = z.map((i) => i.slice()); // zNtt = NTT(z)
554
- for (let i = 0; i < L; i++) NTT.encode(zNtt[i]);
621
+ for (let i = 0; i < L; i++) crystals.NTT.encode(zNtt[i]);
555
622
  const wTick1 = [];
556
623
  const xof = XOF128(rho);
557
624
  for (let i = 0; i < K; i++) {
558
- const ct12d = MultiplyNTTs(NTT.encode(polyShiftl(t1[i])), c); //c * t1 * (2**d)
625
+ const ct12d = MultiplyNTTs(crystals.NTT.encode(polyShiftl(t1[i])), c); //c * t1 * (2**d)
559
626
  const Az = newPoly(N); // // A * z
560
627
  for (let j = 0; j < L; j++) {
561
628
  const aij = RejNTTPoly(xof.get(j, i)); // A[i][j] inplace
562
629
  polyAdd(Az, MultiplyNTTs(aij, zNtt[j]));
563
630
  }
564
631
  // wApprox = A*z - c*t1 * (2**d)
565
- const wApprox = NTT.decode(polySub(Az, ct12d));
632
+ const wApprox = crystals.NTT.decode(polySub(Az, ct12d));
566
633
  // Reconstruction of signer’s commitment
567
634
  wTick1.push(polyUseHint(wApprox, h[i])); // w ′ ← UseHint(h, w'approx )
568
635
  }
@@ -626,34 +693,37 @@ function getDilithium(opts: DilithiumOpts) {
626
693
  }
627
694
 
628
695
  /** ML-DSA-44 for 128-bit security level. Not recommended after 2030, as per ASD. */
629
- export const ml_dsa44: DSA = /* @__PURE__ */ getDilithium({
630
- ...PARAMS[2],
631
- CRH_BYTES: 64,
632
- TR_BYTES: 64,
633
- C_TILDE_BYTES: 32,
634
- XOF128,
635
- XOF256,
636
- securityLevel: 128,
637
- });
696
+ export const ml_dsa44: DSA = /* @__PURE__ */ (() =>
697
+ getDilithium({
698
+ ...PARAMS[2],
699
+ CRH_BYTES: 64,
700
+ TR_BYTES: 64,
701
+ C_TILDE_BYTES: 32,
702
+ XOF128,
703
+ XOF256,
704
+ securityLevel: 128,
705
+ }))();
638
706
 
639
707
  /** ML-DSA-65 for 192-bit security level. Not recommended after 2030, as per ASD. */
640
- export const ml_dsa65: DSA = /* @__PURE__ */ getDilithium({
641
- ...PARAMS[3],
642
- CRH_BYTES: 64,
643
- TR_BYTES: 64,
644
- C_TILDE_BYTES: 48,
645
- XOF128,
646
- XOF256,
647
- securityLevel: 192,
648
- });
708
+ export const ml_dsa65: DSA = /* @__PURE__ */ (() =>
709
+ getDilithium({
710
+ ...PARAMS[3],
711
+ CRH_BYTES: 64,
712
+ TR_BYTES: 64,
713
+ C_TILDE_BYTES: 48,
714
+ XOF128,
715
+ XOF256,
716
+ securityLevel: 192,
717
+ }))();
649
718
 
650
719
  /** ML-DSA-87 for 256-bit security level. OK after 2030, as per ASD. */
651
- export const ml_dsa87: DSA = /* @__PURE__ */ getDilithium({
652
- ...PARAMS[5],
653
- CRH_BYTES: 64,
654
- TR_BYTES: 64,
655
- C_TILDE_BYTES: 64,
656
- XOF128,
657
- XOF256,
658
- securityLevel: 256,
659
- });
720
+ export const ml_dsa87: DSA = /* @__PURE__ */ (() =>
721
+ getDilithium({
722
+ ...PARAMS[5],
723
+ CRH_BYTES: 64,
724
+ TR_BYTES: 64,
725
+ C_TILDE_BYTES: 64,
726
+ XOF128,
727
+ XOF256,
728
+ securityLevel: 256,
729
+ }))();