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