slh-dsa 0.0.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.
@@ -0,0 +1,62 @@
1
+ import { type CHash } from '@noble/hashes/utils.js';
2
+ import { type Signer } from './utils.js';
3
+ /**
4
+ * * N: Security parameter (in bytes). W: Winternitz parameter
5
+ * * H: Hypertree height. D: Hypertree layers
6
+ * * K: FORS trees numbers. A: FORS trees height
7
+ */
8
+ export type SphincsOpts = {
9
+ N: number;
10
+ W: number;
11
+ H: number;
12
+ D: number;
13
+ K: number;
14
+ A: number;
15
+ securityLevel: number;
16
+ };
17
+ export type SphincsHashOpts = {
18
+ isCompressed?: boolean;
19
+ getContext: GetContext;
20
+ };
21
+ /** Winternitz signature params. */
22
+ export declare const PARAMS: Record<string, SphincsOpts>;
23
+ /** Address, byte array of size ADDR_BYTES */
24
+ export type ADRS = Uint8Array;
25
+ export type Context = {
26
+ PRFaddr: (addr: ADRS) => Uint8Array;
27
+ PRFmsg: (skPRF: Uint8Array, random: Uint8Array, msg: Uint8Array) => Uint8Array;
28
+ Hmsg: (R: Uint8Array, pk: Uint8Array, m: Uint8Array, outLen: number) => Uint8Array;
29
+ thash1: (input: Uint8Array, addr: ADRS) => Uint8Array;
30
+ thashN: (blocks: number, input: Uint8Array, addr: ADRS) => Uint8Array;
31
+ clean: () => void;
32
+ };
33
+ export type GetContext = (opts: SphincsOpts) => (pub_seed: Uint8Array, sk_seed?: Uint8Array) => Context;
34
+ export type SphincsSigner = Signer & {
35
+ internal: Signer;
36
+ securityLevel: number;
37
+ prehash: (hash: CHash) => Signer;
38
+ };
39
+ /** SLH-DSA: 128-bit fast SHAKE version. */
40
+ export declare const slh_dsa_shake_128f: SphincsSigner;
41
+ /** SLH-DSA: 128-bit short SHAKE version. */
42
+ export declare const slh_dsa_shake_128s: SphincsSigner;
43
+ /** SLH-DSA: 192-bit fast SHAKE version. */
44
+ export declare const slh_dsa_shake_192f: SphincsSigner;
45
+ /** SLH-DSA: 192-bit short SHAKE version. */
46
+ export declare const slh_dsa_shake_192s: SphincsSigner;
47
+ /** SLH-DSA: 256-bit fast SHAKE version. */
48
+ export declare const slh_dsa_shake_256f: SphincsSigner;
49
+ /** SLH-DSA: 256-bit short SHAKE version. */
50
+ export declare const slh_dsa_shake_256s: SphincsSigner;
51
+ /** SLH-DSA: 128-bit fast SHA2 version. */
52
+ export declare const slh_dsa_sha2_128f: SphincsSigner;
53
+ /** SLH-DSA: 128-bit small SHA2 version. */
54
+ export declare const slh_dsa_sha2_128s: SphincsSigner;
55
+ /** SLH-DSA: 192-bit fast SHA2 version. */
56
+ export declare const slh_dsa_sha2_192f: SphincsSigner;
57
+ /** SLH-DSA: 192-bit small SHA2 version. */
58
+ export declare const slh_dsa_sha2_192s: SphincsSigner;
59
+ /** SLH-DSA: 256-bit fast SHA2 version. */
60
+ export declare const slh_dsa_sha2_256f: SphincsSigner;
61
+ /** SLH-DSA: 256-bit small SHA2 version. */
62
+ export declare const slh_dsa_sha2_256s: SphincsSigner;
package/dist/index.js ADDED
@@ -0,0 +1,625 @@
1
+ /**
2
+ * SLH-DSA: StateLess Hash-based Digital Signature Standard from
3
+ * [FIPS-205](https://csrc.nist.gov/pubs/fips/205/ipd). A.k.a. Sphincs+ v3.1.
4
+ *
5
+ * There are many different kinds of SLH, but basically `sha2` / `shake` indicate internal hash,
6
+ * `128` / `192` / `256` indicate security level, and `s` /`f` indicate trade-off (Small / Fast).
7
+ *
8
+ * Hashes function similarly to signatures. You hash a private key to get a public key,
9
+ * which can be used to verify the private key. However, this only works once since
10
+ * disclosing the pre-image invalidates the key.
11
+ *
12
+ * To address the "one-time" limitation, we can use a Merkle tree root hash:
13
+ * h(h(h(0) || h(1)) || h(h(2) || h(3))))
14
+ *
15
+ * This allows us to have the same public key output from the hash, but disclosing one
16
+ * path in the tree doesn't invalidate the others. By choosing a path related to the
17
+ * message, we can "sign" it.
18
+ *
19
+ * Limitation: Only a fixed number of signatures can be made. For instance, a Merkle tree
20
+ * with depth 8 allows 256 distinct messages. Using different trees for each node can
21
+ * prevent forgeries, but the key will still degrade over time.
22
+ *
23
+ * WOTS: One-time signatures (can be forged if same key used twice).
24
+ * FORS: Forest of Random Subsets
25
+ *
26
+ * Check out [official site](https://sphincs.org) & [repo](https://github.com/sphincs/sphincsplus).
27
+ * @module
28
+ */
29
+ /*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */
30
+ import { hmac } from '@noble/hashes/hmac.js';
31
+ import { sha256, sha512 } from '@noble/hashes/sha2.js';
32
+ import { shake256 } from '@noble/hashes/sha3.js';
33
+ import { bytesToHex, concatBytes, createView, hexToBytes, } from '@noble/hashes/utils.js';
34
+ import { abytes, checkHash, cleanBytes, copyBytes, equalBytes, getMask, getMessage, getMessagePrehash, randomBytes, splitCoder, validateSigOpts, validateVerOpts, vecCoder, } from './utils.js';
35
+ /** Winternitz signature params. */
36
+ export const PARAMS = {
37
+ '128f': { W: 16, N: 16, H: 66, D: 22, K: 33, A: 6, securityLevel: 128 },
38
+ '128s': { W: 16, N: 16, H: 63, D: 7, K: 14, A: 12, securityLevel: 128 },
39
+ '192f': { W: 16, N: 24, H: 66, D: 22, K: 33, A: 8, securityLevel: 192 },
40
+ '192s': { W: 16, N: 24, H: 63, D: 7, K: 17, A: 14, securityLevel: 192 },
41
+ '256f': { W: 16, N: 32, H: 68, D: 17, K: 35, A: 9, securityLevel: 256 },
42
+ '256s': { W: 16, N: 32, H: 64, D: 8, K: 22, A: 14, securityLevel: 256 },
43
+ };
44
+ const AddressType = {
45
+ WOTS: 0,
46
+ WOTSPK: 1,
47
+ HASHTREE: 2,
48
+ FORSTREE: 3,
49
+ FORSPK: 4,
50
+ WOTSPRF: 5,
51
+ FORSPRF: 6,
52
+ };
53
+ function hexToNumber(hex) {
54
+ if (typeof hex !== 'string')
55
+ throw new Error('hex string expected, got ' + typeof hex);
56
+ return BigInt(hex === '' ? '0' : '0x' + hex); // Big Endian
57
+ }
58
+ // BE: Big Endian, LE: Little Endian
59
+ function bytesToNumberBE(bytes) {
60
+ return hexToNumber(bytesToHex(bytes));
61
+ }
62
+ function numberToBytesBE(n, len) {
63
+ return hexToBytes(n.toString(16).padStart(len * 2, '0'));
64
+ }
65
+ // Same as bitsCoder.decode, but maybe spec will change and unify with base2bBE.
66
+ const base2b = (outLen, b) => {
67
+ const mask = getMask(b);
68
+ return (bytes) => {
69
+ const baseB = new Uint32Array(outLen);
70
+ for (let out = 0, pos = 0, bits = 0, total = 0; out < outLen; out++) {
71
+ while (bits < b) {
72
+ total = (total << 8) | bytes[pos++];
73
+ bits += 8;
74
+ }
75
+ bits -= b;
76
+ baseB[out] = (total >>> bits) & mask;
77
+ }
78
+ return baseB;
79
+ };
80
+ };
81
+ function getMaskBig(bits) {
82
+ return (1n << BigInt(bits)) - 1n; // 4 -> 0b1111
83
+ }
84
+ function gen(opts, hashOpts) {
85
+ const { N, W, H, D, K, A, securityLevel: securityLevel } = opts;
86
+ const getContext = hashOpts.getContext(opts);
87
+ if (W !== 16)
88
+ throw new Error('Unsupported Winternitz parameter');
89
+ const WOTS_LOGW = 4;
90
+ const WOTS_LEN1 = Math.floor((8 * N) / WOTS_LOGW);
91
+ const WOTS_LEN2 = N <= 8 ? 2 : N <= 136 ? 3 : 4;
92
+ const TREE_HEIGHT = Math.floor(H / D);
93
+ const WOTS_LEN = WOTS_LEN1 + WOTS_LEN2;
94
+ let ADDR_BYTES = 22;
95
+ let OFFSET_LAYER = 0;
96
+ let OFFSET_TREE = 1;
97
+ let OFFSET_TYPE = 9;
98
+ let OFFSET_KP_ADDR2 = 12;
99
+ let OFFSET_KP_ADDR1 = 13;
100
+ let OFFSET_CHAIN_ADDR = 17;
101
+ let OFFSET_TREE_INDEX = 18;
102
+ let OFFSET_HASH_ADDR = 21;
103
+ if (!hashOpts.isCompressed) {
104
+ ADDR_BYTES = 32;
105
+ OFFSET_LAYER += 3;
106
+ OFFSET_TREE += 7;
107
+ OFFSET_TYPE += 10;
108
+ OFFSET_KP_ADDR2 += 10;
109
+ OFFSET_KP_ADDR1 += 10;
110
+ OFFSET_CHAIN_ADDR += 10;
111
+ OFFSET_TREE_INDEX += 10;
112
+ OFFSET_HASH_ADDR += 10;
113
+ }
114
+ const setAddr = (opts, addr = new Uint8Array(ADDR_BYTES)) => {
115
+ const { type, height, tree, layer, index, chain, hash, keypair } = opts;
116
+ const { subtreeAddr, keypairAddr } = opts;
117
+ const v = createView(addr);
118
+ if (height !== undefined)
119
+ addr[OFFSET_CHAIN_ADDR] = height;
120
+ if (layer !== undefined)
121
+ addr[OFFSET_LAYER] = layer;
122
+ if (type !== undefined)
123
+ addr[OFFSET_TYPE] = type;
124
+ if (chain !== undefined)
125
+ addr[OFFSET_CHAIN_ADDR] = chain;
126
+ if (hash !== undefined)
127
+ addr[OFFSET_HASH_ADDR] = hash;
128
+ if (index !== undefined)
129
+ v.setUint32(OFFSET_TREE_INDEX, index, false);
130
+ if (subtreeAddr)
131
+ addr.set(subtreeAddr.subarray(0, OFFSET_TREE + 8));
132
+ if (tree !== undefined)
133
+ v.setBigUint64(OFFSET_TREE, tree, false);
134
+ if (keypair !== undefined) {
135
+ addr[OFFSET_KP_ADDR1] = keypair;
136
+ if (TREE_HEIGHT > 8)
137
+ addr[OFFSET_KP_ADDR2] = keypair >>> 8;
138
+ }
139
+ if (keypairAddr) {
140
+ addr.set(keypairAddr.subarray(0, OFFSET_TREE + 8));
141
+ addr[OFFSET_KP_ADDR1] = keypairAddr[OFFSET_KP_ADDR1];
142
+ if (TREE_HEIGHT > 8)
143
+ addr[OFFSET_KP_ADDR2] = keypairAddr[OFFSET_KP_ADDR2];
144
+ }
145
+ return addr;
146
+ };
147
+ const chainCoder = base2b(WOTS_LEN2, WOTS_LOGW);
148
+ const chainLengths = (msg) => {
149
+ const W1 = base2b(WOTS_LEN1, WOTS_LOGW)(msg);
150
+ let csum = 0;
151
+ for (let i = 0; i < W1.length; i++)
152
+ csum += W - 1 - W1[i]; // ▷ Compute checksum
153
+ csum <<= (8 - ((WOTS_LEN2 * WOTS_LOGW) % 8)) % 8; // csum ← csum ≪ ((8 − ((len2 · lg(w)) mod 8)) mod 8
154
+ // Checksum to base(LOG_W)
155
+ const W2 = chainCoder(numberToBytesBE(csum, Math.ceil((WOTS_LEN2 * WOTS_LOGW) / 8)));
156
+ // W1 || W2 (concatBytes cannot concat TypedArrays)
157
+ const lengths = new Uint32Array(WOTS_LEN);
158
+ lengths.set(W1);
159
+ lengths.set(W2, W1.length);
160
+ return lengths;
161
+ };
162
+ const messageToIndices = base2b(K, A);
163
+ const TREE_BITS = TREE_HEIGHT * (D - 1);
164
+ const LEAF_BITS = TREE_HEIGHT;
165
+ const hashMsgCoder = splitCoder('hashedMessage', Math.ceil((A * K) / 8), Math.ceil(TREE_BITS / 8), Math.ceil(TREE_HEIGHT / 8));
166
+ const hashMessage = (R, pkSeed, msg, context) => {
167
+ const digest = context.Hmsg(R, pkSeed, msg, hashMsgCoder.bytesLen); // digest ← Hmsg(R, PK.seed, PK.root, M)
168
+ const [md, tmpIdxTree, tmpIdxLeaf] = hashMsgCoder.decode(digest);
169
+ const tree = bytesToNumberBE(tmpIdxTree) & getMaskBig(TREE_BITS);
170
+ const leafIdx = Number(bytesToNumberBE(tmpIdxLeaf)) & getMask(LEAF_BITS);
171
+ return { tree, leafIdx, md };
172
+ };
173
+ const treehash = (height, fn) => function treehash_i(context, leafIdx, idxOffset, treeAddr, info) {
174
+ const maxIdx = (1 << height) - 1;
175
+ const stack = new Uint8Array(height * N);
176
+ const authPath = new Uint8Array(height * N);
177
+ for (let idx = 0;; idx++) {
178
+ const current = new Uint8Array(2 * N);
179
+ const cur0 = current.subarray(0, N);
180
+ const cur1 = current.subarray(N);
181
+ const addrOffset = idx + idxOffset;
182
+ cur1.set(fn(leafIdx, addrOffset, context, info));
183
+ let h = 0;
184
+ for (let i = idx, o = idxOffset, l = leafIdx;; h++, i >>>= 1, l >>>= 1, o >>>= 1) {
185
+ if (h === height)
186
+ return { root: cur1, authPath }; // Returns from here
187
+ if ((i ^ l) === 1)
188
+ authPath.subarray(h * N).set(cur1); // authPath.push(cur1)
189
+ if ((i & 1) === 0 && idx < maxIdx)
190
+ break;
191
+ setAddr({ height: h + 1, index: (i >> 1) + (o >> 1) }, treeAddr);
192
+ cur0.set(stack.subarray(h * N).subarray(0, N));
193
+ cur1.set(context.thashN(2, current, treeAddr));
194
+ }
195
+ stack.subarray(h * N).set(cur1); // stack.push(cur1)
196
+ }
197
+ // @ts-ignore
198
+ throw new Error('Unreachable code path reached, report this error');
199
+ };
200
+ const wotsTreehash = treehash(TREE_HEIGHT, (leafIdx, addrOffset, context, info) => {
201
+ const wotsPk = new Uint8Array(WOTS_LEN * N);
202
+ const wotsKmask = addrOffset === leafIdx ? 0 : ~0 >>> 0;
203
+ setAddr({ keypair: addrOffset }, info.leafAddr);
204
+ setAddr({ keypair: addrOffset }, info.pkAddr);
205
+ for (let i = 0; i < WOTS_LEN; i++) {
206
+ const wotsK = info.wotsSteps[i] | wotsKmask;
207
+ const pk = wotsPk.subarray(i * N, (i + 1) * N);
208
+ setAddr({ chain: i, hash: 0, type: AddressType.WOTSPRF }, info.leafAddr);
209
+ pk.set(context.PRFaddr(info.leafAddr));
210
+ setAddr({ type: AddressType.WOTS }, info.leafAddr);
211
+ for (let k = 0;; k++) {
212
+ if (k === wotsK)
213
+ info.wotsSig.subarray(i * N).set(pk); //wotsSig.push()
214
+ if (k === W - 1)
215
+ break;
216
+ setAddr({ hash: k }, info.leafAddr);
217
+ pk.set(context.thash1(pk, info.leafAddr));
218
+ }
219
+ }
220
+ return context.thashN(WOTS_LEN, wotsPk, info.pkAddr);
221
+ });
222
+ const forsTreehash = treehash(A, (_, addrOffset, context, forsLeafAddr) => {
223
+ setAddr({ type: AddressType.FORSPRF, index: addrOffset }, forsLeafAddr);
224
+ const prf = context.PRFaddr(forsLeafAddr);
225
+ setAddr({ type: AddressType.FORSTREE }, forsLeafAddr);
226
+ return context.thash1(prf, forsLeafAddr);
227
+ });
228
+ const merkleSign = (context, wotsAddr, treeAddr, leafIdx, prevRoot = new Uint8Array(N)) => {
229
+ setAddr({ type: AddressType.HASHTREE }, treeAddr);
230
+ // State variables
231
+ const info = {
232
+ wotsSig: new Uint8Array(wotsCoder.bytesLen),
233
+ wotsSteps: chainLengths(prevRoot),
234
+ leafAddr: setAddr({ subtreeAddr: wotsAddr }),
235
+ pkAddr: setAddr({ type: AddressType.WOTSPK, subtreeAddr: wotsAddr }),
236
+ };
237
+ const { root, authPath } = wotsTreehash(context, leafIdx, 0, treeAddr, info);
238
+ return {
239
+ root,
240
+ sigWots: info.wotsSig.subarray(0, WOTS_LEN * N),
241
+ sigAuth: authPath,
242
+ };
243
+ };
244
+ const computeRoot = (leaf, leafIdx, idxOffset, authPath, treeHeight, context, addr) => {
245
+ const buffer = new Uint8Array(2 * N);
246
+ const b0 = buffer.subarray(0, N);
247
+ const b1 = buffer.subarray(N, 2 * N);
248
+ // First iter
249
+ if ((leafIdx & 1) !== 0) {
250
+ b1.set(leaf.subarray(0, N));
251
+ b0.set(authPath.subarray(0, N));
252
+ }
253
+ else {
254
+ b0.set(leaf.subarray(0, N));
255
+ b1.set(authPath.subarray(0, N));
256
+ }
257
+ leafIdx >>>= 1;
258
+ idxOffset >>>= 1;
259
+ // Rest
260
+ for (let i = 0; i < treeHeight - 1; i++, leafIdx >>= 1, idxOffset >>= 1) {
261
+ setAddr({ height: i + 1, index: leafIdx + idxOffset }, addr);
262
+ const a = authPath.subarray((i + 1) * N, (i + 2) * N);
263
+ if ((leafIdx & 1) !== 0) {
264
+ b1.set(context.thashN(2, buffer, addr));
265
+ b0.set(a);
266
+ }
267
+ else {
268
+ buffer.set(context.thashN(2, buffer, addr));
269
+ b1.set(a);
270
+ }
271
+ }
272
+ // Root
273
+ setAddr({ height: treeHeight, index: leafIdx + idxOffset }, addr);
274
+ return context.thashN(2, buffer, addr);
275
+ };
276
+ const seedCoder = splitCoder('seed', N, N, N);
277
+ const publicCoder = splitCoder('publicKey', N, N);
278
+ const secretCoder = splitCoder('secretKey', N, N, publicCoder.bytesLen);
279
+ const forsCoder = vecCoder(splitCoder('fors', N, N * A), K);
280
+ const wotsCoder = vecCoder(splitCoder('wots', WOTS_LEN * N, TREE_HEIGHT * N), D);
281
+ const sigCoder = splitCoder('signature', N, forsCoder, wotsCoder); // random || fors || wots
282
+ const internal = {
283
+ info: { type: 'internal-slh-dsa' },
284
+ lengths: {
285
+ publicKey: publicCoder.bytesLen,
286
+ secretKey: secretCoder.bytesLen,
287
+ signature: sigCoder.bytesLen,
288
+ seed: seedCoder.bytesLen,
289
+ signRand: N,
290
+ },
291
+ keygen(seed) {
292
+ if (seed !== undefined)
293
+ abytes(seed, seedCoder.bytesLen, 'seed');
294
+ seed = seed === undefined ? randomBytes(seedCoder.bytesLen) : copyBytes(seed);
295
+ // Set SK.seed, SK.prf, and PK.seed to random n-byte
296
+ const [secretSeed, secretPRF, publicSeed] = seedCoder.decode(seed);
297
+ const context = getContext(publicSeed, secretSeed);
298
+ // ADRS.setLayerAddress(d − 1)
299
+ const topTreeAddr = setAddr({ layer: D - 1 });
300
+ const wotsAddr = setAddr({ layer: D - 1 });
301
+ //PK.root ←_xmss node(SK.seed, 0, h′, PK.seed, ADRS)
302
+ const { root } = merkleSign(context, wotsAddr, topTreeAddr, ~0 >>> 0);
303
+ const publicKey = publicCoder.encode([publicSeed, root]);
304
+ const secretKey = secretCoder.encode([secretSeed, secretPRF, publicKey]);
305
+ context.clean();
306
+ cleanBytes(secretSeed, secretPRF, root, wotsAddr, topTreeAddr);
307
+ return { publicKey, secretKey };
308
+ },
309
+ getPublicKey: (secretKey) => {
310
+ const [_skSeed, _skPRF, pk] = secretCoder.decode(secretKey);
311
+ return Uint8Array.from(pk);
312
+ },
313
+ sign: (msg, sk, opts = {}) => {
314
+ validateSigOpts(opts);
315
+ let { extraEntropy: random } = opts;
316
+ const [skSeed, skPRF, pk] = secretCoder.decode(sk); // todo: fix
317
+ const [pkSeed, _] = publicCoder.decode(pk);
318
+ // Set opt_rand to either PK.seed or to a random n-byte string
319
+ if (random === false)
320
+ random = copyBytes(pkSeed);
321
+ else if (random === undefined)
322
+ random = randomBytes(N);
323
+ else
324
+ random = copyBytes(random);
325
+ abytes(random, N);
326
+ const context = getContext(pkSeed, skSeed);
327
+ // Generate randomizer
328
+ const R = context.PRFmsg(skPRF, random, msg); // R ← PRFmsg(SK.prf, opt_rand, M)
329
+ let { tree, leafIdx, md } = hashMessage(R, pk, msg, context);
330
+ // Create FORS signatures
331
+ const wotsAddr = setAddr({
332
+ type: AddressType.WOTS,
333
+ tree,
334
+ keypair: leafIdx,
335
+ });
336
+ const roots = [];
337
+ const forsLeaf = setAddr({ keypairAddr: wotsAddr });
338
+ const forsTreeAddr = setAddr({ keypairAddr: wotsAddr });
339
+ const indices = messageToIndices(md);
340
+ const fors = [];
341
+ for (let i = 0; i < indices.length; i++) {
342
+ const idxOffset = i << A;
343
+ setAddr({
344
+ type: AddressType.FORSPRF,
345
+ height: 0,
346
+ index: indices[i] + idxOffset,
347
+ }, forsTreeAddr);
348
+ const prf = context.PRFaddr(forsTreeAddr);
349
+ setAddr({ type: AddressType.FORSTREE }, forsTreeAddr);
350
+ const { root, authPath } = forsTreehash(context, indices[i], idxOffset, forsTreeAddr, forsLeaf);
351
+ roots.push(root);
352
+ fors.push([prf, authPath]);
353
+ }
354
+ const forsPkAddr = setAddr({
355
+ type: AddressType.FORSPK,
356
+ keypairAddr: wotsAddr,
357
+ });
358
+ const root = context.thashN(K, concatBytes(...roots), forsPkAddr);
359
+ // WOTS signatures
360
+ const treeAddr = setAddr({ type: AddressType.HASHTREE });
361
+ const wots = [];
362
+ for (let i = 0; i < D; i++, tree >>= BigInt(TREE_HEIGHT)) {
363
+ setAddr({ tree, layer: i }, treeAddr);
364
+ setAddr({ subtreeAddr: treeAddr, keypair: leafIdx }, wotsAddr);
365
+ const { sigWots, sigAuth, root: r, } = merkleSign(context, wotsAddr, treeAddr, leafIdx, root);
366
+ root.set(r);
367
+ cleanBytes(r);
368
+ wots.push([sigWots, sigAuth]);
369
+ leafIdx = Number(tree & getMaskBig(TREE_HEIGHT));
370
+ }
371
+ context.clean();
372
+ const SIG = sigCoder.encode([R, fors, wots]);
373
+ cleanBytes(R, random, treeAddr, wotsAddr, forsLeaf, forsTreeAddr, indices, roots);
374
+ return SIG;
375
+ },
376
+ verify: (sig, msg, publicKey) => {
377
+ const [pkSeed, pubRoot] = publicCoder.decode(publicKey);
378
+ const [random, forsVec, wotsVec] = sigCoder.decode(sig);
379
+ const pk = publicKey;
380
+ if (sig.length !== sigCoder.bytesLen)
381
+ return false;
382
+ const context = getContext(pkSeed);
383
+ let { tree, leafIdx, md } = hashMessage(random, pk, msg, context);
384
+ const wotsAddr = setAddr({
385
+ type: AddressType.WOTS,
386
+ tree,
387
+ keypair: leafIdx,
388
+ });
389
+ // FORS signature
390
+ const roots = [];
391
+ const forsTreeAddr = setAddr({
392
+ type: AddressType.FORSTREE,
393
+ keypairAddr: wotsAddr,
394
+ });
395
+ const indices = messageToIndices(md);
396
+ for (let i = 0; i < forsVec.length; i++) {
397
+ const [prf, authPath] = forsVec[i];
398
+ const idxOffset = i << A;
399
+ setAddr({ height: 0, index: indices[i] + idxOffset }, forsTreeAddr);
400
+ const leaf = context.thash1(prf, forsTreeAddr);
401
+ // Compute inplace, because we need all roots in same byte array
402
+ roots.push(computeRoot(leaf, indices[i], idxOffset, authPath, A, context, forsTreeAddr));
403
+ }
404
+ const forsPkAddr = setAddr({
405
+ type: AddressType.FORSPK,
406
+ keypairAddr: wotsAddr,
407
+ });
408
+ let root = context.thashN(K, concatBytes(...roots), forsPkAddr); // root = thash()
409
+ // WOTS signature
410
+ const treeAddr = setAddr({ type: AddressType.HASHTREE });
411
+ const wotsPkAddr = setAddr({ type: AddressType.WOTSPK });
412
+ const wotsPk = new Uint8Array(WOTS_LEN * N);
413
+ for (let i = 0; i < wotsVec.length; i++, tree >>= BigInt(TREE_HEIGHT)) {
414
+ const [wots, sigAuth] = wotsVec[i];
415
+ setAddr({ tree, layer: i }, treeAddr);
416
+ setAddr({ subtreeAddr: treeAddr, keypair: leafIdx }, wotsAddr);
417
+ setAddr({ keypairAddr: wotsAddr }, wotsPkAddr);
418
+ const lengths = chainLengths(root);
419
+ for (let i = 0; i < WOTS_LEN; i++) {
420
+ setAddr({ chain: i }, wotsAddr);
421
+ const steps = W - 1 - lengths[i];
422
+ const start = lengths[i];
423
+ const out = wotsPk.subarray(i * N);
424
+ out.set(wots.subarray(i * N, (i + 1) * N));
425
+ for (let j = start; j < start + steps && j < W; j++) {
426
+ setAddr({ hash: j }, wotsAddr);
427
+ out.set(context.thash1(out, wotsAddr));
428
+ }
429
+ }
430
+ const leaf = context.thashN(WOTS_LEN, wotsPk, wotsPkAddr);
431
+ root = computeRoot(leaf, leafIdx, 0, sigAuth, TREE_HEIGHT, context, treeAddr);
432
+ leafIdx = Number(tree & getMaskBig(TREE_HEIGHT));
433
+ }
434
+ return equalBytes(root, pubRoot);
435
+ },
436
+ };
437
+ return {
438
+ info: { type: 'slh-dsa' },
439
+ internal,
440
+ securityLevel: securityLevel,
441
+ lengths: internal.lengths,
442
+ keygen: internal.keygen,
443
+ getPublicKey: internal.getPublicKey,
444
+ sign: (msg, secretKey, opts = {}) => {
445
+ validateSigOpts(opts);
446
+ const M = getMessage(msg, opts.context);
447
+ const res = internal.sign(M, secretKey, opts);
448
+ cleanBytes(M);
449
+ return res;
450
+ },
451
+ verify: (sig, msg, publicKey, opts = {}) => {
452
+ validateVerOpts(opts);
453
+ return internal.verify(sig, getMessage(msg, opts.context), publicKey);
454
+ },
455
+ prehash: (hash) => {
456
+ checkHash(hash, securityLevel);
457
+ return {
458
+ info: { type: 'hashslh-dsa' },
459
+ lengths: internal.lengths,
460
+ keygen: internal.keygen,
461
+ getPublicKey: internal.getPublicKey,
462
+ sign: (msg, secretKey, opts = {}) => {
463
+ validateSigOpts(opts);
464
+ const M = getMessagePrehash(hash, msg, opts.context);
465
+ const res = internal.sign(M, secretKey, opts);
466
+ cleanBytes(M);
467
+ return res;
468
+ },
469
+ verify: (sig, msg, publicKey, opts = {}) => {
470
+ validateVerOpts(opts);
471
+ return internal.verify(sig, getMessagePrehash(hash, msg, opts.context), publicKey);
472
+ },
473
+ };
474
+ },
475
+ };
476
+ }
477
+ const genShake = () => (opts) => (pubSeed, skSeed) => {
478
+ const { N } = opts;
479
+ const stats = { prf: 0, thash: 0, hmsg: 0, gen_message_random: 0 };
480
+ const h0 = shake256.create({}).update(pubSeed);
481
+ const h0tmp = h0.clone();
482
+ const thash = (blocks, input, addr) => {
483
+ stats.thash++;
484
+ return h0
485
+ ._cloneInto(h0tmp)
486
+ .update(addr)
487
+ .update(input.subarray(0, blocks * N))
488
+ .xof(N);
489
+ };
490
+ return {
491
+ PRFaddr: (addr) => {
492
+ if (!skSeed)
493
+ throw new Error('no sk seed');
494
+ stats.prf++;
495
+ const res = h0._cloneInto(h0tmp).update(addr).update(skSeed).xof(N);
496
+ return res;
497
+ },
498
+ PRFmsg: (skPRF, random, msg) => {
499
+ stats.gen_message_random++;
500
+ return shake256.create({}).update(skPRF).update(random).update(msg).digest().subarray(0, N);
501
+ },
502
+ Hmsg: (R, pk, m, outLen) => {
503
+ stats.hmsg++;
504
+ return shake256.create({}).update(R.subarray(0, N)).update(pk).update(m).xof(outLen);
505
+ },
506
+ thash1: thash.bind(null, 1),
507
+ thashN: thash,
508
+ clean: () => {
509
+ h0.destroy();
510
+ h0tmp.destroy();
511
+ //console.log(stats);
512
+ },
513
+ };
514
+ };
515
+ const SHAKE_SIMPLE = { getContext: genShake() };
516
+ /** SLH-DSA: 128-bit fast SHAKE version. */
517
+ export const slh_dsa_shake_128f = /* @__PURE__ */ gen(PARAMS['128f'], SHAKE_SIMPLE);
518
+ /** SLH-DSA: 128-bit short SHAKE version. */
519
+ export const slh_dsa_shake_128s = /* @__PURE__ */ gen(PARAMS['128s'], SHAKE_SIMPLE);
520
+ /** SLH-DSA: 192-bit fast SHAKE version. */
521
+ export const slh_dsa_shake_192f = /* @__PURE__ */ gen(PARAMS['192f'], SHAKE_SIMPLE);
522
+ /** SLH-DSA: 192-bit short SHAKE version. */
523
+ export const slh_dsa_shake_192s = /* @__PURE__ */ gen(PARAMS['192s'], SHAKE_SIMPLE);
524
+ /** SLH-DSA: 256-bit fast SHAKE version. */
525
+ export const slh_dsa_shake_256f = /* @__PURE__ */ gen(PARAMS['256f'], SHAKE_SIMPLE);
526
+ /** SLH-DSA: 256-bit short SHAKE version. */
527
+ export const slh_dsa_shake_256s = /* @__PURE__ */ gen(PARAMS['256s'], SHAKE_SIMPLE);
528
+ const genSha = (h0, h1) => (opts) => (pub_seed, sk_seed) => {
529
+ const { N } = opts;
530
+ /*
531
+ Perf debug stats, how much hashes we call?
532
+ 128f_simple: { prf: 8305, thash: 96_922, hmsg: 1, gen_message_random: 1, mgf1: 2 }
533
+ 256s_robust: { prf: 497_686, thash: 2_783_203, hmsg: 1, gen_message_random: 1, mgf1: 2_783_205}
534
+ 256f_simple: { prf: 36_179, thash: 309_693, hmsg: 1, gen_message_random: 1, mgf1: 2 }
535
+ */
536
+ const stats = { prf: 0, thash: 0, hmsg: 0, gen_message_random: 0, mgf1: 0 };
537
+ const counterB = new Uint8Array(4);
538
+ const counterV = createView(counterB);
539
+ const h0ps = h0
540
+ .create()
541
+ .update(pub_seed)
542
+ .update(new Uint8Array(h0.blockLen - N));
543
+ const h1ps = h1
544
+ .create()
545
+ .update(pub_seed)
546
+ .update(new Uint8Array(h1.blockLen - N));
547
+ const h0tmp = h0ps.clone();
548
+ const h1tmp = h1ps.clone();
549
+ // https://www.rfc-editor.org/rfc/rfc8017.html#appendix-B.2.1
550
+ function mgf1(seed, length, hash) {
551
+ stats.mgf1++;
552
+ const out = new Uint8Array(Math.ceil(length / hash.outputLen) * hash.outputLen);
553
+ // NOT 2^32-1
554
+ if (length > 2 ** 32)
555
+ throw new Error('mask too long');
556
+ for (let counter = 0, o = out; o.length; counter++) {
557
+ counterV.setUint32(0, counter, false);
558
+ hash.create().update(seed).update(counterB).digestInto(o);
559
+ o = o.subarray(hash.outputLen);
560
+ }
561
+ cleanBytes(out.subarray(length));
562
+ return out.subarray(0, length);
563
+ }
564
+ const thash = (_, h, hTmp) => (blocks, input, addr) => {
565
+ stats.thash++;
566
+ const d = h
567
+ ._cloneInto(hTmp)
568
+ .update(addr)
569
+ .update(input.subarray(0, blocks * N))
570
+ .digest();
571
+ return d.subarray(0, N);
572
+ };
573
+ return {
574
+ PRFaddr: (addr) => {
575
+ if (!sk_seed)
576
+ throw new Error('No sk seed');
577
+ stats.prf++;
578
+ const res = h0ps
579
+ ._cloneInto(h0tmp)
580
+ .update(addr)
581
+ .update(sk_seed)
582
+ .digest()
583
+ .subarray(0, N);
584
+ return res;
585
+ },
586
+ PRFmsg: (skPRF, random, msg) => {
587
+ stats.gen_message_random++;
588
+ return hmac.create(h1, skPRF).update(random).update(msg).digest().subarray(0, N);
589
+ },
590
+ Hmsg: (R, pk, m, outLen) => {
591
+ stats.hmsg++;
592
+ const seed = concatBytes(R.subarray(0, N), pk.subarray(0, N), h1.create().update(R.subarray(0, N)).update(pk).update(m).digest());
593
+ return mgf1(seed, outLen, h1);
594
+ },
595
+ thash1: thash(h0, h0ps, h0tmp).bind(null, 1),
596
+ thashN: thash(h1, h1ps, h1tmp),
597
+ clean: () => {
598
+ h0ps.destroy();
599
+ h1ps.destroy();
600
+ h0tmp.destroy();
601
+ h1tmp.destroy();
602
+ //console.log(stats);
603
+ },
604
+ };
605
+ };
606
+ const SHA256_SIMPLE = {
607
+ isCompressed: true,
608
+ getContext: genSha(sha256, sha256),
609
+ };
610
+ const SHA512_SIMPLE = {
611
+ isCompressed: true,
612
+ getContext: genSha(sha256, sha512),
613
+ };
614
+ /** SLH-DSA: 128-bit fast SHA2 version. */
615
+ export const slh_dsa_sha2_128f = /* @__PURE__ */ gen(PARAMS['128f'], SHA256_SIMPLE);
616
+ /** SLH-DSA: 128-bit small SHA2 version. */
617
+ export const slh_dsa_sha2_128s = /* @__PURE__ */ gen(PARAMS['128s'], SHA256_SIMPLE);
618
+ /** SLH-DSA: 192-bit fast SHA2 version. */
619
+ export const slh_dsa_sha2_192f = /* @__PURE__ */ gen(PARAMS['192f'], SHA512_SIMPLE);
620
+ /** SLH-DSA: 192-bit small SHA2 version. */
621
+ export const slh_dsa_sha2_192s = /* @__PURE__ */ gen(PARAMS['192s'], SHA512_SIMPLE);
622
+ /** SLH-DSA: 256-bit fast SHA2 version. */
623
+ export const slh_dsa_sha2_256f = /* @__PURE__ */ gen(PARAMS['256f'], SHA512_SIMPLE);
624
+ /** SLH-DSA: 256-bit small SHA2 version. */
625
+ export const slh_dsa_sha2_256s = /* @__PURE__ */ gen(PARAMS['256s'], SHA512_SIMPLE);
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Utilities for hex, bytearray and number handling.
3
+ * @module
4
+ */
5
+ /*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */
6
+ import { type CHash, type TypedArray, concatBytes, randomBytes as randb } from '@noble/hashes/utils.js';
7
+ export { abytes } from '@noble/hashes/utils.js';
8
+ export { concatBytes };
9
+ export declare const randomBytes: typeof randb;
10
+ export declare function equalBytes(a: Uint8Array, b: Uint8Array): boolean;
11
+ export declare function copyBytes(bytes: Uint8Array): Uint8Array;
12
+ export type CryptoKeys = {
13
+ info?: {
14
+ type?: string;
15
+ };
16
+ lengths: {
17
+ seed?: number;
18
+ publicKey?: number;
19
+ secretKey?: number;
20
+ };
21
+ keygen: (seed?: Uint8Array) => {
22
+ secretKey: Uint8Array;
23
+ publicKey: Uint8Array;
24
+ };
25
+ getPublicKey: (secretKey: Uint8Array) => Uint8Array;
26
+ };
27
+ export type VerOpts = {
28
+ context?: Uint8Array;
29
+ };
30
+ export type SigOpts = VerOpts & {
31
+ extraEntropy?: Uint8Array | false;
32
+ };
33
+ export declare function validateOpts(opts: object): void;
34
+ export declare function validateVerOpts(opts: VerOpts): void;
35
+ export declare function validateSigOpts(opts: SigOpts): void;
36
+ /** Generic interface for signatures. Has keygen, sign and verify. */
37
+ export type Signer = CryptoKeys & {
38
+ lengths: {
39
+ signRand?: number;
40
+ signature?: number;
41
+ };
42
+ sign: (msg: Uint8Array, secretKey: Uint8Array, opts?: SigOpts) => Uint8Array;
43
+ verify: (sig: Uint8Array, msg: Uint8Array, publicKey: Uint8Array, opts?: VerOpts) => boolean;
44
+ };
45
+ export type KEM = CryptoKeys & {
46
+ lengths: {
47
+ cipherText?: number;
48
+ msg?: number;
49
+ msgRand?: number;
50
+ };
51
+ encapsulate: (publicKey: Uint8Array, msg?: Uint8Array) => {
52
+ cipherText: Uint8Array;
53
+ sharedSecret: Uint8Array;
54
+ };
55
+ decapsulate: (cipherText: Uint8Array, secretKey: Uint8Array) => Uint8Array;
56
+ };
57
+ export interface Coder<F, T> {
58
+ encode(from: F): T;
59
+ decode(to: T): F;
60
+ }
61
+ export interface BytesCoder<T> extends Coder<T, Uint8Array> {
62
+ encode: (data: T) => Uint8Array;
63
+ decode: (bytes: Uint8Array) => T;
64
+ }
65
+ export type BytesCoderLen<T> = BytesCoder<T> & {
66
+ bytesLen: number;
67
+ };
68
+ type UnCoder<T> = T extends BytesCoder<infer U> ? U : never;
69
+ type SplitOut<T extends (number | BytesCoderLen<any>)[]> = {
70
+ [K in keyof T]: T[K] extends number ? Uint8Array : UnCoder<T[K]>;
71
+ };
72
+ export declare function splitCoder<T extends (number | BytesCoderLen<any>)[]>(label: string, ...lengths: T): BytesCoder<SplitOut<T>> & {
73
+ bytesLen: number;
74
+ };
75
+ export declare function vecCoder<T>(c: BytesCoderLen<T>, vecLen: number): BytesCoderLen<T[]>;
76
+ export declare function cleanBytes(...list: (TypedArray | TypedArray[])[]): void;
77
+ export declare function getMask(bits: number): number;
78
+ export declare const EMPTY: Uint8Array;
79
+ export declare function getMessage(msg: Uint8Array, ctx?: Uint8Array): Uint8Array;
80
+ export declare function checkHash(hash: CHash, requiredStrength?: number): void;
81
+ export declare function getMessagePrehash(hash: CHash, msg: Uint8Array, ctx?: Uint8Array): Uint8Array;
package/dist/utils.js ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Utilities for hex, bytearray and number handling.
3
+ * @module
4
+ */
5
+ /*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */
6
+ import { abytes, abytes as abytes_, concatBytes, isBytes, randomBytes as randb, } from '@noble/hashes/utils.js';
7
+ export { abytes } from '@noble/hashes/utils.js';
8
+ export { concatBytes };
9
+ export const randomBytes = randb;
10
+ // Compares 2 u8a-s in kinda constant time
11
+ export function equalBytes(a, b) {
12
+ if (a.length !== b.length)
13
+ return false;
14
+ let diff = 0;
15
+ for (let i = 0; i < a.length; i++)
16
+ diff |= a[i] ^ b[i];
17
+ return diff === 0;
18
+ }
19
+ // copy bytes to new u8a (aligned). Because Buffer.slice is broken.
20
+ export function copyBytes(bytes) {
21
+ return Uint8Array.from(bytes);
22
+ }
23
+ export function validateOpts(opts) {
24
+ // We try to catch u8a, since it was previously valid argument at this position
25
+ if (typeof opts !== 'object' || opts === null || isBytes(opts))
26
+ throw new Error('expected opts to be an object');
27
+ }
28
+ export function validateVerOpts(opts) {
29
+ validateOpts(opts);
30
+ if (opts.context !== undefined)
31
+ abytes(opts.context, undefined, 'opts.context');
32
+ }
33
+ export function validateSigOpts(opts) {
34
+ validateVerOpts(opts);
35
+ if (opts.extraEntropy !== false && opts.extraEntropy !== undefined)
36
+ abytes(opts.extraEntropy, undefined, 'opts.extraEntropy');
37
+ }
38
+ export function splitCoder(label, ...lengths) {
39
+ const getLength = (c) => (typeof c === 'number' ? c : c.bytesLen);
40
+ const bytesLen = lengths.reduce((sum, a) => sum + getLength(a), 0);
41
+ return {
42
+ bytesLen,
43
+ encode: (bufs) => {
44
+ const res = new Uint8Array(bytesLen);
45
+ for (let i = 0, pos = 0; i < lengths.length; i++) {
46
+ const c = lengths[i];
47
+ const l = getLength(c);
48
+ const b = typeof c === 'number' ? bufs[i] : c.encode(bufs[i]);
49
+ abytes_(b, l, label);
50
+ res.set(b, pos);
51
+ if (typeof c !== 'number')
52
+ b.fill(0); // clean
53
+ pos += l;
54
+ }
55
+ return res;
56
+ },
57
+ decode: (buf) => {
58
+ abytes_(buf, bytesLen, label);
59
+ const res = [];
60
+ for (const c of lengths) {
61
+ const l = getLength(c);
62
+ const b = buf.subarray(0, l);
63
+ res.push(typeof c === 'number' ? b : c.decode(b));
64
+ buf = buf.subarray(l);
65
+ }
66
+ return res;
67
+ },
68
+ };
69
+ }
70
+ // nano-packed.array (fixed size)
71
+ export function vecCoder(c, vecLen) {
72
+ const bytesLen = vecLen * c.bytesLen;
73
+ return {
74
+ bytesLen,
75
+ encode: (u) => {
76
+ if (u.length !== vecLen)
77
+ throw new Error(`vecCoder.encode: wrong length=${u.length}. Expected: ${vecLen}`);
78
+ const res = new Uint8Array(bytesLen);
79
+ for (let i = 0, pos = 0; i < u.length; i++) {
80
+ const b = c.encode(u[i]);
81
+ res.set(b, pos);
82
+ b.fill(0); // clean
83
+ pos += b.length;
84
+ }
85
+ return res;
86
+ },
87
+ decode: (a) => {
88
+ abytes_(a, bytesLen);
89
+ const r = [];
90
+ for (let i = 0; i < a.length; i += c.bytesLen)
91
+ r.push(c.decode(a.subarray(i, i + c.bytesLen)));
92
+ return r;
93
+ },
94
+ };
95
+ }
96
+ // cleanBytes(Uint8Array.of(), [Uint16Array.of(), Uint32Array.of()])
97
+ export function cleanBytes(...list) {
98
+ for (const t of list) {
99
+ if (Array.isArray(t))
100
+ for (const b of t)
101
+ b.fill(0);
102
+ else
103
+ t.fill(0);
104
+ }
105
+ }
106
+ export function getMask(bits) {
107
+ return (1 << bits) - 1; // 4 -> 0b1111
108
+ }
109
+ export const EMPTY = Uint8Array.of();
110
+ export function getMessage(msg, ctx = EMPTY) {
111
+ abytes_(msg);
112
+ abytes_(ctx);
113
+ if (ctx.length > 255)
114
+ throw new Error('context should be less than 255 bytes');
115
+ return concatBytes(new Uint8Array([0, ctx.length]), ctx, msg);
116
+ }
117
+ // 06 09 60 86 48 01 65 03 04 02
118
+ const oidNistP = /* @__PURE__ */ Uint8Array.from([6, 9, 0x60, 0x86, 0x48, 1, 0x65, 3, 4, 2]);
119
+ export function checkHash(hash, requiredStrength = 0) {
120
+ if (!hash.oid || !equalBytes(hash.oid.subarray(0, 10), oidNistP))
121
+ throw new Error('hash.oid is invalid: expected NIST hash');
122
+ const collisionResistance = (hash.outputLen * 8) / 2;
123
+ if (requiredStrength > collisionResistance) {
124
+ throw new Error('Pre-hash security strength too low: ' +
125
+ collisionResistance +
126
+ ', required: ' +
127
+ requiredStrength);
128
+ }
129
+ }
130
+ export function getMessagePrehash(hash, msg, ctx = EMPTY) {
131
+ abytes_(msg);
132
+ abytes_(ctx);
133
+ if (ctx.length > 255)
134
+ throw new Error('context should be less than 255 bytes');
135
+ const hashed = hash(msg);
136
+ return concatBytes(new Uint8Array([1, ctx.length]), ctx, hash.oid, hashed);
137
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "slh-dsa",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "Lightweight wrapper for SLH-DSA or SPHINCS+",
6
+ "author": "Nbrthx",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "sideEffects": false,
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "test": "node test/test.js"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "dependencies": {
20
+ "@noble/hashes": "~2.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "^5.9.3"
24
+ },
25
+ "keywords": [
26
+ "post-quantum",
27
+ "cryptography",
28
+ "slh-dsa",
29
+ "sphincs",
30
+ "signature",
31
+ "pqcrypto",
32
+ "nist"
33
+ ],
34
+ "license": "MIT"
35
+ }