@noble/post-quantum 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
package/src/ml-dsa.ts ADDED
@@ -0,0 +1,569 @@
1
+ /*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */
2
+ import { shake256 } from '@noble/hashes/sha3';
3
+ import { genCrystals, XOF, XOF128, XOF256, XOF_AES } from './_crystals.js';
4
+ import {
5
+ BytesCoderLen,
6
+ Signer,
7
+ cleanBytes,
8
+ ensureBytes,
9
+ equalBytes,
10
+ randomBytes,
11
+ splitCoder,
12
+ vecCoder,
13
+ } from './utils.js';
14
+
15
+ /*
16
+ Lattice-based digital signature algorithm. See
17
+ [official site](https://www.pq-crystals.org/dilithium/index.shtml),
18
+ [repo](https://github.com/pq-crystals/dilithium).
19
+ Dilithium has similar internals to Kyber, but their keys and params are different.
20
+
21
+ Three versions are provided:
22
+
23
+ 1. Dilithium v3.0, v3.0 AES
24
+ 2. Dilithium v3.1, v3.1 AES
25
+ 3. ML-DSA aka [FIPS-204](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.ipd.pdf)
26
+ */
27
+
28
+ // Constants
29
+ const N = 256;
30
+ // 2**23 − 2**13 + 1, 23 bits: multiply will be 46. We have enough precision in JS to avoid bigints
31
+ const Q = 8380417;
32
+ const ROOT_OF_UNITY = 1753;
33
+ // f = 256**−1 mod q, pow(256, -1, q) = 8347681 (python3)
34
+ const F = 8347681;
35
+ const D = 13;
36
+ // Dilithium is kinda parametrized over GAMMA2, but everything will break with any other value.
37
+ const GAMMA2_1 = Math.floor((Q - 1) / 88) | 0;
38
+ const GAMMA2_2 = Math.floor((Q - 1) / 32) | 0;
39
+
40
+ type XofGet = ReturnType<ReturnType<XOF>['get']>;
41
+
42
+ type Param = {
43
+ K: number;
44
+ L: number;
45
+ D: number;
46
+ GAMMA1: number;
47
+ GAMMA2: number;
48
+ TAU: number;
49
+ ETA: number;
50
+ OMEGA: number;
51
+ };
52
+ // prettier-ignore
53
+ export const PARAMS: Record<string, Param> = {
54
+ 2: { K: 4, L: 4, D, GAMMA1: 2 ** 17, GAMMA2: GAMMA2_1, TAU: 39, ETA: 2, OMEGA: 80 },
55
+ 3: { K: 6, L: 5, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 49, ETA: 4, OMEGA: 55 },
56
+ 5: { K: 8, L: 7, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 60, ETA: 2, OMEGA: 75 },
57
+ } as const;
58
+
59
+ // NOTE: there is a lot cases where negative numbers used (with smod instead of mod).
60
+ type Poly = Int32Array;
61
+ const newPoly = (n: number) => new Int32Array(n);
62
+
63
+ const { mod, smod, NTT, bitsCoder } = genCrystals({
64
+ N,
65
+ Q,
66
+ F,
67
+ ROOT_OF_UNITY,
68
+ newPoly,
69
+ isKyber: false,
70
+ brvBits: 8,
71
+ });
72
+
73
+ const polyCoder = (d: number, compress?: (n: number) => number) =>
74
+ bitsCoder(d, {
75
+ encode: (i: number) => (compress ? compress(i) : i),
76
+ decode: (i: number) => (compress ? compress(i) : i),
77
+ });
78
+
79
+ const polyAdd = (a: Poly, b: Poly) => {
80
+ for (let i = 0; i < a.length; i++) a[i] = mod(a[i] + b[i]);
81
+ return a;
82
+ };
83
+ const polySub = (a: Poly, b: Poly): Poly => {
84
+ for (let i = 0; i < a.length; i++) a[i] = mod(a[i] - b[i]);
85
+ return a;
86
+ };
87
+
88
+ const polyShiftl = (p: Poly): Poly => {
89
+ for (let i = 0; i < N; i++) p[i] <<= D;
90
+ return p;
91
+ };
92
+
93
+ const polyChknorm = (p: Poly, B: number): boolean => {
94
+ // Not very sure about this, but FIPS204 doesn't provide any function for that :(
95
+ for (let i = 0; i < N; i++) if (Math.abs(smod(p[i])) >= B) return true;
96
+ return false;
97
+ };
98
+
99
+ const MultiplyNTTs = (a: Poly, b: Poly): Poly => {
100
+ // NOTE: we don't use montgomery reduction in code, since it requires 64 bit ints,
101
+ // which is not available in JS. mod(a[i] * b[i]) is ok, since Q is 23 bit,
102
+ // which means a[i] * b[i] is 46 bit, which is safe to use in JS. (number is 53 bits).
103
+ // Barrett reduction is slower than mod :(
104
+ const c = newPoly(N);
105
+ for (let i = 0; i < a.length; i++) c[i] = mod(a[i] * b[i]);
106
+ return c;
107
+ };
108
+
109
+ // Return poly in NTT representation
110
+ function RejNTTPoly(xof: XofGet) {
111
+ // Samples a polynomial ∈ Tq.
112
+ const r = newPoly(N);
113
+ // NOTE: we can represent 3xu24 as 4xu32, but it doesn't improve perf :(
114
+ for (let j = 0; j < N; ) {
115
+ const b = xof();
116
+ if (b.length % 3) throw new Error('RejNTTPoly: unaligned block');
117
+ for (let i = 0; j < N && i <= b.length - 3; i += 3) {
118
+ const t = (b[i + 0] | (b[i + 1] << 8) | (b[i + 2] << 16)) & 0x7fffff; // 3 bytes
119
+ if (t < Q) r[j++] = t;
120
+ }
121
+ }
122
+ return r;
123
+ }
124
+
125
+ type DilithiumOpts = {
126
+ K: number;
127
+ L: number;
128
+ GAMMA1: number;
129
+ GAMMA2: number;
130
+ TAU: number;
131
+ ETA: number;
132
+ OMEGA: number;
133
+ C_TILDE_BYTES: number;
134
+ CRH_BYTES: number;
135
+ TR_BYTES: number;
136
+ XOF128: XOF;
137
+ XOF256: XOF;
138
+ FIPS204?: boolean;
139
+ V31?: boolean;
140
+ };
141
+
142
+ function getDilithium(opts: DilithiumOpts): Signer {
143
+ const { K, L, GAMMA1, GAMMA2, TAU, ETA, OMEGA } = opts;
144
+ const { FIPS204, V31, CRH_BYTES, TR_BYTES, C_TILDE_BYTES, XOF128, XOF256 } = opts;
145
+
146
+ if (![2, 4].includes(ETA)) throw new Error('Wrong ETA');
147
+ if (![1 << 17, 1 << 19].includes(GAMMA1)) throw new Error('Wrong GAMMA1');
148
+ if (![GAMMA2_1, GAMMA2_2].includes(GAMMA2)) throw new Error('Wrong GAMMA2');
149
+ const BETA = TAU * ETA;
150
+
151
+ const decompose = (r: number) => {
152
+ // Decomposes r into (r1, r0) such that r ≡ r1(2γ2) + r0 mod q.
153
+ const rPlus = mod(r);
154
+ const r0 = smod(rPlus, 2 * GAMMA2) | 0;
155
+ if (rPlus - r0 === Q - 1) return { r1: 0 | 0, r0: (r0 - 1) | 0 };
156
+ const r1 = Math.floor((rPlus - r0) / (2 * GAMMA2)) | 0;
157
+ return { r1, r0 }; // r1 = HighBits, r0 = LowBits
158
+ };
159
+
160
+ const HighBits = (r: number) => decompose(r).r1;
161
+ const LowBits = (r: number) => decompose(r).r0;
162
+ const MakeHint = (z: number, r: number) => {
163
+ // Compute hint bit indicating whether adding z to r alters the high bits of r.
164
+
165
+ // From dilithium code
166
+ const res0 = z <= GAMMA2 || z > Q - GAMMA2 || (z === Q - GAMMA2 && r === 0) ? 0 : 1;
167
+ // from FIPS204:
168
+ // // const r1 = HighBits(r);
169
+ // // const v1 = HighBits(r + z);
170
+ // // const res1 = +(r1 !== v1);
171
+ // But they return different results! However, decompose is same.
172
+ // So, either there is a bug in Dilithium ref implementation or in FIPS204.
173
+ // For now, lets use dilithium one, so test vectors can be passed.
174
+ return res0;
175
+ };
176
+
177
+ const UseHint = (h: number, r: number) => {
178
+ // Returns the high bits of r adjusted according to hint h
179
+ const m = Math.floor((Q - 1) / (2 * GAMMA2));
180
+ const { r1, r0 } = decompose(r);
181
+ // 3: if h = 1 and r0 > 0 return (r1 + 1) mod m
182
+ // 4: if h = 1 and r0 ≤ 0 return (r1 − 1) mod m
183
+ if (h === 1) return r0 > 0 ? mod(r1 + 1, m) | 0 : mod(r1 - 1, m) | 0;
184
+ return r1 | 0;
185
+ };
186
+ const Power2Round = (r: number) => {
187
+ // Decomposes r into (r1, r0) such that r ≡ r1*(2**d) + r0 mod q.
188
+ const rPlus = mod(r);
189
+ const r0 = smod(rPlus, 2 ** D) | 0;
190
+ return { r1: Math.floor((rPlus - r0) / 2 ** D) | 0, r0 };
191
+ };
192
+
193
+ const hintCoder: BytesCoderLen<Poly[] | false> = {
194
+ bytesLen: OMEGA + K,
195
+ encode: (h: Poly[] | false) => {
196
+ if (h === false) throw new Error('hint.encode: hint is false'); // should never happen
197
+ const res = new Uint8Array(OMEGA + K);
198
+ for (let i = 0, k = 0; i < K; i++) {
199
+ for (let j = 0; j < N; j++) if (h[i][j] !== 0) res[k++] = j;
200
+ res[OMEGA + i] = k;
201
+ }
202
+ return res;
203
+ },
204
+ decode: (buf: Uint8Array) => {
205
+ const h = [];
206
+ let k = 0;
207
+ for (let i = 0; i < K; i++) {
208
+ const hi = newPoly(N);
209
+ if (buf[OMEGA + i] < k || buf[OMEGA + i] > OMEGA) return false;
210
+ for (let j = k; j < buf[OMEGA + i]; j++) {
211
+ if (j > k && buf[j] <= buf[j - 1]) return false;
212
+ hi[buf[j]] = 1;
213
+ }
214
+ k = buf[OMEGA + i];
215
+ h.push(hi);
216
+ }
217
+ for (let j = k; j < OMEGA; j++) if (buf[j] !== 0) return false;
218
+ return h;
219
+ },
220
+ };
221
+
222
+ const ETACoder = polyCoder(ETA === 2 ? 3 : 4, (i: number) => ETA - i);
223
+ const T0Coder = polyCoder(13, (i: number) => (1 << (D - 1)) - i);
224
+ const T1Coder = polyCoder(10);
225
+ // Requires smod. Need to fix!
226
+ const ZCoder = polyCoder(GAMMA1 === 1 << 17 ? 18 : 20, (i: number) => smod(GAMMA1 - i));
227
+ const W1Coder = polyCoder(GAMMA2 === GAMMA2_1 ? 6 : 4);
228
+ const W1Vec = vecCoder(W1Coder, K);
229
+ // Main structures
230
+ const publicCoder = splitCoder(32, vecCoder(T1Coder, K));
231
+ const secretCoder = splitCoder(
232
+ 32,
233
+ 32,
234
+ TR_BYTES,
235
+ vecCoder(ETACoder, L),
236
+ vecCoder(ETACoder, K),
237
+ vecCoder(T0Coder, K)
238
+ );
239
+ const sigCoder = splitCoder(C_TILDE_BYTES, vecCoder(ZCoder, L), hintCoder);
240
+ const CoefFromHalfByte =
241
+ ETA === 2
242
+ ? (n: number) => (n < 15 ? 2 - (n % 5) : false)
243
+ : (n: number) => (n < 9 ? 4 - n : false);
244
+
245
+ // Return poly in NTT representation
246
+ function RejBoundedPoly(xof: XofGet) {
247
+ // Samples an element a ∈ Rq with coeffcients in [−η, η] computed via rejection sampling from ρ.
248
+ const r: Poly = newPoly(N);
249
+ for (let j = 0; j < N; ) {
250
+ const b = xof();
251
+ for (let i = 0; j < N && i < b.length; i += 1) {
252
+ // half byte. Should be superfast with vector instructions. But very slow with js :(
253
+ const d1 = CoefFromHalfByte(b[i] & 0x0f);
254
+ const d2 = CoefFromHalfByte((b[i] >> 4) & 0x0f);
255
+ if (d1 !== false) r[j++] = d1;
256
+ if (j < N && d2 !== false) r[j++] = d2;
257
+ }
258
+ }
259
+ return r;
260
+ }
261
+
262
+ const SampleInBall = (seed: Uint8Array) => {
263
+ // Samples a polynomial c ∈ Rq with coeffcients from {−1, 0, 1} and Hamming weight τ
264
+ const pre = newPoly(N);
265
+ const s = shake256.create({}).update(seed.slice(0, 32));
266
+ const buf = new Uint8Array(shake256.blockLen);
267
+ s.xofInto(buf);
268
+ const masks = buf.slice(0, 8);
269
+ for (let i = N - TAU, pos = 8, maskPos = 0, maskBit = 0; i < N; i++) {
270
+ let b = i + 1;
271
+ for (; b > i; ) {
272
+ b = buf[pos++];
273
+ if (pos < shake256.blockLen) continue;
274
+ s.xofInto(buf);
275
+ pos = 0;
276
+ }
277
+ pre[i] = pre[b];
278
+ pre[b] = 1 - (((masks[maskPos] >> maskBit++) & 1) << 1);
279
+ if (maskBit >= 8) {
280
+ maskPos++;
281
+ maskBit = 0;
282
+ }
283
+ }
284
+ return pre;
285
+ };
286
+
287
+ const polyPowerRound = (p: Poly) => {
288
+ const res0 = newPoly(N);
289
+ const res1 = newPoly(N);
290
+ for (let i = 0; i < p.length; i++) {
291
+ const { r0, r1 } = Power2Round(p[i]);
292
+ res0[i] = r0;
293
+ res1[i] = r1;
294
+ }
295
+ return { r0: res0, r1: res1 };
296
+ };
297
+ const polyUseHint = (u: Poly, h: Poly): Poly => {
298
+ for (let i = 0; i < N; i++) u[i] = UseHint(h[i], u[i]);
299
+ return u;
300
+ };
301
+ const polyMakeHint = (a: Poly, b: Poly) => {
302
+ const v = newPoly(N);
303
+ let cnt = 0;
304
+ for (let i = 0; i < N; i++) {
305
+ const h = MakeHint(a[i], b[i]);
306
+ v[i] = h;
307
+ cnt += h;
308
+ }
309
+ return { v, cnt };
310
+ };
311
+
312
+ const signRandBytes = FIPS204 ? 32 : CRH_BYTES;
313
+ const seedCoder = splitCoder(32, V31 ? 64 : 32, 32);
314
+ const seedXOF = V31 ? XOF256 : XOF128;
315
+ // API & argument positions are exactly as in FIPS204.
316
+ return {
317
+ signRandBytes,
318
+ keygen: (seed = randomBytes(32)) => {
319
+ const [rho, rhoPrime, K_] = seedCoder.decode(shake256(seed, { dkLen: seedCoder.bytesLen }));
320
+ const xofPrime = seedXOF(rhoPrime);
321
+ const s1 = [];
322
+ for (let i = 0; i < L; i++) s1.push(RejBoundedPoly(xofPrime.get(i & 0xff, (i >> 8) & 0xff)));
323
+ const s2 = [];
324
+ for (let i = L; i < L + K; i++)
325
+ s2.push(RejBoundedPoly(xofPrime.get(i & 0xff, (i >> 8) & 0xff)));
326
+ const s1Hat = s1.map((i) => NTT.encode(i.slice()));
327
+ const t0 = [];
328
+ const t1 = [];
329
+ const xof = XOF128(rho);
330
+ const t = newPoly(N);
331
+ for (let i = 0; i < K; i++) {
332
+ // t ← NTT−1(A*NTT(s1)) + s2
333
+ t.fill(0); // don't-reallocate
334
+ for (let j = 0; j < L; j++) {
335
+ const aij = RejNTTPoly(xof.get(j, i)); // super slow!
336
+ polyAdd(t, MultiplyNTTs(aij, s1Hat[j]));
337
+ }
338
+ NTT.decode(t);
339
+ const { r0, r1 } = polyPowerRound(polyAdd(t, s2[i])); // (t1, t0) ← Power2Round(t, d)
340
+ t0.push(r0);
341
+ t1.push(r1);
342
+ }
343
+ const publicKey = publicCoder.encode([rho, t1]); // pk ← pkEncode(ρ, t1)
344
+ const tr = shake256(publicKey, { dkLen: TR_BYTES }); // tr ← H(BytesToBits(pk), 512)
345
+ const secretKey = secretCoder.encode([rho, K_, tr, s1, s2, t0]); // sk ← skEncode(ρ, K,tr, s1, s2, t0)
346
+ xof.clean();
347
+ xofPrime.clean();
348
+ // STATS
349
+ // Kyber512: { calls: 4, xofs: 12 }, Kyber768: { calls: 9, xofs: 27 }, Kyber1024: { calls: 16, xofs: 48 }
350
+ // DSA44: { calls: 24, xofs: 24 }, DSA65: { calls: 41, xofs: 41 }, DSA87: { calls: 71, xofs: 71 }
351
+ cleanBytes(rho, rhoPrime, K_, s1, s2, s1Hat, t, t0, t1, tr);
352
+ return { publicKey, secretKey };
353
+ },
354
+ // NOTE: random is optional.
355
+ sign: (secretKey: Uint8Array, msg: Uint8Array, random?: Uint8Array) => {
356
+ // This part can be pre-cached per secretKey, but there is only minor performance improvement,
357
+ // since we re-use a lot of variables to computation.
358
+ const [rho, _K, tr, s1, s2, t0] = secretCoder.decode(secretKey); // (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
359
+ // Cache matrix to avoid re-compute later
360
+ const A: Poly[][] = []; // A ← ExpandA(ρ)
361
+ const xof = XOF128(rho);
362
+ for (let i = 0; i < K; i++) {
363
+ const pv = [];
364
+ for (let j = 0; j < L; j++) pv.push(RejNTTPoly(xof.get(j, i)));
365
+ A.push(pv);
366
+ }
367
+ xof.clean();
368
+ for (let i = 0; i < L; i++) NTT.encode(s1[i]); // sˆ1 ← NTT(s1)
369
+ for (let i = 0; i < K; i++) {
370
+ NTT.encode(s2[i]); // sˆ2 ← NTT(s2)
371
+ NTT.encode(t0[i]); // tˆ0 ← NTT(t0)
372
+ }
373
+ // This part is per msg
374
+ const mu = shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest(); // 6: µ ← H(tr||M, 512) ▷ Compute message representative µ
375
+ let rhoprime; // Compute private random seed
376
+ if (FIPS204) {
377
+ const rnd = random ? random : new Uint8Array(32);
378
+ ensureBytes(rnd);
379
+ rhoprime = shake256.create({ dkLen: CRH_BYTES }).update(_K).update(rnd).update(mu).digest(); // ρ′← H(K||rnd||µ, 512)
380
+ } else {
381
+ rhoprime = random
382
+ ? random
383
+ : shake256.create({ dkLen: CRH_BYTES }).update(_K).update(mu).digest();
384
+ }
385
+ ensureBytes(rhoprime, CRH_BYTES);
386
+ const x256 = XOF256(rhoprime, ZCoder.bytesLen);
387
+ // Rejection sampling loop
388
+ main_loop: for (let kappa = 0; ; ) {
389
+ const y = [];
390
+ // y ← ExpandMask(ρ , κ)
391
+ for (let i = 0; i < L; i++, kappa++)
392
+ y.push(ZCoder.decode(x256.get(kappa & 0xff, kappa >> 8)()));
393
+ const z = y.map((i) => NTT.encode(i.slice()));
394
+ const w = [];
395
+ for (let i = 0; i < K; i++) {
396
+ // w ← NTT−1(A ◦ NTT(y))
397
+ const wi = newPoly(N);
398
+ for (let j = 0; j < L; j++) polyAdd(wi, MultiplyNTTs(A[i][j], z[j]));
399
+ NTT.decode(wi);
400
+ w.push(wi);
401
+ }
402
+ const w1 = w.map((j) => j.map(HighBits)); // w1 ← HighBits(w)
403
+ // Commitment hash: c˜ ∈{0, 1 2λ } ← H(µ||w1Encode(w1), 2λ)
404
+ const cTilde = shake256
405
+ .create({ dkLen: C_TILDE_BYTES })
406
+ .update(mu)
407
+ .update(W1Vec.encode(w1))
408
+ .digest();
409
+ // Verifer’s challenge
410
+ const cHat = NTT.encode(SampleInBall(cTilde.subarray(0, 32))); // c ← SampleInBall(c˜1); cˆ ← NTT(c)
411
+ // ⟨⟨cs1⟩⟩ ← NTT−1(cˆ◦ sˆ1)
412
+ const cs1 = s1.map((i) => MultiplyNTTs(i, cHat));
413
+ for (let i = 0; i < L; i++) {
414
+ polyAdd(NTT.decode(cs1[i]), y[i]); // z ← y + ⟨⟨cs1⟩⟩
415
+ if (polyChknorm(cs1[i], GAMMA1 - BETA)) continue main_loop; // ||z||∞ ≥ γ1 − β
416
+ }
417
+ // cs1 is now z (▷ Signer’s response)
418
+ let cnt = 0;
419
+ const h = [];
420
+ for (let i = 0; i < K; i++) {
421
+ const cs2 = NTT.decode(MultiplyNTTs(s2[i], cHat)); // ⟨⟨cs2⟩⟩ ← NTT−1(cˆ◦ sˆ2)
422
+ const r0 = polySub(w[i], cs2).map(LowBits); // r0 ← LowBits(w − ⟨⟨cs2⟩⟩)
423
+ if (polyChknorm(r0, GAMMA2 - BETA)) continue main_loop; // ||r0||∞ ≥ γ2 − β
424
+ const ct0 = NTT.decode(MultiplyNTTs(t0[i], cHat)); // ⟨⟨ct0⟩⟩ ← NTT−1(cˆ◦ tˆ0)
425
+ if (polyChknorm(ct0, GAMMA2)) continue main_loop;
426
+ polyAdd(r0, ct0);
427
+ // ▷ Signer’s hint
428
+ const hint = polyMakeHint(r0, w1[i]); // h ← MakeHint(−⟨⟨ct0⟩⟩, w− ⟨⟨cs2⟩⟩ + ⟨⟨ct0⟩⟩)
429
+ h.push(hint.v);
430
+ cnt += hint.cnt;
431
+ }
432
+ if (cnt > OMEGA) continue; // the number of 1’s in h is greater than ω
433
+ x256.clean();
434
+ const res = sigCoder.encode([cTilde, cs1, h]); // σ ← sigEncode(c˜, z mod±q, h)
435
+ // rho, _K, tr is subarray of secretKey, cannot clean.
436
+ cleanBytes(cTilde, cs1, h, cHat, w1, w, z, y, rhoprime, mu, s1, s2, t0, ...A);
437
+ return res;
438
+ }
439
+ // @ts-ignore
440
+ throw new Error('Unreachable code path reached, report this error');
441
+ },
442
+ verify: (publicKey: Uint8Array, msg: Uint8Array, sig: Uint8Array) => {
443
+ // ML-DSA.Verify(pk, M, σ): Verifes a signature σ for a message M.
444
+ const [rho, t1] = publicCoder.decode(publicKey); // (ρ, t1) ← pkDecode(pk)
445
+ const tr = shake256(publicKey, { dkLen: TR_BYTES }); // 6: tr ← H(BytesToBits(pk), 512)
446
+
447
+ if (sig.length !== sigCoder.bytesLen) return false; // return false instead of exception
448
+ const [cTilde, z, h] = sigCoder.decode(sig); // (c˜, z, h) ← sigDecode(σ), ▷ Signer’s commitment hash c ˜, response z and hint
449
+ if (h === false) return false; // if h = ⊥ then return false
450
+ for (let i = 0; i < L; i++) if (polyChknorm(z[i], GAMMA1 - BETA)) return false;
451
+ const mu = shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest(); // 7: µ ← H(tr||M, 512)
452
+ // Compute verifer’s challenge from c˜
453
+ const c = NTT.encode(SampleInBall(cTilde.subarray(0, 32))); // c ← SampleInBall(c˜1)
454
+ const zNtt = z.map((i) => i.slice()); // zNtt = NTT(z)
455
+ for (let i = 0; i < L; i++) NTT.encode(zNtt[i]);
456
+ const wTick1 = [];
457
+ const xof = XOF128(rho);
458
+ for (let i = 0; i < K; i++) {
459
+ const ct12d = MultiplyNTTs(NTT.encode(polyShiftl(t1[i])), c); //c * t1 * (2**d)
460
+ const Az = newPoly(N); // // A * z
461
+ for (let j = 0; j < L; j++) {
462
+ const aij = RejNTTPoly(xof.get(j, i)); // A[i][j] inplace
463
+ polyAdd(Az, MultiplyNTTs(aij, zNtt[j]));
464
+ }
465
+ // wApprox = A*z - c*t1 * (2**d)
466
+ const wApprox = NTT.decode(polySub(Az, ct12d));
467
+ // Reconstruction of signer’s commitment
468
+ wTick1.push(polyUseHint(wApprox, h[i])); // w ′ ← UseHint(h, w'approx )
469
+ }
470
+ xof.clean();
471
+ // c˜′← H (µ||w1Encode(w′1), 2λ), Hash it; this should match c˜
472
+ const c2 = shake256
473
+ .create({ dkLen: C_TILDE_BYTES })
474
+ .update(mu)
475
+ .update(W1Vec.encode(wTick1))
476
+ .digest();
477
+ if (FIPS204) {
478
+ // Additional checks in FIPS-204:
479
+ // [[ ||z||∞ < γ1 − β ]] and [[c ˜ = c˜′]] and [[number of 1’s in h is ≤ ω]]
480
+ for (const t of h) {
481
+ const sum = t.reduce((acc, i) => acc + i, 0);
482
+ if (!(sum <= OMEGA)) return false;
483
+ }
484
+ for (const t of z) if (polyChknorm(t, GAMMA1 - BETA)) return false;
485
+ }
486
+ return equalBytes(cTilde, c2);
487
+ },
488
+ };
489
+ }
490
+
491
+ function getDilithiumVersions(cfg: Partial<DilithiumOpts>) {
492
+ return {
493
+ dilithium2: getDilithium({ ...PARAMS[2], ...cfg } as DilithiumOpts),
494
+ dilithium3: getDilithium({ ...PARAMS[3], ...cfg } as DilithiumOpts),
495
+ dilithium5: getDilithium({ ...PARAMS[5], ...cfg } as DilithiumOpts),
496
+ };
497
+ }
498
+
499
+ // v30 is NIST round 3 submission, for original vectors and benchmarking.
500
+ // v31 is kyber: more secure than v30.
501
+ // ml-dsa is NIST FIPS 204, but it is still a draft and may change.
502
+
503
+ export const dilithium_v30 = /* @__PURE__ */ getDilithiumVersions({
504
+ CRH_BYTES: 48,
505
+ TR_BYTES: 48,
506
+ C_TILDE_BYTES: 32,
507
+ XOF128,
508
+ XOF256,
509
+ });
510
+
511
+ export const dilithium_v31 = /* @__PURE__ */ getDilithiumVersions({
512
+ CRH_BYTES: 64,
513
+ TR_BYTES: 32,
514
+ C_TILDE_BYTES: 32,
515
+ XOF128,
516
+ XOF256,
517
+ V31: true,
518
+ });
519
+
520
+ export const dilithium_v30_aes = /* @__PURE__ */ getDilithiumVersions({
521
+ CRH_BYTES: 48,
522
+ TR_BYTES: 48,
523
+ C_TILDE_BYTES: 32,
524
+ XOF128: XOF_AES,
525
+ XOF256: XOF_AES,
526
+ });
527
+
528
+ export const dilithium_v31_aes = /* @__PURE__ */ getDilithiumVersions({
529
+ CRH_BYTES: 64,
530
+ TR_BYTES: 32,
531
+ C_TILDE_BYTES: 32,
532
+ XOF128: XOF_AES,
533
+ XOF256: XOF_AES,
534
+ V31: true,
535
+ });
536
+
537
+ // ML-DSA
538
+ export const ml_dsa44 = /* @__PURE__ */ getDilithium({
539
+ ...PARAMS[2],
540
+ CRH_BYTES: 64,
541
+ TR_BYTES: 64,
542
+ C_TILDE_BYTES: 32,
543
+ XOF128,
544
+ XOF256,
545
+ V31: true,
546
+ FIPS204: true,
547
+ });
548
+
549
+ export const ml_dsa65 = /* @__PURE__ */ getDilithium({
550
+ ...PARAMS[3],
551
+ CRH_BYTES: 64,
552
+ TR_BYTES: 64,
553
+ C_TILDE_BYTES: 48,
554
+ XOF128,
555
+ XOF256,
556
+ V31: true,
557
+ FIPS204: true,
558
+ });
559
+
560
+ export const ml_dsa87 = /* @__PURE__ */ getDilithium({
561
+ ...PARAMS[5],
562
+ CRH_BYTES: 64,
563
+ TR_BYTES: 64,
564
+ C_TILDE_BYTES: 64,
565
+ XOF128,
566
+ XOF256,
567
+ V31: true,
568
+ FIPS204: true,
569
+ });