@noble/post-quantum 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/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,
@@ -58,6 +53,13 @@ import {
58
53
  type VerOpts,
59
54
  } from './utils.ts';
60
55
 
56
+ // Keys the internal SLH-DSA surface accepts. `context` is deliberately absent: the public
57
+ // wrappers consume it when they format M' and must not forward it, because a key that is
58
+ // accepted and then never read is the same silent downgrade this validation exists to prevent.
59
+ // `extraEntropy` is signing-only, so verification (which takes no options of its own) has none.
60
+ const INTERNAL_SIG_OPT_KEYS = /* @__PURE__ */ Object.freeze(['extraEntropy'] as const);
61
+ const INTERNAL_VER_OPT_KEYS = /* @__PURE__ */ Object.freeze([] as const);
62
+
61
63
  /**
62
64
  * * N: Security parameter (in bytes). W: Winternitz parameter
63
65
  * * H: Hypertree height. D: Hypertree layers
@@ -88,6 +90,8 @@ export type SphincsHashOpts = {
88
90
  getContext: GetContext;
89
91
  };
90
92
 
93
+ type InternalSphincsHashOpts = SphincsHashOpts & { isCompressed: boolean };
94
+
91
95
  /** Winternitz signature params. */
92
96
  /**
93
97
  * Built-in SLH-DSA Table 2 subset keyed by strength/profile.
@@ -123,12 +127,16 @@ const AddressType = {
123
127
  /** Address byte array of size `ADDR_BYTES`. */
124
128
  export type ADRS = Uint8Array;
125
129
 
126
- /** Hash and tweakable-hash callbacks bound to one SLH-DSA keypair context. */
130
+ /** Hash and tweakable-hash callbacks bound to one SLH-DSA keypair context.
131
+ * Buffer-aliasing contract: `PRFaddr`, `thash1` and `thashN` return views into per-context
132
+ * scratch buffers (one per lane), so callers must consume or copy a result before the next
133
+ * call on the same lane. `clean()` wipes the scratch buffers along with the hash states.
134
+ */
127
135
  export type Context = {
128
136
  /**
129
137
  * Derive a PRF output for one address.
130
138
  * @param addr - Address bytes.
131
- * @returns PRF output bytes.
139
+ * @returns PRF output bytes (scratch view; copy to retain).
132
140
  */
133
141
  PRFaddr: (addr: TArg<ADRS>) => TRet<Uint8Array>;
134
142
  /**
@@ -180,21 +188,6 @@ export type GetContext = (
180
188
  opts: SphincsOpts
181
189
  ) => (pub_seed: TArg<Uint8Array>, sk_seed?: TArg<Uint8Array>) => TRet<Context>;
182
190
 
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
191
  // Local FIPS 205 Algorithm 4 `base_2^b(...)` implementation. Bits are consumed in big-endian
199
192
  // order within each input byte, and callers must provide at least `ceil(outLen * b / 8)` bytes;
200
193
  // short inputs are not rejected and would zero-extend implicitly.
@@ -214,8 +207,12 @@ const base2b = (outLen: number, b: number) => {
214
207
  };
215
208
  };
216
209
 
210
+ const _1n = /* @__PURE__ */ BigInt(1);
211
+ const _8n = /* @__PURE__ */ BigInt(8);
212
+ const _0xffn = /* @__PURE__ */ BigInt(0xff);
213
+
217
214
  function getMaskBig(bits: number) {
218
- return (1n << BigInt(bits)) - 1n; // 4 -> 0b1111
215
+ return (_1n << BigInt(bits)) - _1n; // 4 -> 0b1111
219
216
  }
220
217
 
221
218
  /** Public SLH-DSA signer with prehash customization. */
@@ -230,8 +227,8 @@ export type SphincsSigner = Signer & {
230
227
  * and `getPublicKey(secretKey)` only extracts the embedded public key
231
228
  * instead of recomputing `PK.root`.
232
229
  */
233
- function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsSigner> {
234
- const hashOpts = hashOpts_ as SphincsHashOpts;
230
+ function gen(opts: SphincsOpts, hashOpts_: TArg<InternalSphincsHashOpts>): TRet<SphincsSigner> {
231
+ const hashOpts = hashOpts_ as InternalSphincsHashOpts;
235
232
  const { N, W, H, D, K, A, securityLevel: securityLevel } = opts;
236
233
  const getContext = hashOpts.getContext(opts);
237
234
  if (W !== 16) throw new Error('Unsupported Winternitz parameter');
@@ -282,18 +279,37 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
282
279
  }>,
283
280
  addr: TArg<ADRS> = new Uint8Array(ADDR_BYTES)
284
281
  ) => {
285
- const { type, height, tree, layer, index, chain, hash, keypair } = opts;
286
- const { subtreeAddr, keypairAddr } = opts;
287
- const v = createView(addr);
282
+ // These objects are created in hot internal loops, so avoid cloning them. Read only own fields:
283
+ // absent address words must stay absent even if Object.prototype was polluted.
284
+ const type = Object.hasOwn(opts, 'type') ? opts.type : undefined;
285
+ const height = Object.hasOwn(opts, 'height') ? opts.height : undefined;
286
+ const tree = Object.hasOwn(opts, 'tree') ? opts.tree : undefined;
287
+ const layer = Object.hasOwn(opts, 'layer') ? opts.layer : undefined;
288
+ const index = Object.hasOwn(opts, 'index') ? opts.index : undefined;
289
+ const chain = Object.hasOwn(opts, 'chain') ? opts.chain : undefined;
290
+ const hash = Object.hasOwn(opts, 'hash') ? opts.hash : undefined;
291
+ const keypair = Object.hasOwn(opts, 'keypair') ? opts.keypair : undefined;
292
+ const subtreeAddr = Object.hasOwn(opts, 'subtreeAddr') ? opts.subtreeAddr : undefined;
293
+ const keypairAddr = Object.hasOwn(opts, 'keypairAddr') ? opts.keypairAddr : undefined;
288
294
 
289
295
  if (height !== undefined) addr[OFFSET_CHAIN_ADDR] = height;
290
296
  if (layer !== undefined) addr[OFFSET_LAYER] = layer;
291
297
  if (type !== undefined) addr[OFFSET_TYPE] = type;
292
298
  if (chain !== undefined) addr[OFFSET_CHAIN_ADDR] = chain;
293
299
  if (hash !== undefined) addr[OFFSET_HASH_ADDR] = hash;
294
- if (index !== undefined) v.setUint32(OFFSET_TREE_INDEX, index, false);
300
+ // Manual big-endian writes: setAddr runs in the innermost WOTS/tree loops, and creating a
301
+ // DataView per call was a measurable share of sign() time.
302
+ if (index !== undefined) {
303
+ addr[OFFSET_TREE_INDEX + 0] = index >>> 24;
304
+ addr[OFFSET_TREE_INDEX + 1] = index >>> 16;
305
+ addr[OFFSET_TREE_INDEX + 2] = index >>> 8;
306
+ addr[OFFSET_TREE_INDEX + 3] = index;
307
+ }
295
308
  if (subtreeAddr) addr.set(subtreeAddr.subarray(0, OFFSET_TREE + 8));
296
- if (tree !== undefined) v.setBigUint64(OFFSET_TREE, tree, false);
309
+ if (tree !== undefined) {
310
+ let t = tree;
311
+ for (let i = 7; i >= 0; i--, t >>= _8n) addr[OFFSET_TREE + i] = Number(t & _0xffn);
312
+ }
297
313
  if (keypair !== undefined) {
298
314
  addr[OFFSET_KP_ADDR1] = keypair;
299
315
  if (TREE_HEIGHT > 8) addr[OFFSET_KP_ADDR2] = keypair >>> 8;
@@ -372,10 +388,12 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
372
388
  const maxIdx = (1 << height) - 1;
373
389
  const stack = new Uint8Array(height * N);
374
390
  const authPath = new Uint8Array(height * N);
391
+ // One node buffer per treehash call (not per leaf): both halves are fully overwritten at
392
+ // each use, and the returned root aliases cur1, which is never reused after return.
393
+ const current = new Uint8Array(2 * N);
394
+ const cur0 = current.subarray(0, N);
395
+ const cur1 = current.subarray(N);
375
396
  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
397
  const addrOffset = idx + idxOffset;
380
398
  cur1.set(leafFn(leafIdx, addrOffset, rawContext, info));
381
399
  let h = 0;
@@ -548,7 +566,7 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
548
566
  return Uint8Array.from(pk) as TRet<Uint8Array>;
549
567
  },
550
568
  sign: (msg: TArg<Uint8Array>, sk: TArg<Uint8Array>, opts: TArg<SigOpts> = {}) => {
551
- validateSigOpts(opts);
569
+ opts = validateSigOpts(opts, INTERNAL_SIG_OPT_KEYS);
552
570
  let { extraEntropy: random } = opts;
553
571
  const [skSeed, skPRF, pk] = secretCoder.decode(sk); // todo: fix
554
572
  const [pkSeed, _] = publicCoder.decode(pk);
@@ -582,7 +600,9 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
582
600
  },
583
601
  forsTreeAddr
584
602
  );
585
- const prf = context.PRFaddr(forsTreeAddr);
603
+ // Copy: PRFaddr returns a per-context scratch view, and this value is retained in
604
+ // `fors` across the many PRFaddr calls inside forsTreehash below.
605
+ const prf = copyBytes(context.PRFaddr(forsTreeAddr));
586
606
  setAddr({ type: AddressType.FORSTREE }, forsTreeAddr);
587
607
  const { root, authPath } = forsTreehash(
588
608
  context,
@@ -598,7 +618,9 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
598
618
  type: AddressType.FORSPK,
599
619
  keypairAddr: wotsAddr,
600
620
  });
601
- const root = context.thashN(K, concatBytes(...roots), forsPkAddr);
621
+ // Copy: thashN returns a per-context scratch view, and `root` lives across every hash
622
+ // call in the hypertree loop below (it is also mutated via root.set).
623
+ const root = copyBytes(context.thashN(K, concatBytes(...roots), forsPkAddr));
602
624
  // WOTS signatures
603
625
  const treeAddr = setAddr({ type: AddressType.HASHTREE });
604
626
  const wots: [Uint8Array, Uint8Array][] = [];
@@ -620,11 +642,24 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
620
642
  cleanBytes(R, random, treeAddr, wotsAddr, forsLeaf, forsTreeAddr, indices, roots);
621
643
  return SIG as TRet<Uint8Array>;
622
644
  },
623
- verify: (sig: TArg<Uint8Array>, msg: TArg<Uint8Array>, publicKey: TArg<Uint8Array>) => {
645
+ verify: (
646
+ sig: TArg<Uint8Array>,
647
+ msg: TArg<Uint8Array>,
648
+ publicKey: TArg<Uint8Array>,
649
+ opts: TArg<VerOpts> = {}
650
+ ) => {
651
+ // The internal verify reads no options; reject any so a stray key (e.g. a caller
652
+ // mistaking this for the public verify and passing `context`) is reported rather than
653
+ // silently swallowed by this function's arity.
654
+ validateVerOpts(opts, INTERNAL_VER_OPT_KEYS);
624
655
  const [pkSeed, pubRoot] = publicCoder.decode(publicKey);
625
- const [random, forsVec, wotsVec] = sigCoder.decode(sig);
626
656
  const pk = publicKey;
657
+ // FIPS 205 Algorithm 20 step 1: wrong-length signatures return false instead of throwing
658
+ // (same as ml-dsa). Must run before sigCoder.decode, which throws on length mismatch.
659
+ // Preserve TypeError for non-byte API arguments before treating byte lengths as invalid.
660
+ abytes(sig, undefined, 'signature');
627
661
  if (sig.length !== sigCoder.bytesLen) return false;
662
+ const [random, forsVec, wotsVec] = sigCoder.decode(sig);
628
663
  const context = getContext(pkSeed);
629
664
  let { tree, leafIdx, md } = hashMessage(random, pk, msg, context);
630
665
  const wotsAddr = setAddr({
@@ -644,14 +679,18 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
644
679
  const idxOffset = i << A;
645
680
  setAddr({ height: 0, index: indices[i] + idxOffset }, forsTreeAddr);
646
681
  const leaf = context.thash1(prf, forsTreeAddr);
647
- // Compute inplace, because we need all roots in same byte array
648
- roots.push(computeRoot(leaf, indices[i], idxOffset, authPath, A, context, forsTreeAddr));
682
+ // Copy: computeRoot returns a thashN scratch view, and roots are retained across the
683
+ // remaining FORS iterations (computeRoot itself copies `leaf` before hashing).
684
+ roots.push(
685
+ copyBytes(computeRoot(leaf, indices[i], idxOffset, authPath, A, context, forsTreeAddr))
686
+ );
649
687
  }
650
688
  const forsPkAddr = setAddr({
651
689
  type: AddressType.FORSPK,
652
690
  keypairAddr: wotsAddr,
653
691
  });
654
- let root = context.thashN(K, concatBytes(...roots), forsPkAddr); // root = thash()
692
+ // Copy: `root` must survive the thash1/thashN calls of the WOTS chain loop below.
693
+ let root = copyBytes(context.thashN(K, concatBytes(...roots), forsPkAddr)); // root = thash()
655
694
  // WOTS signature
656
695
  const treeAddr = setAddr({ type: AddressType.HASHTREE });
657
696
  const wotsPkAddr = setAddr({ type: AddressType.WOTSPK });
@@ -674,7 +713,8 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
674
713
  }
675
714
  }
676
715
  const leaf = context.thashN(WOTS_LEN, wotsPk, wotsPkAddr);
677
- root = computeRoot(leaf, leafIdx, 0, sigAuth, TREE_HEIGHT, context, treeAddr);
716
+ // Copy: `root` is read by chainLengths / equalBytes after later hash calls.
717
+ root = copyBytes(computeRoot(leaf, leafIdx, 0, sigAuth, TREE_HEIGHT, context, treeAddr));
678
718
  leafIdx = Number(tree & getMaskBig(TREE_HEIGHT));
679
719
  }
680
720
  return equalBytes(root, pubRoot);
@@ -688,9 +728,11 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
688
728
  keygen: internal.keygen,
689
729
  getPublicKey: internal.getPublicKey,
690
730
  sign: (msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>, opts: TArg<SigOpts> = {}) => {
691
- validateSigOpts(opts);
731
+ opts = validateSigOpts(opts);
692
732
  const M = getMessage(msg, opts.context);
693
- const res = internal.sign(M, secretKey, opts);
733
+ // `context` is consumed by getMessage() above; forwarding it would make the internal
734
+ // surface accept a key it never reads.
735
+ const res = internal.sign(M, secretKey, { extraEntropy: opts.extraEntropy });
694
736
  cleanBytes(M);
695
737
  return res as TRet<Uint8Array>;
696
738
  },
@@ -700,7 +742,7 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
700
742
  publicKey: TArg<Uint8Array>,
701
743
  opts: TArg<VerOpts> = {}
702
744
  ) => {
703
- validateVerOpts(opts);
745
+ opts = validateVerOpts(opts);
704
746
  return internal.verify(sig, getMessage(msg, opts.context), publicKey);
705
747
  },
706
748
  prehash: (hash: TArg<CHash>): TRet<Signer> => {
@@ -712,9 +754,10 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
712
754
  keygen: internal.keygen,
713
755
  getPublicKey: internal.getPublicKey,
714
756
  sign: (msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>, opts: TArg<SigOpts> = {}) => {
715
- validateSigOpts(opts);
757
+ opts = validateSigOpts(opts);
716
758
  const M = getMessagePrehash(rawHash, msg, opts.context);
717
- const res = internal.sign(M, secretKey, opts);
759
+ // As above: getMessagePrehash() consumes `context`, so it must not travel further.
760
+ const res = internal.sign(M, secretKey, { extraEntropy: opts.extraEntropy });
718
761
  cleanBytes(M);
719
762
  return res as TRet<Uint8Array>;
720
763
  },
@@ -724,7 +767,7 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
724
767
  publicKey: TArg<Uint8Array>,
725
768
  opts: TArg<VerOpts> = {}
726
769
  ) => {
727
- validateVerOpts(opts);
770
+ opts = validateVerOpts(opts);
728
771
  return internal.verify(sig, getMessagePrehash(rawHash, msg, opts.context), publicKey);
729
772
  },
730
773
  });
@@ -744,20 +787,27 @@ const genShake =
744
787
  // for each address-bound call instead of reabsorbing the same seed every time.
745
788
  const h0 = shake256.create({}).update(pubSeed);
746
789
  const h0tmp = h0.clone();
790
+ // Per-context output scratch: thash1/thashN/PRFaddr return these buffers directly, so
791
+ // callers must consume or copy a result before the next call on the same lane.
792
+ const thashOut = new Uint8Array(N);
793
+ const prfOut = new Uint8Array(N);
747
794
  const thash = (blocks: number, input: TArg<Uint8Array>, addr: TArg<ADRS>): TRet<Uint8Array> => {
748
795
  stats.thash++;
749
- return h0
750
- ._cloneInto(h0tmp)
796
+ const len = blocks * N;
797
+ h0._cloneInto(h0tmp)
751
798
  .update(addr)
752
- .update(input.subarray(0, blocks * N))
753
- .xof(N) as TRet<Uint8Array>;
799
+ .update(
800
+ input.length === len ? (input as Uint8Array) : (input as Uint8Array).subarray(0, len)
801
+ )
802
+ .xofInto(thashOut);
803
+ return thashOut as TRet<Uint8Array>;
754
804
  };
755
805
  return {
756
806
  PRFaddr: (addr: TArg<ADRS>): TRet<Uint8Array> => {
757
807
  if (!skSeed) throw new Error('no sk seed');
758
808
  stats.prf++;
759
- const res = h0._cloneInto(h0tmp).update(addr).update(skSeed).xof(N);
760
- return res as TRet<Uint8Array>;
809
+ h0._cloneInto(h0tmp).update(addr).update(skSeed).xofInto(prfOut);
810
+ return prfOut as TRet<Uint8Array>;
761
811
  },
762
812
  PRFmsg: (
763
813
  skPRF: TArg<Uint8Array>,
@@ -787,12 +837,13 @@ const genShake =
787
837
  clean: () => {
788
838
  h0.destroy();
789
839
  h0tmp.destroy();
840
+ cleanBytes(thashOut, prfOut);
790
841
  //console.log(stats);
791
842
  },
792
843
  } as TRet<Context>;
793
844
  };
794
845
 
795
- const SHAKE_SIMPLE = /* @__PURE__ */ (() => ({ getContext: genShake() }))();
846
+ const SHAKE_SIMPLE = /* @__PURE__ */ (() => ({ isCompressed: false, getContext: genShake() }))();
796
847
 
797
848
  /**
798
849
  * SLH-DSA-SHAKE-128f: Table 2 row `n=16, h=66, d=22, h'=3, a=6, k=33, lg w=4, m=34`;
@@ -869,6 +920,16 @@ const genSha =
869
920
 
870
921
  const h0tmp = h0ps.clone();
871
922
  const h1tmp = h1ps.clone();
923
+ // Per-context output scratch: thash1/thashN/PRFaddr return views into these buffers, so
924
+ // callers must consume or copy a result before the next call on the same lane (see Context
925
+ // docs). digestInto also skips digest()'s per-call destroy(): the tmp states are fully
926
+ // overwritten by the next _cloneInto and wiped in clean().
927
+ const h0out = new Uint8Array(h0.outputLen);
928
+ const h1out = new Uint8Array(h1.outputLen);
929
+ const prfOut = new Uint8Array(h0.outputLen);
930
+ const h0outN = h0out.subarray(0, N);
931
+ const h1outN = h1out.subarray(0, N);
932
+ const prfOutN = prfOut.subarray(0, N);
872
933
 
873
934
  // https://www.rfc-editor.org/rfc/rfc8017.html#appendix-B.2.1
874
935
  // This local helper is intentionally stricter than generic MGF1 reuse: current SLH-DSA callers
@@ -889,27 +950,28 @@ const genSha =
889
950
  }
890
951
 
891
952
  const thash =
892
- (_: ShaType, h: typeof h0ps, hTmp: typeof h0ps) =>
953
+ (h: typeof h0ps, hTmp: typeof h0ps, out: TArg<Uint8Array>, outN: TArg<Uint8Array>) =>
893
954
  (blocks: number, input: TArg<Uint8Array>, addr: TArg<ADRS>): TRet<Uint8Array> => {
894
955
  stats.thash++;
895
- const d = h
896
- ._cloneInto(hTmp as any)
956
+ const len = blocks * N;
957
+ h._cloneInto(hTmp as any)
897
958
  .update(addr)
898
- .update(input.subarray(0, blocks * N))
899
- .digest();
900
- return d.subarray(0, N) as TRet<Uint8Array>;
959
+ .update(
960
+ input.length === len ? (input as Uint8Array) : (input as Uint8Array).subarray(0, len)
961
+ )
962
+ .digestInto(out);
963
+ return outN as TRet<Uint8Array>;
901
964
  };
902
965
  return {
903
966
  PRFaddr: (addr: TArg<ADRS>): TRet<Uint8Array> => {
904
967
  if (!sk_seed) throw new Error('No sk seed');
905
968
  stats.prf++;
906
- const res = h0ps
969
+ h0ps
907
970
  ._cloneInto(h0tmp as any)
908
971
  .update(addr)
909
972
  .update(sk_seed)
910
- .digest()
911
- .subarray(0, N);
912
- return res as TRet<Uint8Array>;
973
+ .digestInto(prfOut);
974
+ return prfOutN as TRet<Uint8Array>;
913
975
  },
914
976
  PRFmsg: (
915
977
  skPRF: TArg<Uint8Array>,
@@ -938,13 +1000,14 @@ const genSha =
938
1000
  );
939
1001
  return mgf1(seed, outLen, h1);
940
1002
  },
941
- thash1: thash(h0, h0ps, h0tmp).bind(null, 1),
942
- thashN: thash(h1, h1ps, h1tmp),
1003
+ thash1: thash(h0ps, h0tmp, h0out, h0outN).bind(null, 1),
1004
+ thashN: thash(h1ps, h1tmp, h1out, h1outN),
943
1005
  clean: () => {
944
1006
  h0ps.destroy();
945
1007
  h1ps.destroy();
946
1008
  h0tmp.destroy();
947
1009
  h1tmp.destroy();
1010
+ cleanBytes(h0out, h1out, prfOut);
948
1011
  //console.log(stats);
949
1012
  },
950
1013
  } as TRet<Context>;
@@ -963,6 +1026,23 @@ const SHA512_SIMPLE = /* @__PURE__ */ (() => ({
963
1026
  * 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
1027
  * lengths `publicKey=32`, `secretKey=64`, `signature=17088`, `seed=48`, `signRand=16`.
965
1028
  * Also exposes `.prehash(...)`.
1029
+ * @example
1030
+ * Generate deterministic SLH-DSA keys, sign one message, and verify the signature.
1031
+ * ```ts
1032
+ * import { sha256 } from '@noble/hashes/sha2.js';
1033
+ * import { slh_dsa_sha2_128f } from '@noble/post-quantum/slh-dsa.js';
1034
+ * const seed = new Uint8Array(slh_dsa_sha2_128f.lengths.seed!);
1035
+ * const { secretKey, publicKey } = slh_dsa_sha2_128f.keygen(seed);
1036
+ * const msg = new TextEncoder().encode('hello noble');
1037
+ * const sig = slh_dsa_sha2_128f.sign(msg, secretKey);
1038
+ * const isValid = slh_dsa_sha2_128f.verify(sig, msg, publicKey);
1039
+ * const recovered = slh_dsa_sha2_128f.getPublicKey(secretKey);
1040
+ * const context = new Uint8Array([1, 2, 3]);
1041
+ * const prehash = slh_dsa_sha2_128f.prehash(sha256);
1042
+ * const preSig = prehash.sign(msg, secretKey, { context });
1043
+ * const preValid = prehash.verify(preSig, msg, publicKey, { context });
1044
+ * const internalSig = slh_dsa_sha2_128f.internal.sign(msg, secretKey);
1045
+ * ```
966
1046
  */
967
1047
  export const slh_dsa_sha2_128f: TRet<SphincsSigner> = /* @__PURE__ */ (() =>
968
1048
  gen(PARAMS['128f'], SHA256_SIMPLE))();