@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,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-Quantum Receipt Signer
|
|
3
|
+
* Main entry point for PQ-enabled receipts
|
|
4
|
+
*
|
|
5
|
+
* @module @unrdf/receipts/pq-signer
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { blake3 } from 'hash-wasm';
|
|
10
|
+
import {
|
|
11
|
+
generateHybridKeyPair,
|
|
12
|
+
signHybrid,
|
|
13
|
+
verifyHybrid,
|
|
14
|
+
serializeHybridSignature,
|
|
15
|
+
deserializeHybridSignature,
|
|
16
|
+
getHybridSecurityLevel,
|
|
17
|
+
HybridKeyPairSchema,
|
|
18
|
+
HybridSignatureSchema,
|
|
19
|
+
} from './hybrid-signature.mjs';
|
|
20
|
+
import {
|
|
21
|
+
generateDilithium3KeyPair,
|
|
22
|
+
signDilithium3,
|
|
23
|
+
verifyDilithium3,
|
|
24
|
+
getDilithium3SecurityLevel,
|
|
25
|
+
} from './dilithium3.mjs';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* PQ Receipt Schema
|
|
29
|
+
*/
|
|
30
|
+
export const PQReceiptSchema = z.object({
|
|
31
|
+
Q_ID: z.string().regex(/^Q\*_[a-f0-9]{16}$/),
|
|
32
|
+
Q_RDF: z.string().url(),
|
|
33
|
+
Q_PROV: z.object({
|
|
34
|
+
timestamp: z.bigint(),
|
|
35
|
+
batchSize: z.number().int().positive(),
|
|
36
|
+
operationType: z.string(),
|
|
37
|
+
universeID: z.string(),
|
|
38
|
+
contentHash: z.string(),
|
|
39
|
+
merkleRoot: z.string().optional(),
|
|
40
|
+
signatureScheme: z.enum(['classical', 'postQuantum', 'hybrid']),
|
|
41
|
+
signature: z.any().optional(), // Signature data (flexible schema)
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create PQ Receipt
|
|
47
|
+
* Generates receipt with optional post-quantum signature
|
|
48
|
+
*
|
|
49
|
+
* @param {Object} options - Receipt options
|
|
50
|
+
* @param {string} options.universeID - Universe Q*_ID
|
|
51
|
+
* @param {Array<Object>} options.operations - Operations to batch
|
|
52
|
+
* @param {string} options.operationType - Operation type
|
|
53
|
+
* @param {string} [options.signatureScheme='classical'] - Signature scheme
|
|
54
|
+
* @param {Object} [options.keyPair] - Key pair for signing
|
|
55
|
+
* @param {string} [options.merkleRoot] - Optional Merkle root
|
|
56
|
+
* @returns {Promise<Object>} PQ-enabled receipt
|
|
57
|
+
* @throws {Error} If validation fails
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* // Classical receipt (backward compatible)
|
|
61
|
+
* const receipt1 = await createPQReceipt({
|
|
62
|
+
* universeID: 'Q*_0123456789abcdef',
|
|
63
|
+
* operations: [...],
|
|
64
|
+
* operationType: 'insert',
|
|
65
|
+
* });
|
|
66
|
+
*
|
|
67
|
+
* // Post-quantum receipt
|
|
68
|
+
* const keyPair = await generateDilithium3KeyPair();
|
|
69
|
+
* const receipt2 = await createPQReceipt({
|
|
70
|
+
* universeID: 'Q*_0123456789abcdef',
|
|
71
|
+
* operations: [...],
|
|
72
|
+
* operationType: 'insert',
|
|
73
|
+
* signatureScheme: 'postQuantum',
|
|
74
|
+
* keyPair,
|
|
75
|
+
* });
|
|
76
|
+
*
|
|
77
|
+
* // Hybrid receipt (both signatures)
|
|
78
|
+
* const hybridKeyPair = await generateHybridKeyPair();
|
|
79
|
+
* const receipt3 = await createPQReceipt({
|
|
80
|
+
* universeID: 'Q*_0123456789abcdef',
|
|
81
|
+
* operations: [...],
|
|
82
|
+
* operationType: 'insert',
|
|
83
|
+
* signatureScheme: 'hybrid',
|
|
84
|
+
* keyPair: hybridKeyPair,
|
|
85
|
+
* });
|
|
86
|
+
*/
|
|
87
|
+
export async function createPQReceipt(options) {
|
|
88
|
+
// Validate options
|
|
89
|
+
if (!options || typeof options !== 'object') {
|
|
90
|
+
throw new TypeError('createPQReceipt: options must be object');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (typeof options.universeID !== 'string' || !options.universeID.startsWith('Q*_')) {
|
|
94
|
+
throw new TypeError('createPQReceipt: universeID must be Q* identifier');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!Array.isArray(options.operations) || options.operations.length === 0) {
|
|
98
|
+
throw new TypeError('createPQReceipt: operations must be non-empty array');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (typeof options.operationType !== 'string') {
|
|
102
|
+
throw new TypeError('createPQReceipt: operationType must be string');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const signatureScheme = options.signatureScheme || 'classical';
|
|
106
|
+
if (!['classical', 'postQuantum', 'hybrid'].includes(signatureScheme)) {
|
|
107
|
+
throw new TypeError('createPQReceipt: signatureScheme must be classical, postQuantum, or hybrid');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Generate timestamp
|
|
111
|
+
const timestamp = typeof process !== 'undefined' && process.hrtime
|
|
112
|
+
? process.hrtime.bigint()
|
|
113
|
+
: BigInt(Date.now()) * 1_000_000n;
|
|
114
|
+
|
|
115
|
+
// Compute content hash
|
|
116
|
+
const contentHash = await computeContentHash(options.operations);
|
|
117
|
+
|
|
118
|
+
// Generate receipt ID
|
|
119
|
+
const Q_ID = await generateReceiptID(options.universeID, timestamp);
|
|
120
|
+
const Q_RDF = `http://kgc.io/receipts/${Q_ID.slice(3)}`;
|
|
121
|
+
|
|
122
|
+
// Build provenance
|
|
123
|
+
const Q_PROV = {
|
|
124
|
+
timestamp,
|
|
125
|
+
batchSize: options.operations.length,
|
|
126
|
+
operationType: options.operationType,
|
|
127
|
+
universeID: options.universeID,
|
|
128
|
+
contentHash,
|
|
129
|
+
signatureScheme,
|
|
130
|
+
...(options.merkleRoot && { merkleRoot: options.merkleRoot }),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Add signature if requested
|
|
134
|
+
if (signatureScheme !== 'classical') {
|
|
135
|
+
if (!options.keyPair) {
|
|
136
|
+
throw new TypeError('createPQReceipt: keyPair required for PQ signatures');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const receiptData = JSON.stringify({ Q_ID, Q_RDF, Q_PROV }, (key, value) =>
|
|
140
|
+
typeof value === 'bigint' ? value.toString() : value
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
if (signatureScheme === 'postQuantum') {
|
|
144
|
+
const signature = await signDilithium3(receiptData, options.keyPair);
|
|
145
|
+
Q_PROV.signature = {
|
|
146
|
+
type: 'Dilithium3',
|
|
147
|
+
signature: Buffer.from(signature.signature).toString('base64'),
|
|
148
|
+
publicKey: Buffer.from(signature.publicKey).toString('base64'),
|
|
149
|
+
};
|
|
150
|
+
} else if (signatureScheme === 'hybrid') {
|
|
151
|
+
const signature = await signHybrid(receiptData, options.keyPair);
|
|
152
|
+
Q_PROV.signature = {
|
|
153
|
+
type: 'Hybrid',
|
|
154
|
+
data: serializeHybridSignature(signature),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const receipt = { Q_ID, Q_RDF, Q_PROV };
|
|
160
|
+
|
|
161
|
+
// Validate receipt
|
|
162
|
+
PQReceiptSchema.parse(receipt);
|
|
163
|
+
|
|
164
|
+
return receipt;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Verify PQ Receipt
|
|
169
|
+
* Verifies receipt including optional PQ signature
|
|
170
|
+
*
|
|
171
|
+
* @param {Object} receipt - Receipt to verify
|
|
172
|
+
* @param {Array<Object>} operations - Original operations
|
|
173
|
+
* @returns {Promise<Object>} Verification result
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* const result = await verifyPQReceipt(receipt, operations);
|
|
177
|
+
* console.log('Valid:', result.valid);
|
|
178
|
+
* console.log('Signature verified:', result.signatureValid);
|
|
179
|
+
*/
|
|
180
|
+
export async function verifyPQReceipt(receipt, operations) {
|
|
181
|
+
try {
|
|
182
|
+
// Validate receipt schema
|
|
183
|
+
PQReceiptSchema.parse(receipt);
|
|
184
|
+
|
|
185
|
+
// Validate operations
|
|
186
|
+
if (!Array.isArray(operations) || operations.length === 0) {
|
|
187
|
+
return {
|
|
188
|
+
valid: false,
|
|
189
|
+
reason: 'Operations must be non-empty array',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Verify batch size
|
|
194
|
+
if (operations.length !== receipt.Q_PROV.batchSize) {
|
|
195
|
+
return {
|
|
196
|
+
valid: false,
|
|
197
|
+
reason: `Batch size mismatch: expected ${receipt.Q_PROV.batchSize}, got ${operations.length}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Recompute content hash
|
|
202
|
+
const recomputedHash = await computeContentHash(operations);
|
|
203
|
+
if (recomputedHash !== receipt.Q_PROV.contentHash) {
|
|
204
|
+
return {
|
|
205
|
+
valid: false,
|
|
206
|
+
reason: 'Content hash mismatch',
|
|
207
|
+
expected: receipt.Q_PROV.contentHash,
|
|
208
|
+
actual: recomputedHash,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Verify signature if present
|
|
213
|
+
let signatureValid = true;
|
|
214
|
+
if (receipt.Q_PROV.signature) {
|
|
215
|
+
const cleanReceipt = {
|
|
216
|
+
Q_ID: receipt.Q_ID,
|
|
217
|
+
Q_RDF: receipt.Q_RDF,
|
|
218
|
+
Q_PROV: {
|
|
219
|
+
...receipt.Q_PROV,
|
|
220
|
+
signature: undefined,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
const dataToVerify = JSON.stringify(cleanReceipt, (key, value) =>
|
|
224
|
+
typeof value === 'bigint' ? value.toString() : value
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
if (receipt.Q_PROV.signature.type === 'Dilithium3') {
|
|
228
|
+
const sig = {
|
|
229
|
+
signature: new Uint8Array(Buffer.from(receipt.Q_PROV.signature.signature, 'base64')),
|
|
230
|
+
publicKey: new Uint8Array(Buffer.from(receipt.Q_PROV.signature.publicKey, 'base64')),
|
|
231
|
+
algorithm: 'Dilithium3',
|
|
232
|
+
};
|
|
233
|
+
signatureValid = await verifyDilithium3(dataToVerify, sig);
|
|
234
|
+
} else if (receipt.Q_PROV.signature.type === 'Hybrid') {
|
|
235
|
+
const sig = deserializeHybridSignature(receipt.Q_PROV.signature.data);
|
|
236
|
+
const result = await verifyHybrid(dataToVerify, sig);
|
|
237
|
+
signatureValid = result.valid;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
valid: true,
|
|
243
|
+
signatureValid,
|
|
244
|
+
signatureScheme: receipt.Q_PROV.signatureScheme,
|
|
245
|
+
receiptID: receipt.Q_ID,
|
|
246
|
+
timestamp: receipt.Q_PROV.timestamp,
|
|
247
|
+
batchSize: receipt.Q_PROV.batchSize,
|
|
248
|
+
contentHash: receipt.Q_PROV.contentHash,
|
|
249
|
+
};
|
|
250
|
+
} catch (err) {
|
|
251
|
+
return {
|
|
252
|
+
valid: false,
|
|
253
|
+
reason: `Verification error: ${err.message}`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Generate Receipt ID
|
|
260
|
+
* Creates unique Q* identifier
|
|
261
|
+
*
|
|
262
|
+
* @param {string} universeID - Universe Q*_ID
|
|
263
|
+
* @param {bigint} timestamp - Nanosecond timestamp
|
|
264
|
+
* @returns {Promise<string>} Receipt Q*_ID
|
|
265
|
+
*/
|
|
266
|
+
async function generateReceiptID(universeID, timestamp) {
|
|
267
|
+
const combined = `receipt-${universeID}-${timestamp}`;
|
|
268
|
+
const hash = await blake3(combined);
|
|
269
|
+
return `Q*_${hash.slice(0, 16)}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Compute Content Hash
|
|
274
|
+
* Computes BLAKE3 hash of operations
|
|
275
|
+
*
|
|
276
|
+
* @param {Array<Object>} operations - Operations array
|
|
277
|
+
* @returns {Promise<string>} BLAKE3 hash
|
|
278
|
+
*/
|
|
279
|
+
async function computeContentHash(operations) {
|
|
280
|
+
const sorted = [...operations].sort((a, b) => {
|
|
281
|
+
const tCompare = (a.timestamp || 0n) < (b.timestamp || 0n) ? -1 :
|
|
282
|
+
(a.timestamp || 0n) > (b.timestamp || 0n) ? 1 : 0;
|
|
283
|
+
if (tCompare !== 0) return tCompare;
|
|
284
|
+
return a.subject < b.subject ? -1 : a.subject > b.subject ? 1 : 0;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const serialized = JSON.stringify(sorted, (key, value) =>
|
|
288
|
+
typeof value === 'bigint' ? value.toString() : value
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
return blake3(serialized);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Batch Sign Multiple Receipts
|
|
296
|
+
* Signs multiple receipts in a single operation (performance optimization)
|
|
297
|
+
*
|
|
298
|
+
* @param {Array<Object>} receipts - Array of receipts to sign
|
|
299
|
+
* @param {Object} keyPair - Signing key pair
|
|
300
|
+
* @param {string} signatureScheme - Signature scheme
|
|
301
|
+
* @returns {Promise<Array<Object>>} Signed receipts
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* const signed = await batchSignReceipts(receipts, keyPair, 'hybrid');
|
|
305
|
+
*/
|
|
306
|
+
export async function batchSignReceipts(receipts, keyPair, signatureScheme) {
|
|
307
|
+
if (!Array.isArray(receipts) || receipts.length === 0) {
|
|
308
|
+
throw new TypeError('batchSignReceipts: receipts must be non-empty array');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const signed = [];
|
|
312
|
+
|
|
313
|
+
for (const receipt of receipts) {
|
|
314
|
+
const receiptData = JSON.stringify(receipt, (key, value) =>
|
|
315
|
+
typeof value === 'bigint' ? value.toString() : value
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
let signature;
|
|
319
|
+
if (signatureScheme === 'postQuantum') {
|
|
320
|
+
signature = await signDilithium3(receiptData, keyPair);
|
|
321
|
+
receipt.Q_PROV.signature = {
|
|
322
|
+
type: 'Dilithium3',
|
|
323
|
+
signature: Buffer.from(signature.signature).toString('base64'),
|
|
324
|
+
publicKey: Buffer.from(signature.publicKey).toString('base64'),
|
|
325
|
+
};
|
|
326
|
+
} else if (signatureScheme === 'hybrid') {
|
|
327
|
+
signature = await signHybrid(receiptData, keyPair);
|
|
328
|
+
receipt.Q_PROV.signature = {
|
|
329
|
+
type: 'Hybrid',
|
|
330
|
+
data: serializeHybridSignature(signature),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
receipt.Q_PROV.signatureScheme = signatureScheme;
|
|
335
|
+
signed.push(receipt);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return signed;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Get PQ Capabilities
|
|
343
|
+
* Returns available PQ signature schemes and their properties
|
|
344
|
+
*
|
|
345
|
+
* @returns {Object} PQ capabilities
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* const caps = getPQCapabilities();
|
|
349
|
+
* console.log('Supported schemes:', caps.schemes);
|
|
350
|
+
* console.log('Hybrid security:', caps.hybrid.securityLevel);
|
|
351
|
+
*/
|
|
352
|
+
export function getPQCapabilities() {
|
|
353
|
+
return {
|
|
354
|
+
schemes: ['classical', 'postQuantum', 'hybrid'],
|
|
355
|
+
classical: {
|
|
356
|
+
algorithm: 'BLAKE3',
|
|
357
|
+
securityBits: 256,
|
|
358
|
+
quantumResistant: false,
|
|
359
|
+
},
|
|
360
|
+
postQuantum: getDilithium3SecurityLevel(),
|
|
361
|
+
hybrid: getHybridSecurityLevel(),
|
|
362
|
+
recommended: 'hybrid',
|
|
363
|
+
migrationPath: {
|
|
364
|
+
v1: 'classical (backward compatible)',
|
|
365
|
+
v2: 'postQuantum (opt-in)',
|
|
366
|
+
v3: 'hybrid (future-proof)',
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Re-export key generation functions
|
|
372
|
+
export {
|
|
373
|
+
generateHybridKeyPair,
|
|
374
|
+
generateDilithium3KeyPair,
|
|
375
|
+
signHybrid,
|
|
376
|
+
verifyHybrid,
|
|
377
|
+
signDilithium3,
|
|
378
|
+
verifyDilithium3,
|
|
379
|
+
getHybridSecurityLevel,
|
|
380
|
+
getDilithium3SecurityLevel,
|
|
381
|
+
};
|