@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.
- package/package.json +69 -0
- package/src/batch-receipt-generator.mjs +308 -0
- package/src/dilithium3.mjs +274 -0
- package/src/hybrid-signature.mjs +307 -0
- package/src/index.mjs +64 -0
- package/src/merkle-batcher.mjs +395 -0
- package/src/pq-merkle.mjs +438 -0
- package/src/pq-signer.mjs +381 -0
|
@@ -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 };
|