@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,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
+ };