@noble/post-quantum 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -89
- package/_crystals.d.ts +8 -3
- package/_crystals.js +37 -9
- package/falcon.d.ts +0 -1
- package/falcon.js +82 -54
- package/hybrid.d.ts +5 -16
- package/hybrid.js +121 -51
- package/index.d.ts +0 -1
- package/index.js +0 -1
- package/ml-dsa.d.ts +28 -2
- package/ml-dsa.js +62 -15
- package/ml-kem.d.ts +45 -4
- package/ml-kem.js +132 -41
- package/package.json +9 -17
- package/slh-dsa.d.ts +23 -3
- package/slh-dsa.js +93 -49
- package/src/_crystals.ts +44 -10
- package/src/falcon.ts +80 -52
- package/src/hybrid.ts +115 -50
- package/src/ml-dsa.ts +71 -19
- package/src/ml-kem.ts +174 -42
- package/src/slh-dsa.ts +105 -56
- package/src/utils.ts +71 -15
- package/utils.d.ts +18 -2
- package/utils.js +63 -17
- package/_crystals.d.ts.map +0 -1
- package/_crystals.js.map +0 -1
- package/falcon.d.ts.map +0 -1
- package/falcon.js.map +0 -1
- package/hybrid.d.ts.map +0 -1
- package/hybrid.js.map +0 -1
- package/index.d.ts.map +0 -1
- package/index.js.map +0 -1
- package/ml-dsa.d.ts.map +0 -1
- package/ml-dsa.js.map +0 -1
- package/ml-kem.d.ts.map +0 -1
- package/ml-kem.js.map +0 -1
- package/slh-dsa.d.ts.map +0 -1
- package/slh-dsa.js.map +0 -1
- package/utils.d.ts.map +0 -1
- package/utils.js.map +0 -1
package/src/ml-kem.ts
CHANGED
|
@@ -131,23 +131,33 @@ const polyCoder = (d: number) => (d === 12 ? byteCoder(12) : crystals.bitsCoder(
|
|
|
131
131
|
// Poly is mod Q, so 12 bits
|
|
132
132
|
type Poly = Uint16Array;
|
|
133
133
|
|
|
134
|
+
// Coefficients always stay reduced in [0, Q) here (samplers, NTT and coders all reduce),
|
|
135
|
+
// so one conditional correction replaces the generic mod().
|
|
134
136
|
function polyAdd(a_: TArg<Poly>, b_: TArg<Poly>) {
|
|
135
137
|
const a = a_ as Poly;
|
|
136
138
|
const b = b_ as Poly;
|
|
137
139
|
// Mutates `a` in place; callers must pass two N=256 polynomials.
|
|
138
|
-
for (let i = 0; i < N; i++)
|
|
140
|
+
for (let i = 0; i < N; i++) {
|
|
141
|
+
const r = a[i] + b[i]; // a += b
|
|
142
|
+
a[i] = r >= Q ? r - Q : r;
|
|
143
|
+
}
|
|
139
144
|
}
|
|
140
145
|
function polySub(a_: TArg<Poly>, b_: TArg<Poly>) {
|
|
141
146
|
const a = a_ as Poly;
|
|
142
147
|
const b = b_ as Poly;
|
|
143
148
|
// Mutates `a` in place; callers must pass two N=256 polynomials.
|
|
144
|
-
for (let i = 0; i < N; i++)
|
|
149
|
+
for (let i = 0; i < N; i++) {
|
|
150
|
+
const r = a[i] - b[i]; // a -= b
|
|
151
|
+
a[i] = r < 0 ? r + Q : r;
|
|
152
|
+
}
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
// FIPS-203: Computes the product of two degree-one polynomials with respect to a quadratic modulus
|
|
148
156
|
function BaseCaseMultiply(a0: number, a1: number, b0: number, b1: number, zeta: number) {
|
|
149
157
|
// `zeta` here is Algorithm 11's γ = ζ^(2BitRev_7(i)+1).
|
|
150
|
-
|
|
158
|
+
// Reduce a1*b1 before multiplying by zeta: a1*b1*zeta would reach ~2^35, forcing JS engines
|
|
159
|
+
// into slow float fmod; with the extra reduction every intermediate fits int32.
|
|
160
|
+
const c0 = crystals.mod(crystals.mod(a1 * b1) * zeta + a0 * b0);
|
|
151
161
|
const c1 = crystals.mod(a0 * b1 + a1 * b0);
|
|
152
162
|
return { c0, c1 };
|
|
153
163
|
}
|
|
@@ -169,6 +179,33 @@ function MultiplyNTTs(f_: TArg<Poly>, g_: TArg<Poly>): TRet<Poly> {
|
|
|
169
179
|
|
|
170
180
|
type PRF = (l: number, key: Uint8Array, nonce: number) => Uint8Array;
|
|
171
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Prepared (pre-expanded) ML-KEM public key. Experimental prototype.
|
|
184
|
+
* Caches only public data: packed ek, the expanded matrix Â, decoded t̂ and H(ek). No secret
|
|
185
|
+
* material is retained between calls; secret keys passed to `decapsulate` are decoded and wiped
|
|
186
|
+
* per call, exactly like the one-shot API. `clean()` wipes the expanded Â/t̂ cache; the packed
|
|
187
|
+
* public key and H(ek) are public and are not wiped. The object must not be used afterwards.
|
|
188
|
+
*/
|
|
189
|
+
export type KEMPrepared = {
|
|
190
|
+
/**
|
|
191
|
+
* Detached copy of the source public key. Treat as read-only while the prepared object is in use.
|
|
192
|
+
* Callers may wipe it after final use; any mutation invalidates subsequent operations.
|
|
193
|
+
*/
|
|
194
|
+
publicKey: Uint8Array;
|
|
195
|
+
/** Same as `KEM.encapsulate`, minus per-call ek re-validation and  re-expansion. */
|
|
196
|
+
encapsulate: (msg?: Uint8Array) => { cipherText: Uint8Array; sharedSecret: Uint8Array };
|
|
197
|
+
/**
|
|
198
|
+
* Same as `KEM.decapsulate`; throws if `secretKey` does not embed this public key.
|
|
199
|
+
* The embedded-ek byte comparison plus stored-hash comparison is equivalent to the
|
|
200
|
+
* FIPS 203 §7.3 hash input check.
|
|
201
|
+
*/
|
|
202
|
+
decapsulate: (cipherText: Uint8Array, secretKey: Uint8Array) => Uint8Array;
|
|
203
|
+
/** Wipe cached (public) data. */
|
|
204
|
+
clean: () => void;
|
|
205
|
+
};
|
|
206
|
+
/** KEM with prepared-key support. */
|
|
207
|
+
export type MLKEM = KEM & { prepare: (publicKey: Uint8Array) => KEMPrepared };
|
|
208
|
+
|
|
172
209
|
type XofGet = ReturnType<ReturnType<XOF>['get']>;
|
|
173
210
|
|
|
174
211
|
type KyberOpts = KEMParam & {
|
|
@@ -253,6 +290,38 @@ const genKPKE = (opts_: TArg<KyberOpts>) => {
|
|
|
253
290
|
const secretCoder = vecCoder(polyCoder(12), K);
|
|
254
291
|
const cipherCoder = splitCoder('ciphertext', vecCoder(polyU, K), polyV);
|
|
255
292
|
const seedCoder = splitCoder('seed', 32, 32);
|
|
293
|
+
// Algorithm 14 (K-PKE.Encrypt) core, after ek parsing. `tHat` and every poly returned by
|
|
294
|
+
// `getA(i, j)` are treated as disposable scratch: they are mutated in place and wiped/dropped,
|
|
295
|
+
// so callers holding cached copies must pass fresh copies.
|
|
296
|
+
const encryptCore = (
|
|
297
|
+
tHat: TArg<Poly[]>,
|
|
298
|
+
getA: TArg<(i: number, j: number) => Poly>,
|
|
299
|
+
msg: TArg<Uint8Array>,
|
|
300
|
+
seed: TArg<Uint8Array>
|
|
301
|
+
): TRet<Uint8Array> => {
|
|
302
|
+
const rHat = [];
|
|
303
|
+
for (let i = 0; i < K; i++) rHat.push(crystals.NTT.encode(sampleCBD(PRF, seed, i, ETA1)));
|
|
304
|
+
const tmp2 = new Uint16Array(N);
|
|
305
|
+
const u = [];
|
|
306
|
+
for (let i = 0; i < K; i++) {
|
|
307
|
+
const e1 = sampleCBD(PRF, seed, K + i, ETA2);
|
|
308
|
+
const tmp = new Uint16Array(N);
|
|
309
|
+
for (let j = 0; j < K; j++) {
|
|
310
|
+
const aij = getA(i, j); // A[j][i], inplace transpose access
|
|
311
|
+
polyAdd(tmp, MultiplyNTTs(aij, rHat[j])); // t += aij * rHat[j]
|
|
312
|
+
}
|
|
313
|
+
polyAdd(e1, crystals.NTT.decode(tmp)); // e1 += tmp
|
|
314
|
+
u.push(e1);
|
|
315
|
+
polyAdd(tmp2, MultiplyNTTs(tHat[i], rHat[i])); // t2 += tHat[i] * rHat[i]
|
|
316
|
+
cleanBytes(tmp);
|
|
317
|
+
}
|
|
318
|
+
const e2 = sampleCBD(PRF, seed, 2 * K, ETA2);
|
|
319
|
+
polyAdd(e2, crystals.NTT.decode(tmp2)); // e2 += tmp2
|
|
320
|
+
const v = poly1.decode(msg); // encode plaintext m into polynomial v
|
|
321
|
+
polyAdd(v, e2); // v += e2
|
|
322
|
+
cleanBytes(tHat, rHat, tmp2, e2);
|
|
323
|
+
return cipherCoder.encode([u, v]) as TRet<Uint8Array>;
|
|
324
|
+
};
|
|
256
325
|
return {
|
|
257
326
|
secretCoder,
|
|
258
327
|
lengths: {
|
|
@@ -297,30 +366,31 @@ const genKPKE = (opts_: TArg<KyberOpts>) => {
|
|
|
297
366
|
seed: TArg<Uint8Array>
|
|
298
367
|
): TRet<Uint8Array> => {
|
|
299
368
|
const [tHat, rho] = publicCoder.decode(publicKey);
|
|
300
|
-
const rHat = [];
|
|
301
|
-
for (let i = 0; i < K; i++) rHat.push(crystals.NTT.encode(sampleCBD(PRF, seed, i, ETA1)));
|
|
302
369
|
const x = XOF(rho);
|
|
303
|
-
const
|
|
304
|
-
const u = [];
|
|
305
|
-
for (let i = 0; i < K; i++) {
|
|
306
|
-
const e1 = sampleCBD(PRF, seed, K + i, ETA2);
|
|
307
|
-
const tmp = new Uint16Array(N);
|
|
308
|
-
for (let j = 0; j < K; j++) {
|
|
309
|
-
const aij = SampleNTT(x.get(i, j)); // A[j][i], inplace transpose access
|
|
310
|
-
polyAdd(tmp, MultiplyNTTs(aij, rHat[j])); // t += aij * rHat[j]
|
|
311
|
-
}
|
|
312
|
-
polyAdd(e1, crystals.NTT.decode(tmp)); // e1 += tmp
|
|
313
|
-
u.push(e1);
|
|
314
|
-
polyAdd(tmp2, MultiplyNTTs(tHat[i], rHat[i])); // t2 += tHat[i] * rHat[i]
|
|
315
|
-
cleanBytes(tmp);
|
|
316
|
-
}
|
|
370
|
+
const res = encryptCore(tHat as Poly[], (i, j) => SampleNTT(x.get(i, j)) as Poly, msg, seed);
|
|
317
371
|
x.clean();
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
372
|
+
return res;
|
|
373
|
+
},
|
|
374
|
+
// Expands the full  matrix (public data derived from rho) once, so repeated encryptions
|
|
375
|
+
// against the same ek skip the K² SampleNTT XOF expansions. Cached polys are copied per
|
|
376
|
+
// call because encryptCore mutates its inputs in place.
|
|
377
|
+
prepare: (publicKey: TArg<Uint8Array>) => {
|
|
378
|
+
const [tHat, rho] = publicCoder.decode(publicKey);
|
|
379
|
+
const x = XOF(rho);
|
|
380
|
+
const A: Poly[] = [];
|
|
381
|
+
for (let i = 0; i < K; i++)
|
|
382
|
+
for (let j = 0; j < K; j++) A.push(SampleNTT(x.get(i, j)) as Poly);
|
|
383
|
+
x.clean();
|
|
384
|
+
return {
|
|
385
|
+
encrypt: (msg: TArg<Uint8Array>, seed: TArg<Uint8Array>): TRet<Uint8Array> =>
|
|
386
|
+
encryptCore(
|
|
387
|
+
(tHat as Poly[]).map((p) => p.slice() as Poly),
|
|
388
|
+
(i, j) => A[i * K + j].slice() as Poly,
|
|
389
|
+
msg,
|
|
390
|
+
seed
|
|
391
|
+
),
|
|
392
|
+
clean: () => cleanBytes(tHat as Poly[], A),
|
|
393
|
+
};
|
|
324
394
|
},
|
|
325
395
|
decrypt: (cipherText: TArg<Uint8Array>, privateKey: TArg<Uint8Array>): TRet<Uint8Array> => {
|
|
326
396
|
const [u, v] = cipherCoder.decode(cipherText);
|
|
@@ -344,7 +414,7 @@ const genKPKE = (opts_: TArg<KyberOpts>) => {
|
|
|
344
414
|
* mismatch, and zeroizing the non-returned shared-secret candidate; JS/JIT still provides no
|
|
345
415
|
* constant-time guarantees for that path.
|
|
346
416
|
*/
|
|
347
|
-
function createKyber(opts: TArg<KyberOpts>): TRet<
|
|
417
|
+
function createKyber(opts: TArg<KyberOpts>): TRet<MLKEM> {
|
|
348
418
|
const rawOpts = opts as KyberOpts;
|
|
349
419
|
const KPKE = genKPKE(rawOpts);
|
|
350
420
|
const { HASH256, HASH512, KDF } = rawOpts;
|
|
@@ -352,6 +422,17 @@ function createKyber(opts: TArg<KyberOpts>): TRet<KEM> {
|
|
|
352
422
|
const secretCoder = splitCoder('secretKey', lengths.secretKey, lengths.publicKey, 32, 32);
|
|
353
423
|
const msgLen = 32;
|
|
354
424
|
const seedLen = 64;
|
|
425
|
+
// FIPS-203 includes additional verification check for modulus
|
|
426
|
+
const validateModulus = (publicKey: TArg<Uint8Array>, fn: string) => {
|
|
427
|
+
const eke = (publicKey as Uint8Array).subarray(0, 384 * rawOpts.K);
|
|
428
|
+
// Copy because of inplace encoding
|
|
429
|
+
const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(copyBytes(eke)));
|
|
430
|
+
// (Modulus check.) Perform the computation ek ← ByteEncode12(ByteDecode12(eke)).
|
|
431
|
+
// If ek = ̸ eke, the input is invalid. (See Section 4.2.1.)
|
|
432
|
+
const ok = equalBytes(ek, eke);
|
|
433
|
+
cleanBytes(ek);
|
|
434
|
+
if (!ok) throw new Error(`ML-KEM.${fn}: wrong publicKey modulus`);
|
|
435
|
+
};
|
|
355
436
|
const kemLengths = Object.freeze({
|
|
356
437
|
...lengths,
|
|
357
438
|
seed: 64,
|
|
@@ -381,18 +462,7 @@ function createKyber(opts: TArg<KyberOpts>): TRet<KEM> {
|
|
|
381
462
|
encapsulate: (publicKey: TArg<Uint8Array>, msg: TArg<Uint8Array> = randomBytes(msgLen)) => {
|
|
382
463
|
abytes(publicKey, lengths.publicKey, 'publicKey');
|
|
383
464
|
abytes(msg, msgLen, 'message');
|
|
384
|
-
|
|
385
|
-
// FIPS-203 includes additional verification check for modulus
|
|
386
|
-
const eke = publicKey.subarray(0, 384 * opts.K);
|
|
387
|
-
// Copy because of inplace encoding
|
|
388
|
-
const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(copyBytes(eke)));
|
|
389
|
-
// (Modulus check.) Perform the computation ek ← ByteEncode12(ByteDecode12(eke)).
|
|
390
|
-
// If ek = ̸ eke, the input is invalid. (See Section 4.2.1.)
|
|
391
|
-
if (!equalBytes(ek, eke)) {
|
|
392
|
-
cleanBytes(ek);
|
|
393
|
-
throw new Error('ML-KEM.encapsulate: wrong publicKey modulus');
|
|
394
|
-
}
|
|
395
|
-
cleanBytes(ek);
|
|
465
|
+
validateModulus(publicKey, 'encapsulate');
|
|
396
466
|
// derive randomness
|
|
397
467
|
const kr = HASH512.create().update(msg).update(HASH256(publicKey)).digest();
|
|
398
468
|
const cipherText = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64));
|
|
@@ -422,9 +492,60 @@ function createKyber(opts: TArg<KyberOpts>): TRet<KEM> {
|
|
|
422
492
|
// if ciphertexts do not match, “implicitly reject”
|
|
423
493
|
const isValid = equalBytes(cipherText, cipherText2);
|
|
424
494
|
const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
|
|
425
|
-
|
|
495
|
+
// kr[32:64] is the derived K-PKE encryption randomness: wipe it like encapsulate() does.
|
|
496
|
+
cleanBytes(msg, cipherText2, kr.subarray(32), !isValid ? Khat : Kbar);
|
|
426
497
|
return (isValid ? Khat : Kbar) as TRet<Uint8Array>;
|
|
427
498
|
},
|
|
499
|
+
/**
|
|
500
|
+
* Experimental prototype: pre-expand a public key so repeated encapsulate/decapsulate
|
|
501
|
+
* against the same key skip re-validation, H(ek), t̂ decoding and the K² SampleNTT
|
|
502
|
+
* XOF expansions of Â. Only public data is cached; see {@link KEMPrepared}.
|
|
503
|
+
*/
|
|
504
|
+
prepare: (publicKey: TArg<Uint8Array>): TRet<KEMPrepared> => {
|
|
505
|
+
abytes(publicKey, lengths.publicKey, 'publicKey');
|
|
506
|
+
validateModulus(publicKey, 'prepare');
|
|
507
|
+
const ek = copyBytes(publicKey); // detach from the caller before caching
|
|
508
|
+
const publicKeyHash = HASH256(ek);
|
|
509
|
+
const cached = KPKE.prepare(ek);
|
|
510
|
+
return Object.freeze({
|
|
511
|
+
publicKey: ek as TRet<Uint8Array>,
|
|
512
|
+
encapsulate: (msg: TArg<Uint8Array> = randomBytes(msgLen)) => {
|
|
513
|
+
abytes(msg, msgLen, 'message');
|
|
514
|
+
const kr = HASH512.create().update(msg).update(publicKeyHash).digest();
|
|
515
|
+
const cipherText = cached.encrypt(msg, kr.subarray(32, 64));
|
|
516
|
+
cleanBytes(kr.subarray(32));
|
|
517
|
+
return {
|
|
518
|
+
cipherText: cipherText as TRet<Uint8Array>,
|
|
519
|
+
sharedSecret: kr.subarray(0, 32) as TRet<Uint8Array>,
|
|
520
|
+
};
|
|
521
|
+
},
|
|
522
|
+
decapsulate: (
|
|
523
|
+
cipherText: TArg<Uint8Array>,
|
|
524
|
+
secretKey: TArg<Uint8Array>
|
|
525
|
+
): TRet<Uint8Array> => {
|
|
526
|
+
abytes(secretKey, secretCoder.bytesLen, 'secretKey');
|
|
527
|
+
abytes(cipherText, lengths.cipherText, 'cipherText');
|
|
528
|
+
const [sk, ekEmbedded, storedHash, z] = secretCoder.decode(secretKey);
|
|
529
|
+
// Under KEMPrepared's read-only publicKey contract, bind dk to the prepared key.
|
|
530
|
+
// Together with publicKeyHash = H(ek) computed in prepare(), this is equivalent to (and
|
|
531
|
+
// stronger than) FIPS 203 §7.3's `H(dk[384k : 768k+32]) == dk[768k+32 : 768k+64]`.
|
|
532
|
+
if (!equalBytes(ekEmbedded, ek) || !equalBytes(storedHash, publicKeyHash))
|
|
533
|
+
throw new Error('ML-KEM.decapsulate: secretKey does not match prepared publicKey');
|
|
534
|
+
const msg = KPKE.decrypt(cipherText, sk);
|
|
535
|
+
// derive randomness, Khat, rHat = G(mHat || h)
|
|
536
|
+
const kr = HASH512.create().update(msg).update(publicKeyHash).digest();
|
|
537
|
+
const Khat = kr.subarray(0, 32);
|
|
538
|
+
// re-encrypt using the derived randomness and cached Â/t̂
|
|
539
|
+
const cipherText2 = cached.encrypt(msg, kr.subarray(32, 64));
|
|
540
|
+
// if ciphertexts do not match, “implicitly reject”
|
|
541
|
+
const isValid = equalBytes(cipherText, cipherText2);
|
|
542
|
+
const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
|
|
543
|
+
cleanBytes(msg, cipherText2, kr.subarray(32), !isValid ? Khat : Kbar);
|
|
544
|
+
return (isValid ? Khat : Kbar) as TRet<Uint8Array>;
|
|
545
|
+
},
|
|
546
|
+
clean: cached.clean,
|
|
547
|
+
}) as TRet<KEMPrepared>;
|
|
548
|
+
},
|
|
428
549
|
});
|
|
429
550
|
}
|
|
430
551
|
|
|
@@ -458,18 +579,29 @@ const mk = (params: KEMParam) =>
|
|
|
458
579
|
/**
|
|
459
580
|
* ML-KEM-512: Table 2 row `k=2, η1=3, η2=2, du=10, dv=4`; Table 3 sizes `800/1632/768/32`.
|
|
460
581
|
* The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
|
|
582
|
+
* @example
|
|
583
|
+
* Generate deterministic ML-KEM-512 keys, encapsulate a shared secret, and decapsulate it.
|
|
584
|
+
* ```ts
|
|
585
|
+
* import { ml_kem512 } from '@noble/post-quantum/ml-kem.js';
|
|
586
|
+
* const seed = new Uint8Array(ml_kem512.lengths.seed!);
|
|
587
|
+
* const { secretKey, publicKey } = ml_kem512.keygen(seed);
|
|
588
|
+
* const msg = new Uint8Array(ml_kem512.lengths.msgRand!);
|
|
589
|
+
* const { cipherText, sharedSecret } = ml_kem512.encapsulate(publicKey, msg);
|
|
590
|
+
* const recovered = ml_kem512.decapsulate(cipherText, secretKey);
|
|
591
|
+
* const publicKey2 = ml_kem512.getPublicKey(secretKey);
|
|
592
|
+
* ```
|
|
461
593
|
*/
|
|
462
|
-
export const ml_kem512: TRet<
|
|
594
|
+
export const ml_kem512: TRet<MLKEM> = /* @__PURE__ */ (() => mk(PARAMS[512]))();
|
|
463
595
|
/**
|
|
464
596
|
* ML-KEM-768: Table 2 row `k=3, η1=2, η2=2, du=10, dv=4`; Table 3 sizes `1184/2400/1088/32`.
|
|
465
597
|
* The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
|
|
466
598
|
*/
|
|
467
|
-
export const ml_kem768: TRet<
|
|
599
|
+
export const ml_kem768: TRet<MLKEM> = /* @__PURE__ */ (() => mk(PARAMS[768]))();
|
|
468
600
|
/**
|
|
469
601
|
* ML-KEM-1024: Table 2 row `k=4, η1=2, η2=2, du=11, dv=5`; Table 3 sizes `1568/3168/1568/32`.
|
|
470
602
|
* The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
|
|
471
603
|
*/
|
|
472
|
-
export const ml_kem1024: TRet<
|
|
604
|
+
export const ml_kem1024: TRet<MLKEM> = /* @__PURE__ */ (() => mk(PARAMS[1024]))();
|
|
473
605
|
|
|
474
606
|
// NOTE: for tests only, don't use. This keeps the exact internal ML-KEM math surfaces available
|
|
475
607
|
// without re-implementing them in separate test code.
|
package/src/slh-dsa.ts
CHANGED
|
@@ -28,15 +28,10 @@
|
|
|
28
28
|
*/
|
|
29
29
|
/*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */
|
|
30
30
|
import { hmac } from '@noble/hashes/hmac.js';
|
|
31
|
+
import { bytesToNumberBE, numberToBytesBE } from '@noble/curves/utils.js';
|
|
31
32
|
import { sha256, sha512 } from '@noble/hashes/sha2.js';
|
|
32
33
|
import { shake256 } from '@noble/hashes/sha3.js';
|
|
33
|
-
import {
|
|
34
|
-
bytesToHex,
|
|
35
|
-
concatBytes,
|
|
36
|
-
createView,
|
|
37
|
-
hexToBytes,
|
|
38
|
-
type CHash,
|
|
39
|
-
} from '@noble/hashes/utils.js';
|
|
34
|
+
import { concatBytes, createView, type CHash } from '@noble/hashes/utils.js';
|
|
40
35
|
import {
|
|
41
36
|
abytes,
|
|
42
37
|
checkHash,
|
|
@@ -123,12 +118,16 @@ const AddressType = {
|
|
|
123
118
|
/** Address byte array of size `ADDR_BYTES`. */
|
|
124
119
|
export type ADRS = Uint8Array;
|
|
125
120
|
|
|
126
|
-
/** Hash and tweakable-hash callbacks bound to one SLH-DSA keypair context.
|
|
121
|
+
/** Hash and tweakable-hash callbacks bound to one SLH-DSA keypair context.
|
|
122
|
+
* Buffer-aliasing contract: `PRFaddr`, `thash1` and `thashN` return views into per-context
|
|
123
|
+
* scratch buffers (one per lane), so callers must consume or copy a result before the next
|
|
124
|
+
* call on the same lane. `clean()` wipes the scratch buffers along with the hash states.
|
|
125
|
+
*/
|
|
127
126
|
export type Context = {
|
|
128
127
|
/**
|
|
129
128
|
* Derive a PRF output for one address.
|
|
130
129
|
* @param addr - Address bytes.
|
|
131
|
-
* @returns PRF output bytes.
|
|
130
|
+
* @returns PRF output bytes (scratch view; copy to retain).
|
|
132
131
|
*/
|
|
133
132
|
PRFaddr: (addr: TArg<ADRS>) => TRet<Uint8Array>;
|
|
134
133
|
/**
|
|
@@ -180,21 +179,6 @@ export type GetContext = (
|
|
|
180
179
|
opts: SphincsOpts
|
|
181
180
|
) => (pub_seed: TArg<Uint8Array>, sk_seed?: TArg<Uint8Array>) => TRet<Context>;
|
|
182
181
|
|
|
183
|
-
function hexToNumber(hex: string): bigint {
|
|
184
|
-
if (typeof hex !== 'string') throw new Error('hex string expected, got ' + typeof hex);
|
|
185
|
-
return BigInt(hex === '' ? '0' : '0x' + hex); // Big Endian
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// BE: Big Endian, LE: Little Endian. This is the local FIPS 205 `toInt(...)` equivalent.
|
|
189
|
-
function bytesToNumberBE(bytes: TArg<Uint8Array>): bigint {
|
|
190
|
-
return hexToNumber(bytesToHex(bytes));
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Local in-range FIPS 205 `toByte(x, n)` equivalent; callers must keep `n < 256^len`.
|
|
194
|
-
function numberToBytesBE(n: number | bigint, len: number): TRet<Uint8Array> {
|
|
195
|
-
return hexToBytes(n.toString(16).padStart(len * 2, '0'));
|
|
196
|
-
}
|
|
197
|
-
|
|
198
182
|
// Local FIPS 205 Algorithm 4 `base_2^b(...)` implementation. Bits are consumed in big-endian
|
|
199
183
|
// order within each input byte, and callers must provide at least `ceil(outLen * b / 8)` bytes;
|
|
200
184
|
// short inputs are not rejected and would zero-extend implicitly.
|
|
@@ -214,8 +198,12 @@ const base2b = (outLen: number, b: number) => {
|
|
|
214
198
|
};
|
|
215
199
|
};
|
|
216
200
|
|
|
201
|
+
const _1n = /* @__PURE__ */ BigInt(1);
|
|
202
|
+
const _8n = /* @__PURE__ */ BigInt(8);
|
|
203
|
+
const _0xffn = /* @__PURE__ */ BigInt(0xff);
|
|
204
|
+
|
|
217
205
|
function getMaskBig(bits: number) {
|
|
218
|
-
return (
|
|
206
|
+
return (_1n << BigInt(bits)) - _1n; // 4 -> 0b1111
|
|
219
207
|
}
|
|
220
208
|
|
|
221
209
|
/** Public SLH-DSA signer with prehash customization. */
|
|
@@ -284,16 +272,25 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
|
|
|
284
272
|
) => {
|
|
285
273
|
const { type, height, tree, layer, index, chain, hash, keypair } = opts;
|
|
286
274
|
const { subtreeAddr, keypairAddr } = opts;
|
|
287
|
-
const v = createView(addr);
|
|
288
275
|
|
|
289
276
|
if (height !== undefined) addr[OFFSET_CHAIN_ADDR] = height;
|
|
290
277
|
if (layer !== undefined) addr[OFFSET_LAYER] = layer;
|
|
291
278
|
if (type !== undefined) addr[OFFSET_TYPE] = type;
|
|
292
279
|
if (chain !== undefined) addr[OFFSET_CHAIN_ADDR] = chain;
|
|
293
280
|
if (hash !== undefined) addr[OFFSET_HASH_ADDR] = hash;
|
|
294
|
-
|
|
281
|
+
// Manual big-endian writes: setAddr runs in the innermost WOTS/tree loops, and creating a
|
|
282
|
+
// DataView per call was a measurable share of sign() time.
|
|
283
|
+
if (index !== undefined) {
|
|
284
|
+
addr[OFFSET_TREE_INDEX + 0] = index >>> 24;
|
|
285
|
+
addr[OFFSET_TREE_INDEX + 1] = index >>> 16;
|
|
286
|
+
addr[OFFSET_TREE_INDEX + 2] = index >>> 8;
|
|
287
|
+
addr[OFFSET_TREE_INDEX + 3] = index;
|
|
288
|
+
}
|
|
295
289
|
if (subtreeAddr) addr.set(subtreeAddr.subarray(0, OFFSET_TREE + 8));
|
|
296
|
-
if (tree !== undefined)
|
|
290
|
+
if (tree !== undefined) {
|
|
291
|
+
let t = tree;
|
|
292
|
+
for (let i = 7; i >= 0; i--, t >>= _8n) addr[OFFSET_TREE + i] = Number(t & _0xffn);
|
|
293
|
+
}
|
|
297
294
|
if (keypair !== undefined) {
|
|
298
295
|
addr[OFFSET_KP_ADDR1] = keypair;
|
|
299
296
|
if (TREE_HEIGHT > 8) addr[OFFSET_KP_ADDR2] = keypair >>> 8;
|
|
@@ -372,10 +369,12 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
|
|
|
372
369
|
const maxIdx = (1 << height) - 1;
|
|
373
370
|
const stack = new Uint8Array(height * N);
|
|
374
371
|
const authPath = new Uint8Array(height * N);
|
|
372
|
+
// One node buffer per treehash call (not per leaf): both halves are fully overwritten at
|
|
373
|
+
// each use, and the returned root aliases cur1, which is never reused after return.
|
|
374
|
+
const current = new Uint8Array(2 * N);
|
|
375
|
+
const cur0 = current.subarray(0, N);
|
|
376
|
+
const cur1 = current.subarray(N);
|
|
375
377
|
for (let idx = 0; ; idx++) {
|
|
376
|
-
const current = new Uint8Array(2 * N);
|
|
377
|
-
const cur0 = current.subarray(0, N);
|
|
378
|
-
const cur1 = current.subarray(N);
|
|
379
378
|
const addrOffset = idx + idxOffset;
|
|
380
379
|
cur1.set(leafFn(leafIdx, addrOffset, rawContext, info));
|
|
381
380
|
let h = 0;
|
|
@@ -582,7 +581,9 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
|
|
|
582
581
|
},
|
|
583
582
|
forsTreeAddr
|
|
584
583
|
);
|
|
585
|
-
|
|
584
|
+
// Copy: PRFaddr returns a per-context scratch view, and this value is retained in
|
|
585
|
+
// `fors` across the many PRFaddr calls inside forsTreehash below.
|
|
586
|
+
const prf = copyBytes(context.PRFaddr(forsTreeAddr));
|
|
586
587
|
setAddr({ type: AddressType.FORSTREE }, forsTreeAddr);
|
|
587
588
|
const { root, authPath } = forsTreehash(
|
|
588
589
|
context,
|
|
@@ -598,7 +599,9 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
|
|
|
598
599
|
type: AddressType.FORSPK,
|
|
599
600
|
keypairAddr: wotsAddr,
|
|
600
601
|
});
|
|
601
|
-
|
|
602
|
+
// Copy: thashN returns a per-context scratch view, and `root` lives across every hash
|
|
603
|
+
// call in the hypertree loop below (it is also mutated via root.set).
|
|
604
|
+
const root = copyBytes(context.thashN(K, concatBytes(...roots), forsPkAddr));
|
|
602
605
|
// WOTS signatures
|
|
603
606
|
const treeAddr = setAddr({ type: AddressType.HASHTREE });
|
|
604
607
|
const wots: [Uint8Array, Uint8Array][] = [];
|
|
@@ -622,9 +625,13 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
|
|
|
622
625
|
},
|
|
623
626
|
verify: (sig: TArg<Uint8Array>, msg: TArg<Uint8Array>, publicKey: TArg<Uint8Array>) => {
|
|
624
627
|
const [pkSeed, pubRoot] = publicCoder.decode(publicKey);
|
|
625
|
-
const [random, forsVec, wotsVec] = sigCoder.decode(sig);
|
|
626
628
|
const pk = publicKey;
|
|
629
|
+
// FIPS 205 Algorithm 20 step 1: wrong-length signatures return false instead of throwing
|
|
630
|
+
// (same as ml-dsa). Must run before sigCoder.decode, which throws on length mismatch.
|
|
631
|
+
// Preserve TypeError for non-byte API arguments before treating byte lengths as invalid.
|
|
632
|
+
abytes(sig, undefined, 'signature');
|
|
627
633
|
if (sig.length !== sigCoder.bytesLen) return false;
|
|
634
|
+
const [random, forsVec, wotsVec] = sigCoder.decode(sig);
|
|
628
635
|
const context = getContext(pkSeed);
|
|
629
636
|
let { tree, leafIdx, md } = hashMessage(random, pk, msg, context);
|
|
630
637
|
const wotsAddr = setAddr({
|
|
@@ -644,14 +651,18 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
|
|
|
644
651
|
const idxOffset = i << A;
|
|
645
652
|
setAddr({ height: 0, index: indices[i] + idxOffset }, forsTreeAddr);
|
|
646
653
|
const leaf = context.thash1(prf, forsTreeAddr);
|
|
647
|
-
//
|
|
648
|
-
|
|
654
|
+
// Copy: computeRoot returns a thashN scratch view, and roots are retained across the
|
|
655
|
+
// remaining FORS iterations (computeRoot itself copies `leaf` before hashing).
|
|
656
|
+
roots.push(
|
|
657
|
+
copyBytes(computeRoot(leaf, indices[i], idxOffset, authPath, A, context, forsTreeAddr))
|
|
658
|
+
);
|
|
649
659
|
}
|
|
650
660
|
const forsPkAddr = setAddr({
|
|
651
661
|
type: AddressType.FORSPK,
|
|
652
662
|
keypairAddr: wotsAddr,
|
|
653
663
|
});
|
|
654
|
-
|
|
664
|
+
// Copy: `root` must survive the thash1/thashN calls of the WOTS chain loop below.
|
|
665
|
+
let root = copyBytes(context.thashN(K, concatBytes(...roots), forsPkAddr)); // root = thash()
|
|
655
666
|
// WOTS signature
|
|
656
667
|
const treeAddr = setAddr({ type: AddressType.HASHTREE });
|
|
657
668
|
const wotsPkAddr = setAddr({ type: AddressType.WOTSPK });
|
|
@@ -674,7 +685,8 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
|
|
|
674
685
|
}
|
|
675
686
|
}
|
|
676
687
|
const leaf = context.thashN(WOTS_LEN, wotsPk, wotsPkAddr);
|
|
677
|
-
root
|
|
688
|
+
// Copy: `root` is read by chainLengths / equalBytes after later hash calls.
|
|
689
|
+
root = copyBytes(computeRoot(leaf, leafIdx, 0, sigAuth, TREE_HEIGHT, context, treeAddr));
|
|
678
690
|
leafIdx = Number(tree & getMaskBig(TREE_HEIGHT));
|
|
679
691
|
}
|
|
680
692
|
return equalBytes(root, pubRoot);
|
|
@@ -744,20 +756,27 @@ const genShake =
|
|
|
744
756
|
// for each address-bound call instead of reabsorbing the same seed every time.
|
|
745
757
|
const h0 = shake256.create({}).update(pubSeed);
|
|
746
758
|
const h0tmp = h0.clone();
|
|
759
|
+
// Per-context output scratch: thash1/thashN/PRFaddr return these buffers directly, so
|
|
760
|
+
// callers must consume or copy a result before the next call on the same lane.
|
|
761
|
+
const thashOut = new Uint8Array(N);
|
|
762
|
+
const prfOut = new Uint8Array(N);
|
|
747
763
|
const thash = (blocks: number, input: TArg<Uint8Array>, addr: TArg<ADRS>): TRet<Uint8Array> => {
|
|
748
764
|
stats.thash++;
|
|
749
|
-
|
|
750
|
-
|
|
765
|
+
const len = blocks * N;
|
|
766
|
+
h0._cloneInto(h0tmp)
|
|
751
767
|
.update(addr)
|
|
752
|
-
.update(
|
|
753
|
-
|
|
768
|
+
.update(
|
|
769
|
+
input.length === len ? (input as Uint8Array) : (input as Uint8Array).subarray(0, len)
|
|
770
|
+
)
|
|
771
|
+
.xofInto(thashOut);
|
|
772
|
+
return thashOut as TRet<Uint8Array>;
|
|
754
773
|
};
|
|
755
774
|
return {
|
|
756
775
|
PRFaddr: (addr: TArg<ADRS>): TRet<Uint8Array> => {
|
|
757
776
|
if (!skSeed) throw new Error('no sk seed');
|
|
758
777
|
stats.prf++;
|
|
759
|
-
|
|
760
|
-
return
|
|
778
|
+
h0._cloneInto(h0tmp).update(addr).update(skSeed).xofInto(prfOut);
|
|
779
|
+
return prfOut as TRet<Uint8Array>;
|
|
761
780
|
},
|
|
762
781
|
PRFmsg: (
|
|
763
782
|
skPRF: TArg<Uint8Array>,
|
|
@@ -787,6 +806,7 @@ const genShake =
|
|
|
787
806
|
clean: () => {
|
|
788
807
|
h0.destroy();
|
|
789
808
|
h0tmp.destroy();
|
|
809
|
+
cleanBytes(thashOut, prfOut);
|
|
790
810
|
//console.log(stats);
|
|
791
811
|
},
|
|
792
812
|
} as TRet<Context>;
|
|
@@ -869,6 +889,16 @@ const genSha =
|
|
|
869
889
|
|
|
870
890
|
const h0tmp = h0ps.clone();
|
|
871
891
|
const h1tmp = h1ps.clone();
|
|
892
|
+
// Per-context output scratch: thash1/thashN/PRFaddr return views into these buffers, so
|
|
893
|
+
// callers must consume or copy a result before the next call on the same lane (see Context
|
|
894
|
+
// docs). digestInto also skips digest()'s per-call destroy(): the tmp states are fully
|
|
895
|
+
// overwritten by the next _cloneInto and wiped in clean().
|
|
896
|
+
const h0out = new Uint8Array(h0.outputLen);
|
|
897
|
+
const h1out = new Uint8Array(h1.outputLen);
|
|
898
|
+
const prfOut = new Uint8Array(h0.outputLen);
|
|
899
|
+
const h0outN = h0out.subarray(0, N);
|
|
900
|
+
const h1outN = h1out.subarray(0, N);
|
|
901
|
+
const prfOutN = prfOut.subarray(0, N);
|
|
872
902
|
|
|
873
903
|
// https://www.rfc-editor.org/rfc/rfc8017.html#appendix-B.2.1
|
|
874
904
|
// This local helper is intentionally stricter than generic MGF1 reuse: current SLH-DSA callers
|
|
@@ -889,27 +919,28 @@ const genSha =
|
|
|
889
919
|
}
|
|
890
920
|
|
|
891
921
|
const thash =
|
|
892
|
-
(
|
|
922
|
+
(h: typeof h0ps, hTmp: typeof h0ps, out: TArg<Uint8Array>, outN: TArg<Uint8Array>) =>
|
|
893
923
|
(blocks: number, input: TArg<Uint8Array>, addr: TArg<ADRS>): TRet<Uint8Array> => {
|
|
894
924
|
stats.thash++;
|
|
895
|
-
const
|
|
896
|
-
|
|
925
|
+
const len = blocks * N;
|
|
926
|
+
h._cloneInto(hTmp as any)
|
|
897
927
|
.update(addr)
|
|
898
|
-
.update(
|
|
899
|
-
|
|
900
|
-
|
|
928
|
+
.update(
|
|
929
|
+
input.length === len ? (input as Uint8Array) : (input as Uint8Array).subarray(0, len)
|
|
930
|
+
)
|
|
931
|
+
.digestInto(out);
|
|
932
|
+
return outN as TRet<Uint8Array>;
|
|
901
933
|
};
|
|
902
934
|
return {
|
|
903
935
|
PRFaddr: (addr: TArg<ADRS>): TRet<Uint8Array> => {
|
|
904
936
|
if (!sk_seed) throw new Error('No sk seed');
|
|
905
937
|
stats.prf++;
|
|
906
|
-
|
|
938
|
+
h0ps
|
|
907
939
|
._cloneInto(h0tmp as any)
|
|
908
940
|
.update(addr)
|
|
909
941
|
.update(sk_seed)
|
|
910
|
-
.
|
|
911
|
-
|
|
912
|
-
return res as TRet<Uint8Array>;
|
|
942
|
+
.digestInto(prfOut);
|
|
943
|
+
return prfOutN as TRet<Uint8Array>;
|
|
913
944
|
},
|
|
914
945
|
PRFmsg: (
|
|
915
946
|
skPRF: TArg<Uint8Array>,
|
|
@@ -938,13 +969,14 @@ const genSha =
|
|
|
938
969
|
);
|
|
939
970
|
return mgf1(seed, outLen, h1);
|
|
940
971
|
},
|
|
941
|
-
thash1: thash(
|
|
942
|
-
thashN: thash(
|
|
972
|
+
thash1: thash(h0ps, h0tmp, h0out, h0outN).bind(null, 1),
|
|
973
|
+
thashN: thash(h1ps, h1tmp, h1out, h1outN),
|
|
943
974
|
clean: () => {
|
|
944
975
|
h0ps.destroy();
|
|
945
976
|
h1ps.destroy();
|
|
946
977
|
h0tmp.destroy();
|
|
947
978
|
h1tmp.destroy();
|
|
979
|
+
cleanBytes(h0out, h1out, prfOut);
|
|
948
980
|
//console.log(stats);
|
|
949
981
|
},
|
|
950
982
|
} as TRet<Context>;
|
|
@@ -963,6 +995,23 @@ const SHA512_SIMPLE = /* @__PURE__ */ (() => ({
|
|
|
963
995
|
* SLH-DSA-SHA2-128f: Table 2 row `n=16, h=66, d=22, h'=3, a=6, k=33, lg w=4, m=34`;
|
|
964
996
|
* lengths `publicKey=32`, `secretKey=64`, `signature=17088`, `seed=48`, `signRand=16`.
|
|
965
997
|
* Also exposes `.prehash(...)`.
|
|
998
|
+
* @example
|
|
999
|
+
* Generate deterministic SLH-DSA keys, sign one message, and verify the signature.
|
|
1000
|
+
* ```ts
|
|
1001
|
+
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
1002
|
+
* import { slh_dsa_sha2_128f } from '@noble/post-quantum/slh-dsa.js';
|
|
1003
|
+
* const seed = new Uint8Array(slh_dsa_sha2_128f.lengths.seed!);
|
|
1004
|
+
* const { secretKey, publicKey } = slh_dsa_sha2_128f.keygen(seed);
|
|
1005
|
+
* const msg = new TextEncoder().encode('hello noble');
|
|
1006
|
+
* const sig = slh_dsa_sha2_128f.sign(msg, secretKey);
|
|
1007
|
+
* const isValid = slh_dsa_sha2_128f.verify(sig, msg, publicKey);
|
|
1008
|
+
* const recovered = slh_dsa_sha2_128f.getPublicKey(secretKey);
|
|
1009
|
+
* const context = new Uint8Array([1, 2, 3]);
|
|
1010
|
+
* const prehash = slh_dsa_sha2_128f.prehash(sha256);
|
|
1011
|
+
* const preSig = prehash.sign(msg, secretKey, { context });
|
|
1012
|
+
* const preValid = prehash.verify(preSig, msg, publicKey, { context });
|
|
1013
|
+
* const internalSig = slh_dsa_sha2_128f.internal.sign(msg, secretKey);
|
|
1014
|
+
* ```
|
|
966
1015
|
*/
|
|
967
1016
|
export const slh_dsa_sha2_128f: TRet<SphincsSigner> = /* @__PURE__ */ (() =>
|
|
968
1017
|
gen(PARAMS['128f'], SHA256_SIMPLE))();
|