@noble/post-quantum 0.5.4 → 0.6.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
@@ -18,25 +18,43 @@ function validateInternalOpts(opts) {
18
18
  abool(opts.externalMu, 'opts.externalMu');
19
19
  }
20
20
  // Constants
21
+ // FIPS 204 fixes ML-DSA over R = Z[X]/(X^256 + 1), so every polynomial has 256 coefficients.
21
22
  const N = 256;
22
23
  // 2**23 − 2**13 + 1, 23 bits: multiply will be 46. We have enough precision in JS to avoid bigints
23
24
  const Q = 8380417;
25
+ // FIPS 204 §2.5 / Table 1 fixes zeta = 1753 as the 512th root of unity used by ML-DSA's NTT.
24
26
  const ROOT_OF_UNITY = 1753;
25
27
  // f = 256**−1 mod q, pow(256, -1, q) = 8347681 (python3)
26
28
  const F = 8347681;
29
+ // FIPS 204 Table 1 / §7.4 fixes d = 13 dropped low bits for Power2Round on t.
27
30
  const D = 13;
31
+ // FIPS 204 Table 1 fixes gamma2 to (q-1)/88 for ML-DSA-44 and (q-1)/32 for ML-DSA-65/87;
32
+ // §7.4 then uses alpha = 2*gamma2 for Decompose / MakeHint / UseHint.
28
33
  // Dilithium is kinda parametrized over GAMMA2, but everything will break with any other value.
29
34
  const GAMMA2_1 = Math.floor((Q - 1) / 88) | 0;
30
35
  const GAMMA2_2 = Math.floor((Q - 1) / 32) | 0;
31
36
  /** Internal params for different versions of ML-DSA */
32
37
  // prettier-ignore
33
- export const PARAMS = {
34
- 2: { K: 4, L: 4, D, GAMMA1: 2 ** 17, GAMMA2: GAMMA2_1, TAU: 39, ETA: 2, OMEGA: 80 },
35
- 3: { K: 6, L: 5, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 49, ETA: 4, OMEGA: 55 },
36
- 5: { K: 8, L: 7, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 60, ETA: 2, OMEGA: 75 },
37
- };
38
+ /** Built-in ML-DSA parameter presets keyed by security categories `2/3/5`
39
+ * for `ml_dsa44` / `ml_dsa65` / `ml_dsa87`.
40
+ * This is only the Table 1 subset used directly here: `BETA = TAU * ETA` is derived later,
41
+ * while `C_TILDE_BYTES`, `TR_BYTES`, `CRH_BYTES`, and `securityLevel` live in the preset wrappers.
42
+ */
43
+ export const PARAMS = /* @__PURE__ */ (() => Object.freeze({
44
+ 2: Object.freeze({
45
+ K: 4, L: 4, D, GAMMA1: 2 ** 17, GAMMA2: GAMMA2_1, TAU: 39, ETA: 2, OMEGA: 80
46
+ }),
47
+ 3: Object.freeze({
48
+ K: 6, L: 5, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 49, ETA: 4, OMEGA: 55
49
+ }),
50
+ 5: Object.freeze({
51
+ K: 8, L: 7, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 60, ETA: 2, OMEGA: 75
52
+ }),
53
+ }))();
38
54
  const newPoly = (n) => new Int32Array(n);
39
- const { mod, smod, NTT, bitsCoder } = genCrystals({
55
+ // Shared CRYSTALS helper in the ML-DSA branch: non-Kyber mode, 8-bit bit-reversal,
56
+ // and Int32Array polys because ordinary-form coefficients can be negative / centered.
57
+ const crystals = /* @__PURE__ */ genCrystals({
40
58
  N,
41
59
  Q,
42
60
  F,
@@ -46,45 +64,61 @@ const { mod, smod, NTT, bitsCoder } = genCrystals({
46
64
  brvBits: 8,
47
65
  });
48
66
  const id = (n) => n;
49
- const polyCoder = (d, compress = id, verify = id) => bitsCoder(d, {
67
+ // compress()/verify() must be compatible in both directions:
68
+ // wrap the shared d-bit packer with the FIPS 204 SimpleBitPack / BitPack coefficient maps.
69
+ // malformed-input rejection only happens through the optional verify hook.
70
+ const polyCoder = (d, compress = id, verify = id) => crystals.bitsCoder(d, {
50
71
  encode: (i) => compress(verify(i)),
51
72
  decode: (i) => verify(compress(i)),
52
73
  });
53
- const polyAdd = (a, b) => {
74
+ // Mutates `a` in place; callers must pass same-length polynomials.
75
+ const polyAdd = (a_, b_) => {
76
+ const a = a_;
77
+ const b = b_;
54
78
  for (let i = 0; i < a.length; i++)
55
- a[i] = mod(a[i] + b[i]);
79
+ a[i] = crystals.mod(a[i] + b[i]);
56
80
  return a;
57
81
  };
58
- const polySub = (a, b) => {
82
+ // Mutates `a` in place; callers must pass same-length polynomials.
83
+ const polySub = (a_, b_) => {
84
+ const a = a_;
85
+ const b = b_;
59
86
  for (let i = 0; i < a.length; i++)
60
- a[i] = mod(a[i] - b[i]);
87
+ a[i] = crystals.mod(a[i] - b[i]);
61
88
  return a;
62
89
  };
63
- const polyShiftl = (p) => {
90
+ // Mutates `p` in place and assumes it is a decoded `t1`-range polynomial.
91
+ const polyShiftl = (p_) => {
92
+ const p = p_;
64
93
  for (let i = 0; i < N; i++)
65
94
  p[i] <<= D;
66
95
  return p;
67
96
  };
68
- const polyChknorm = (p, B) => {
69
- // Not very sure about this, but FIPS204 doesn't provide any function for that :(
97
+ const polyChknorm = (p_, B) => {
98
+ const p = p_;
99
+ // FIPS 204 Algorithms 7 and 8 express the same centered-norm check with explicit inequalities.
70
100
  for (let i = 0; i < N; i++)
71
- if (Math.abs(smod(p[i])) >= B)
101
+ if (Math.abs(crystals.smod(p[i])) >= B)
72
102
  return true;
73
103
  return false;
74
104
  };
75
- const MultiplyNTTs = (a, b) => {
105
+ // Both inputs must already be in NTT / `T_q` form.
106
+ const MultiplyNTTs = (a_, b_) => {
107
+ const a = a_;
108
+ const b = b_;
76
109
  // NOTE: we don't use montgomery reduction in code, since it requires 64 bit ints,
77
110
  // which is not available in JS. mod(a[i] * b[i]) is ok, since Q is 23 bit,
78
111
  // which means a[i] * b[i] is 46 bit, which is safe to use in JS. (number is 53 bits).
79
112
  // Barrett reduction is slower than mod :(
80
113
  const c = newPoly(N);
81
114
  for (let i = 0; i < a.length; i++)
82
- c[i] = mod(a[i] * b[i]);
115
+ c[i] = crystals.mod(a[i] * b[i]);
83
116
  return c;
84
117
  };
85
118
  // Return poly in NTT representation
86
- function RejNTTPoly(xof) {
87
- // Samples a polynomial ∈ Tq.
119
+ function RejNTTPoly(xof_) {
120
+ const xof = xof_;
121
+ // Samples a polynomial ∈ Tq. xof() must return byte lengths divisible by 3.
88
122
  const r = newPoly(N);
89
123
  // NOTE: we can represent 3xu24 as 4xu32, but it doesn't improve perf :(
90
124
  for (let j = 0; j < N;) {
@@ -92,6 +126,7 @@ function RejNTTPoly(xof) {
92
126
  if (b.length % 3)
93
127
  throw new Error('RejNTTPoly: unaligned block');
94
128
  for (let i = 0; j < N && i <= b.length - 3; i += 3) {
129
+ // FIPS 204 Algorithm 14 clears the top bit of b2 before forming the 23-bit candidate.
95
130
  const t = (b[i + 0] | (b[i + 1] << 8) | (b[i + 2] << 16)) & 0x7fffff; // 3 bytes
96
131
  if (t < Q)
97
132
  r[j++] = t;
@@ -99,7 +134,10 @@ function RejNTTPoly(xof) {
99
134
  }
100
135
  return r;
101
136
  }
102
- function getDilithium(opts) {
137
+ // Instantiate one ML-DSA parameter set from the Table 1 lattice constants plus the
138
+ // Table 2 byte lengths / hash-width choices used by the public wrappers below.
139
+ function getDilithium(opts_) {
140
+ const opts = opts_;
103
141
  const { K, L, GAMMA1, GAMMA2, TAU, ETA, OMEGA } = opts;
104
142
  const { CRH_BYTES, TR_BYTES, C_TILDE_BYTES, XOF128, XOF256, securityLevel } = opts;
105
143
  if (![2, 4].includes(ETA))
@@ -111,8 +149,9 @@ function getDilithium(opts) {
111
149
  const BETA = TAU * ETA;
112
150
  const decompose = (r) => {
113
151
  // Decomposes r into (r1, r0) such that r ≡ r1(2γ2) + r0 mod q.
114
- const rPlus = mod(r);
115
- const r0 = smod(rPlus, 2 * GAMMA2) | 0;
152
+ const rPlus = crystals.mod(r);
153
+ const r0 = crystals.smod(rPlus, 2 * GAMMA2) | 0;
154
+ // FIPS 204 Algorithm 36 folds the top bucket `q-1` back to `(r1, r0) = (0, r0-1)`.
116
155
  if (rPlus - r0 === Q - 1)
117
156
  return { r1: 0 | 0, r0: (r0 - 1) | 0 };
118
157
  const r1 = Math.floor((rPlus - r0) / (2 * GAMMA2)) | 0;
@@ -122,6 +161,10 @@ function getDilithium(opts) {
122
161
  const LowBits = (r) => decompose(r).r0;
123
162
  const MakeHint = (z, r) => {
124
163
  // Compute hint bit indicating whether adding z to r alters the high bits of r.
164
+ // FIPS 204 §6.2 also permits the Section 5.1 alternative from [6], which uses the
165
+ // transformed low-bits/high-bits state at this call site instead of Algorithm 39 literally.
166
+ // This optimized predicate only applies to those transformed Section 5.1 inputs; it is
167
+ // not a drop-in replacement for Algorithm 39 on arbitrary `(z, r)` pairs.
125
168
  // From dilithium code
126
169
  const res0 = z <= GAMMA2 || z > Q - GAMMA2 || (z === Q - GAMMA2 && r === 0) ? 0 : 1;
127
170
  // from FIPS204:
@@ -131,8 +174,9 @@ function getDilithium(opts) {
131
174
  // But they return different results! However, decompose is same.
132
175
  // So, either there is a bug in Dilithium ref implementation or in FIPS204.
133
176
  // For now, lets use dilithium one, so test vectors can be passed.
134
- // See
135
- // https://github.com/GiacomoPope/dilithium-py?tab=readme-ov-file#optimising-decomposition-and-making-hints
177
+ // The round-3 Dilithium / ML-DSA code uses the same low-bits / high-bits convention after
178
+ // `r0 += ct0`.
179
+ // See dilithium-py README section "Optimising decomposition and making hints".
136
180
  return res0;
137
181
  };
138
182
  const UseHint = (h, r) => {
@@ -142,18 +186,19 @@ function getDilithium(opts) {
142
186
  // 3: if h = 1 and r0 > 0 return (r1 + 1) mod m
143
187
  // 4: if h = 1 and r0 ≤ 0 return (r1 − 1) mod m
144
188
  if (h === 1)
145
- return r0 > 0 ? mod(r1 + 1, m) | 0 : mod(r1 - 1, m) | 0;
189
+ return r0 > 0 ? crystals.mod(r1 + 1, m) | 0 : crystals.mod(r1 - 1, m) | 0;
146
190
  return r1 | 0;
147
191
  };
148
192
  const Power2Round = (r) => {
149
193
  // Decomposes r into (r1, r0) such that r ≡ r1*(2**d) + r0 mod q.
150
- const rPlus = mod(r);
151
- const r0 = smod(rPlus, 2 ** D) | 0;
194
+ const rPlus = crystals.mod(r);
195
+ const r0 = crystals.smod(rPlus, 2 ** D) | 0;
152
196
  return { r1: Math.floor((rPlus - r0) / 2 ** D) | 0, r0 };
153
197
  };
154
198
  const hintCoder = {
155
199
  bytesLen: OMEGA + K,
156
- encode: (h) => {
200
+ encode: (h_) => {
201
+ const h = h_;
157
202
  if (h === false)
158
203
  throw new Error('hint.encode: hint is false'); // should never happen
159
204
  const res = new Uint8Array(OMEGA + K);
@@ -194,7 +239,7 @@ function getDilithium(opts) {
194
239
  const T0Coder = polyCoder(13, (i) => (1 << (D - 1)) - i);
195
240
  const T1Coder = polyCoder(10);
196
241
  // Requires smod. Need to fix!
197
- const ZCoder = polyCoder(GAMMA1 === 1 << 17 ? 18 : 20, (i) => smod(GAMMA1 - i));
242
+ const ZCoder = polyCoder(GAMMA1 === 1 << 17 ? 18 : 20, (i) => crystals.smod(GAMMA1 - i));
198
243
  const W1Coder = polyCoder(GAMMA2 === GAMMA2_1 ? 6 : 4);
199
244
  const W1Vec = vecCoder(W1Coder, K);
200
245
  // Main structures
@@ -204,8 +249,11 @@ function getDilithium(opts) {
204
249
  const CoefFromHalfByte = ETA === 2
205
250
  ? (n) => (n < 15 ? 2 - (n % 5) : false)
206
251
  : (n) => (n < 9 ? 4 - n : false);
207
- // Return poly in NTT representation
208
- function RejBoundedPoly(xof) {
252
+ // Return poly in ordinary representation.
253
+ // This helper returns ordinary-form `[-ETA, ETA]` coefficients for ExpandS; callers apply
254
+ // `NTT.encode()` later when needed.
255
+ function RejBoundedPoly(xof_) {
256
+ const xof = xof_;
209
257
  // Samples an element a ∈ Rq with coeffcients in [−η, η] computed via rejection sampling from ρ.
210
258
  const r = newPoly(N);
211
259
  for (let j = 0; j < N;) {
@@ -228,6 +276,8 @@ function getDilithium(opts) {
228
276
  const s = shake256.create({}).update(seed);
229
277
  const buf = new Uint8Array(shake256.blockLen);
230
278
  s.xofInto(buf);
279
+ // FIPS 204 Algorithm 29 uses the first 8 squeezed bytes as the 64 sign bits `h`,
280
+ // then rejection-samples coefficient positions from the remaining XOF stream.
231
281
  const masks = buf.slice(0, 8);
232
282
  for (let i = N - TAU, pos = 8, maskPos = 0, maskBit = 0; i < N; i++) {
233
283
  let b = i + 1;
@@ -247,7 +297,8 @@ function getDilithium(opts) {
247
297
  }
248
298
  return pre;
249
299
  };
250
- const polyPowerRound = (p) => {
300
+ const polyPowerRound = (p_) => {
301
+ const p = p_;
251
302
  const res0 = newPoly(N);
252
303
  const res1 = newPoly(N);
253
304
  for (let i = 0; i < p.length; i++) {
@@ -257,12 +308,18 @@ function getDilithium(opts) {
257
308
  }
258
309
  return { r0: res0, r1: res1 };
259
310
  };
260
- const polyUseHint = (u, h) => {
311
+ const polyUseHint = (u_, h_) => {
312
+ const u = u_;
313
+ const h = h_;
314
+ // In-place on `u`: verification only needs the recovered high bits, so reuse the
315
+ // temporary `wApprox` buffer instead of allocating another polynomial.
261
316
  for (let i = 0; i < N; i++)
262
317
  u[i] = UseHint(h[i], u[i]);
263
318
  return u;
264
319
  };
265
- const polyMakeHint = (a, b) => {
320
+ const polyMakeHint = (a_, b_) => {
321
+ const a = a_;
322
+ const b = b_;
266
323
  const v = newPoly(N);
267
324
  let cnt = 0;
268
325
  for (let i = 0; i < N; i++) {
@@ -275,15 +332,15 @@ function getDilithium(opts) {
275
332
  const signRandBytes = 32;
276
333
  const seedCoder = splitCoder('seed', 32, 64, 32);
277
334
  // API & argument positions are exactly as in FIPS204.
278
- const internal = {
279
- info: { type: 'internal-ml-dsa' },
280
- lengths: {
335
+ const internal = Object.freeze({
336
+ info: Object.freeze({ type: 'internal-ml-dsa' }),
337
+ lengths: Object.freeze({
281
338
  secretKey: secretCoder.bytesLen,
282
339
  publicKey: publicCoder.bytesLen,
283
340
  seed: 32,
284
341
  signature: sigCoder.bytesLen,
285
342
  signRand: signRandBytes,
286
- },
343
+ }),
287
344
  keygen: (seed) => {
288
345
  // H(𝜉||IntegerToBytes(𝑘, 1)||IntegerToBytes(ℓ, 1), 128) 2: ▷ expand seed
289
346
  const seedDst = new Uint8Array(32 + 2);
@@ -304,7 +361,7 @@ function getDilithium(opts) {
304
361
  const s2 = [];
305
362
  for (let i = L; i < L + K; i++)
306
363
  s2.push(RejBoundedPoly(xofPrime.get(i & 0xff, (i >> 8) & 0xff)));
307
- const s1Hat = s1.map((i) => NTT.encode(i.slice()));
364
+ const s1Hat = s1.map((i) => crystals.NTT.encode(i.slice()));
308
365
  const t0 = [];
309
366
  const t1 = [];
310
367
  const xof = XOF128(rho);
@@ -316,26 +373,33 @@ function getDilithium(opts) {
316
373
  const aij = RejNTTPoly(xof.get(j, i)); // super slow!
317
374
  polyAdd(t, MultiplyNTTs(aij, s1Hat[j]));
318
375
  }
319
- NTT.decode(t);
376
+ crystals.NTT.decode(t);
320
377
  const { r0, r1 } = polyPowerRound(polyAdd(t, s2[i])); // (t1, t0) ← Power2Round(t, d)
321
378
  t0.push(r0);
322
379
  t1.push(r1);
323
380
  }
324
381
  const publicKey = publicCoder.encode([rho, t1]); // pk ← pkEncode(ρ, t1)
325
382
  const tr = shake256(publicKey, { dkLen: TR_BYTES }); // tr ← H(BytesToBits(pk), 512)
326
- const secretKey = secretCoder.encode([rho, K_, tr, s1, s2, t0]); // sk ← skEncode(ρ, K,tr, s1, s2, t0)
383
+ // sk ← skEncode(ρ, K,tr, s1, s2, t0)
384
+ const secretKey = secretCoder.encode([rho, K_, tr, s1, s2, t0]);
327
385
  xof.clean();
328
386
  xofPrime.clean();
329
387
  // STATS
330
- // Kyber512: { calls: 4, xofs: 12 }, Kyber768: { calls: 9, xofs: 27 }, Kyber1024: { calls: 16, xofs: 48 }
331
- // DSA44: { calls: 24, xofs: 24 }, DSA65: { calls: 41, xofs: 41 }, DSA87: { calls: 71, xofs: 71 }
388
+ // Kyber512: { calls: 4, xofs: 12 }, Kyber768: { calls: 9, xofs: 27 },
389
+ // Kyber1024: { calls: 16, xofs: 48 }
390
+ // DSA44: { calls: 24, xofs: 24 }, DSA65: { calls: 41, xofs: 41 },
391
+ // DSA87: { calls: 71, xofs: 71 }
332
392
  cleanBytes(rho, rhoPrime, K_, s1, s2, s1Hat, t, t0, t1, tr, seedDst);
333
- return { publicKey, secretKey };
393
+ return {
394
+ publicKey: publicKey,
395
+ secretKey: secretKey,
396
+ };
334
397
  },
335
398
  getPublicKey: (secretKey) => {
336
- const [rho, _K, _tr, s1, s2, _t0] = secretCoder.decode(secretKey); // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
399
+ // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
400
+ const [rho, _K, _tr, s1, s2, _t0] = secretCoder.decode(secretKey);
337
401
  const xof = XOF128(rho);
338
- const s1Hat = s1.map((p) => NTT.encode(p.slice()));
402
+ const s1Hat = s1.map((p) => crystals.NTT.encode(p.slice()));
339
403
  const t1 = [];
340
404
  const tmp = newPoly(N);
341
405
  for (let i = 0; i < K; i++) {
@@ -344,7 +408,7 @@ function getDilithium(opts) {
344
408
  const aij = RejNTTPoly(xof.get(j, i)); // A_ij in NTT
345
409
  polyAdd(tmp, MultiplyNTTs(aij, s1Hat[j])); // += A_ij * s1_j
346
410
  }
347
- NTT.decode(tmp); // NTT⁻¹
411
+ crystals.NTT.decode(tmp); // NTT⁻¹
348
412
  polyAdd(tmp, s2[i]); // t_i = A·s1 + s2
349
413
  const { r1 } = polyPowerRound(tmp); // r1 = t1, r0 ≈ t0
350
414
  t1.push(r1);
@@ -360,7 +424,8 @@ function getDilithium(opts) {
360
424
  let { extraEntropy: random, externalMu = false } = opts;
361
425
  // This part can be pre-cached per secretKey, but there is only minor performance improvement,
362
426
  // since we re-use a lot of variables to computation.
363
- const [rho, _K, tr, s1, s2, t0] = secretCoder.decode(secretKey); // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
427
+ // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
428
+ const [rho, _K, tr, s1, s2, t0] = secretCoder.decode(secretKey);
364
429
  // Cache matrix to avoid re-compute later
365
430
  const A = []; // A ← ExpandA(ρ)
366
431
  const xof = XOF128(rho);
@@ -372,15 +437,17 @@ function getDilithium(opts) {
372
437
  }
373
438
  xof.clean();
374
439
  for (let i = 0; i < L; i++)
375
- NTT.encode(s1[i]); // sˆ1 ← NTT(s1)
440
+ crystals.NTT.encode(s1[i]); // sˆ1 ← NTT(s1)
376
441
  for (let i = 0; i < K; i++) {
377
- NTT.encode(s2[i]); // sˆ2 ← NTT(s2)
378
- NTT.encode(t0[i]); // tˆ0 ← NTT(t0)
442
+ crystals.NTT.encode(s2[i]); // sˆ2 ← NTT(s2)
443
+ crystals.NTT.encode(t0[i]); // tˆ0 ← NTT(t0)
379
444
  }
380
445
  // This part is per msg
381
446
  const mu = externalMu
382
447
  ? msg
383
- : shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest(); // 6: µ ← H(tr||M, 512) ▷ Compute message representative µ
448
+ : // 6: µ ← H(tr||M, 512)
449
+ // ▷ Compute message representative µ
450
+ shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest();
384
451
  // Compute private random seed
385
452
  const rnd = random === false
386
453
  ? new Uint8Array(32)
@@ -402,14 +469,14 @@ function getDilithium(opts) {
402
469
  // y ← ExpandMask(ρ , κ)
403
470
  for (let i = 0; i < L; i++, kappa++)
404
471
  y.push(ZCoder.decode(x256.get(kappa & 0xff, kappa >> 8)()));
405
- const z = y.map((i) => NTT.encode(i.slice()));
472
+ const z = y.map((i) => crystals.NTT.encode(i.slice()));
406
473
  const w = [];
407
474
  for (let i = 0; i < K; i++) {
408
475
  // w ← NTT−1(A ◦ NTT(y))
409
476
  const wi = newPoly(N);
410
477
  for (let j = 0; j < L; j++)
411
478
  polyAdd(wi, MultiplyNTTs(A[i][j], z[j]));
412
- NTT.decode(wi);
479
+ crystals.NTT.decode(wi);
413
480
  w.push(wi);
414
481
  }
415
482
  const w1 = w.map((j) => j.map(HighBits)); // w1 ← HighBits(w)
@@ -420,11 +487,12 @@ function getDilithium(opts) {
420
487
  .update(W1Vec.encode(w1))
421
488
  .digest();
422
489
  // Verifer’s challenge
423
- const cHat = NTT.encode(SampleInBall(cTilde)); // c ← SampleInBall(c˜1); cˆ ← NTT(c)
490
+ // c ← SampleInBall(c˜1); cˆ ← NTT(c)
491
+ const cHat = crystals.NTT.encode(SampleInBall(cTilde));
424
492
  // ⟨⟨cs1⟩⟩ ← NTT−1(cˆ◦ sˆ1)
425
493
  const cs1 = s1.map((i) => MultiplyNTTs(i, cHat));
426
494
  for (let i = 0; i < L; i++) {
427
- polyAdd(NTT.decode(cs1[i]), y[i]); // z ← y + ⟨⟨cs1⟩⟩
495
+ polyAdd(crystals.NTT.decode(cs1[i]), y[i]); // z ← y + ⟨⟨cs1⟩⟩
428
496
  if (polyChknorm(cs1[i], GAMMA1 - BETA))
429
497
  continue main_loop; // ||z||∞ ≥ γ1 − β
430
498
  }
@@ -432,11 +500,11 @@ function getDilithium(opts) {
432
500
  let cnt = 0;
433
501
  const h = [];
434
502
  for (let i = 0; i < K; i++) {
435
- const cs2 = NTT.decode(MultiplyNTTs(s2[i], cHat)); // ⟨⟨cs2⟩⟩ ← NTT−1(cˆ◦ sˆ2)
503
+ const cs2 = crystals.NTT.decode(MultiplyNTTs(s2[i], cHat)); // ⟨⟨cs2⟩⟩ ← NTT−1(cˆ◦ sˆ2)
436
504
  const r0 = polySub(w[i], cs2).map(LowBits); // r0 ← LowBits(w − ⟨⟨cs2⟩⟩)
437
505
  if (polyChknorm(r0, GAMMA2 - BETA))
438
506
  continue main_loop; // ||r0||∞ ≥ γ2 − β
439
- const ct0 = NTT.decode(MultiplyNTTs(t0[i], cHat)); // ⟨⟨ct0⟩⟩ ← NTT−1(cˆ◦ tˆ0)
507
+ const ct0 = crystals.NTT.decode(MultiplyNTTs(t0[i], cHat)); // ⟨⟨ct0⟩⟩ ← NTT−1(cˆ◦ tˆ0)
440
508
  if (polyChknorm(ct0, GAMMA2))
441
509
  continue main_loop;
442
510
  polyAdd(r0, ct0);
@@ -450,7 +518,12 @@ function getDilithium(opts) {
450
518
  x256.clean();
451
519
  const res = sigCoder.encode([cTilde, cs1, h]); // σ ← sigEncode(c˜, z mod±q, h)
452
520
  // rho, _K, tr is subarray of secretKey, cannot clean.
453
- cleanBytes(cTilde, cs1, h, cHat, w1, w, z, y, rhoprime, mu, s1, s2, t0, ...A);
521
+ cleanBytes(cTilde, cs1, h, cHat, w1, w, z, y, rhoprime, s1, s2, t0, ...A);
522
+ // `externalMu` hands ownership of `mu` to the caller,
523
+ // so only wipe the internally derived digest form here;
524
+ // zeroizing caller memory would break the caller's own reuse / verify path.
525
+ if (!externalMu)
526
+ cleanBytes(mu);
454
527
  return res;
455
528
  }
456
529
  // @ts-ignore
@@ -464,7 +537,9 @@ function getDilithium(opts) {
464
537
  const tr = shake256(publicKey, { dkLen: TR_BYTES }); // 6: tr ← H(BytesToBits(pk), 512)
465
538
  if (sig.length !== sigCoder.bytesLen)
466
539
  return false; // return false instead of exception
467
- const [cTilde, z, h] = sigCoder.decode(sig); // (c˜, z, h) ← sigDecode(σ), ▷ Signer’s commitment hash c ˜, response z and hint
540
+ // (c˜, z, h) ← sigDecode(σ)
541
+ // ▷ Signer’s commitment hash c ˜, response z and hint
542
+ const [cTilde, z, h] = sigCoder.decode(sig);
468
543
  if (h === false)
469
544
  return false; // if h = ⊥ then return false
470
545
  for (let i = 0; i < L; i++)
@@ -472,23 +547,24 @@ function getDilithium(opts) {
472
547
  return false;
473
548
  const mu = externalMu
474
549
  ? msg
475
- : shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest(); // 7: µ ← H(tr||M, 512)
550
+ : // 7: µ ← H(tr||M, 512)
551
+ shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest();
476
552
  // Compute verifer’s challenge from c˜
477
- const c = NTT.encode(SampleInBall(cTilde)); // c ← SampleInBall(c˜1)
553
+ const c = crystals.NTT.encode(SampleInBall(cTilde)); // c ← SampleInBall(c˜1)
478
554
  const zNtt = z.map((i) => i.slice()); // zNtt = NTT(z)
479
555
  for (let i = 0; i < L; i++)
480
- NTT.encode(zNtt[i]);
556
+ crystals.NTT.encode(zNtt[i]);
481
557
  const wTick1 = [];
482
558
  const xof = XOF128(rho);
483
559
  for (let i = 0; i < K; i++) {
484
- const ct12d = MultiplyNTTs(NTT.encode(polyShiftl(t1[i])), c); //c * t1 * (2**d)
560
+ const ct12d = MultiplyNTTs(crystals.NTT.encode(polyShiftl(t1[i])), c); //c * t1 * (2**d)
485
561
  const Az = newPoly(N); // // A * z
486
562
  for (let j = 0; j < L; j++) {
487
563
  const aij = RejNTTPoly(xof.get(j, i)); // A[i][j] inplace
488
564
  polyAdd(Az, MultiplyNTTs(aij, zNtt[j]));
489
565
  }
490
566
  // wApprox = A*z - c*t1 * (2**d)
491
- const wApprox = NTT.decode(polySub(Az, ct12d));
567
+ const wApprox = crystals.NTT.decode(polySub(Az, ct12d));
492
568
  // Reconstruction of signer’s commitment
493
569
  wTick1.push(polyUseHint(wApprox, h[i])); // w ′ ← UseHint(h, w'approx )
494
570
  }
@@ -511,9 +587,9 @@ function getDilithium(opts) {
511
587
  return false;
512
588
  return equalBytes(cTilde, c2);
513
589
  },
514
- };
515
- return {
516
- info: { type: 'ml-dsa' },
590
+ });
591
+ return Object.freeze({
592
+ info: Object.freeze({ type: 'ml-dsa' }),
517
593
  internal,
518
594
  securityLevel: securityLevel,
519
595
  keygen: internal.keygen,
@@ -532,8 +608,8 @@ function getDilithium(opts) {
532
608
  },
533
609
  prehash: (hash) => {
534
610
  checkHash(hash, securityLevel);
535
- return {
536
- info: { type: 'hashml-dsa' },
611
+ return Object.freeze({
612
+ info: Object.freeze({ type: 'hashml-dsa' }),
537
613
  securityLevel: securityLevel,
538
614
  lengths: internal.lengths,
539
615
  keygen: internal.keygen,
@@ -549,12 +625,12 @@ function getDilithium(opts) {
549
625
  validateVerOpts(opts);
550
626
  return internal.verify(sig, getMessagePrehash(hash, msg, opts.context), publicKey);
551
627
  },
552
- };
628
+ });
553
629
  },
554
- };
630
+ });
555
631
  }
556
632
  /** ML-DSA-44 for 128-bit security level. Not recommended after 2030, as per ASD. */
557
- export const ml_dsa44 = /* @__PURE__ */ getDilithium({
633
+ export const ml_dsa44 = /* @__PURE__ */ (() => getDilithium({
558
634
  ...PARAMS[2],
559
635
  CRH_BYTES: 64,
560
636
  TR_BYTES: 64,
@@ -562,9 +638,9 @@ export const ml_dsa44 = /* @__PURE__ */ getDilithium({
562
638
  XOF128,
563
639
  XOF256,
564
640
  securityLevel: 128,
565
- });
641
+ }))();
566
642
  /** ML-DSA-65 for 192-bit security level. Not recommended after 2030, as per ASD. */
567
- export const ml_dsa65 = /* @__PURE__ */ getDilithium({
643
+ export const ml_dsa65 = /* @__PURE__ */ (() => getDilithium({
568
644
  ...PARAMS[3],
569
645
  CRH_BYTES: 64,
570
646
  TR_BYTES: 64,
@@ -572,9 +648,9 @@ export const ml_dsa65 = /* @__PURE__ */ getDilithium({
572
648
  XOF128,
573
649
  XOF256,
574
650
  securityLevel: 192,
575
- });
651
+ }))();
576
652
  /** ML-DSA-87 for 256-bit security level. OK after 2030, as per ASD. */
577
- export const ml_dsa87 = /* @__PURE__ */ getDilithium({
653
+ export const ml_dsa87 = /* @__PURE__ */ (() => getDilithium({
578
654
  ...PARAMS[5],
579
655
  CRH_BYTES: 64,
580
656
  TR_BYTES: 64,
@@ -582,5 +658,5 @@ export const ml_dsa87 = /* @__PURE__ */ getDilithium({
582
658
  XOF128,
583
659
  XOF256,
584
660
  securityLevel: 256,
585
- });
661
+ }))();
586
662
  //# sourceMappingURL=ml-dsa.js.map