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.
- package/dist/index.d.ts +62 -0
- package/dist/index.js +625 -0
- package/dist/utils.d.ts +81 -0
- package/dist/utils.js +137 -0
- package/package.json +35 -0
package/dist/index.d.ts
ADDED
|
@@ -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);
|
package/dist/utils.d.ts
ADDED
|
@@ -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
|
+
}
|