@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/README.md +106 -80
- package/_crystals.d.ts +23 -16
- package/_crystals.js +50 -12
- package/falcon.d.ts +7 -8
- package/falcon.js +109 -74
- package/hybrid.d.ts +21 -32
- package/hybrid.js +157 -78
- package/index.d.ts +0 -1
- package/index.js +8 -1
- package/ml-dsa.d.ts +35 -9
- package/ml-dsa.js +116 -43
- package/ml-kem.d.ts +46 -5
- package/ml-kem.js +175 -67
- package/package.json +10 -18
- package/slh-dsa.d.ts +44 -24
- package/slh-dsa.js +150 -84
- package/src/_crystals.ts +86 -35
- package/src/falcon.ts +230 -169
- package/src/hybrid.ts +243 -129
- package/src/index.ts +8 -0
- package/src/ml-dsa.ts +194 -87
- package/src/ml-kem.ts +283 -122
- package/src/slh-dsa.ts +310 -182
- package/src/utils.ts +244 -48
- package/utils.d.ts +99 -24
- package/utils.js +92 -24
- 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,
|
|
@@ -53,6 +48,8 @@ import {
|
|
|
53
48
|
vecCoder,
|
|
54
49
|
type Signer,
|
|
55
50
|
type SigOpts,
|
|
51
|
+
type TArg,
|
|
52
|
+
type TRet,
|
|
56
53
|
type VerOpts,
|
|
57
54
|
} from './utils.ts';
|
|
58
55
|
|
|
@@ -96,14 +93,14 @@ export type SphincsHashOpts = {
|
|
|
96
93
|
* stay derived at the export layer.
|
|
97
94
|
*/
|
|
98
95
|
export const PARAMS: Record<string, SphincsOpts> = /* @__PURE__ */ (() =>
|
|
99
|
-
({
|
|
100
|
-
'128f': { W: 16, N: 16, H: 66, D: 22, K: 33, A: 6, securityLevel: 128 },
|
|
101
|
-
'128s': { W: 16, N: 16, H: 63, D: 7, K: 14, A: 12, securityLevel: 128 },
|
|
102
|
-
'192f': { W: 16, N: 24, H: 66, D: 22, K: 33, A: 8, securityLevel: 192 },
|
|
103
|
-
'192s': { W: 16, N: 24, H: 63, D: 7, K: 17, A: 14, securityLevel: 192 },
|
|
104
|
-
'256f': { W: 16, N: 32, H: 68, D: 17, K: 35, A: 9, securityLevel: 256 },
|
|
105
|
-
'256s': { W: 16, N: 32, H: 64, D: 8, K: 22, A: 14, securityLevel: 256 },
|
|
106
|
-
}
|
|
96
|
+
Object.freeze({
|
|
97
|
+
'128f': Object.freeze({ W: 16, N: 16, H: 66, D: 22, K: 33, A: 6, securityLevel: 128 }),
|
|
98
|
+
'128s': Object.freeze({ W: 16, N: 16, H: 63, D: 7, K: 14, A: 12, securityLevel: 128 }),
|
|
99
|
+
'192f': Object.freeze({ W: 16, N: 24, H: 66, D: 22, K: 33, A: 8, securityLevel: 192 }),
|
|
100
|
+
'192s': Object.freeze({ W: 16, N: 24, H: 63, D: 7, K: 17, A: 14, securityLevel: 192 }),
|
|
101
|
+
'256f': Object.freeze({ W: 16, N: 32, H: 68, D: 17, K: 35, A: 9, securityLevel: 256 }),
|
|
102
|
+
'256s': Object.freeze({ W: 16, N: 32, H: 64, D: 8, K: 22, A: 14, securityLevel: 256 }),
|
|
103
|
+
} as const))();
|
|
107
104
|
|
|
108
105
|
// FIPS 205 `ADRS.setTypeAndClear(...)` selectors. Local names shorten the spec labels
|
|
109
106
|
// (`WOTS_HASH` -> `WOTS`, `TREE` -> `HASHTREE`, `FORS_ROOTS` -> `FORSPK`), and `setAddr({ type })`
|
|
@@ -121,14 +118,18 @@ const AddressType = {
|
|
|
121
118
|
/** Address byte array of size `ADDR_BYTES`. */
|
|
122
119
|
export type ADRS = Uint8Array;
|
|
123
120
|
|
|
124
|
-
/** 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
|
+
*/
|
|
125
126
|
export type Context = {
|
|
126
127
|
/**
|
|
127
128
|
* Derive a PRF output for one address.
|
|
128
129
|
* @param addr - Address bytes.
|
|
129
|
-
* @returns PRF output bytes.
|
|
130
|
+
* @returns PRF output bytes (scratch view; copy to retain).
|
|
130
131
|
*/
|
|
131
|
-
PRFaddr: (addr: ADRS) => Uint8Array
|
|
132
|
+
PRFaddr: (addr: TArg<ADRS>) => TRet<Uint8Array>;
|
|
132
133
|
/**
|
|
133
134
|
* Derive the randomized message hash prefix.
|
|
134
135
|
* @param skPRF - Secret PRF seed.
|
|
@@ -136,7 +137,11 @@ export type Context = {
|
|
|
136
137
|
* @param msg - Message bytes.
|
|
137
138
|
* @returns PRF output bytes.
|
|
138
139
|
*/
|
|
139
|
-
PRFmsg: (
|
|
140
|
+
PRFmsg: (
|
|
141
|
+
skPRF: TArg<Uint8Array>,
|
|
142
|
+
random: TArg<Uint8Array>,
|
|
143
|
+
msg: TArg<Uint8Array>
|
|
144
|
+
) => TRet<Uint8Array>;
|
|
140
145
|
/**
|
|
141
146
|
* Hash one randomized message transcript.
|
|
142
147
|
* @param R - Randomized message prefix.
|
|
@@ -145,14 +150,19 @@ export type Context = {
|
|
|
145
150
|
* @param outLen - Output length in bytes.
|
|
146
151
|
* @returns Transcript hash bytes.
|
|
147
152
|
*/
|
|
148
|
-
Hmsg: (
|
|
153
|
+
Hmsg: (
|
|
154
|
+
R: TArg<Uint8Array>,
|
|
155
|
+
pk: TArg<Uint8Array>,
|
|
156
|
+
m: TArg<Uint8Array>,
|
|
157
|
+
outLen: number
|
|
158
|
+
) => TRet<Uint8Array>;
|
|
149
159
|
/**
|
|
150
160
|
* Tweakable hash over one input block.
|
|
151
161
|
* @param input - Input block.
|
|
152
162
|
* @param addr - Address bytes.
|
|
153
163
|
* @returns Hash output bytes.
|
|
154
164
|
*/
|
|
155
|
-
thash1: (input: Uint8Array
|
|
165
|
+
thash1: (input: TArg<Uint8Array>, addr: TArg<ADRS>) => TRet<Uint8Array>;
|
|
156
166
|
/**
|
|
157
167
|
* Tweakable hash over multiple input blocks.
|
|
158
168
|
* @param blocks - Number of input blocks.
|
|
@@ -160,36 +170,21 @@ export type Context = {
|
|
|
160
170
|
* @param addr - Address bytes.
|
|
161
171
|
* @returns Hash output bytes.
|
|
162
172
|
*/
|
|
163
|
-
thashN: (blocks: number, input: Uint8Array
|
|
173
|
+
thashN: (blocks: number, input: TArg<Uint8Array>, addr: TArg<ADRS>) => TRet<Uint8Array>;
|
|
164
174
|
/** Wipe any buffered hash state for the current context. */
|
|
165
175
|
clean: () => void;
|
|
166
176
|
};
|
|
167
177
|
/** Factory that creates a context generator for one SLH-DSA parameter set. */
|
|
168
178
|
export type GetContext = (
|
|
169
179
|
opts: SphincsOpts
|
|
170
|
-
) => (pub_seed: Uint8Array
|
|
171
|
-
|
|
172
|
-
function hexToNumber(hex: string): bigint {
|
|
173
|
-
if (typeof hex !== 'string') throw new Error('hex string expected, got ' + typeof hex);
|
|
174
|
-
return BigInt(hex === '' ? '0' : '0x' + hex); // Big Endian
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// BE: Big Endian, LE: Little Endian. This is the local FIPS 205 `toInt(...)` equivalent.
|
|
178
|
-
function bytesToNumberBE(bytes: Uint8Array): bigint {
|
|
179
|
-
return hexToNumber(bytesToHex(bytes));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Local in-range FIPS 205 `toByte(x, n)` equivalent; callers must keep `n < 256^len`.
|
|
183
|
-
function numberToBytesBE(n: number | bigint, len: number): Uint8Array {
|
|
184
|
-
return hexToBytes(n.toString(16).padStart(len * 2, '0'));
|
|
185
|
-
}
|
|
180
|
+
) => (pub_seed: TArg<Uint8Array>, sk_seed?: TArg<Uint8Array>) => TRet<Context>;
|
|
186
181
|
|
|
187
182
|
// Local FIPS 205 Algorithm 4 `base_2^b(...)` implementation. Bits are consumed in big-endian
|
|
188
183
|
// order within each input byte, and callers must provide at least `ceil(outLen * b / 8)` bytes;
|
|
189
184
|
// short inputs are not rejected and would zero-extend implicitly.
|
|
190
185
|
const base2b = (outLen: number, b: number) => {
|
|
191
186
|
const mask = getMask(b);
|
|
192
|
-
return (bytes: Uint8Array) => {
|
|
187
|
+
return (bytes: TArg<Uint8Array>): TRet<Uint32Array> => {
|
|
193
188
|
const baseB = new Uint32Array(outLen);
|
|
194
189
|
for (let out = 0, pos = 0, bits = 0, total = 0; out < outLen; out++) {
|
|
195
190
|
while (bits < b) {
|
|
@@ -199,19 +194,23 @@ const base2b = (outLen: number, b: number) => {
|
|
|
199
194
|
bits -= b;
|
|
200
195
|
baseB[out] = (total >>> bits) & mask;
|
|
201
196
|
}
|
|
202
|
-
return baseB
|
|
197
|
+
return baseB as TRet<Uint32Array>;
|
|
203
198
|
};
|
|
204
199
|
};
|
|
205
200
|
|
|
201
|
+
const _1n = /* @__PURE__ */ BigInt(1);
|
|
202
|
+
const _8n = /* @__PURE__ */ BigInt(8);
|
|
203
|
+
const _0xffn = /* @__PURE__ */ BigInt(0xff);
|
|
204
|
+
|
|
206
205
|
function getMaskBig(bits: number) {
|
|
207
|
-
return (
|
|
206
|
+
return (_1n << BigInt(bits)) - _1n; // 4 -> 0b1111
|
|
208
207
|
}
|
|
209
208
|
|
|
210
209
|
/** Public SLH-DSA signer with prehash customization. */
|
|
211
210
|
export type SphincsSigner = Signer & {
|
|
212
|
-
internal: Signer
|
|
211
|
+
internal: TRet<Signer>;
|
|
213
212
|
securityLevel: number;
|
|
214
|
-
prehash: (hash: CHash) => Signer
|
|
213
|
+
prehash: (hash: TArg<CHash>) => TRet<Signer>;
|
|
215
214
|
};
|
|
216
215
|
|
|
217
216
|
/** One parameter/hash instantiation of the public SLH-DSA API.
|
|
@@ -219,7 +218,8 @@ export type SphincsSigner = Signer & {
|
|
|
219
218
|
* and `getPublicKey(secretKey)` only extracts the embedded public key
|
|
220
219
|
* instead of recomputing `PK.root`.
|
|
221
220
|
*/
|
|
222
|
-
function gen(opts: SphincsOpts,
|
|
221
|
+
function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsSigner> {
|
|
222
|
+
const hashOpts = hashOpts_ as SphincsHashOpts;
|
|
223
223
|
const { N, W, H, D, K, A, securityLevel: securityLevel } = opts;
|
|
224
224
|
const getContext = hashOpts.getContext(opts);
|
|
225
225
|
if (W !== 16) throw new Error('Unsupported Winternitz parameter');
|
|
@@ -256,7 +256,7 @@ function gen(opts: SphincsOpts, hashOpts: SphincsHashOpts): SphincsSigner {
|
|
|
256
256
|
// `height` / `chain` and `index` / `hash` share the same spec words, so callers must use the
|
|
257
257
|
// address-type-specific combinations instead of mixing both meanings in one call.
|
|
258
258
|
const setAddr = (
|
|
259
|
-
opts: {
|
|
259
|
+
opts: TArg<{
|
|
260
260
|
type?: (typeof AddressType)[keyof typeof AddressType];
|
|
261
261
|
height?: number;
|
|
262
262
|
tree?: bigint;
|
|
@@ -267,21 +267,30 @@ function gen(opts: SphincsOpts, hashOpts: SphincsHashOpts): SphincsSigner {
|
|
|
267
267
|
keypair?: number;
|
|
268
268
|
subtreeAddr?: ADRS;
|
|
269
269
|
keypairAddr?: ADRS;
|
|
270
|
-
}
|
|
271
|
-
addr: ADRS = new Uint8Array(ADDR_BYTES)
|
|
270
|
+
}>,
|
|
271
|
+
addr: TArg<ADRS> = new Uint8Array(ADDR_BYTES)
|
|
272
272
|
) => {
|
|
273
273
|
const { type, height, tree, layer, index, chain, hash, keypair } = opts;
|
|
274
274
|
const { subtreeAddr, keypairAddr } = opts;
|
|
275
|
-
const v = createView(addr);
|
|
276
275
|
|
|
277
276
|
if (height !== undefined) addr[OFFSET_CHAIN_ADDR] = height;
|
|
278
277
|
if (layer !== undefined) addr[OFFSET_LAYER] = layer;
|
|
279
278
|
if (type !== undefined) addr[OFFSET_TYPE] = type;
|
|
280
279
|
if (chain !== undefined) addr[OFFSET_CHAIN_ADDR] = chain;
|
|
281
280
|
if (hash !== undefined) addr[OFFSET_HASH_ADDR] = hash;
|
|
282
|
-
|
|
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
|
+
}
|
|
283
289
|
if (subtreeAddr) addr.set(subtreeAddr.subarray(0, OFFSET_TREE + 8));
|
|
284
|
-
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
|
+
}
|
|
285
294
|
if (keypair !== undefined) {
|
|
286
295
|
addr[OFFSET_KP_ADDR1] = keypair;
|
|
287
296
|
if (TREE_HEIGHT > 8) addr[OFFSET_KP_ADDR2] = keypair >>> 8;
|
|
@@ -295,7 +304,7 @@ function gen(opts: SphincsOpts, hashOpts: SphincsHashOpts): SphincsSigner {
|
|
|
295
304
|
};
|
|
296
305
|
|
|
297
306
|
const chainCoder = base2b(WOTS_LEN2, WOTS_LOGW);
|
|
298
|
-
const chainLengths = (msg: Uint8Array) => {
|
|
307
|
+
const chainLengths = (msg: TArg<Uint8Array>) => {
|
|
299
308
|
const W1 = base2b(WOTS_LEN1, WOTS_LOGW)(msg);
|
|
300
309
|
let csum = 0;
|
|
301
310
|
for (let i = 0; i < W1.length; i++) csum += W - 1 - W1[i]; // ▷ Compute checksum
|
|
@@ -321,9 +330,15 @@ function gen(opts: SphincsOpts, hashOpts: SphincsHashOpts): SphincsSigner {
|
|
|
321
330
|
);
|
|
322
331
|
// `pkSeed` is the full public key byte string `PK.seed || PK.root`; after splitting `Hmsg`,
|
|
323
332
|
// mask away any spare high bits so `idx_tree` / `idx_leaf` match the spec's final mod-2^k steps.
|
|
324
|
-
const hashMessage = (
|
|
333
|
+
const hashMessage = (
|
|
334
|
+
R: TArg<Uint8Array>,
|
|
335
|
+
pkSeed: TArg<Uint8Array>,
|
|
336
|
+
msg: TArg<Uint8Array>,
|
|
337
|
+
context: TArg<Context>
|
|
338
|
+
) => {
|
|
339
|
+
const rawContext = context as Context;
|
|
325
340
|
// digest ← Hmsg(R, PK.seed, PK.root, M)
|
|
326
|
-
const digest =
|
|
341
|
+
const digest = rawContext.Hmsg(R, pkSeed, msg, hashMsgCoder.bytesLen);
|
|
327
342
|
const [md, tmpIdxTree, tmpIdxLeaf] = hashMsgCoder.decode(digest);
|
|
328
343
|
const tree = bytesToNumberBE(tmpIdxTree) & getMaskBig(TREE_BITS);
|
|
329
344
|
const leafIdx = Number(bytesToNumberBE(tmpIdxLeaf)) & getMask(LEAF_BITS);
|
|
@@ -335,24 +350,33 @@ function gen(opts: SphincsOpts, hashOpts: SphincsHashOpts): SphincsSigner {
|
|
|
335
350
|
// neighbor of the target leaf at that height.
|
|
336
351
|
const treehash = <T>(
|
|
337
352
|
height: number,
|
|
338
|
-
fn: (leafIdx: number, addrOffset: number, context: Context, info: T) => Uint8Array
|
|
353
|
+
fn: TArg<(leafIdx: number, addrOffset: number, context: Context, info: T) => Uint8Array>
|
|
339
354
|
) =>
|
|
340
355
|
function treehash_i(
|
|
341
|
-
context: Context
|
|
356
|
+
context: TArg<Context>,
|
|
342
357
|
leafIdx: number,
|
|
343
358
|
idxOffset: number,
|
|
344
|
-
treeAddr: ADRS
|
|
359
|
+
treeAddr: TArg<ADRS>,
|
|
345
360
|
info: T
|
|
346
361
|
) {
|
|
362
|
+
const rawContext = context as Context;
|
|
363
|
+
const leafFn = fn as (
|
|
364
|
+
leafIdx: number,
|
|
365
|
+
addrOffset: number,
|
|
366
|
+
context: Context,
|
|
367
|
+
info: T
|
|
368
|
+
) => Uint8Array;
|
|
347
369
|
const maxIdx = (1 << height) - 1;
|
|
348
370
|
const stack = new Uint8Array(height * N);
|
|
349
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);
|
|
350
377
|
for (let idx = 0; ; idx++) {
|
|
351
|
-
const current = new Uint8Array(2 * N);
|
|
352
|
-
const cur0 = current.subarray(0, N);
|
|
353
|
-
const cur1 = current.subarray(N);
|
|
354
378
|
const addrOffset = idx + idxOffset;
|
|
355
|
-
cur1.set(
|
|
379
|
+
cur1.set(leafFn(leafIdx, addrOffset, rawContext, info));
|
|
356
380
|
let h = 0;
|
|
357
381
|
for (let i = idx, o = idxOffset, l = leafIdx; ; h++, i >>>= 1, l >>>= 1, o >>>= 1) {
|
|
358
382
|
if (h === height) return { root: cur1, authPath }; // Returns from here
|
|
@@ -360,7 +384,7 @@ function gen(opts: SphincsOpts, hashOpts: SphincsHashOpts): SphincsSigner {
|
|
|
360
384
|
if ((i & 1) === 0 && idx < maxIdx) break;
|
|
361
385
|
setAddr({ height: h + 1, index: (i >> 1) + (o >> 1) }, treeAddr);
|
|
362
386
|
cur0.set(stack.subarray(h * N).subarray(0, N));
|
|
363
|
-
cur1.set(
|
|
387
|
+
cur1.set(rawContext.thashN(2, current, treeAddr));
|
|
364
388
|
}
|
|
365
389
|
stack.subarray(h * N).set(cur1); // stack.push(cur1)
|
|
366
390
|
}
|
|
@@ -374,45 +398,53 @@ function gen(opts: SphincsOpts, hashOpts: SphincsHashOpts): SphincsSigner {
|
|
|
374
398
|
leafAddr: ADRS;
|
|
375
399
|
pkAddr: ADRS;
|
|
376
400
|
};
|
|
377
|
-
const wotsTreehash = treehash(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
401
|
+
const wotsTreehash = treehash(
|
|
402
|
+
TREE_HEIGHT,
|
|
403
|
+
(leafIdx: number, addrOffset: number, context: TArg<Context>, info: TArg<LeafInfo>) => {
|
|
404
|
+
const rawContext = context as Context;
|
|
405
|
+
const wotsPk = new Uint8Array(WOTS_LEN * N);
|
|
406
|
+
// `keygen()` passes `leafIdx = ~0 >>> 0`, so no real XMSS leaf matches and this suppresses
|
|
407
|
+
// WOTS signature capture while still hashing every chain to its public-key endpoint.
|
|
408
|
+
const wotsKmask = addrOffset === leafIdx ? 0 : ~0 >>> 0;
|
|
409
|
+
setAddr({ keypair: addrOffset }, info.leafAddr);
|
|
410
|
+
setAddr({ keypair: addrOffset }, info.pkAddr);
|
|
411
|
+
for (let i = 0; i < WOTS_LEN; i++) {
|
|
412
|
+
const wotsK = info.wotsSteps[i] | wotsKmask;
|
|
413
|
+
const pk = wotsPk.subarray(i * N, (i + 1) * N);
|
|
414
|
+
setAddr({ chain: i, hash: 0, type: AddressType.WOTSPRF }, info.leafAddr);
|
|
415
|
+
pk.set(rawContext.PRFaddr(info.leafAddr));
|
|
416
|
+
setAddr({ type: AddressType.WOTS }, info.leafAddr);
|
|
417
|
+
for (let k = 0; ; k++) {
|
|
418
|
+
if (k === wotsK) info.wotsSig.subarray(i * N).set(pk); //wotsSig.push()
|
|
419
|
+
if (k === W - 1) break;
|
|
420
|
+
setAddr({ hash: k }, info.leafAddr);
|
|
421
|
+
pk.set(rawContext.thash1(pk, info.leafAddr));
|
|
422
|
+
}
|
|
395
423
|
}
|
|
424
|
+
return rawContext.thashN(WOTS_LEN, wotsPk, info.pkAddr);
|
|
396
425
|
}
|
|
397
|
-
|
|
398
|
-
});
|
|
426
|
+
);
|
|
399
427
|
|
|
400
|
-
const forsTreehash = treehash(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
428
|
+
const forsTreehash = treehash(
|
|
429
|
+
A,
|
|
430
|
+
(_: number, addrOffset: number, context: TArg<Context>, forsLeafAddr: TArg<ForsLeafInfo>) => {
|
|
431
|
+
const rawContext = context as Context;
|
|
432
|
+
setAddr({ type: AddressType.FORSPRF, index: addrOffset }, forsLeafAddr);
|
|
433
|
+
const prf = rawContext.PRFaddr(forsLeafAddr);
|
|
434
|
+
setAddr({ type: AddressType.FORSTREE }, forsLeafAddr);
|
|
435
|
+
return rawContext.thash1(prf, forsLeafAddr);
|
|
436
|
+
}
|
|
437
|
+
);
|
|
406
438
|
|
|
407
439
|
// Fuse `xmss_sign` with the subtree-root computation needed by `ht_sign`, so one tree walk
|
|
408
440
|
// yields both the WOTS/auth-path signature and the root that the next hypertree layer signs.
|
|
409
441
|
const merkleSign = (
|
|
410
|
-
context: Context
|
|
411
|
-
wotsAddr: ADRS
|
|
412
|
-
treeAddr: ADRS
|
|
442
|
+
context: TArg<Context>,
|
|
443
|
+
wotsAddr: TArg<ADRS>,
|
|
444
|
+
treeAddr: TArg<ADRS>,
|
|
413
445
|
leafIdx: number,
|
|
414
|
-
prevRoot: Uint8Array = new Uint8Array(N)
|
|
415
|
-
) => {
|
|
446
|
+
prevRoot: TArg<Uint8Array> = new Uint8Array(N)
|
|
447
|
+
): TRet<{ root: Uint8Array; sigWots: Uint8Array; sigAuth: Uint8Array }> => {
|
|
416
448
|
setAddr({ type: AddressType.HASHTREE }, treeAddr);
|
|
417
449
|
// State variables
|
|
418
450
|
const info = {
|
|
@@ -426,20 +458,21 @@ function gen(opts: SphincsOpts, hashOpts: SphincsHashOpts): SphincsSigner {
|
|
|
426
458
|
root,
|
|
427
459
|
sigWots: info.wotsSig.subarray(0, WOTS_LEN * N),
|
|
428
460
|
sigAuth: authPath,
|
|
429
|
-
};
|
|
461
|
+
} as TRet<{ root: Uint8Array; sigWots: Uint8Array; sigAuth: Uint8Array }>;
|
|
430
462
|
};
|
|
431
463
|
|
|
432
464
|
type ForsLeafInfo = ADRS;
|
|
433
465
|
|
|
434
466
|
const computeRoot = (
|
|
435
|
-
leaf: Uint8Array
|
|
467
|
+
leaf: TArg<Uint8Array>,
|
|
436
468
|
leafIdx: number,
|
|
437
469
|
idxOffset: number,
|
|
438
|
-
authPath: Uint8Array
|
|
470
|
+
authPath: TArg<Uint8Array>,
|
|
439
471
|
treeHeight: number,
|
|
440
|
-
context: Context
|
|
441
|
-
addr: ADRS
|
|
472
|
+
context: TArg<Context>,
|
|
473
|
+
addr: TArg<ADRS>
|
|
442
474
|
) => {
|
|
475
|
+
const rawContext = context as Context;
|
|
443
476
|
const buffer = new Uint8Array(2 * N);
|
|
444
477
|
const b0 = buffer.subarray(0, N);
|
|
445
478
|
const b1 = buffer.subarray(N, 2 * N);
|
|
@@ -462,16 +495,16 @@ function gen(opts: SphincsOpts, hashOpts: SphincsHashOpts): SphincsSigner {
|
|
|
462
495
|
setAddr({ height: i + 1, index: leafIdx + idxOffset }, addr);
|
|
463
496
|
const a = authPath.subarray((i + 1) * N, (i + 2) * N);
|
|
464
497
|
if ((leafIdx & 1) !== 0) {
|
|
465
|
-
b1.set(
|
|
498
|
+
b1.set(rawContext.thashN(2, buffer, addr));
|
|
466
499
|
b0.set(a);
|
|
467
500
|
} else {
|
|
468
|
-
buffer.set(
|
|
501
|
+
buffer.set(rawContext.thashN(2, buffer, addr));
|
|
469
502
|
b1.set(a);
|
|
470
503
|
}
|
|
471
504
|
}
|
|
472
505
|
// Root
|
|
473
506
|
setAddr({ height: treeHeight, index: leafIdx + idxOffset }, addr);
|
|
474
|
-
return
|
|
507
|
+
return rawContext.thashN(2, buffer, addr);
|
|
475
508
|
};
|
|
476
509
|
|
|
477
510
|
const seedCoder = splitCoder('seed', N, N, N);
|
|
@@ -480,16 +513,16 @@ function gen(opts: SphincsOpts, hashOpts: SphincsHashOpts): SphincsSigner {
|
|
|
480
513
|
const forsCoder = vecCoder(splitCoder('fors', N, N * A), K);
|
|
481
514
|
const wotsCoder = vecCoder(splitCoder('wots', WOTS_LEN * N, TREE_HEIGHT * N), D);
|
|
482
515
|
const sigCoder = splitCoder('signature', N, forsCoder, wotsCoder); // random || fors || wots
|
|
483
|
-
const internal: Signer = {
|
|
484
|
-
info: { type: 'internal-slh-dsa' },
|
|
485
|
-
lengths: {
|
|
516
|
+
const internal: TRet<Signer> = Object.freeze({
|
|
517
|
+
info: Object.freeze({ type: 'internal-slh-dsa' }),
|
|
518
|
+
lengths: Object.freeze({
|
|
486
519
|
publicKey: publicCoder.bytesLen,
|
|
487
520
|
secretKey: secretCoder.bytesLen,
|
|
488
521
|
signature: sigCoder.bytesLen,
|
|
489
522
|
seed: seedCoder.bytesLen,
|
|
490
523
|
signRand: N,
|
|
491
|
-
},
|
|
492
|
-
keygen(seed?: Uint8Array) {
|
|
524
|
+
}),
|
|
525
|
+
keygen(seed?: TArg<Uint8Array>) {
|
|
493
526
|
if (seed !== undefined) abytes(seed, seedCoder.bytesLen, 'seed');
|
|
494
527
|
seed = seed === undefined ? randomBytes(seedCoder.bytesLen) : copyBytes(seed);
|
|
495
528
|
// Set SK.seed, SK.prf, and PK.seed to random n-byte
|
|
@@ -504,13 +537,16 @@ function gen(opts: SphincsOpts, hashOpts: SphincsHashOpts): SphincsSigner {
|
|
|
504
537
|
const secretKey = secretCoder.encode([secretSeed, secretPRF, publicKey]);
|
|
505
538
|
context.clean();
|
|
506
539
|
cleanBytes(secretSeed, secretPRF, root, wotsAddr, topTreeAddr);
|
|
507
|
-
return {
|
|
540
|
+
return {
|
|
541
|
+
publicKey: publicKey as TRet<Uint8Array>,
|
|
542
|
+
secretKey: secretKey as TRet<Uint8Array>,
|
|
543
|
+
};
|
|
508
544
|
},
|
|
509
|
-
getPublicKey: (secretKey: Uint8Array) => {
|
|
545
|
+
getPublicKey: (secretKey: TArg<Uint8Array>): TRet<Uint8Array> => {
|
|
510
546
|
const [_skSeed, _skPRF, pk] = secretCoder.decode(secretKey);
|
|
511
|
-
return Uint8Array.from(pk)
|
|
547
|
+
return Uint8Array.from(pk) as TRet<Uint8Array>;
|
|
512
548
|
},
|
|
513
|
-
sign: (msg: Uint8Array
|
|
549
|
+
sign: (msg: TArg<Uint8Array>, sk: TArg<Uint8Array>, opts: TArg<SigOpts> = {}) => {
|
|
514
550
|
validateSigOpts(opts);
|
|
515
551
|
let { extraEntropy: random } = opts;
|
|
516
552
|
const [skSeed, skPRF, pk] = secretCoder.decode(sk); // todo: fix
|
|
@@ -545,7 +581,9 @@ function gen(opts: SphincsOpts, hashOpts: SphincsHashOpts): SphincsSigner {
|
|
|
545
581
|
},
|
|
546
582
|
forsTreeAddr
|
|
547
583
|
);
|
|
548
|
-
|
|
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));
|
|
549
587
|
setAddr({ type: AddressType.FORSTREE }, forsTreeAddr);
|
|
550
588
|
const { root, authPath } = forsTreehash(
|
|
551
589
|
context,
|
|
@@ -561,7 +599,9 @@ function gen(opts: SphincsOpts, hashOpts: SphincsHashOpts): SphincsSigner {
|
|
|
561
599
|
type: AddressType.FORSPK,
|
|
562
600
|
keypairAddr: wotsAddr,
|
|
563
601
|
});
|
|
564
|
-
|
|
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));
|
|
565
605
|
// WOTS signatures
|
|
566
606
|
const treeAddr = setAddr({ type: AddressType.HASHTREE });
|
|
567
607
|
const wots: [Uint8Array, Uint8Array][] = [];
|
|
@@ -581,13 +621,17 @@ function gen(opts: SphincsOpts, hashOpts: SphincsHashOpts): SphincsSigner {
|
|
|
581
621
|
context.clean();
|
|
582
622
|
const SIG = sigCoder.encode([R, fors, wots]);
|
|
583
623
|
cleanBytes(R, random, treeAddr, wotsAddr, forsLeaf, forsTreeAddr, indices, roots);
|
|
584
|
-
return SIG
|
|
624
|
+
return SIG as TRet<Uint8Array>;
|
|
585
625
|
},
|
|
586
|
-
verify: (sig: Uint8Array
|
|
626
|
+
verify: (sig: TArg<Uint8Array>, msg: TArg<Uint8Array>, publicKey: TArg<Uint8Array>) => {
|
|
587
627
|
const [pkSeed, pubRoot] = publicCoder.decode(publicKey);
|
|
588
|
-
const [random, forsVec, wotsVec] = sigCoder.decode(sig);
|
|
589
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');
|
|
590
633
|
if (sig.length !== sigCoder.bytesLen) return false;
|
|
634
|
+
const [random, forsVec, wotsVec] = sigCoder.decode(sig);
|
|
591
635
|
const context = getContext(pkSeed);
|
|
592
636
|
let { tree, leafIdx, md } = hashMessage(random, pk, msg, context);
|
|
593
637
|
const wotsAddr = setAddr({
|
|
@@ -607,14 +651,18 @@ function gen(opts: SphincsOpts, hashOpts: SphincsHashOpts): SphincsSigner {
|
|
|
607
651
|
const idxOffset = i << A;
|
|
608
652
|
setAddr({ height: 0, index: indices[i] + idxOffset }, forsTreeAddr);
|
|
609
653
|
const leaf = context.thash1(prf, forsTreeAddr);
|
|
610
|
-
//
|
|
611
|
-
|
|
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
|
+
);
|
|
612
659
|
}
|
|
613
660
|
const forsPkAddr = setAddr({
|
|
614
661
|
type: AddressType.FORSPK,
|
|
615
662
|
keypairAddr: wotsAddr,
|
|
616
663
|
});
|
|
617
|
-
|
|
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()
|
|
618
666
|
// WOTS signature
|
|
619
667
|
const treeAddr = setAddr({ type: AddressType.HASHTREE });
|
|
620
668
|
const wotsPkAddr = setAddr({ type: AddressType.WOTSPK });
|
|
@@ -637,83 +685,119 @@ function gen(opts: SphincsOpts, hashOpts: SphincsHashOpts): SphincsSigner {
|
|
|
637
685
|
}
|
|
638
686
|
}
|
|
639
687
|
const leaf = context.thashN(WOTS_LEN, wotsPk, wotsPkAddr);
|
|
640
|
-
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));
|
|
641
690
|
leafIdx = Number(tree & getMaskBig(TREE_HEIGHT));
|
|
642
691
|
}
|
|
643
692
|
return equalBytes(root, pubRoot);
|
|
644
693
|
},
|
|
645
|
-
};
|
|
646
|
-
return {
|
|
647
|
-
info: { type: 'slh-dsa' },
|
|
694
|
+
});
|
|
695
|
+
return Object.freeze({
|
|
696
|
+
info: Object.freeze({ type: 'slh-dsa' }),
|
|
648
697
|
internal,
|
|
649
698
|
securityLevel: securityLevel,
|
|
650
699
|
lengths: internal.lengths,
|
|
651
700
|
keygen: internal.keygen,
|
|
652
701
|
getPublicKey: internal.getPublicKey,
|
|
653
|
-
sign: (msg: Uint8Array
|
|
702
|
+
sign: (msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>, opts: TArg<SigOpts> = {}) => {
|
|
654
703
|
validateSigOpts(opts);
|
|
655
704
|
const M = getMessage(msg, opts.context);
|
|
656
705
|
const res = internal.sign(M, secretKey, opts);
|
|
657
706
|
cleanBytes(M);
|
|
658
|
-
return res
|
|
707
|
+
return res as TRet<Uint8Array>;
|
|
659
708
|
},
|
|
660
|
-
verify: (
|
|
709
|
+
verify: (
|
|
710
|
+
sig: TArg<Uint8Array>,
|
|
711
|
+
msg: TArg<Uint8Array>,
|
|
712
|
+
publicKey: TArg<Uint8Array>,
|
|
713
|
+
opts: TArg<VerOpts> = {}
|
|
714
|
+
) => {
|
|
661
715
|
validateVerOpts(opts);
|
|
662
716
|
return internal.verify(sig, getMessage(msg, opts.context), publicKey);
|
|
663
717
|
},
|
|
664
|
-
prehash: (hash: CHash) => {
|
|
665
|
-
checkHash(hash, securityLevel);
|
|
666
|
-
|
|
667
|
-
|
|
718
|
+
prehash: (hash: TArg<CHash>): TRet<Signer> => {
|
|
719
|
+
checkHash(hash as CHash, securityLevel);
|
|
720
|
+
const rawHash = hash as CHash;
|
|
721
|
+
return Object.freeze({
|
|
722
|
+
info: Object.freeze({ type: 'hashslh-dsa' }),
|
|
668
723
|
lengths: internal.lengths,
|
|
669
724
|
keygen: internal.keygen,
|
|
670
725
|
getPublicKey: internal.getPublicKey,
|
|
671
|
-
sign: (msg: Uint8Array
|
|
726
|
+
sign: (msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>, opts: TArg<SigOpts> = {}) => {
|
|
672
727
|
validateSigOpts(opts);
|
|
673
|
-
const M = getMessagePrehash(
|
|
728
|
+
const M = getMessagePrehash(rawHash, msg, opts.context);
|
|
674
729
|
const res = internal.sign(M, secretKey, opts);
|
|
675
730
|
cleanBytes(M);
|
|
676
|
-
return res
|
|
731
|
+
return res as TRet<Uint8Array>;
|
|
677
732
|
},
|
|
678
|
-
verify: (
|
|
733
|
+
verify: (
|
|
734
|
+
sig: TArg<Uint8Array>,
|
|
735
|
+
msg: TArg<Uint8Array>,
|
|
736
|
+
publicKey: TArg<Uint8Array>,
|
|
737
|
+
opts: TArg<VerOpts> = {}
|
|
738
|
+
) => {
|
|
679
739
|
validateVerOpts(opts);
|
|
680
|
-
return internal.verify(sig, getMessagePrehash(
|
|
740
|
+
return internal.verify(sig, getMessagePrehash(rawHash, msg, opts.context), publicKey);
|
|
681
741
|
},
|
|
682
|
-
};
|
|
742
|
+
});
|
|
683
743
|
},
|
|
684
|
-
};
|
|
744
|
+
});
|
|
685
745
|
}
|
|
686
746
|
|
|
687
747
|
// FIPS 205 §11.1 SHAKE instantiation: this path hashes the full uncompressed address bytes,
|
|
688
748
|
// unlike the compressed 22-byte SHA2 path in §11.2.
|
|
689
749
|
const genShake =
|
|
690
|
-
(): GetContext =>
|
|
750
|
+
(): TRet<GetContext> =>
|
|
751
|
+
(opts: SphincsOpts) =>
|
|
752
|
+
(pubSeed: TArg<Uint8Array>, skSeed?: TArg<Uint8Array>): TRet<Context> => {
|
|
691
753
|
const { N } = opts;
|
|
692
754
|
const stats = { prf: 0, thash: 0, hmsg: 0, gen_message_random: 0 };
|
|
693
755
|
// §11.1 prefixes PRF/F/H/T_l with `PK.seed`, so cache that absorbed prefix once and clone it
|
|
694
756
|
// for each address-bound call instead of reabsorbing the same seed every time.
|
|
695
757
|
const h0 = shake256.create({}).update(pubSeed);
|
|
696
758
|
const h0tmp = h0.clone();
|
|
697
|
-
|
|
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);
|
|
763
|
+
const thash = (blocks: number, input: TArg<Uint8Array>, addr: TArg<ADRS>): TRet<Uint8Array> => {
|
|
698
764
|
stats.thash++;
|
|
699
|
-
|
|
700
|
-
|
|
765
|
+
const len = blocks * N;
|
|
766
|
+
h0._cloneInto(h0tmp)
|
|
701
767
|
.update(addr)
|
|
702
|
-
.update(
|
|
703
|
-
|
|
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>;
|
|
704
773
|
};
|
|
705
774
|
return {
|
|
706
|
-
PRFaddr: (addr: ADRS) => {
|
|
775
|
+
PRFaddr: (addr: TArg<ADRS>): TRet<Uint8Array> => {
|
|
707
776
|
if (!skSeed) throw new Error('no sk seed');
|
|
708
777
|
stats.prf++;
|
|
709
|
-
|
|
710
|
-
return
|
|
778
|
+
h0._cloneInto(h0tmp).update(addr).update(skSeed).xofInto(prfOut);
|
|
779
|
+
return prfOut as TRet<Uint8Array>;
|
|
711
780
|
},
|
|
712
|
-
PRFmsg: (
|
|
781
|
+
PRFmsg: (
|
|
782
|
+
skPRF: TArg<Uint8Array>,
|
|
783
|
+
random: TArg<Uint8Array>,
|
|
784
|
+
msg: TArg<Uint8Array>
|
|
785
|
+
): TRet<Uint8Array> => {
|
|
713
786
|
stats.gen_message_random++;
|
|
714
|
-
return shake256
|
|
787
|
+
return shake256
|
|
788
|
+
.create({})
|
|
789
|
+
.update(skPRF)
|
|
790
|
+
.update(random)
|
|
791
|
+
.update(msg)
|
|
792
|
+
.digest()
|
|
793
|
+
.subarray(0, N) as TRet<Uint8Array>;
|
|
715
794
|
},
|
|
716
|
-
Hmsg: (
|
|
795
|
+
Hmsg: (
|
|
796
|
+
R: TArg<Uint8Array>,
|
|
797
|
+
pk: TArg<Uint8Array>,
|
|
798
|
+
m: TArg<Uint8Array>,
|
|
799
|
+
outLen
|
|
800
|
+
): TRet<Uint8Array> => {
|
|
717
801
|
stats.hmsg++;
|
|
718
802
|
return shake256.create({}).update(R.subarray(0, N)).update(pk).update(m).xof(outLen);
|
|
719
803
|
},
|
|
@@ -722,9 +806,10 @@ const genShake =
|
|
|
722
806
|
clean: () => {
|
|
723
807
|
h0.destroy();
|
|
724
808
|
h0tmp.destroy();
|
|
809
|
+
cleanBytes(thashOut, prfOut);
|
|
725
810
|
//console.log(stats);
|
|
726
811
|
},
|
|
727
|
-
}
|
|
812
|
+
} as TRet<Context>;
|
|
728
813
|
};
|
|
729
814
|
|
|
730
815
|
const SHAKE_SIMPLE = /* @__PURE__ */ (() => ({ getContext: genShake() }))();
|
|
@@ -734,42 +819,42 @@ const SHAKE_SIMPLE = /* @__PURE__ */ (() => ({ getContext: genShake() }))();
|
|
|
734
819
|
* lengths `publicKey=32`, `secretKey=64`, `signature=17088`, `seed=48`, `signRand=16`.
|
|
735
820
|
* Also exposes `.prehash(...)`.
|
|
736
821
|
*/
|
|
737
|
-
export const slh_dsa_shake_128f: SphincsSigner = /* @__PURE__ */ (() =>
|
|
822
|
+
export const slh_dsa_shake_128f: TRet<SphincsSigner> = /* @__PURE__ */ (() =>
|
|
738
823
|
gen(PARAMS['128f'], SHAKE_SIMPLE))();
|
|
739
824
|
/**
|
|
740
825
|
* SLH-DSA-SHAKE-128s: Table 2 row `n=16, h=63, d=7, h'=9, a=12, k=14, lg w=4, m=30`;
|
|
741
826
|
* lengths `publicKey=32`, `secretKey=64`, `signature=7856`, `seed=48`, `signRand=16`.
|
|
742
827
|
* Also exposes `.prehash(...)`.
|
|
743
828
|
*/
|
|
744
|
-
export const slh_dsa_shake_128s: SphincsSigner = /* @__PURE__ */ (() =>
|
|
829
|
+
export const slh_dsa_shake_128s: TRet<SphincsSigner> = /* @__PURE__ */ (() =>
|
|
745
830
|
gen(PARAMS['128s'], SHAKE_SIMPLE))();
|
|
746
831
|
/**
|
|
747
832
|
* SLH-DSA-SHAKE-192f: Table 2 row `n=24, h=66, d=22, h'=3, a=8, k=33, lg w=4, m=42`;
|
|
748
833
|
* lengths `publicKey=48`, `secretKey=96`, `signature=35664`, `seed=72`, `signRand=24`.
|
|
749
834
|
* Also exposes `.prehash(...)`.
|
|
750
835
|
*/
|
|
751
|
-
export const slh_dsa_shake_192f: SphincsSigner = /* @__PURE__ */ (() =>
|
|
836
|
+
export const slh_dsa_shake_192f: TRet<SphincsSigner> = /* @__PURE__ */ (() =>
|
|
752
837
|
gen(PARAMS['192f'], SHAKE_SIMPLE))();
|
|
753
838
|
/**
|
|
754
839
|
* SLH-DSA-SHAKE-192s: Table 2 row `n=24, h=63, d=7, h'=9, a=14, k=17, lg w=4, m=39`;
|
|
755
840
|
* lengths `publicKey=48`, `secretKey=96`, `signature=16224`, `seed=72`, `signRand=24`.
|
|
756
841
|
* Also exposes `.prehash(...)`.
|
|
757
842
|
*/
|
|
758
|
-
export const slh_dsa_shake_192s: SphincsSigner = /* @__PURE__ */ (() =>
|
|
843
|
+
export const slh_dsa_shake_192s: TRet<SphincsSigner> = /* @__PURE__ */ (() =>
|
|
759
844
|
gen(PARAMS['192s'], SHAKE_SIMPLE))();
|
|
760
845
|
/**
|
|
761
846
|
* SLH-DSA-SHAKE-256f: Table 2 row `n=32, h=68, d=17, h'=4, a=9, k=35, lg w=4, m=49`;
|
|
762
847
|
* lengths `publicKey=64`, `secretKey=128`, `signature=49856`, `seed=96`, `signRand=32`.
|
|
763
848
|
* Also exposes `.prehash(...)`.
|
|
764
849
|
*/
|
|
765
|
-
export const slh_dsa_shake_256f: SphincsSigner = /* @__PURE__ */ (() =>
|
|
850
|
+
export const slh_dsa_shake_256f: TRet<SphincsSigner> = /* @__PURE__ */ (() =>
|
|
766
851
|
gen(PARAMS['256f'], SHAKE_SIMPLE))();
|
|
767
852
|
/**
|
|
768
853
|
* SLH-DSA-SHAKE-256s: Table 2 row `n=32, h=64, d=8, h'=8, a=14, k=22, lg w=4, m=47`;
|
|
769
854
|
* lengths `publicKey=64`, `secretKey=128`, `signature=29792`, `seed=96`, `signRand=32`.
|
|
770
855
|
* Also exposes `.prehash(...)`.
|
|
771
856
|
*/
|
|
772
|
-
export const slh_dsa_shake_256s: SphincsSigner = /* @__PURE__ */ (() =>
|
|
857
|
+
export const slh_dsa_shake_256s: TRet<SphincsSigner> = /* @__PURE__ */ (() =>
|
|
773
858
|
gen(PARAMS['256s'], SHAKE_SIMPLE))();
|
|
774
859
|
|
|
775
860
|
type ShaType = typeof sha256 | typeof sha512;
|
|
@@ -777,9 +862,9 @@ type ShaType = typeof sha256 | typeof sha512;
|
|
|
777
862
|
// category-1 keeps everything on SHA-256, while category-3/5 keep `PRFaddr` / `thash1`
|
|
778
863
|
// on SHA-256 but switch `PRFmsg`, `Hmsg`, and multi-block `thashN` to SHA-512.
|
|
779
864
|
const genSha =
|
|
780
|
-
(h0: ShaType, h1: ShaType): GetContext =>
|
|
865
|
+
(h0: ShaType, h1: ShaType): TRet<GetContext> =>
|
|
781
866
|
(opts) =>
|
|
782
|
-
(pub_seed
|
|
867
|
+
(pub_seed: TArg<Uint8Array>, sk_seed?: TArg<Uint8Array>): TRet<Context> => {
|
|
783
868
|
const { N } = opts;
|
|
784
869
|
/*
|
|
785
870
|
Perf debug stats, how much hashes we call?
|
|
@@ -804,12 +889,22 @@ const genSha =
|
|
|
804
889
|
|
|
805
890
|
const h0tmp = h0ps.clone();
|
|
806
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);
|
|
807
902
|
|
|
808
903
|
// https://www.rfc-editor.org/rfc/rfc8017.html#appendix-B.2.1
|
|
809
904
|
// This local helper is intentionally stricter than generic MGF1 reuse: current SLH-DSA callers
|
|
810
905
|
// only request tiny `m`-byte outputs, but the guard below rejects `length > 2^32` instead of
|
|
811
906
|
// RFC 8017's broader `maskLen > 2^32 * hLen` bound.
|
|
812
|
-
function mgf1(seed: Uint8Array
|
|
907
|
+
function mgf1(seed: TArg<Uint8Array>, length: number, hash: ShaType): TRet<Uint8Array> {
|
|
813
908
|
stats.mgf1++;
|
|
814
909
|
const out = new Uint8Array(Math.ceil(length / hash.outputLen) * hash.outputLen);
|
|
815
910
|
// NOT 2^32-1
|
|
@@ -820,37 +915,52 @@ const genSha =
|
|
|
820
915
|
o = o.subarray(hash.outputLen);
|
|
821
916
|
}
|
|
822
917
|
cleanBytes(out.subarray(length));
|
|
823
|
-
return out.subarray(0, length)
|
|
918
|
+
return out.subarray(0, length) as TRet<Uint8Array>;
|
|
824
919
|
}
|
|
825
920
|
|
|
826
921
|
const thash =
|
|
827
|
-
(
|
|
828
|
-
(blocks: number, input: Uint8Array
|
|
922
|
+
(h: typeof h0ps, hTmp: typeof h0ps, out: TArg<Uint8Array>, outN: TArg<Uint8Array>) =>
|
|
923
|
+
(blocks: number, input: TArg<Uint8Array>, addr: TArg<ADRS>): TRet<Uint8Array> => {
|
|
829
924
|
stats.thash++;
|
|
830
|
-
const
|
|
831
|
-
|
|
925
|
+
const len = blocks * N;
|
|
926
|
+
h._cloneInto(hTmp as any)
|
|
832
927
|
.update(addr)
|
|
833
|
-
.update(
|
|
834
|
-
|
|
835
|
-
|
|
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>;
|
|
836
933
|
};
|
|
837
934
|
return {
|
|
838
|
-
PRFaddr: (addr: ADRS) => {
|
|
935
|
+
PRFaddr: (addr: TArg<ADRS>): TRet<Uint8Array> => {
|
|
839
936
|
if (!sk_seed) throw new Error('No sk seed');
|
|
840
937
|
stats.prf++;
|
|
841
|
-
|
|
938
|
+
h0ps
|
|
842
939
|
._cloneInto(h0tmp as any)
|
|
843
940
|
.update(addr)
|
|
844
941
|
.update(sk_seed)
|
|
845
|
-
.
|
|
846
|
-
|
|
847
|
-
return res;
|
|
942
|
+
.digestInto(prfOut);
|
|
943
|
+
return prfOutN as TRet<Uint8Array>;
|
|
848
944
|
},
|
|
849
|
-
PRFmsg: (
|
|
945
|
+
PRFmsg: (
|
|
946
|
+
skPRF: TArg<Uint8Array>,
|
|
947
|
+
random: TArg<Uint8Array>,
|
|
948
|
+
msg: TArg<Uint8Array>
|
|
949
|
+
): TRet<Uint8Array> => {
|
|
850
950
|
stats.gen_message_random++;
|
|
851
|
-
return hmac
|
|
951
|
+
return hmac
|
|
952
|
+
.create(h1, skPRF)
|
|
953
|
+
.update(random)
|
|
954
|
+
.update(msg)
|
|
955
|
+
.digest()
|
|
956
|
+
.subarray(0, N) as TRet<Uint8Array>;
|
|
852
957
|
},
|
|
853
|
-
Hmsg: (
|
|
958
|
+
Hmsg: (
|
|
959
|
+
R: TArg<Uint8Array>,
|
|
960
|
+
pk: TArg<Uint8Array>,
|
|
961
|
+
m: TArg<Uint8Array>,
|
|
962
|
+
outLen
|
|
963
|
+
): TRet<Uint8Array> => {
|
|
854
964
|
stats.hmsg++;
|
|
855
965
|
const seed = concatBytes(
|
|
856
966
|
R.subarray(0, N),
|
|
@@ -859,16 +969,17 @@ const genSha =
|
|
|
859
969
|
);
|
|
860
970
|
return mgf1(seed, outLen, h1);
|
|
861
971
|
},
|
|
862
|
-
thash1: thash(
|
|
863
|
-
thashN: thash(
|
|
972
|
+
thash1: thash(h0ps, h0tmp, h0out, h0outN).bind(null, 1),
|
|
973
|
+
thashN: thash(h1ps, h1tmp, h1out, h1outN),
|
|
864
974
|
clean: () => {
|
|
865
975
|
h0ps.destroy();
|
|
866
976
|
h1ps.destroy();
|
|
867
977
|
h0tmp.destroy();
|
|
868
978
|
h1tmp.destroy();
|
|
979
|
+
cleanBytes(h0out, h1out, prfOut);
|
|
869
980
|
//console.log(stats);
|
|
870
981
|
},
|
|
871
|
-
}
|
|
982
|
+
} as TRet<Context>;
|
|
872
983
|
};
|
|
873
984
|
|
|
874
985
|
const SHA256_SIMPLE = /* @__PURE__ */ (() => ({
|
|
@@ -884,41 +995,58 @@ const SHA512_SIMPLE = /* @__PURE__ */ (() => ({
|
|
|
884
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`;
|
|
885
996
|
* lengths `publicKey=32`, `secretKey=64`, `signature=17088`, `seed=48`, `signRand=16`.
|
|
886
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
|
+
* ```
|
|
887
1015
|
*/
|
|
888
|
-
export const slh_dsa_sha2_128f: SphincsSigner = /* @__PURE__ */ (() =>
|
|
1016
|
+
export const slh_dsa_sha2_128f: TRet<SphincsSigner> = /* @__PURE__ */ (() =>
|
|
889
1017
|
gen(PARAMS['128f'], SHA256_SIMPLE))();
|
|
890
1018
|
/**
|
|
891
1019
|
* SLH-DSA-SHA2-128s: Table 2 row `n=16, h=63, d=7, h'=9, a=12, k=14, lg w=4, m=30`;
|
|
892
1020
|
* lengths `publicKey=32`, `secretKey=64`, `signature=7856`, `seed=48`, `signRand=16`.
|
|
893
1021
|
* Also exposes `.prehash(...)`.
|
|
894
1022
|
*/
|
|
895
|
-
export const slh_dsa_sha2_128s: SphincsSigner = /* @__PURE__ */ (() =>
|
|
1023
|
+
export const slh_dsa_sha2_128s: TRet<SphincsSigner> = /* @__PURE__ */ (() =>
|
|
896
1024
|
gen(PARAMS['128s'], SHA256_SIMPLE))();
|
|
897
1025
|
/**
|
|
898
1026
|
* SLH-DSA-SHA2-192f: Table 2 row `n=24, h=66, d=22, h'=3, a=8, k=33, lg w=4, m=42`;
|
|
899
1027
|
* lengths `publicKey=48`, `secretKey=96`, `signature=35664`, `seed=72`, `signRand=24`.
|
|
900
1028
|
* Also exposes `.prehash(...)`.
|
|
901
1029
|
*/
|
|
902
|
-
export const slh_dsa_sha2_192f: SphincsSigner = /* @__PURE__ */ (() =>
|
|
1030
|
+
export const slh_dsa_sha2_192f: TRet<SphincsSigner> = /* @__PURE__ */ (() =>
|
|
903
1031
|
gen(PARAMS['192f'], SHA512_SIMPLE))();
|
|
904
1032
|
/**
|
|
905
1033
|
* SLH-DSA-SHA2-192s: Table 2 row `n=24, h=63, d=7, h'=9, a=14, k=17, lg w=4, m=39`;
|
|
906
1034
|
* lengths `publicKey=48`, `secretKey=96`, `signature=16224`, `seed=72`, `signRand=24`.
|
|
907
1035
|
* Also exposes `.prehash(...)`.
|
|
908
1036
|
*/
|
|
909
|
-
export const slh_dsa_sha2_192s: SphincsSigner = /* @__PURE__ */ (() =>
|
|
1037
|
+
export const slh_dsa_sha2_192s: TRet<SphincsSigner> = /* @__PURE__ */ (() =>
|
|
910
1038
|
gen(PARAMS['192s'], SHA512_SIMPLE))();
|
|
911
1039
|
/**
|
|
912
1040
|
* SLH-DSA-SHA2-256f: Table 2 row `n=32, h=68, d=17, h'=4, a=9, k=35, lg w=4, m=49`;
|
|
913
1041
|
* lengths `publicKey=64`, `secretKey=128`, `signature=49856`, `seed=96`, `signRand=32`.
|
|
914
1042
|
* Also exposes `.prehash(...)`.
|
|
915
1043
|
*/
|
|
916
|
-
export const slh_dsa_sha2_256f: SphincsSigner = /* @__PURE__ */ (() =>
|
|
1044
|
+
export const slh_dsa_sha2_256f: TRet<SphincsSigner> = /* @__PURE__ */ (() =>
|
|
917
1045
|
gen(PARAMS['256f'], SHA512_SIMPLE))();
|
|
918
1046
|
/**
|
|
919
1047
|
* SLH-DSA-SHA2-256s: Table 2 row `n=32, h=64, d=8, h'=8, a=14, k=22, lg w=4, m=47`;
|
|
920
1048
|
* lengths `publicKey=64`, `secretKey=128`, `signature=29792`, `seed=96`, `signRand=32`.
|
|
921
1049
|
* Also exposes `.prehash(...)`.
|
|
922
1050
|
*/
|
|
923
|
-
export const slh_dsa_sha2_256s: SphincsSigner = /* @__PURE__ */ (() =>
|
|
1051
|
+
export const slh_dsa_sha2_256s: TRet<SphincsSigner> = /* @__PURE__ */ (() =>
|
|
924
1052
|
gen(PARAMS['256s'], SHA512_SIMPLE))();
|