@unrdf/receipts 26.4.2

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,438 @@
1
+ /**
2
+ * Post-Quantum Merkle Trees (XMSS-inspired)
3
+ * Quantum-resistant hash-based signatures for Merkle trees
4
+ *
5
+ * @module @unrdf/receipts/pq-merkle
6
+ */
7
+
8
+ import { blake3 } from 'hash-wasm';
9
+ import { sha3_256, sha3_512 } from '@noble/hashes/sha3.js';
10
+ import { z } from 'zod';
11
+ import {
12
+ signDilithium3,
13
+ verifyDilithium3,
14
+ generateDilithium3KeyPair,
15
+ } from './dilithium3.mjs';
16
+
17
+ /**
18
+ * XMSS Node Schema
19
+ */
20
+ const XMSSNodeSchema = z.object({
21
+ hash: z.string(),
22
+ left: z.any().optional(),
23
+ right: z.any().optional(),
24
+ data: z.any().optional(),
25
+ index: z.number().int().nonnegative(),
26
+ signature: z.any().optional(), // Optional PQ signature
27
+ });
28
+
29
+ /**
30
+ * XMSS Proof Schema
31
+ */
32
+ const XMSSProofSchema = z.object({
33
+ leaf: z.string(),
34
+ index: z.number().int().nonnegative(),
35
+ proof: z.array(z.object({
36
+ hash: z.string(),
37
+ position: z.enum(['left', 'right']),
38
+ signature: z.any().optional(),
39
+ })),
40
+ root: z.string(),
41
+ rootSignature: z.any().optional(),
42
+ });
43
+
44
+ /**
45
+ * Compute PQ-Enhanced Leaf Hash
46
+ * Uses SHA3-256 (quantum-resistant hash function)
47
+ *
48
+ * @param {*} data - Leaf data
49
+ * @returns {Promise<string>} SHA3-256 hash (hex)
50
+ *
51
+ * @example
52
+ * const hash = await computePQLeafHash({ subject: 'ex:s1', ... });
53
+ * console.assert(hash.length === 64);
54
+ */
55
+ async function computePQLeafHash(data) {
56
+ const serialized = JSON.stringify(data, (key, value) =>
57
+ typeof value === 'bigint' ? value.toString() : value
58
+ );
59
+
60
+ // Use SHA3-256 instead of BLAKE3 for quantum resistance
61
+ const hash = sha3_256(new TextEncoder().encode(serialized));
62
+ return Buffer.from(hash).toString('hex');
63
+ }
64
+
65
+ /**
66
+ * Compute PQ Parent Hash
67
+ * Combines child hashes with SHA3-512 for extra security margin
68
+ *
69
+ * @param {string} leftHash - Left child hash
70
+ * @param {string} rightHash - Right child hash
71
+ * @returns {Promise<string>} Parent hash (truncated to 64 hex chars)
72
+ *
73
+ * @example
74
+ * const parent = await computePQParentHash(hashA, hashB);
75
+ */
76
+ async function computePQParentHash(leftHash, rightHash) {
77
+ const combined = leftHash + rightHash;
78
+ const hash = sha3_512(new TextEncoder().encode(combined));
79
+
80
+ // Truncate to 64 hex chars (256 bits) for consistency
81
+ return Buffer.from(hash).toString('hex').slice(0, 64);
82
+ }
83
+
84
+ /**
85
+ * Build PQ Merkle Tree
86
+ * Constructs quantum-resistant Merkle tree with SHA3
87
+ *
88
+ * @param {Array<*>} data - Array of leaf data
89
+ * @param {Object} [options] - Options
90
+ * @param {boolean} [options.signNodes=false] - Sign nodes with Dilithium3
91
+ * @param {Object} [options.keyPair] - Key pair for signing
92
+ * @returns {Promise<Object>} XMSS tree root node
93
+ * @throws {Error} If data is empty or invalid
94
+ *
95
+ * @example
96
+ * // Basic PQ Merkle tree
97
+ * const tree = await buildPQMerkleTree([...data]);
98
+ *
99
+ * // Fully signed XMSS tree
100
+ * const keyPair = await generateDilithium3KeyPair();
101
+ * const xmssTree = await buildPQMerkleTree([...data], {
102
+ * signNodes: true,
103
+ * keyPair,
104
+ * });
105
+ */
106
+ export async function buildPQMerkleTree(data, options = {}) {
107
+ if (!Array.isArray(data) || data.length === 0) {
108
+ throw new TypeError('buildPQMerkleTree: data must be non-empty array');
109
+ }
110
+
111
+ const { signNodes = false, keyPair } = options;
112
+
113
+ if (signNodes && !keyPair) {
114
+ throw new TypeError('buildPQMerkleTree: keyPair required when signNodes=true');
115
+ }
116
+
117
+ // Build leaf nodes
118
+ const leaves = await Promise.all(
119
+ data.map(async (item, index) => {
120
+ const hash = await computePQLeafHash(item);
121
+ const node = {
122
+ hash,
123
+ data: item,
124
+ index,
125
+ };
126
+
127
+ // Optionally sign leaf
128
+ if (signNodes) {
129
+ const signature = await signDilithium3(hash, keyPair);
130
+ node.signature = {
131
+ signature: Buffer.from(signature.signature).toString('base64'),
132
+ publicKey: Buffer.from(signature.publicKey).toString('base64'),
133
+ };
134
+ }
135
+
136
+ return node;
137
+ })
138
+ );
139
+
140
+ // Build tree bottom-up
141
+ let currentLevel = leaves;
142
+
143
+ while (currentLevel.length > 1) {
144
+ const nextLevel = [];
145
+
146
+ for (let i = 0; i < currentLevel.length; i += 2) {
147
+ const left = currentLevel[i];
148
+ const right = currentLevel[i + 1];
149
+
150
+ if (right) {
151
+ // Two children
152
+ const parentHash = await computePQParentHash(left.hash, right.hash);
153
+ const parent = {
154
+ hash: parentHash,
155
+ left,
156
+ right,
157
+ index: Math.floor(i / 2),
158
+ };
159
+
160
+ // Optionally sign parent
161
+ if (signNodes) {
162
+ const signature = await signDilithium3(parentHash, keyPair);
163
+ parent.signature = {
164
+ signature: Buffer.from(signature.signature).toString('base64'),
165
+ publicKey: Buffer.from(signature.publicKey).toString('base64'),
166
+ };
167
+ }
168
+
169
+ nextLevel.push(parent);
170
+ } else {
171
+ // Odd number - duplicate for complete binary tree
172
+ const parentHash = await computePQParentHash(left.hash, left.hash);
173
+ const parent = {
174
+ hash: parentHash,
175
+ left,
176
+ right: left,
177
+ index: Math.floor(i / 2),
178
+ };
179
+
180
+ if (signNodes) {
181
+ const signature = await signDilithium3(parentHash, keyPair);
182
+ parent.signature = {
183
+ signature: Buffer.from(signature.signature).toString('base64'),
184
+ publicKey: Buffer.from(signature.publicKey).toString('base64'),
185
+ };
186
+ }
187
+
188
+ nextLevel.push(parent);
189
+ }
190
+ }
191
+
192
+ currentLevel = nextLevel;
193
+ }
194
+
195
+ // Return root
196
+ return currentLevel[0];
197
+ }
198
+
199
+ /**
200
+ * Generate PQ Merkle Proof
201
+ * Creates quantum-resistant proof path from leaf to root
202
+ *
203
+ * @param {Object} tree - PQ Merkle tree root
204
+ * @param {number} leafIndex - Index of leaf to prove
205
+ * @returns {Object} XMSS proof
206
+ * @throws {Error} If leaf index invalid
207
+ *
208
+ * @example
209
+ * const proof = generatePQMerkleProof(tree, 1);
210
+ * console.log('Proof includes PQ signatures:', proof.proof[0].signature !== undefined);
211
+ */
212
+ export function generatePQMerkleProof(tree, leafIndex) {
213
+ if (typeof leafIndex !== 'number' || leafIndex < 0) {
214
+ throw new TypeError('generatePQMerkleProof: leafIndex must be non-negative number');
215
+ }
216
+
217
+ const proof = [];
218
+ let leaf = null;
219
+
220
+ // Find leaf
221
+ function findLeaf(node) {
222
+ if (node.data !== undefined && node.index === leafIndex) {
223
+ leaf = node;
224
+ return true;
225
+ }
226
+ if (node.left && findLeaf(node.left)) return true;
227
+ if (node.right && findLeaf(node.right)) return true;
228
+ return false;
229
+ }
230
+
231
+ findLeaf(tree);
232
+
233
+ if (!leaf) {
234
+ throw new Error(`generatePQMerkleProof: Leaf at index ${leafIndex} not found`);
235
+ }
236
+
237
+ // Build proof path
238
+ function buildProof(node, targetIndex) {
239
+ if (node.data !== undefined) {
240
+ return node.index === targetIndex;
241
+ }
242
+
243
+ const leftMatch = node.left && buildProof(node.left, targetIndex);
244
+ const rightMatch = node.right && buildProof(node.right, targetIndex);
245
+
246
+ if (leftMatch) {
247
+ if (node.right && node.right !== node.left) {
248
+ const step = {
249
+ hash: node.right.hash,
250
+ position: 'right',
251
+ };
252
+ if (node.right.signature) {
253
+ step.signature = node.right.signature;
254
+ }
255
+ proof.push(step);
256
+ }
257
+ return true;
258
+ }
259
+
260
+ if (rightMatch) {
261
+ if (node.left && node.left !== node.right) {
262
+ const step = {
263
+ hash: node.left.hash,
264
+ position: 'left',
265
+ };
266
+ if (node.left.signature) {
267
+ step.signature = node.left.signature;
268
+ }
269
+ proof.push(step);
270
+ }
271
+ return true;
272
+ }
273
+
274
+ return false;
275
+ }
276
+
277
+ buildProof(tree, leafIndex);
278
+
279
+ const result = {
280
+ leaf: leaf.hash,
281
+ index: leafIndex,
282
+ proof,
283
+ root: tree.hash,
284
+ };
285
+
286
+ // Include root signature if present
287
+ if (tree.signature) {
288
+ result.rootSignature = tree.signature;
289
+ }
290
+
291
+ // Validate proof schema
292
+ XMSSProofSchema.parse(result);
293
+
294
+ return result;
295
+ }
296
+
297
+ /**
298
+ * Verify PQ Merkle Proof
299
+ * Verifies quantum-resistant Merkle proof
300
+ *
301
+ * @param {Object} proof - XMSS proof
302
+ * @param {string} leafHash - Leaf hash to verify
303
+ * @param {Object} [options] - Verification options
304
+ * @param {boolean} [options.verifySignatures=false] - Verify PQ signatures in proof
305
+ * @returns {Promise<Object>} Verification result
306
+ *
307
+ * @example
308
+ * const result = await verifyPQMerkleProof(proof, leafHash, { verifySignatures: true });
309
+ * console.log('Proof valid:', result.valid);
310
+ * console.log('All signatures valid:', result.signaturesValid);
311
+ */
312
+ export async function verifyPQMerkleProof(proof, leafHash, options = {}) {
313
+ const { verifySignatures = false } = options;
314
+
315
+ try {
316
+ // Validate proof schema
317
+ XMSSProofSchema.parse(proof);
318
+
319
+ // Verify leaf hash matches
320
+ if (proof.leaf !== leafHash) {
321
+ return {
322
+ valid: false,
323
+ reason: 'Leaf hash mismatch',
324
+ };
325
+ }
326
+
327
+ // Compute root by following proof path
328
+ let currentHash = leafHash;
329
+
330
+ for (const step of proof.proof) {
331
+ // Verify signature if requested
332
+ if (verifySignatures && step.signature) {
333
+ const sig = {
334
+ signature: new Uint8Array(Buffer.from(step.signature.signature, 'base64')),
335
+ publicKey: new Uint8Array(Buffer.from(step.signature.publicKey, 'base64')),
336
+ algorithm: 'Dilithium3',
337
+ };
338
+
339
+ const sigValid = await verifyDilithium3(step.hash, sig);
340
+ if (!sigValid) {
341
+ return {
342
+ valid: false,
343
+ reason: 'Invalid PQ signature in proof path',
344
+ };
345
+ }
346
+ }
347
+
348
+ // Compute parent hash
349
+ if (step.position === 'left') {
350
+ currentHash = await computePQParentHash(step.hash, currentHash);
351
+ } else {
352
+ currentHash = await computePQParentHash(currentHash, step.hash);
353
+ }
354
+ }
355
+
356
+ // Compare computed root with proof root
357
+ if (currentHash !== proof.root) {
358
+ return {
359
+ valid: false,
360
+ reason: 'Root hash mismatch',
361
+ };
362
+ }
363
+
364
+ // Verify root signature if present
365
+ if (verifySignatures && proof.rootSignature) {
366
+ const rootSig = {
367
+ signature: new Uint8Array(Buffer.from(proof.rootSignature.signature, 'base64')),
368
+ publicKey: new Uint8Array(Buffer.from(proof.rootSignature.publicKey, 'base64')),
369
+ algorithm: 'Dilithium3',
370
+ };
371
+
372
+ const rootSigValid = await verifyDilithium3(proof.root, rootSig);
373
+ if (!rootSigValid) {
374
+ return {
375
+ valid: false,
376
+ reason: 'Invalid PQ signature on root',
377
+ };
378
+ }
379
+ }
380
+
381
+ return {
382
+ valid: true,
383
+ signaturesValid: verifySignatures,
384
+ quantumResistant: true,
385
+ };
386
+ } catch (err) {
387
+ return {
388
+ valid: false,
389
+ reason: `Verification error: ${err.message}`,
390
+ };
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Get PQ Merkle Tree Info
396
+ * Returns metadata about PQ Merkle tree
397
+ *
398
+ * @param {Object} tree - PQ Merkle tree
399
+ * @returns {Object} Tree information
400
+ *
401
+ * @example
402
+ * const info = getPQMerkleTreeInfo(tree);
403
+ * console.log('Hash function:', info.hashFunction); // SHA3-256
404
+ * console.log('Quantum resistant:', info.quantumResistant); // true
405
+ * console.log('Has signatures:', info.hasPQSignatures);
406
+ */
407
+ export function getPQMerkleTreeInfo(tree) {
408
+ function countNodes(node) {
409
+ if (!node) return 0;
410
+ if (node.data !== undefined) return 1;
411
+ return 1 + countNodes(node.left) + (node.right !== node.left ? countNodes(node.right) : 0);
412
+ }
413
+
414
+ function hasSignatures(node) {
415
+ if (!node) return false;
416
+ if (node.signature) return true;
417
+ return hasSignatures(node.left) || hasSignatures(node.right);
418
+ }
419
+
420
+ function getDepth(node) {
421
+ if (!node || node.data !== undefined) return 0;
422
+ return 1 + Math.max(getDepth(node.left), getDepth(node.right));
423
+ }
424
+
425
+ return {
426
+ hashFunction: 'SHA3-256/512',
427
+ quantumResistant: true,
428
+ signatureScheme: hasSignatures(tree) ? 'Dilithium3' : 'none',
429
+ hasPQSignatures: hasSignatures(tree),
430
+ rootHash: tree.hash,
431
+ totalNodes: countNodes(tree),
432
+ depth: getDepth(tree),
433
+ xmssCompliant: true,
434
+ };
435
+ }
436
+
437
+ // Export hash functions for testing
438
+ export { computePQLeafHash, computePQParentHash };