@noble/post-quantum 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- package/LICENSE +21 -0
- package/README.md +300 -0
- package/_crystals.d.ts +34 -0
- package/_crystals.d.ts.map +1 -0
- package/_crystals.js +171 -0
- package/_crystals.js.map +1 -0
- package/esm/_crystals.js +167 -0
- package/esm/_crystals.js.map +1 -0
- package/esm/index.js +3 -0
- package/esm/index.js.map +1 -0
- package/esm/ml-dsa.js +529 -0
- package/esm/ml-dsa.js.map +1 -0
- package/esm/ml-kem.js +361 -0
- package/esm/ml-kem.js.map +1 -0
- package/esm/package.json +10 -0
- package/esm/slh-dsa.js +602 -0
- package/esm/slh-dsa.js.map +1 -0
- package/esm/utils.js +86 -0
- package/esm/utils.js.map +1 -0
- package/index.d.ts +1 -0
- package/index.d.ts.map +1 -0
- package/index.js +3 -0
- package/index.js.map +1 -0
- package/ml-dsa.d.ts +37 -0
- package/ml-dsa.d.ts.map +1 -0
- package/ml-dsa.js +532 -0
- package/ml-dsa.js.map +1 -0
- package/ml-kem.d.ts +134 -0
- package/ml-kem.d.ts.map +1 -0
- package/ml-kem.js +364 -0
- package/ml-kem.js.map +1 -0
- package/package.json +100 -0
- package/slh-dsa.d.ts +70 -0
- package/slh-dsa.d.ts.map +1 -0
- package/slh-dsa.js +605 -0
- package/slh-dsa.js.map +1 -0
- package/src/_crystals.ts +197 -0
- package/src/index.ts +1 -0
- package/src/ml-dsa.ts +569 -0
- package/src/ml-kem.ts +403 -0
- package/src/package.json +3 -0
- package/src/slh-dsa.ts +771 -0
- package/src/utils.ts +113 -0
- package/utils.d.ts +38 -0
- package/utils.d.ts.map +1 -0
- package/utils.js +94 -0
- package/utils.js.map +1 -0
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
|
+
});
|