@finalbosstech/pqc-receipt-sdk 1.0.0
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/LICENSE.md +31 -0
- package/LICENSES/FinalBoss-Commercial.txt +69 -0
- package/LICENSES/PolyForm-Noncommercial-1.0.0.txt +129 -0
- package/README.md +167 -0
- package/bin/pqc-verify.js +112 -0
- package/contracts/ReceiptAnchor.sol +160 -0
- package/package.json +40 -0
- package/src/crypto.js +153 -0
- package/src/crypto.ts +183 -0
- package/src/index.js +65 -0
- package/src/index.ts +78 -0
- package/src/log.js +193 -0
- package/src/log.ts +195 -0
- package/src/receipt.js +141 -0
- package/src/receipt.ts +148 -0
- package/src/types.ts +86 -0
- package/src/verifier.js +115 -0
- package/src/verifier.ts +175 -0
- package/test/demo.js +159 -0
- package/tsconfig.json +20 -0
package/src/verifier.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PQC Receipt SDK - Independent Verification
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { verifySignature, hash, canonicalizePayload } from './crypto.js';
|
|
6
|
+
import { verifyLogChain } from './log.js';
|
|
7
|
+
import type { Receipt, LogEntry, VerificationResult, ChainVerificationResult } from './types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Verify a single receipt
|
|
11
|
+
*/
|
|
12
|
+
export async function verifyReceipt(
|
|
13
|
+
receipt: Receipt,
|
|
14
|
+
publicKey: Buffer
|
|
15
|
+
): Promise<VerificationResult> {
|
|
16
|
+
return verifySignature(receipt, publicKey);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Verify receipt hash integrity (without signature verification)
|
|
21
|
+
*/
|
|
22
|
+
export function verifyReceiptHash(receipt: Receipt): VerificationResult {
|
|
23
|
+
const receiptCopy = { ...receipt } as any;
|
|
24
|
+
delete receiptCopy.receipt_hash;
|
|
25
|
+
|
|
26
|
+
const expectedHash = hash(canonicalizePayload(receiptCopy));
|
|
27
|
+
|
|
28
|
+
if (receipt.receipt_hash !== expectedHash) {
|
|
29
|
+
return { valid: false, error: 'HASH_MISMATCH' };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { valid: true };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Verify a chain of receipts
|
|
37
|
+
*/
|
|
38
|
+
export async function verifyReceiptChain(
|
|
39
|
+
receipts: Receipt[],
|
|
40
|
+
publicKey: Buffer
|
|
41
|
+
): Promise<{
|
|
42
|
+
valid: boolean;
|
|
43
|
+
verified: number;
|
|
44
|
+
failed: Array<{ index: number; receipt_id: string; error: string }>;
|
|
45
|
+
}> {
|
|
46
|
+
const failed: Array<{ index: number; receipt_id: string; error: string }> = [];
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < receipts.length; i++) {
|
|
49
|
+
const receipt = receipts[i];
|
|
50
|
+
|
|
51
|
+
// Verify signature
|
|
52
|
+
const sigResult = await verifySignature(receipt, publicKey);
|
|
53
|
+
if (!sigResult.valid) {
|
|
54
|
+
failed.push({
|
|
55
|
+
index: i,
|
|
56
|
+
receipt_id: receipt.receipt_id,
|
|
57
|
+
error: sigResult.error || 'UNKNOWN'
|
|
58
|
+
});
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Verify chain linkage
|
|
63
|
+
if (i > 0) {
|
|
64
|
+
const prevReceipt = receipts[i - 1];
|
|
65
|
+
if (receipt.chain.prev_receipt_hash !== prevReceipt.receipt_hash) {
|
|
66
|
+
failed.push({
|
|
67
|
+
index: i,
|
|
68
|
+
receipt_id: receipt.receipt_id,
|
|
69
|
+
error: 'CHAIN_BREAK'
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
// First receipt should have null prev_receipt_hash
|
|
74
|
+
if (receipt.chain.prev_receipt_hash !== null) {
|
|
75
|
+
failed.push({
|
|
76
|
+
index: i,
|
|
77
|
+
receipt_id: receipt.receipt_id,
|
|
78
|
+
error: 'GENESIS_INVALID'
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Verify sequence
|
|
84
|
+
if (receipt.chain.sequence !== i + 1) {
|
|
85
|
+
failed.push({
|
|
86
|
+
index: i,
|
|
87
|
+
receipt_id: receipt.receipt_id,
|
|
88
|
+
error: 'SEQUENCE_GAP'
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
valid: failed.length === 0,
|
|
95
|
+
verified: receipts.length - failed.length,
|
|
96
|
+
failed
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Full verification: receipts + log entries
|
|
102
|
+
*/
|
|
103
|
+
export async function verifyFull(
|
|
104
|
+
receipts: Receipt[],
|
|
105
|
+
logEntries: LogEntry[],
|
|
106
|
+
publicKey: Buffer
|
|
107
|
+
): Promise<{
|
|
108
|
+
receipts: { valid: boolean; verified: number; failed: any[] };
|
|
109
|
+
log: ChainVerificationResult;
|
|
110
|
+
crossCheck: { valid: boolean; mismatches: number[] };
|
|
111
|
+
}> {
|
|
112
|
+
// Verify receipts
|
|
113
|
+
const receiptResult = await verifyReceiptChain(receipts, publicKey);
|
|
114
|
+
|
|
115
|
+
// Verify log
|
|
116
|
+
const logResult = verifyLogChain(logEntries);
|
|
117
|
+
|
|
118
|
+
// Cross-check: each log entry should reference corresponding receipt
|
|
119
|
+
const mismatches: number[] = [];
|
|
120
|
+
for (let i = 0; i < Math.min(receipts.length, logEntries.length); i++) {
|
|
121
|
+
if (logEntries[i].receipt_hash !== receipts[i].receipt_hash) {
|
|
122
|
+
mismatches.push(i);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
receipts: receiptResult,
|
|
128
|
+
log: logResult,
|
|
129
|
+
crossCheck: {
|
|
130
|
+
valid: mismatches.length === 0,
|
|
131
|
+
mismatches
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Verify an anchor exists on-chain
|
|
138
|
+
*/
|
|
139
|
+
export async function verifyAnchor(
|
|
140
|
+
logRoot: string,
|
|
141
|
+
contractAddress: string,
|
|
142
|
+
rpcUrl: string
|
|
143
|
+
): Promise<{
|
|
144
|
+
exists: boolean;
|
|
145
|
+
sequence?: number;
|
|
146
|
+
timestamp?: number;
|
|
147
|
+
error?: string;
|
|
148
|
+
}> {
|
|
149
|
+
try {
|
|
150
|
+
const { ethers } = await import('ethers');
|
|
151
|
+
|
|
152
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
153
|
+
|
|
154
|
+
const abi = [
|
|
155
|
+
'function verify(bytes32 logRoot) view returns (bool exists, uint256 sequence, uint256 timestamp)'
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const contract = new ethers.Contract(contractAddress, abi, provider);
|
|
159
|
+
|
|
160
|
+
const [exists, sequence, timestamp] = await contract.verify(
|
|
161
|
+
'0x' + logRoot
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
exists,
|
|
166
|
+
sequence: Number(sequence),
|
|
167
|
+
timestamp: Number(timestamp)
|
|
168
|
+
};
|
|
169
|
+
} catch (error: any) {
|
|
170
|
+
return {
|
|
171
|
+
exists: false,
|
|
172
|
+
error: error.message
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
package/test/demo.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PQC Receipt SDK - Demonstration Script
|
|
3
|
+
*
|
|
4
|
+
* This script demonstrates the complete flow:
|
|
5
|
+
* 1. Generate ML-DSA-65 key pair
|
|
6
|
+
* 2. Create receipts with PQC signatures
|
|
7
|
+
* 3. Append to tamper-evident log
|
|
8
|
+
* 4. Verify independently
|
|
9
|
+
*
|
|
10
|
+
* Run: node test/demo.js
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
generateKeyPair,
|
|
15
|
+
ReceiptGenerator,
|
|
16
|
+
AppendOnlyLog,
|
|
17
|
+
verifyReceipt,
|
|
18
|
+
verifyReceiptChain,
|
|
19
|
+
ALGORITHM,
|
|
20
|
+
VERSION,
|
|
21
|
+
PUBLIC_KEY_SIZE,
|
|
22
|
+
SECRET_KEY_SIZE,
|
|
23
|
+
SIGNATURE_SIZE
|
|
24
|
+
} from '../src/index.js';
|
|
25
|
+
|
|
26
|
+
console.log(`
|
|
27
|
+
╔═══════════════════════════════════════════════════════════════╗
|
|
28
|
+
║ PQC Receipt SDK Demo - ML-DSA-65 (FIPS 204) ║
|
|
29
|
+
║ Version ${VERSION} ║
|
|
30
|
+
║ Using @noble/post-quantum ║
|
|
31
|
+
╚═══════════════════════════════════════════════════════════════╝
|
|
32
|
+
`);
|
|
33
|
+
|
|
34
|
+
async function runDemo() {
|
|
35
|
+
console.log('═══════════════════════════════════════════════════════════════');
|
|
36
|
+
|
|
37
|
+
// Step 1: Generate key pair
|
|
38
|
+
console.log('\n📐 Step 1: Generating ML-DSA-65 key pair...');
|
|
39
|
+
const startKey = Date.now();
|
|
40
|
+
|
|
41
|
+
const keyPair = await generateKeyPair();
|
|
42
|
+
|
|
43
|
+
console.log(` ✓ Key generated in ${Date.now() - startKey}ms`);
|
|
44
|
+
console.log(` ✓ Public key: ${keyPair.publicKey.length} bytes (expected: ${PUBLIC_KEY_SIZE})`);
|
|
45
|
+
console.log(` ✓ Secret key: ${keyPair.secretKey.length} bytes (expected: ${SECRET_KEY_SIZE})`);
|
|
46
|
+
console.log(` ✓ Key ID: ${keyPair.keyId.slice(0, 32)}...`);
|
|
47
|
+
|
|
48
|
+
// Step 2: Create receipt generator
|
|
49
|
+
console.log('\n📝 Step 2: Creating receipt generator...');
|
|
50
|
+
const generator = new ReceiptGenerator(keyPair);
|
|
51
|
+
console.log(' ✓ Generator initialized');
|
|
52
|
+
|
|
53
|
+
// Step 3: Generate receipts
|
|
54
|
+
console.log('\n🔏 Step 3: Generating PQC-signed receipts...');
|
|
55
|
+
const receipts = [];
|
|
56
|
+
const log = new AppendOnlyLog();
|
|
57
|
+
|
|
58
|
+
const operations = [
|
|
59
|
+
{ type: 'intercept', method: 'POST', endpoint: '/api/v1/verify', req: { user: 'alice' }, res: { verified: true } },
|
|
60
|
+
{ type: 'verify', method: 'GET', endpoint: '/api/v1/status', req: {}, res: { status: 'active' } },
|
|
61
|
+
{ type: 'intercept', method: 'POST', endpoint: '/api/v1/consent', req: { purpose: 'marketing' }, res: { granted: true } },
|
|
62
|
+
{ type: 'revoke', method: 'DELETE', endpoint: '/api/v1/consent/123', req: {}, res: { revoked: true } },
|
|
63
|
+
{ type: 'anchor', method: 'POST', endpoint: '/api/v1/anchor', req: { checkpoint: true }, res: { anchored: true } }
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
for (const op of operations) {
|
|
67
|
+
const startSign = Date.now();
|
|
68
|
+
|
|
69
|
+
const receipt = await generator.generate({
|
|
70
|
+
type: op.type,
|
|
71
|
+
method: op.method,
|
|
72
|
+
endpoint: op.endpoint,
|
|
73
|
+
requestBody: op.req,
|
|
74
|
+
responseBody: op.res,
|
|
75
|
+
actorId: 'demo-user',
|
|
76
|
+
orgId: 'demo-org'
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
receipts.push(receipt);
|
|
80
|
+
log.append(receipt);
|
|
81
|
+
|
|
82
|
+
const sigSize = Buffer.from(receipt.pqc_signature.signature, 'base64').length;
|
|
83
|
+
console.log(` ✓ Receipt #${receipt.chain.sequence}: ${op.type.padEnd(10)} ${op.endpoint.padEnd(25)} (${Date.now() - startSign}ms, sig: ${sigSize} bytes)`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Step 4: Verify receipts individually
|
|
87
|
+
console.log('\n✅ Step 4: Verifying receipts independently...');
|
|
88
|
+
let allValid = true;
|
|
89
|
+
for (const receipt of receipts) {
|
|
90
|
+
const result = await verifyReceipt(receipt, keyPair.publicKey);
|
|
91
|
+
const status = result.valid ? '✓' : '✗';
|
|
92
|
+
console.log(` ${status} Receipt #${receipt.chain.sequence}: ${result.valid ? 'VALID' : result.error}`);
|
|
93
|
+
if (!result.valid) allValid = false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Step 5: Verify chain
|
|
97
|
+
console.log('\n🔗 Step 5: Verifying receipt chain integrity...');
|
|
98
|
+
const chainResult = await verifyReceiptChain(receipts, keyPair.publicKey);
|
|
99
|
+
console.log(` Chain valid: ${chainResult.valid}`);
|
|
100
|
+
console.log(` Verified: ${chainResult.verified}/${receipts.length}`);
|
|
101
|
+
if (chainResult.failed.length > 0) {
|
|
102
|
+
console.log(` Failed: ${JSON.stringify(chainResult.failed)}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Step 6: Verify log
|
|
106
|
+
console.log('\n📋 Step 6: Verifying append-only log...');
|
|
107
|
+
const logResult = log.verify();
|
|
108
|
+
console.log(` Log valid: ${logResult.valid}`);
|
|
109
|
+
console.log(` Entries: ${logResult.length}`);
|
|
110
|
+
console.log(` Log root: ${log.getLogRoot()?.slice(0, 32)}...`);
|
|
111
|
+
if (logResult.breaks.length > 0) {
|
|
112
|
+
console.log(` Breaks: ${JSON.stringify(logResult.breaks)}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Step 7: Tamper detection test
|
|
116
|
+
console.log('\n🔍 Step 7: Testing tamper detection...');
|
|
117
|
+
const tamperedReceipt = JSON.parse(JSON.stringify(receipts[2]));
|
|
118
|
+
tamperedReceipt.operation.response_hash = 'tampered_hash_value';
|
|
119
|
+
|
|
120
|
+
const tamperResult = await verifyReceipt(tamperedReceipt, keyPair.publicKey);
|
|
121
|
+
console.log(` Tampered receipt verification: ${tamperResult.valid ? 'FAILED TO DETECT' : 'DETECTED ✓'}`);
|
|
122
|
+
console.log(` Error returned: ${tamperResult.error}`);
|
|
123
|
+
|
|
124
|
+
// Summary
|
|
125
|
+
const allPassed = allValid && chainResult.valid && logResult.valid && !tamperResult.valid;
|
|
126
|
+
|
|
127
|
+
console.log(`
|
|
128
|
+
╔═══════════════════════════════════════════════════════════════╗
|
|
129
|
+
║ DEMO COMPLETE ║
|
|
130
|
+
╠═══════════════════════════════════════════════════════════════╣
|
|
131
|
+
║ Algorithm: ${ALGORITHM.padEnd(43)}║
|
|
132
|
+
║ Hash: SHA3-256 ║
|
|
133
|
+
║ Receipts: ${receipts.length.toString().padEnd(43)}║
|
|
134
|
+
║ All signatures: ${(allValid ? 'VALID' : 'INVALID').padEnd(43)}║
|
|
135
|
+
║ Chain integrity: ${(chainResult.valid ? 'VERIFIED' : 'BROKEN').padEnd(43)}║
|
|
136
|
+
║ Log integrity: ${(logResult.valid ? 'VERIFIED' : 'BROKEN').padEnd(43)}║
|
|
137
|
+
║ Tamper detection: ${(!tamperResult.valid ? 'WORKING' : 'FAILED').padEnd(43)}║
|
|
138
|
+
╠═══════════════════════════════════════════════════════════════╣
|
|
139
|
+
║ OVERALL: ${(allPassed ? '✓ ALL TESTS PASSED' : '✗ SOME TESTS FAILED').padEnd(43)}║
|
|
140
|
+
╚═══════════════════════════════════════════════════════════════╝
|
|
141
|
+
`);
|
|
142
|
+
|
|
143
|
+
// Output sample receipt
|
|
144
|
+
console.log('\n📄 Sample Receipt (JSON):');
|
|
145
|
+
console.log(JSON.stringify(receipts[0], null, 2));
|
|
146
|
+
|
|
147
|
+
// Output log entry
|
|
148
|
+
console.log('\n📜 Sample Log Entry:');
|
|
149
|
+
console.log(JSON.stringify(log.getEntries()[0], null, 2));
|
|
150
|
+
|
|
151
|
+
return allPassed;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
runDemo()
|
|
155
|
+
.then(passed => process.exit(passed ? 0 : 1))
|
|
156
|
+
.catch(err => {
|
|
157
|
+
console.error('Demo failed:', err);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"resolveJsonModule": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "test", "contracts"]
|
|
20
|
+
}
|