@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/src/ml-dsa.ts CHANGED
@@ -25,6 +25,8 @@ import {
25
25
  type Signer,
26
26
  type SigOpts,
27
27
  splitCoder,
28
+ type TArg,
29
+ type TRet,
28
30
  validateOpts,
29
31
  validateSigOpts,
30
32
  validateVerOpts,
@@ -32,33 +34,51 @@ import {
32
34
  type VerOpts,
33
35
  } from './utils.ts';
34
36
 
35
- export type DSAInternalOpts = { externalMu?: boolean };
36
- function validateInternalOpts(opts: DSAInternalOpts) {
37
+ /** Internal ML-DSA options. */
38
+ export type DSAInternalOpts = {
39
+ /**
40
+ * Whether `internal.sign` / `internal.verify` receive a caller-supplied 64-byte `mu`
41
+ * instead of the usual FIPS 204 formatted message `M'` / prehash-formatted message.
42
+ * validateInternalOpts() only checks this flag; callers still must supply the right input length.
43
+ */
44
+ externalMu?: boolean;
45
+ };
46
+ function validateInternalOpts(opts: TArg<DSAInternalOpts>) {
37
47
  validateOpts(opts);
38
48
  if (opts.externalMu !== undefined) abool(opts.externalMu, 'opts.externalMu');
39
49
  }
40
50
 
41
- /** Signer API, containing internal methods */
51
+ /** ML-DSA signer surface with access to the internal message formatting mode. */
42
52
  export type DSAInternal = CryptoKeys & {
43
53
  lengths: Signer['lengths'];
44
- sign: (msg: Uint8Array, secretKey: Uint8Array, opts?: SigOpts & DSAInternalOpts) => Uint8Array;
54
+ sign: (
55
+ msg: TArg<Uint8Array>,
56
+ secretKey: TArg<Uint8Array>,
57
+ opts?: TArg<SigOpts & DSAInternalOpts>
58
+ ) => TRet<Uint8Array>;
45
59
  verify: (
46
- sig: Uint8Array,
47
- msg: Uint8Array,
48
- pubKey: Uint8Array,
49
- opts?: VerOpts & DSAInternalOpts
60
+ sig: TArg<Uint8Array>,
61
+ msg: TArg<Uint8Array>,
62
+ pubKey: TArg<Uint8Array>,
63
+ opts?: TArg<VerOpts & DSAInternalOpts>
50
64
  ) => boolean;
51
65
  };
52
- export type DSA = Signer & { internal: DSAInternal };
66
+ /** Public ML-DSA signer surface. */
67
+ export type DSA = Signer & { internal: TRet<DSAInternal> };
53
68
 
54
69
  // Constants
70
+ // FIPS 204 fixes ML-DSA over R = Z[X]/(X^256 + 1), so every polynomial has 256 coefficients.
55
71
  const N = 256;
56
72
  // 2**23 − 2**13 + 1, 23 bits: multiply will be 46. We have enough precision in JS to avoid bigints
57
73
  const Q = 8380417;
74
+ // FIPS 204 §2.5 / Table 1 fixes zeta = 1753 as the 512th root of unity used by ML-DSA's NTT.
58
75
  const ROOT_OF_UNITY = 1753;
59
76
  // f = 256**−1 mod q, pow(256, -1, q) = 8347681 (python3)
60
77
  const F = 8347681;
78
+ // FIPS 204 Table 1 / §7.4 fixes d = 13 dropped low bits for Power2Round on t.
61
79
  const D = 13;
80
+ // FIPS 204 Table 1 fixes gamma2 to (q-1)/88 for ML-DSA-44 and (q-1)/32 for ML-DSA-65/87;
81
+ // §7.4 then uses alpha = 2*gamma2 for Decompose / MakeHint / UseHint.
62
82
  // Dilithium is kinda parametrized over GAMMA2, but everything will break with any other value.
63
83
  const GAMMA2_1 = Math.floor((Q - 1) / 88) | 0;
64
84
  const GAMMA2_2 = Math.floor((Q - 1) / 32) | 0;
@@ -66,29 +86,52 @@ const GAMMA2_2 = Math.floor((Q - 1) / 32) | 0;
66
86
  type XofGet = ReturnType<ReturnType<XOF>['get']>;
67
87
 
68
88
  /** Various lattice params. */
89
+ /** Public ML-DSA parameter-set description. */
69
90
  export type DSAParam = {
91
+ /** Matrix row count. */
70
92
  K: number;
93
+ /** Matrix column count. */
71
94
  L: number;
95
+ /** Bit width used when rounding `t`. */
72
96
  D: number;
97
+ /** Bound used for the `y` sampling range. */
73
98
  GAMMA1: number;
99
+ /** Bound used during decomposition and hints. */
74
100
  GAMMA2: number;
101
+ /** Number of non-zero challenge coefficients. */
75
102
  TAU: number;
103
+ /** Centered-binomial noise parameter. */
76
104
  ETA: number;
105
+ /** Maximum number of hint bits in a signature. */
77
106
  OMEGA: number;
78
107
  };
79
108
  /** Internal params for different versions of ML-DSA */
80
109
  // prettier-ignore
81
- export const PARAMS: Record<string, DSAParam> = {
82
- 2: { K: 4, L: 4, D, GAMMA1: 2 ** 17, GAMMA2: GAMMA2_1, TAU: 39, ETA: 2, OMEGA: 80 },
83
- 3: { K: 6, L: 5, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 49, ETA: 4, OMEGA: 55 },
84
- 5: { K: 8, L: 7, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 60, ETA: 2, OMEGA: 75 },
85
- } as const;
110
+ /** Built-in ML-DSA parameter presets keyed by security categories `2/3/5`
111
+ * for `ml_dsa44` / `ml_dsa65` / `ml_dsa87`.
112
+ * This is only the Table 1 subset used directly here: `BETA = TAU * ETA` is derived later,
113
+ * while `C_TILDE_BYTES`, `TR_BYTES`, `CRH_BYTES`, and `securityLevel` live in the preset wrappers.
114
+ */
115
+ export const PARAMS: Record<string, DSAParam> = /* @__PURE__ */ (() =>
116
+ Object.freeze({
117
+ 2: Object.freeze({
118
+ K: 4, L: 4, D, GAMMA1: 2 ** 17, GAMMA2: GAMMA2_1, TAU: 39, ETA: 2, OMEGA: 80
119
+ }),
120
+ 3: Object.freeze({
121
+ K: 6, L: 5, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 49, ETA: 4, OMEGA: 55
122
+ }),
123
+ 5: Object.freeze({
124
+ K: 8, L: 7, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 60, ETA: 2, OMEGA: 75
125
+ }),
126
+ } as const))();
86
127
 
87
128
  // NOTE: there is a lot cases where negative numbers used (with smod instead of mod).
88
129
  type Poly = Int32Array;
89
- const newPoly = (n: number): Int32Array => new Int32Array(n);
130
+ const newPoly = (n: number): TRet<Int32Array> => new Int32Array(n) as TRet<Int32Array>;
90
131
 
91
- const { mod, smod, NTT, bitsCoder } = genCrystals({
132
+ // Shared CRYSTALS helper in the ML-DSA branch: non-Kyber mode, 8-bit bit-reversal,
133
+ // and Int32Array polys because ordinary-form coefficients can be negative / centered.
134
+ const crystals = /* @__PURE__ */ genCrystals({
92
135
  N,
93
136
  Q,
94
137
  F,
@@ -101,56 +144,73 @@ const { mod, smod, NTT, bitsCoder } = genCrystals({
101
144
  const id = <T>(n: T): T => n;
102
145
  type IdNum = (n: number) => number;
103
146
 
147
+ // compress()/verify() must be compatible in both directions:
148
+ // wrap the shared d-bit packer with the FIPS 204 SimpleBitPack / BitPack coefficient maps.
149
+ // malformed-input rejection only happens through the optional verify hook.
104
150
  const polyCoder = (d: number, compress: IdNum = id, verify: IdNum = id) =>
105
- bitsCoder(d, {
151
+ crystals.bitsCoder(d, {
106
152
  encode: (i: number) => compress(verify(i)),
107
153
  decode: (i: number) => verify(compress(i)),
108
154
  });
109
155
 
110
- const polyAdd = (a: Poly, b: Poly) => {
111
- for (let i = 0; i < a.length; i++) a[i] = mod(a[i] + b[i]);
112
- return a;
156
+ // Mutates `a` in place; callers must pass same-length polynomials.
157
+ const polyAdd = (a_: TArg<Poly>, b_: TArg<Poly>): TRet<Poly> => {
158
+ const a = a_ as Poly;
159
+ const b = b_ as Poly;
160
+ for (let i = 0; i < a.length; i++) a[i] = crystals.mod(a[i] + b[i]);
161
+ return a as TRet<Poly>;
113
162
  };
114
- const polySub = (a: Poly, b: Poly): Poly => {
115
- for (let i = 0; i < a.length; i++) a[i] = mod(a[i] - b[i]);
116
- return a;
163
+ // Mutates `a` in place; callers must pass same-length polynomials.
164
+ const polySub = (a_: TArg<Poly>, b_: TArg<Poly>): TRet<Poly> => {
165
+ const a = a_ as Poly;
166
+ const b = b_ as Poly;
167
+ for (let i = 0; i < a.length; i++) a[i] = crystals.mod(a[i] - b[i]);
168
+ return a as TRet<Poly>;
117
169
  };
118
170
 
119
- const polyShiftl = (p: Poly): Poly => {
171
+ // Mutates `p` in place and assumes it is a decoded `t1`-range polynomial.
172
+ const polyShiftl = (p_: TArg<Poly>): TRet<Poly> => {
173
+ const p = p_ as Poly;
120
174
  for (let i = 0; i < N; i++) p[i] <<= D;
121
- return p;
175
+ return p as TRet<Poly>;
122
176
  };
123
177
 
124
- 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;
178
+ const polyChknorm = (p_: TArg<Poly>, B: number): boolean => {
179
+ const p = p_ as Poly;
180
+ // FIPS 204 Algorithms 7 and 8 express the same centered-norm check with explicit inequalities.
181
+ for (let i = 0; i < N; i++) if (Math.abs(crystals.smod(p[i])) >= B) return true;
127
182
  return false;
128
183
  };
129
184
 
130
- const MultiplyNTTs = (a: Poly, b: Poly): Poly => {
185
+ // Both inputs must already be in NTT / `T_q` form.
186
+ const MultiplyNTTs = (a_: TArg<Poly>, b_: TArg<Poly>): TRet<Poly> => {
187
+ const a = a_ as Poly;
188
+ const b = b_ as Poly;
131
189
  // NOTE: we don't use montgomery reduction in code, since it requires 64 bit ints,
132
190
  // which is not available in JS. mod(a[i] * b[i]) is ok, since Q is 23 bit,
133
191
  // which means a[i] * b[i] is 46 bit, which is safe to use in JS. (number is 53 bits).
134
192
  // Barrett reduction is slower than mod :(
135
193
  const c = newPoly(N);
136
- for (let i = 0; i < a.length; i++) c[i] = mod(a[i] * b[i]);
137
- return c;
194
+ for (let i = 0; i < a.length; i++) c[i] = crystals.mod(a[i] * b[i]);
195
+ return c as TRet<Poly>;
138
196
  };
139
197
 
140
198
  // Return poly in NTT representation
141
- function RejNTTPoly(xof: XofGet) {
142
- // Samples a polynomial Tq.
199
+ function RejNTTPoly(xof_: TArg<XofGet>): TRet<Poly> {
200
+ const xof = xof_ as XofGet;
201
+ // Samples a polynomial ∈ Tq. xof() must return byte lengths divisible by 3.
143
202
  const r = newPoly(N);
144
203
  // NOTE: we can represent 3xu24 as 4xu32, but it doesn't improve perf :(
145
204
  for (let j = 0; j < N; ) {
146
205
  const b = xof();
147
206
  if (b.length % 3) throw new Error('RejNTTPoly: unaligned block');
148
207
  for (let i = 0; j < N && i <= b.length - 3; i += 3) {
208
+ // FIPS 204 Algorithm 14 clears the top bit of b2 before forming the 23-bit candidate.
149
209
  const t = (b[i + 0] | (b[i + 1] << 8) | (b[i + 2] << 16)) & 0x7fffff; // 3 bytes
150
210
  if (t < Q) r[j++] = t;
151
211
  }
152
212
  }
153
- return r;
213
+ return r as TRet<Poly>;
154
214
  }
155
215
 
156
216
  type DilithiumOpts = {
@@ -169,7 +229,10 @@ type DilithiumOpts = {
169
229
  securityLevel: number;
170
230
  };
171
231
 
172
- function getDilithium(opts: DilithiumOpts) {
232
+ // Instantiate one ML-DSA parameter set from the Table 1 lattice constants plus the
233
+ // Table 2 byte lengths / hash-width choices used by the public wrappers below.
234
+ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
235
+ const opts = opts_ as DilithiumOpts;
173
236
  const { K, L, GAMMA1, GAMMA2, TAU, ETA, OMEGA } = opts;
174
237
  const { CRH_BYTES, TR_BYTES, C_TILDE_BYTES, XOF128, XOF256, securityLevel } = opts;
175
238
 
@@ -180,8 +243,9 @@ function getDilithium(opts: DilithiumOpts) {
180
243
 
181
244
  const decompose = (r: number) => {
182
245
  // 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;
246
+ const rPlus = crystals.mod(r);
247
+ const r0 = crystals.smod(rPlus, 2 * GAMMA2) | 0;
248
+ // FIPS 204 Algorithm 36 folds the top bucket `q-1` back to `(r1, r0) = (0, r0-1)`.
185
249
  if (rPlus - r0 === Q - 1) return { r1: 0 | 0, r0: (r0 - 1) | 0 };
186
250
  const r1 = Math.floor((rPlus - r0) / (2 * GAMMA2)) | 0;
187
251
  return { r1, r0 }; // r1 = HighBits, r0 = LowBits
@@ -191,6 +255,10 @@ function getDilithium(opts: DilithiumOpts) {
191
255
  const LowBits = (r: number) => decompose(r).r0;
192
256
  const MakeHint = (z: number, r: number) => {
193
257
  // Compute hint bit indicating whether adding z to r alters the high bits of r.
258
+ // FIPS 204 §6.2 also permits the Section 5.1 alternative from [6], which uses the
259
+ // transformed low-bits/high-bits state at this call site instead of Algorithm 39 literally.
260
+ // This optimized predicate only applies to those transformed Section 5.1 inputs; it is
261
+ // not a drop-in replacement for Algorithm 39 on arbitrary `(z, r)` pairs.
194
262
 
195
263
  // From dilithium code
196
264
  const res0 = z <= GAMMA2 || z > Q - GAMMA2 || (z === Q - GAMMA2 && r === 0) ? 0 : 1;
@@ -201,8 +269,9 @@ function getDilithium(opts: DilithiumOpts) {
201
269
  // But they return different results! However, decompose is same.
202
270
  // So, either there is a bug in Dilithium ref implementation or in FIPS204.
203
271
  // 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
272
+ // The round-3 Dilithium / ML-DSA code uses the same low-bits / high-bits convention after
273
+ // `r0 += ct0`.
274
+ // See dilithium-py README section "Optimising decomposition and making hints".
206
275
  return res0;
207
276
  };
208
277
 
@@ -212,42 +281,43 @@ function getDilithium(opts: DilithiumOpts) {
212
281
  const { r1, r0 } = decompose(r);
213
282
  // 3: if h = 1 and r0 > 0 return (r1 + 1) mod m
214
283
  // 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;
284
+ if (h === 1) return r0 > 0 ? crystals.mod(r1 + 1, m) | 0 : crystals.mod(r1 - 1, m) | 0;
216
285
  return r1 | 0;
217
286
  };
218
287
  const Power2Round = (r: number) => {
219
288
  // 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;
289
+ const rPlus = crystals.mod(r);
290
+ const r0 = crystals.smod(rPlus, 2 ** D) | 0;
222
291
  return { r1: Math.floor((rPlus - r0) / 2 ** D) | 0, r0 };
223
292
  };
224
293
 
225
294
  const hintCoder: BytesCoderLen<Poly[] | false> = {
226
295
  bytesLen: OMEGA + K,
227
- encode: (h: Poly[] | false) => {
296
+ encode: (h_: TArg<Poly[] | false>): TRet<Uint8Array> => {
297
+ const h = h_ as Poly[] | false;
228
298
  if (h === false) throw new Error('hint.encode: hint is false'); // should never happen
229
299
  const res = new Uint8Array(OMEGA + K);
230
300
  for (let i = 0, k = 0; i < K; i++) {
231
301
  for (let j = 0; j < N; j++) if (h[i][j] !== 0) res[k++] = j;
232
302
  res[OMEGA + i] = k;
233
303
  }
234
- return res;
304
+ return res as TRet<Uint8Array>;
235
305
  },
236
- decode: (buf: Uint8Array) => {
306
+ decode: (buf: TArg<Uint8Array>): TRet<Poly[] | false> => {
237
307
  const h = [];
238
308
  let k = 0;
239
309
  for (let i = 0; i < K; i++) {
240
310
  const hi = newPoly(N);
241
- if (buf[OMEGA + i] < k || buf[OMEGA + i] > OMEGA) return false;
311
+ if (buf[OMEGA + i] < k || buf[OMEGA + i] > OMEGA) return false as TRet<false>;
242
312
  for (let j = k; j < buf[OMEGA + i]; j++) {
243
- if (j > k && buf[j] <= buf[j - 1]) return false;
313
+ if (j > k && buf[j] <= buf[j - 1]) return false as TRet<false>;
244
314
  hi[buf[j]] = 1;
245
315
  }
246
316
  k = buf[OMEGA + i];
247
317
  h.push(hi);
248
318
  }
249
- for (let j = k; j < OMEGA; j++) if (buf[j] !== 0) return false;
250
- return h;
319
+ for (let j = k; j < OMEGA; j++) if (buf[j] !== 0) return false as TRet<false>;
320
+ return h as TRet<Poly[]>;
251
321
  },
252
322
  };
253
323
 
@@ -263,7 +333,7 @@ function getDilithium(opts: DilithiumOpts) {
263
333
  const T0Coder = polyCoder(13, (i: number) => (1 << (D - 1)) - i);
264
334
  const T1Coder = polyCoder(10);
265
335
  // Requires smod. Need to fix!
266
- const ZCoder = polyCoder(GAMMA1 === 1 << 17 ? 18 : 20, (i: number) => smod(GAMMA1 - i));
336
+ const ZCoder = polyCoder(GAMMA1 === 1 << 17 ? 18 : 20, (i: number) => crystals.smod(GAMMA1 - i));
267
337
  const W1Coder = polyCoder(GAMMA2 === GAMMA2_1 ? 6 : 4);
268
338
  const W1Vec = vecCoder(W1Coder, K);
269
339
  // Main structures
@@ -283,8 +353,11 @@ function getDilithium(opts: DilithiumOpts) {
283
353
  ? (n: number) => (n < 15 ? 2 - (n % 5) : false)
284
354
  : (n: number) => (n < 9 ? 4 - n : false);
285
355
 
286
- // Return poly in NTT representation
287
- function RejBoundedPoly(xof: XofGet) {
356
+ // Return poly in ordinary representation.
357
+ // This helper returns ordinary-form `[-ETA, ETA]` coefficients for ExpandS; callers apply
358
+ // `NTT.encode()` later when needed.
359
+ function RejBoundedPoly(xof_: TArg<XofGet>): TRet<Poly> {
360
+ const xof = xof_ as XofGet;
288
361
  // Samples an element a ∈ Rq with coeffcients in [−η, η] computed via rejection sampling from ρ.
289
362
  const r: Poly = newPoly(N);
290
363
  for (let j = 0; j < N; ) {
@@ -297,15 +370,17 @@ function getDilithium(opts: DilithiumOpts) {
297
370
  if (j < N && d2 !== false) r[j++] = d2;
298
371
  }
299
372
  }
300
- return r;
373
+ return r as TRet<Poly>;
301
374
  }
302
375
 
303
- const SampleInBall = (seed: Uint8Array) => {
376
+ const SampleInBall = (seed: TArg<Uint8Array>): TRet<Poly> => {
304
377
  // Samples a polynomial c ∈ Rq with coeffcients from {−1, 0, 1} and Hamming weight τ
305
378
  const pre = newPoly(N);
306
379
  const s = shake256.create({}).update(seed);
307
380
  const buf = new Uint8Array(shake256.blockLen);
308
381
  s.xofInto(buf);
382
+ // FIPS 204 Algorithm 29 uses the first 8 squeezed bytes as the 64 sign bits `h`,
383
+ // then rejection-samples coefficient positions from the remaining XOF stream.
309
384
  const masks = buf.slice(0, 8);
310
385
  for (let i = N - TAU, pos = 8, maskPos = 0, maskBit = 0; i < N; i++) {
311
386
  let b = i + 1;
@@ -322,10 +397,11 @@ function getDilithium(opts: DilithiumOpts) {
322
397
  maskBit = 0;
323
398
  }
324
399
  }
325
- return pre;
400
+ return pre as TRet<Poly>;
326
401
  };
327
402
 
328
- const polyPowerRound = (p: Poly) => {
403
+ const polyPowerRound = (p_: TArg<Poly>) => {
404
+ const p = p_ as Poly;
329
405
  const res0 = newPoly(N);
330
406
  const res1 = newPoly(N);
331
407
  for (let i = 0; i < p.length; i++) {
@@ -335,11 +411,17 @@ function getDilithium(opts: DilithiumOpts) {
335
411
  }
336
412
  return { r0: res0, r1: res1 };
337
413
  };
338
- const polyUseHint = (u: Poly, h: Poly): Poly => {
414
+ const polyUseHint = (u_: TArg<Poly>, h_: TArg<Poly>): TRet<Poly> => {
415
+ const u = u_ as Poly;
416
+ const h = h_ as Poly;
417
+ // In-place on `u`: verification only needs the recovered high bits, so reuse the
418
+ // temporary `wApprox` buffer instead of allocating another polynomial.
339
419
  for (let i = 0; i < N; i++) u[i] = UseHint(h[i], u[i]);
340
- return u;
420
+ return u as TRet<Poly>;
341
421
  };
342
- const polyMakeHint = (a: Poly, b: Poly) => {
422
+ const polyMakeHint = (a_: TArg<Poly>, b_: TArg<Poly>) => {
423
+ const a = a_ as Poly;
424
+ const b = b_ as Poly;
343
425
  const v = newPoly(N);
344
426
  let cnt = 0;
345
427
  for (let i = 0; i < N; i++) {
@@ -353,16 +435,16 @@ function getDilithium(opts: DilithiumOpts) {
353
435
  const signRandBytes = 32;
354
436
  const seedCoder = splitCoder('seed', 32, 64, 32);
355
437
  // API & argument positions are exactly as in FIPS204.
356
- const internal: DSAInternal = {
357
- info: { type: 'internal-ml-dsa' },
358
- lengths: {
438
+ const internal: TRet<DSAInternal> = Object.freeze({
439
+ info: Object.freeze({ type: 'internal-ml-dsa' }),
440
+ lengths: Object.freeze({
359
441
  secretKey: secretCoder.bytesLen,
360
442
  publicKey: publicCoder.bytesLen,
361
443
  seed: 32,
362
444
  signature: sigCoder.bytesLen,
363
445
  signRand: signRandBytes,
364
- },
365
- keygen: (seed?: Uint8Array) => {
446
+ }),
447
+ keygen: (seed?: TArg<Uint8Array>) => {
366
448
  // H(𝜉||IntegerToBytes(𝑘, 1)||IntegerToBytes(ℓ, 1), 128) 2: ▷ expand seed
367
449
  const seedDst = new Uint8Array(32 + 2);
368
450
  const randSeed = seed === undefined;
@@ -381,7 +463,7 @@ function getDilithium(opts: DilithiumOpts) {
381
463
  const s2 = [];
382
464
  for (let i = L; i < L + K; i++)
383
465
  s2.push(RejBoundedPoly(xofPrime.get(i & 0xff, (i >> 8) & 0xff)));
384
- const s1Hat = s1.map((i) => NTT.encode(i.slice()));
466
+ const s1Hat = s1.map((i) => crystals.NTT.encode(i.slice()));
385
467
  const t0 = [];
386
468
  const t1 = [];
387
469
  const xof = XOF128(rho);
@@ -393,26 +475,33 @@ function getDilithium(opts: DilithiumOpts) {
393
475
  const aij = RejNTTPoly(xof.get(j, i)); // super slow!
394
476
  polyAdd(t, MultiplyNTTs(aij, s1Hat[j]));
395
477
  }
396
- NTT.decode(t);
478
+ crystals.NTT.decode(t);
397
479
  const { r0, r1 } = polyPowerRound(polyAdd(t, s2[i])); // (t1, t0) ← Power2Round(t, d)
398
480
  t0.push(r0);
399
481
  t1.push(r1);
400
482
  }
401
483
  const publicKey = publicCoder.encode([rho, t1]); // pk ← pkEncode(ρ, t1)
402
484
  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)
485
+ // sk ← skEncode(ρ, K,tr, s1, s2, t0)
486
+ const secretKey = secretCoder.encode([rho, K_, tr, s1, s2, t0]);
404
487
  xof.clean();
405
488
  xofPrime.clean();
406
489
  // 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 }
490
+ // Kyber512: { calls: 4, xofs: 12 }, Kyber768: { calls: 9, xofs: 27 },
491
+ // Kyber1024: { calls: 16, xofs: 48 }
492
+ // DSA44: { calls: 24, xofs: 24 }, DSA65: { calls: 41, xofs: 41 },
493
+ // DSA87: { calls: 71, xofs: 71 }
409
494
  cleanBytes(rho, rhoPrime, K_, s1, s2, s1Hat, t, t0, t1, tr, seedDst);
410
- return { publicKey, secretKey };
495
+ return {
496
+ publicKey: publicKey as TRet<Uint8Array>,
497
+ secretKey: secretKey as TRet<Uint8Array>,
498
+ };
411
499
  },
412
- getPublicKey: (secretKey: Uint8Array) => {
413
- const [rho, _K, _tr, s1, s2, _t0] = secretCoder.decode(secretKey); // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
500
+ getPublicKey: (secretKey: TArg<Uint8Array>): TRet<Uint8Array> => {
501
+ // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
502
+ const [rho, _K, _tr, s1, s2, _t0] = secretCoder.decode(secretKey);
414
503
  const xof = XOF128(rho);
415
- const s1Hat = s1.map((p) => NTT.encode(p.slice()));
504
+ const s1Hat = s1.map((p) => crystals.NTT.encode(p.slice()));
416
505
  const t1: Poly[] = [];
417
506
  const tmp = newPoly(N);
418
507
  for (let i = 0; i < K; i++) {
@@ -421,7 +510,7 @@ function getDilithium(opts: DilithiumOpts) {
421
510
  const aij = RejNTTPoly(xof.get(j, i)); // A_ij in NTT
422
511
  polyAdd(tmp, MultiplyNTTs(aij, s1Hat[j])); // += A_ij * s1_j
423
512
  }
424
- NTT.decode(tmp); // NTT⁻¹
513
+ crystals.NTT.decode(tmp); // NTT⁻¹
425
514
  polyAdd(tmp, s2[i]); // t_i = A·s1 + s2
426
515
  const { r1 } = polyPowerRound(tmp); // r1 = t1, r0 ≈ t0
427
516
  t1.push(r1);
@@ -431,13 +520,18 @@ function getDilithium(opts: DilithiumOpts) {
431
520
  return publicCoder.encode([rho, t1]);
432
521
  },
433
522
  // NOTE: random is optional.
434
- sign: (msg: Uint8Array, secretKey: Uint8Array, opts: SigOpts & DSAInternalOpts = {}) => {
523
+ sign: (
524
+ msg: TArg<Uint8Array>,
525
+ secretKey: TArg<Uint8Array>,
526
+ opts: TArg<SigOpts & DSAInternalOpts> = {}
527
+ ): TRet<Uint8Array> => {
435
528
  validateSigOpts(opts);
436
529
  validateInternalOpts(opts);
437
530
  let { extraEntropy: random, externalMu = false } = opts;
438
531
  // This part can be pre-cached per secretKey, but there is only minor performance improvement,
439
532
  // 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)
533
+ // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
534
+ const [rho, _K, tr, s1, s2, t0] = secretCoder.decode(secretKey);
441
535
  // Cache matrix to avoid re-compute later
442
536
  const A: Poly[][] = []; // A ← ExpandA(ρ)
443
537
  const xof = XOF128(rho);
@@ -447,15 +541,17 @@ function getDilithium(opts: DilithiumOpts) {
447
541
  A.push(pv);
448
542
  }
449
543
  xof.clean();
450
- for (let i = 0; i < L; i++) NTT.encode(s1[i]); // sˆ1 ← NTT(s1)
544
+ for (let i = 0; i < L; i++) crystals.NTT.encode(s1[i]); // sˆ1 ← NTT(s1)
451
545
  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)
546
+ crystals.NTT.encode(s2[i]); // sˆ2 ← NTT(s2)
547
+ crystals.NTT.encode(t0[i]); // tˆ0 ← NTT(t0)
454
548
  }
455
549
  // This part is per msg
456
550
  const mu = externalMu
457
551
  ? msg
458
- : shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest(); // 6: µ ← H(tr||M, 512) ▷ Compute message representative µ
552
+ : // 6: µ ← H(tr||M, 512)
553
+ // ▷ Compute message representative µ
554
+ shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest();
459
555
 
460
556
  // Compute private random seed
461
557
  const rnd =
@@ -480,13 +576,13 @@ function getDilithium(opts: DilithiumOpts) {
480
576
  // y ← ExpandMask(ρ , κ)
481
577
  for (let i = 0; i < L; i++, kappa++)
482
578
  y.push(ZCoder.decode(x256.get(kappa & 0xff, kappa >> 8)()));
483
- const z = y.map((i) => NTT.encode(i.slice()));
579
+ const z = y.map((i) => crystals.NTT.encode(i.slice()));
484
580
  const w = [];
485
581
  for (let i = 0; i < K; i++) {
486
582
  // w ← NTT−1(A ◦ NTT(y))
487
583
  const wi = newPoly(N);
488
584
  for (let j = 0; j < L; j++) polyAdd(wi, MultiplyNTTs(A[i][j], z[j]));
489
- NTT.decode(wi);
585
+ crystals.NTT.decode(wi);
490
586
  w.push(wi);
491
587
  }
492
588
  const w1 = w.map((j) => j.map(HighBits)); // w1 ← HighBits(w)
@@ -497,21 +593,22 @@ function getDilithium(opts: DilithiumOpts) {
497
593
  .update(W1Vec.encode(w1))
498
594
  .digest();
499
595
  // Verifer’s challenge
500
- const cHat = NTT.encode(SampleInBall(cTilde)); // c ← SampleInBall(c˜1); cˆ ← NTT(c)
596
+ // c ← SampleInBall(c˜1); cˆ ← NTT(c)
597
+ const cHat = crystals.NTT.encode(SampleInBall(cTilde));
501
598
  // ⟨⟨cs1⟩⟩ ← NTT−1(cˆ◦ sˆ1)
502
599
  const cs1 = s1.map((i) => MultiplyNTTs(i, cHat));
503
600
  for (let i = 0; i < L; i++) {
504
- polyAdd(NTT.decode(cs1[i]), y[i]); // z ← y + ⟨⟨cs1⟩⟩
601
+ polyAdd(crystals.NTT.decode(cs1[i]), y[i]); // z ← y + ⟨⟨cs1⟩⟩
505
602
  if (polyChknorm(cs1[i], GAMMA1 - BETA)) continue main_loop; // ||z||∞ ≥ γ1 − β
506
603
  }
507
604
  // cs1 is now z (▷ Signer’s response)
508
605
  let cnt = 0;
509
606
  const h = [];
510
607
  for (let i = 0; i < K; i++) {
511
- const cs2 = NTT.decode(MultiplyNTTs(s2[i], cHat)); // ⟨⟨cs2⟩⟩ ← NTT−1(cˆ◦ sˆ2)
608
+ const cs2 = crystals.NTT.decode(MultiplyNTTs(s2[i], cHat)); // ⟨⟨cs2⟩⟩ ← NTT−1(cˆ◦ sˆ2)
512
609
  const r0 = polySub(w[i], cs2).map(LowBits); // r0 ← LowBits(w − ⟨⟨cs2⟩⟩)
513
610
  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)
611
+ const ct0 = crystals.NTT.decode(MultiplyNTTs(t0[i], cHat)); // ⟨⟨ct0⟩⟩ ← NTT−1(cˆ◦ tˆ0)
515
612
  if (polyChknorm(ct0, GAMMA2)) continue main_loop;
516
613
  polyAdd(r0, ct0);
517
614
  // ▷ Signer’s hint
@@ -523,17 +620,21 @@ function getDilithium(opts: DilithiumOpts) {
523
620
  x256.clean();
524
621
  const res = sigCoder.encode([cTilde, cs1, h]); // σ ← sigEncode(c˜, z mod±q, h)
525
622
  // 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);
527
- return res;
623
+ cleanBytes(cTilde, cs1, h, cHat, w1, w, z, y, rhoprime, s1, s2, t0, ...A);
624
+ // `externalMu` hands ownership of `mu` to the caller,
625
+ // so only wipe the internally derived digest form here;
626
+ // zeroizing caller memory would break the caller's own reuse / verify path.
627
+ if (!externalMu) cleanBytes(mu);
628
+ return res as TRet<Uint8Array>;
528
629
  }
529
630
  // @ts-ignore
530
631
  throw new Error('Unreachable code path reached, report this error');
531
632
  },
532
633
  verify: (
533
- sig: Uint8Array,
534
- msg: Uint8Array,
535
- publicKey: Uint8Array,
536
- opts: DSAInternalOpts = {}
634
+ sig: TArg<Uint8Array>,
635
+ msg: TArg<Uint8Array>,
636
+ publicKey: TArg<Uint8Array>,
637
+ opts: TArg<DSAInternalOpts> = {}
537
638
  ) => {
538
639
  validateInternalOpts(opts);
539
640
  const { externalMu = false } = opts;
@@ -542,27 +643,30 @@ function getDilithium(opts: DilithiumOpts) {
542
643
  const tr = shake256(publicKey, { dkLen: TR_BYTES }); // 6: tr ← H(BytesToBits(pk), 512)
543
644
 
544
645
  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
646
+ // (c˜, z, h) ← sigDecode(σ)
647
+ // ▷ Signer’s commitment hash c ˜, response z and hint
648
+ const [cTilde, z, h] = sigCoder.decode(sig);
546
649
  if (h === false) return false; // if h = ⊥ then return false
547
650
  for (let i = 0; i < L; i++) if (polyChknorm(z[i], GAMMA1 - BETA)) return false;
548
651
  const mu = externalMu
549
652
  ? msg
550
- : shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest(); // 7: µ ← H(tr||M, 512)
653
+ : // 7: µ ← H(tr||M, 512)
654
+ shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest();
551
655
  // Compute verifer’s challenge from c˜
552
- const c = NTT.encode(SampleInBall(cTilde)); // c ← SampleInBall(c˜1)
656
+ const c = crystals.NTT.encode(SampleInBall(cTilde)); // c ← SampleInBall(c˜1)
553
657
  const zNtt = z.map((i) => i.slice()); // zNtt = NTT(z)
554
- for (let i = 0; i < L; i++) NTT.encode(zNtt[i]);
658
+ for (let i = 0; i < L; i++) crystals.NTT.encode(zNtt[i]);
555
659
  const wTick1 = [];
556
660
  const xof = XOF128(rho);
557
661
  for (let i = 0; i < K; i++) {
558
- const ct12d = MultiplyNTTs(NTT.encode(polyShiftl(t1[i])), c); //c * t1 * (2**d)
662
+ const ct12d = MultiplyNTTs(crystals.NTT.encode(polyShiftl(t1[i])), c); //c * t1 * (2**d)
559
663
  const Az = newPoly(N); // // A * z
560
664
  for (let j = 0; j < L; j++) {
561
665
  const aij = RejNTTPoly(xof.get(j, i)); // A[i][j] inplace
562
666
  polyAdd(Az, MultiplyNTTs(aij, zNtt[j]));
563
667
  }
564
668
  // wApprox = A*z - c*t1 * (2**d)
565
- const wApprox = NTT.decode(polySub(Az, ct12d));
669
+ const wApprox = crystals.NTT.decode(polySub(Az, ct12d));
566
670
  // Reconstruction of signer’s commitment
567
671
  wTick1.push(polyUseHint(wApprox, h[i])); // w ′ ← UseHint(h, w'approx )
568
672
  }
@@ -582,78 +686,99 @@ function getDilithium(opts: DilithiumOpts) {
582
686
  for (const t of z) if (polyChknorm(t, GAMMA1 - BETA)) return false;
583
687
  return equalBytes(cTilde, c2);
584
688
  },
585
- };
586
- return {
587
- info: { type: 'ml-dsa' },
689
+ });
690
+ return Object.freeze({
691
+ info: Object.freeze({ type: 'ml-dsa' }),
588
692
  internal,
589
693
  securityLevel: securityLevel,
590
694
  keygen: internal.keygen,
591
695
  lengths: internal.lengths,
592
696
  getPublicKey: internal.getPublicKey,
593
- sign: (msg: Uint8Array, secretKey: Uint8Array, opts: SigOpts = {}) => {
697
+ sign: (
698
+ msg: TArg<Uint8Array>,
699
+ secretKey: TArg<Uint8Array>,
700
+ opts: TArg<SigOpts> = {}
701
+ ): TRet<Uint8Array> => {
594
702
  validateSigOpts(opts);
595
703
  const M = getMessage(msg, opts.context);
596
704
  const res = internal.sign(M, secretKey, opts);
597
705
  cleanBytes(M);
598
- return res;
706
+ return res as TRet<Uint8Array>;
599
707
  },
600
- verify: (sig: Uint8Array, msg: Uint8Array, publicKey: Uint8Array, opts: VerOpts = {}) => {
708
+ verify: (
709
+ sig: TArg<Uint8Array>,
710
+ msg: TArg<Uint8Array>,
711
+ publicKey: TArg<Uint8Array>,
712
+ opts: TArg<VerOpts> = {}
713
+ ) => {
601
714
  validateVerOpts(opts);
602
715
  return internal.verify(sig, getMessage(msg, opts.context), publicKey);
603
716
  },
604
717
  prehash: (hash: CHash) => {
605
718
  checkHash(hash, securityLevel);
606
- return {
607
- info: { type: 'hashml-dsa' },
719
+ return Object.freeze({
720
+ info: Object.freeze({ type: 'hashml-dsa' }),
608
721
  securityLevel: securityLevel,
609
722
  lengths: internal.lengths,
610
723
  keygen: internal.keygen,
611
724
  getPublicKey: internal.getPublicKey,
612
- sign: (msg: Uint8Array, secretKey: Uint8Array, opts: SigOpts = {}) => {
725
+ sign: (
726
+ msg: TArg<Uint8Array>,
727
+ secretKey: TArg<Uint8Array>,
728
+ opts: TArg<SigOpts> = {}
729
+ ): TRet<Uint8Array> => {
613
730
  validateSigOpts(opts);
614
731
  const M = getMessagePrehash(hash, msg, opts.context);
615
732
  const res = internal.sign(M, secretKey, opts);
616
733
  cleanBytes(M);
617
- return res;
734
+ return res as TRet<Uint8Array>;
618
735
  },
619
- verify: (sig: Uint8Array, msg: Uint8Array, publicKey: Uint8Array, opts: VerOpts = {}) => {
736
+ verify: (
737
+ sig: TArg<Uint8Array>,
738
+ msg: TArg<Uint8Array>,
739
+ publicKey: TArg<Uint8Array>,
740
+ opts: TArg<VerOpts> = {}
741
+ ) => {
620
742
  validateVerOpts(opts);
621
743
  return internal.verify(sig, getMessagePrehash(hash, msg, opts.context), publicKey);
622
744
  },
623
- };
745
+ });
624
746
  },
625
- };
747
+ });
626
748
  }
627
749
 
628
750
  /** 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
- });
751
+ export const ml_dsa44: TRet<DSA> = /* @__PURE__ */ (() =>
752
+ getDilithium({
753
+ ...PARAMS[2],
754
+ CRH_BYTES: 64,
755
+ TR_BYTES: 64,
756
+ C_TILDE_BYTES: 32,
757
+ XOF128,
758
+ XOF256,
759
+ securityLevel: 128,
760
+ }))();
638
761
 
639
762
  /** 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
- });
763
+ export const ml_dsa65: TRet<DSA> = /* @__PURE__ */ (() =>
764
+ getDilithium({
765
+ ...PARAMS[3],
766
+ CRH_BYTES: 64,
767
+ TR_BYTES: 64,
768
+ C_TILDE_BYTES: 48,
769
+ XOF128,
770
+ XOF256,
771
+ securityLevel: 192,
772
+ }))();
649
773
 
650
774
  /** 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
- });
775
+ export const ml_dsa87: TRet<DSA> = /* @__PURE__ */ (() =>
776
+ getDilithium({
777
+ ...PARAMS[5],
778
+ CRH_BYTES: 64,
779
+ TR_BYTES: 64,
780
+ C_TILDE_BYTES: 64,
781
+ XOF128,
782
+ XOF256,
783
+ securityLevel: 256,
784
+ }))();