@noble/post-quantum 0.6.0 → 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/slh-dsa.js CHANGED
@@ -28,9 +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 { bytesToHex, concatBytes, createView, hexToBytes, } from '@noble/hashes/utils.js';
34
+ import { concatBytes, createView } from '@noble/hashes/utils.js';
34
35
  import { abytes, checkHash, cleanBytes, copyBytes, equalBytes, getMask, getMessage, getMessagePrehash, randomBytes, splitCoder, validateSigOpts, validateVerOpts, vecCoder, } from "./utils.js";
35
36
  /** Winternitz signature params. */
36
37
  /**
@@ -41,13 +42,13 @@ import { abytes, checkHash, cleanBytes, copyBytes, equalBytes, getMask, getMessa
41
42
  * Other Table 2 columns such as `m`, public-key bytes, and signature bytes
42
43
  * stay derived at the export layer.
43
44
  */
44
- export const PARAMS = /* @__PURE__ */ (() => ({
45
- '128f': { W: 16, N: 16, H: 66, D: 22, K: 33, A: 6, securityLevel: 128 },
46
- '128s': { W: 16, N: 16, H: 63, D: 7, K: 14, A: 12, securityLevel: 128 },
47
- '192f': { W: 16, N: 24, H: 66, D: 22, K: 33, A: 8, securityLevel: 192 },
48
- '192s': { W: 16, N: 24, H: 63, D: 7, K: 17, A: 14, securityLevel: 192 },
49
- '256f': { W: 16, N: 32, H: 68, D: 17, K: 35, A: 9, securityLevel: 256 },
50
- '256s': { W: 16, N: 32, H: 64, D: 8, K: 22, A: 14, securityLevel: 256 },
45
+ export const PARAMS = /* @__PURE__ */ (() => Object.freeze({
46
+ '128f': Object.freeze({ W: 16, N: 16, H: 66, D: 22, K: 33, A: 6, securityLevel: 128 }),
47
+ '128s': Object.freeze({ W: 16, N: 16, H: 63, D: 7, K: 14, A: 12, securityLevel: 128 }),
48
+ '192f': Object.freeze({ W: 16, N: 24, H: 66, D: 22, K: 33, A: 8, securityLevel: 192 }),
49
+ '192s': Object.freeze({ W: 16, N: 24, H: 63, D: 7, K: 17, A: 14, securityLevel: 192 }),
50
+ '256f': Object.freeze({ W: 16, N: 32, H: 68, D: 17, K: 35, A: 9, securityLevel: 256 }),
51
+ '256s': Object.freeze({ W: 16, N: 32, H: 64, D: 8, K: 22, A: 14, securityLevel: 256 }),
51
52
  }))();
52
53
  // FIPS 205 `ADRS.setTypeAndClear(...)` selectors. Local names shorten the spec labels
53
54
  // (`WOTS_HASH` -> `WOTS`, `TREE` -> `HASHTREE`, `FORS_ROOTS` -> `FORSPK`), and `setAddr({ type })`
@@ -61,19 +62,6 @@ const AddressType = {
61
62
  WOTSPRF: 5,
62
63
  FORSPRF: 6,
63
64
  };
64
- function hexToNumber(hex) {
65
- if (typeof hex !== 'string')
66
- throw new Error('hex string expected, got ' + typeof hex);
67
- return BigInt(hex === '' ? '0' : '0x' + hex); // Big Endian
68
- }
69
- // BE: Big Endian, LE: Little Endian. This is the local FIPS 205 `toInt(...)` equivalent.
70
- function bytesToNumberBE(bytes) {
71
- return hexToNumber(bytesToHex(bytes));
72
- }
73
- // Local in-range FIPS 205 `toByte(x, n)` equivalent; callers must keep `n < 256^len`.
74
- function numberToBytesBE(n, len) {
75
- return hexToBytes(n.toString(16).padStart(len * 2, '0'));
76
- }
77
65
  // Local FIPS 205 Algorithm 4 `base_2^b(...)` implementation. Bits are consumed in big-endian
78
66
  // order within each input byte, and callers must provide at least `ceil(outLen * b / 8)` bytes;
79
67
  // short inputs are not rejected and would zero-extend implicitly.
@@ -92,15 +80,19 @@ const base2b = (outLen, b) => {
92
80
  return baseB;
93
81
  };
94
82
  };
83
+ const _1n = /* @__PURE__ */ BigInt(1);
84
+ const _8n = /* @__PURE__ */ BigInt(8);
85
+ const _0xffn = /* @__PURE__ */ BigInt(0xff);
95
86
  function getMaskBig(bits) {
96
- return (1n << BigInt(bits)) - 1n; // 4 -> 0b1111
87
+ return (_1n << BigInt(bits)) - _1n; // 4 -> 0b1111
97
88
  }
98
89
  /** One parameter/hash instantiation of the public SLH-DSA API.
99
90
  * `keygen(seed)` is a deterministic 3N-byte library hook around the internal keygen flow,
100
91
  * and `getPublicKey(secretKey)` only extracts the embedded public key
101
92
  * instead of recomputing `PK.root`.
102
93
  */
103
- function gen(opts, hashOpts) {
94
+ function gen(opts, hashOpts_) {
95
+ const hashOpts = hashOpts_;
104
96
  const { N, W, H, D, K, A, securityLevel: securityLevel } = opts;
105
97
  const getContext = hashOpts.getContext(opts);
106
98
  if (W !== 16)
@@ -138,7 +130,6 @@ function gen(opts, hashOpts) {
138
130
  const setAddr = (opts, addr = new Uint8Array(ADDR_BYTES)) => {
139
131
  const { type, height, tree, layer, index, chain, hash, keypair } = opts;
140
132
  const { subtreeAddr, keypairAddr } = opts;
141
- const v = createView(addr);
142
133
  if (height !== undefined)
143
134
  addr[OFFSET_CHAIN_ADDR] = height;
144
135
  if (layer !== undefined)
@@ -149,12 +140,21 @@ function gen(opts, hashOpts) {
149
140
  addr[OFFSET_CHAIN_ADDR] = chain;
150
141
  if (hash !== undefined)
151
142
  addr[OFFSET_HASH_ADDR] = hash;
152
- if (index !== undefined)
153
- v.setUint32(OFFSET_TREE_INDEX, index, false);
143
+ // Manual big-endian writes: setAddr runs in the innermost WOTS/tree loops, and creating a
144
+ // DataView per call was a measurable share of sign() time.
145
+ if (index !== undefined) {
146
+ addr[OFFSET_TREE_INDEX + 0] = index >>> 24;
147
+ addr[OFFSET_TREE_INDEX + 1] = index >>> 16;
148
+ addr[OFFSET_TREE_INDEX + 2] = index >>> 8;
149
+ addr[OFFSET_TREE_INDEX + 3] = index;
150
+ }
154
151
  if (subtreeAddr)
155
152
  addr.set(subtreeAddr.subarray(0, OFFSET_TREE + 8));
156
- if (tree !== undefined)
157
- v.setBigUint64(OFFSET_TREE, tree, false);
153
+ if (tree !== undefined) {
154
+ let t = tree;
155
+ for (let i = 7; i >= 0; i--, t >>= _8n)
156
+ addr[OFFSET_TREE + i] = Number(t & _0xffn);
157
+ }
158
158
  if (keypair !== undefined) {
159
159
  addr[OFFSET_KP_ADDR1] = keypair;
160
160
  if (TREE_HEIGHT > 8)
@@ -191,8 +191,9 @@ function gen(opts, hashOpts) {
191
191
  // `pkSeed` is the full public key byte string `PK.seed || PK.root`; after splitting `Hmsg`,
192
192
  // mask away any spare high bits so `idx_tree` / `idx_leaf` match the spec's final mod-2^k steps.
193
193
  const hashMessage = (R, pkSeed, msg, context) => {
194
+ const rawContext = context;
194
195
  // digest ← Hmsg(R, PK.seed, PK.root, M)
195
- const digest = context.Hmsg(R, pkSeed, msg, hashMsgCoder.bytesLen);
196
+ const digest = rawContext.Hmsg(R, pkSeed, msg, hashMsgCoder.bytesLen);
196
197
  const [md, tmpIdxTree, tmpIdxLeaf] = hashMsgCoder.decode(digest);
197
198
  const tree = bytesToNumberBE(tmpIdxTree) & getMaskBig(TREE_BITS);
198
199
  const leafIdx = Number(bytesToNumberBE(tmpIdxLeaf)) & getMask(LEAF_BITS);
@@ -202,15 +203,19 @@ function gen(opts, hashOpts) {
202
203
  // sibling pairs on `stack`, and record the sibling whenever the current subtree is the auth-path
203
204
  // neighbor of the target leaf at that height.
204
205
  const treehash = (height, fn) => function treehash_i(context, leafIdx, idxOffset, treeAddr, info) {
206
+ const rawContext = context;
207
+ const leafFn = fn;
205
208
  const maxIdx = (1 << height) - 1;
206
209
  const stack = new Uint8Array(height * N);
207
210
  const authPath = new Uint8Array(height * N);
211
+ // One node buffer per treehash call (not per leaf): both halves are fully overwritten at
212
+ // each use, and the returned root aliases cur1, which is never reused after return.
213
+ const current = new Uint8Array(2 * N);
214
+ const cur0 = current.subarray(0, N);
215
+ const cur1 = current.subarray(N);
208
216
  for (let idx = 0;; idx++) {
209
- const current = new Uint8Array(2 * N);
210
- const cur0 = current.subarray(0, N);
211
- const cur1 = current.subarray(N);
212
217
  const addrOffset = idx + idxOffset;
213
- cur1.set(fn(leafIdx, addrOffset, context, info));
218
+ cur1.set(leafFn(leafIdx, addrOffset, rawContext, info));
214
219
  let h = 0;
215
220
  for (let i = idx, o = idxOffset, l = leafIdx;; h++, i >>>= 1, l >>>= 1, o >>>= 1) {
216
221
  if (h === height)
@@ -221,7 +226,7 @@ function gen(opts, hashOpts) {
221
226
  break;
222
227
  setAddr({ height: h + 1, index: (i >> 1) + (o >> 1) }, treeAddr);
223
228
  cur0.set(stack.subarray(h * N).subarray(0, N));
224
- cur1.set(context.thashN(2, current, treeAddr));
229
+ cur1.set(rawContext.thashN(2, current, treeAddr));
225
230
  }
226
231
  stack.subarray(h * N).set(cur1); // stack.push(cur1)
227
232
  }
@@ -229,6 +234,7 @@ function gen(opts, hashOpts) {
229
234
  throw new Error('Unreachable code path reached, report this error');
230
235
  };
231
236
  const wotsTreehash = treehash(TREE_HEIGHT, (leafIdx, addrOffset, context, info) => {
237
+ const rawContext = context;
232
238
  const wotsPk = new Uint8Array(WOTS_LEN * N);
233
239
  // `keygen()` passes `leafIdx = ~0 >>> 0`, so no real XMSS leaf matches and this suppresses
234
240
  // WOTS signature capture while still hashing every chain to its public-key endpoint.
@@ -239,7 +245,7 @@ function gen(opts, hashOpts) {
239
245
  const wotsK = info.wotsSteps[i] | wotsKmask;
240
246
  const pk = wotsPk.subarray(i * N, (i + 1) * N);
241
247
  setAddr({ chain: i, hash: 0, type: AddressType.WOTSPRF }, info.leafAddr);
242
- pk.set(context.PRFaddr(info.leafAddr));
248
+ pk.set(rawContext.PRFaddr(info.leafAddr));
243
249
  setAddr({ type: AddressType.WOTS }, info.leafAddr);
244
250
  for (let k = 0;; k++) {
245
251
  if (k === wotsK)
@@ -247,16 +253,17 @@ function gen(opts, hashOpts) {
247
253
  if (k === W - 1)
248
254
  break;
249
255
  setAddr({ hash: k }, info.leafAddr);
250
- pk.set(context.thash1(pk, info.leafAddr));
256
+ pk.set(rawContext.thash1(pk, info.leafAddr));
251
257
  }
252
258
  }
253
- return context.thashN(WOTS_LEN, wotsPk, info.pkAddr);
259
+ return rawContext.thashN(WOTS_LEN, wotsPk, info.pkAddr);
254
260
  });
255
261
  const forsTreehash = treehash(A, (_, addrOffset, context, forsLeafAddr) => {
262
+ const rawContext = context;
256
263
  setAddr({ type: AddressType.FORSPRF, index: addrOffset }, forsLeafAddr);
257
- const prf = context.PRFaddr(forsLeafAddr);
264
+ const prf = rawContext.PRFaddr(forsLeafAddr);
258
265
  setAddr({ type: AddressType.FORSTREE }, forsLeafAddr);
259
- return context.thash1(prf, forsLeafAddr);
266
+ return rawContext.thash1(prf, forsLeafAddr);
260
267
  });
261
268
  // Fuse `xmss_sign` with the subtree-root computation needed by `ht_sign`, so one tree walk
262
269
  // yields both the WOTS/auth-path signature and the root that the next hypertree layer signs.
@@ -277,6 +284,7 @@ function gen(opts, hashOpts) {
277
284
  };
278
285
  };
279
286
  const computeRoot = (leaf, leafIdx, idxOffset, authPath, treeHeight, context, addr) => {
287
+ const rawContext = context;
280
288
  const buffer = new Uint8Array(2 * N);
281
289
  const b0 = buffer.subarray(0, N);
282
290
  const b1 = buffer.subarray(N, 2 * N);
@@ -300,17 +308,17 @@ function gen(opts, hashOpts) {
300
308
  setAddr({ height: i + 1, index: leafIdx + idxOffset }, addr);
301
309
  const a = authPath.subarray((i + 1) * N, (i + 2) * N);
302
310
  if ((leafIdx & 1) !== 0) {
303
- b1.set(context.thashN(2, buffer, addr));
311
+ b1.set(rawContext.thashN(2, buffer, addr));
304
312
  b0.set(a);
305
313
  }
306
314
  else {
307
- buffer.set(context.thashN(2, buffer, addr));
315
+ buffer.set(rawContext.thashN(2, buffer, addr));
308
316
  b1.set(a);
309
317
  }
310
318
  }
311
319
  // Root
312
320
  setAddr({ height: treeHeight, index: leafIdx + idxOffset }, addr);
313
- return context.thashN(2, buffer, addr);
321
+ return rawContext.thashN(2, buffer, addr);
314
322
  };
315
323
  const seedCoder = splitCoder('seed', N, N, N);
316
324
  const publicCoder = splitCoder('publicKey', N, N);
@@ -318,15 +326,15 @@ function gen(opts, hashOpts) {
318
326
  const forsCoder = vecCoder(splitCoder('fors', N, N * A), K);
319
327
  const wotsCoder = vecCoder(splitCoder('wots', WOTS_LEN * N, TREE_HEIGHT * N), D);
320
328
  const sigCoder = splitCoder('signature', N, forsCoder, wotsCoder); // random || fors || wots
321
- const internal = {
322
- info: { type: 'internal-slh-dsa' },
323
- lengths: {
329
+ const internal = Object.freeze({
330
+ info: Object.freeze({ type: 'internal-slh-dsa' }),
331
+ lengths: Object.freeze({
324
332
  publicKey: publicCoder.bytesLen,
325
333
  secretKey: secretCoder.bytesLen,
326
334
  signature: sigCoder.bytesLen,
327
335
  seed: seedCoder.bytesLen,
328
336
  signRand: N,
329
- },
337
+ }),
330
338
  keygen(seed) {
331
339
  if (seed !== undefined)
332
340
  abytes(seed, seedCoder.bytesLen, 'seed');
@@ -343,7 +351,10 @@ function gen(opts, hashOpts) {
343
351
  const secretKey = secretCoder.encode([secretSeed, secretPRF, publicKey]);
344
352
  context.clean();
345
353
  cleanBytes(secretSeed, secretPRF, root, wotsAddr, topTreeAddr);
346
- return { publicKey, secretKey };
354
+ return {
355
+ publicKey: publicKey,
356
+ secretKey: secretKey,
357
+ };
347
358
  },
348
359
  getPublicKey: (secretKey) => {
349
360
  const [_skSeed, _skPRF, pk] = secretCoder.decode(secretKey);
@@ -384,7 +395,9 @@ function gen(opts, hashOpts) {
384
395
  height: 0,
385
396
  index: indices[i] + idxOffset,
386
397
  }, forsTreeAddr);
387
- const prf = context.PRFaddr(forsTreeAddr);
398
+ // Copy: PRFaddr returns a per-context scratch view, and this value is retained in
399
+ // `fors` across the many PRFaddr calls inside forsTreehash below.
400
+ const prf = copyBytes(context.PRFaddr(forsTreeAddr));
388
401
  setAddr({ type: AddressType.FORSTREE }, forsTreeAddr);
389
402
  const { root, authPath } = forsTreehash(context, indices[i], idxOffset, forsTreeAddr, forsLeaf);
390
403
  roots.push(root);
@@ -394,7 +407,9 @@ function gen(opts, hashOpts) {
394
407
  type: AddressType.FORSPK,
395
408
  keypairAddr: wotsAddr,
396
409
  });
397
- const root = context.thashN(K, concatBytes(...roots), forsPkAddr);
410
+ // Copy: thashN returns a per-context scratch view, and `root` lives across every hash
411
+ // call in the hypertree loop below (it is also mutated via root.set).
412
+ const root = copyBytes(context.thashN(K, concatBytes(...roots), forsPkAddr));
398
413
  // WOTS signatures
399
414
  const treeAddr = setAddr({ type: AddressType.HASHTREE });
400
415
  const wots = [];
@@ -414,10 +429,14 @@ function gen(opts, hashOpts) {
414
429
  },
415
430
  verify: (sig, msg, publicKey) => {
416
431
  const [pkSeed, pubRoot] = publicCoder.decode(publicKey);
417
- const [random, forsVec, wotsVec] = sigCoder.decode(sig);
418
432
  const pk = publicKey;
433
+ // FIPS 205 Algorithm 20 step 1: wrong-length signatures return false instead of throwing
434
+ // (same as ml-dsa). Must run before sigCoder.decode, which throws on length mismatch.
435
+ // Preserve TypeError for non-byte API arguments before treating byte lengths as invalid.
436
+ abytes(sig, undefined, 'signature');
419
437
  if (sig.length !== sigCoder.bytesLen)
420
438
  return false;
439
+ const [random, forsVec, wotsVec] = sigCoder.decode(sig);
421
440
  const context = getContext(pkSeed);
422
441
  let { tree, leafIdx, md } = hashMessage(random, pk, msg, context);
423
442
  const wotsAddr = setAddr({
@@ -437,14 +456,16 @@ function gen(opts, hashOpts) {
437
456
  const idxOffset = i << A;
438
457
  setAddr({ height: 0, index: indices[i] + idxOffset }, forsTreeAddr);
439
458
  const leaf = context.thash1(prf, forsTreeAddr);
440
- // Compute inplace, because we need all roots in same byte array
441
- roots.push(computeRoot(leaf, indices[i], idxOffset, authPath, A, context, forsTreeAddr));
459
+ // Copy: computeRoot returns a thashN scratch view, and roots are retained across the
460
+ // remaining FORS iterations (computeRoot itself copies `leaf` before hashing).
461
+ roots.push(copyBytes(computeRoot(leaf, indices[i], idxOffset, authPath, A, context, forsTreeAddr)));
442
462
  }
443
463
  const forsPkAddr = setAddr({
444
464
  type: AddressType.FORSPK,
445
465
  keypairAddr: wotsAddr,
446
466
  });
447
- let root = context.thashN(K, concatBytes(...roots), forsPkAddr); // root = thash()
467
+ // Copy: `root` must survive the thash1/thashN calls of the WOTS chain loop below.
468
+ let root = copyBytes(context.thashN(K, concatBytes(...roots), forsPkAddr)); // root = thash()
448
469
  // WOTS signature
449
470
  const treeAddr = setAddr({ type: AddressType.HASHTREE });
450
471
  const wotsPkAddr = setAddr({ type: AddressType.WOTSPK });
@@ -467,14 +488,15 @@ function gen(opts, hashOpts) {
467
488
  }
468
489
  }
469
490
  const leaf = context.thashN(WOTS_LEN, wotsPk, wotsPkAddr);
470
- root = computeRoot(leaf, leafIdx, 0, sigAuth, TREE_HEIGHT, context, treeAddr);
491
+ // Copy: `root` is read by chainLengths / equalBytes after later hash calls.
492
+ root = copyBytes(computeRoot(leaf, leafIdx, 0, sigAuth, TREE_HEIGHT, context, treeAddr));
471
493
  leafIdx = Number(tree & getMaskBig(TREE_HEIGHT));
472
494
  }
473
495
  return equalBytes(root, pubRoot);
474
496
  },
475
- };
476
- return {
477
- info: { type: 'slh-dsa' },
497
+ });
498
+ return Object.freeze({
499
+ info: Object.freeze({ type: 'slh-dsa' }),
478
500
  internal,
479
501
  securityLevel: securityLevel,
480
502
  lengths: internal.lengths,
@@ -493,25 +515,26 @@ function gen(opts, hashOpts) {
493
515
  },
494
516
  prehash: (hash) => {
495
517
  checkHash(hash, securityLevel);
496
- return {
497
- info: { type: 'hashslh-dsa' },
518
+ const rawHash = hash;
519
+ return Object.freeze({
520
+ info: Object.freeze({ type: 'hashslh-dsa' }),
498
521
  lengths: internal.lengths,
499
522
  keygen: internal.keygen,
500
523
  getPublicKey: internal.getPublicKey,
501
524
  sign: (msg, secretKey, opts = {}) => {
502
525
  validateSigOpts(opts);
503
- const M = getMessagePrehash(hash, msg, opts.context);
526
+ const M = getMessagePrehash(rawHash, msg, opts.context);
504
527
  const res = internal.sign(M, secretKey, opts);
505
528
  cleanBytes(M);
506
529
  return res;
507
530
  },
508
531
  verify: (sig, msg, publicKey, opts = {}) => {
509
532
  validateVerOpts(opts);
510
- return internal.verify(sig, getMessagePrehash(hash, msg, opts.context), publicKey);
533
+ return internal.verify(sig, getMessagePrehash(rawHash, msg, opts.context), publicKey);
511
534
  },
512
- };
535
+ });
513
536
  },
514
- };
537
+ });
515
538
  }
516
539
  // FIPS 205 §11.1 SHAKE instantiation: this path hashes the full uncompressed address bytes,
517
540
  // unlike the compressed 22-byte SHA2 path in §11.2.
@@ -522,25 +545,36 @@ const genShake = () => (opts) => (pubSeed, skSeed) => {
522
545
  // for each address-bound call instead of reabsorbing the same seed every time.
523
546
  const h0 = shake256.create({}).update(pubSeed);
524
547
  const h0tmp = h0.clone();
548
+ // Per-context output scratch: thash1/thashN/PRFaddr return these buffers directly, so
549
+ // callers must consume or copy a result before the next call on the same lane.
550
+ const thashOut = new Uint8Array(N);
551
+ const prfOut = new Uint8Array(N);
525
552
  const thash = (blocks, input, addr) => {
526
553
  stats.thash++;
527
- return h0
528
- ._cloneInto(h0tmp)
554
+ const len = blocks * N;
555
+ h0._cloneInto(h0tmp)
529
556
  .update(addr)
530
- .update(input.subarray(0, blocks * N))
531
- .xof(N);
557
+ .update(input.length === len ? input : input.subarray(0, len))
558
+ .xofInto(thashOut);
559
+ return thashOut;
532
560
  };
533
561
  return {
534
562
  PRFaddr: (addr) => {
535
563
  if (!skSeed)
536
564
  throw new Error('no sk seed');
537
565
  stats.prf++;
538
- const res = h0._cloneInto(h0tmp).update(addr).update(skSeed).xof(N);
539
- return res;
566
+ h0._cloneInto(h0tmp).update(addr).update(skSeed).xofInto(prfOut);
567
+ return prfOut;
540
568
  },
541
569
  PRFmsg: (skPRF, random, msg) => {
542
570
  stats.gen_message_random++;
543
- return shake256.create({}).update(skPRF).update(random).update(msg).digest().subarray(0, N);
571
+ return shake256
572
+ .create({})
573
+ .update(skPRF)
574
+ .update(random)
575
+ .update(msg)
576
+ .digest()
577
+ .subarray(0, N);
544
578
  },
545
579
  Hmsg: (R, pk, m, outLen) => {
546
580
  stats.hmsg++;
@@ -551,6 +585,7 @@ const genShake = () => (opts) => (pubSeed, skSeed) => {
551
585
  clean: () => {
552
586
  h0.destroy();
553
587
  h0tmp.destroy();
588
+ cleanBytes(thashOut, prfOut);
554
589
  //console.log(stats);
555
590
  },
556
591
  };
@@ -618,6 +653,16 @@ const genSha = (h0, h1) => (opts) => (pub_seed, sk_seed) => {
618
653
  .update(new Uint8Array(h1.blockLen - N));
619
654
  const h0tmp = h0ps.clone();
620
655
  const h1tmp = h1ps.clone();
656
+ // Per-context output scratch: thash1/thashN/PRFaddr return views into these buffers, so
657
+ // callers must consume or copy a result before the next call on the same lane (see Context
658
+ // docs). digestInto also skips digest()'s per-call destroy(): the tmp states are fully
659
+ // overwritten by the next _cloneInto and wiped in clean().
660
+ const h0out = new Uint8Array(h0.outputLen);
661
+ const h1out = new Uint8Array(h1.outputLen);
662
+ const prfOut = new Uint8Array(h0.outputLen);
663
+ const h0outN = h0out.subarray(0, N);
664
+ const h1outN = h1out.subarray(0, N);
665
+ const prfOutN = prfOut.subarray(0, N);
621
666
  // https://www.rfc-editor.org/rfc/rfc8017.html#appendix-B.2.1
622
667
  // This local helper is intentionally stricter than generic MGF1 reuse: current SLH-DSA callers
623
668
  // only request tiny `m`-byte outputs, but the guard below rejects `length > 2^32` instead of
@@ -636,44 +681,49 @@ const genSha = (h0, h1) => (opts) => (pub_seed, sk_seed) => {
636
681
  cleanBytes(out.subarray(length));
637
682
  return out.subarray(0, length);
638
683
  }
639
- const thash = (_, h, hTmp) => (blocks, input, addr) => {
684
+ const thash = (h, hTmp, out, outN) => (blocks, input, addr) => {
640
685
  stats.thash++;
641
- const d = h
642
- ._cloneInto(hTmp)
686
+ const len = blocks * N;
687
+ h._cloneInto(hTmp)
643
688
  .update(addr)
644
- .update(input.subarray(0, blocks * N))
645
- .digest();
646
- return d.subarray(0, N);
689
+ .update(input.length === len ? input : input.subarray(0, len))
690
+ .digestInto(out);
691
+ return outN;
647
692
  };
648
693
  return {
649
694
  PRFaddr: (addr) => {
650
695
  if (!sk_seed)
651
696
  throw new Error('No sk seed');
652
697
  stats.prf++;
653
- const res = h0ps
698
+ h0ps
654
699
  ._cloneInto(h0tmp)
655
700
  .update(addr)
656
701
  .update(sk_seed)
657
- .digest()
658
- .subarray(0, N);
659
- return res;
702
+ .digestInto(prfOut);
703
+ return prfOutN;
660
704
  },
661
705
  PRFmsg: (skPRF, random, msg) => {
662
706
  stats.gen_message_random++;
663
- return hmac.create(h1, skPRF).update(random).update(msg).digest().subarray(0, N);
707
+ return hmac
708
+ .create(h1, skPRF)
709
+ .update(random)
710
+ .update(msg)
711
+ .digest()
712
+ .subarray(0, N);
664
713
  },
665
714
  Hmsg: (R, pk, m, outLen) => {
666
715
  stats.hmsg++;
667
716
  const seed = concatBytes(R.subarray(0, N), pk.subarray(0, N), h1.create().update(R.subarray(0, N)).update(pk).update(m).digest());
668
717
  return mgf1(seed, outLen, h1);
669
718
  },
670
- thash1: thash(h0, h0ps, h0tmp).bind(null, 1),
671
- thashN: thash(h1, h1ps, h1tmp),
719
+ thash1: thash(h0ps, h0tmp, h0out, h0outN).bind(null, 1),
720
+ thashN: thash(h1ps, h1tmp, h1out, h1outN),
672
721
  clean: () => {
673
722
  h0ps.destroy();
674
723
  h1ps.destroy();
675
724
  h0tmp.destroy();
676
725
  h1tmp.destroy();
726
+ cleanBytes(h0out, h1out, prfOut);
677
727
  //console.log(stats);
678
728
  },
679
729
  };
@@ -690,6 +740,23 @@ const SHA512_SIMPLE = /* @__PURE__ */ (() => ({
690
740
  * SLH-DSA-SHA2-128f: Table 2 row `n=16, h=66, d=22, h'=3, a=6, k=33, lg w=4, m=34`;
691
741
  * lengths `publicKey=32`, `secretKey=64`, `signature=17088`, `seed=48`, `signRand=16`.
692
742
  * Also exposes `.prehash(...)`.
743
+ * @example
744
+ * Generate deterministic SLH-DSA keys, sign one message, and verify the signature.
745
+ * ```ts
746
+ * import { sha256 } from '@noble/hashes/sha2.js';
747
+ * import { slh_dsa_sha2_128f } from '@noble/post-quantum/slh-dsa.js';
748
+ * const seed = new Uint8Array(slh_dsa_sha2_128f.lengths.seed!);
749
+ * const { secretKey, publicKey } = slh_dsa_sha2_128f.keygen(seed);
750
+ * const msg = new TextEncoder().encode('hello noble');
751
+ * const sig = slh_dsa_sha2_128f.sign(msg, secretKey);
752
+ * const isValid = slh_dsa_sha2_128f.verify(sig, msg, publicKey);
753
+ * const recovered = slh_dsa_sha2_128f.getPublicKey(secretKey);
754
+ * const context = new Uint8Array([1, 2, 3]);
755
+ * const prehash = slh_dsa_sha2_128f.prehash(sha256);
756
+ * const preSig = prehash.sign(msg, secretKey, { context });
757
+ * const preValid = prehash.verify(preSig, msg, publicKey, { context });
758
+ * const internalSig = slh_dsa_sha2_128f.internal.sign(msg, secretKey);
759
+ * ```
693
760
  */
694
761
  export const slh_dsa_sha2_128f = /* @__PURE__ */ (() => gen(PARAMS['128f'], SHA256_SIMPLE))();
695
762
  /**
@@ -722,4 +789,3 @@ export const slh_dsa_sha2_256f = /* @__PURE__ */ (() => gen(PARAMS['256f'], SHA5
722
789
  * Also exposes `.prehash(...)`.
723
790
  */
724
791
  export const slh_dsa_sha2_256s = /* @__PURE__ */ (() => gen(PARAMS['256s'], SHA512_SIMPLE))();
725
- //# sourceMappingURL=slh-dsa.js.map