@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/README.md +191 -104
- package/_crystals.d.ts +8 -3
- package/_crystals.js +38 -10
- package/falcon.d.ts +1 -2
- package/falcon.js +202 -115
- package/hybrid.d.ts +38 -28
- package/hybrid.js +215 -86
- package/index.d.ts +0 -1
- package/index.js +1 -2
- package/ml-dsa.d.ts +31 -5
- package/ml-dsa.js +118 -34
- package/ml-kem.d.ts +45 -4
- package/ml-kem.js +206 -64
- package/package.json +16 -20
- package/slh-dsa.d.ts +23 -3
- package/slh-dsa.js +127 -60
- package/src/_crystals.ts +45 -11
- package/src/falcon.ts +206 -121
- package/src/hybrid.ts +217 -83
- package/src/index.ts +1 -1
- package/src/ml-dsa.ts +140 -43
- package/src/ml-kem.ts +239 -66
- package/src/slh-dsa.ts +149 -69
- package/src/utils.ts +186 -25
- package/src/webcrypto.ts +322 -0
- package/utils.d.ts +54 -4
- package/utils.js +167 -28
- package/webcrypto.d.ts +91 -0
- package/webcrypto.js +213 -0
- 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/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 (
|
|
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<
|
|
234
|
-
const hashOpts = hashOpts_ as
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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: (
|
|
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
|
-
//
|
|
648
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
750
|
-
|
|
796
|
+
const len = blocks * N;
|
|
797
|
+
h0._cloneInto(h0tmp)
|
|
751
798
|
.update(addr)
|
|
752
|
-
.update(
|
|
753
|
-
|
|
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
|
-
|
|
760
|
-
return
|
|
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
|
-
(
|
|
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
|
|
896
|
-
|
|
956
|
+
const len = blocks * N;
|
|
957
|
+
h._cloneInto(hTmp as any)
|
|
897
958
|
.update(addr)
|
|
898
|
-
.update(
|
|
899
|
-
|
|
900
|
-
|
|
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
|
-
|
|
969
|
+
h0ps
|
|
907
970
|
._cloneInto(h0tmp as any)
|
|
908
971
|
.update(addr)
|
|
909
972
|
.update(sk_seed)
|
|
910
|
-
.
|
|
911
|
-
|
|
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(
|
|
942
|
-
thashN: thash(
|
|
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))();
|