@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/log.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PQC Receipt SDK - Append-Only Log Management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
6
|
+
import { hash } from './crypto.js';
|
|
7
|
+
import type { LogEntry, Receipt, ChainVerificationResult } from './types.js';
|
|
8
|
+
|
|
9
|
+
export class AppendOnlyLog {
|
|
10
|
+
private entries: LogEntry[] = [];
|
|
11
|
+
private sequence: number = 0;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Append a receipt to the log
|
|
15
|
+
*/
|
|
16
|
+
append(receipt: Receipt): LogEntry {
|
|
17
|
+
this.sequence++;
|
|
18
|
+
|
|
19
|
+
const prevEntry = this.entries[this.entries.length - 1];
|
|
20
|
+
const prevEntryHash = prevEntry ? prevEntry.entry_hash : null;
|
|
21
|
+
|
|
22
|
+
const entry: LogEntry = {
|
|
23
|
+
entry_id: uuidv4(),
|
|
24
|
+
sequence: this.sequence,
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
receipt_hash: receipt.receipt_hash,
|
|
27
|
+
prev_entry_hash: prevEntryHash,
|
|
28
|
+
entry_hash: '', // Computed below
|
|
29
|
+
anchor: { type: 'none' }
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Compute entry hash
|
|
33
|
+
entry.entry_hash = this.computeEntryHash(entry);
|
|
34
|
+
|
|
35
|
+
this.entries.push(entry);
|
|
36
|
+
return entry;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Compute hash of a log entry
|
|
41
|
+
*/
|
|
42
|
+
private computeEntryHash(entry: Omit<LogEntry, 'entry_hash'>): string {
|
|
43
|
+
const preimage = [
|
|
44
|
+
entry.sequence.toString(),
|
|
45
|
+
entry.receipt_hash,
|
|
46
|
+
entry.prev_entry_hash || 'GENESIS',
|
|
47
|
+
entry.timestamp
|
|
48
|
+
].join('|');
|
|
49
|
+
|
|
50
|
+
return hash(preimage);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get all entries
|
|
55
|
+
*/
|
|
56
|
+
getEntries(): LogEntry[] {
|
|
57
|
+
return [...this.entries];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get entry by sequence number
|
|
62
|
+
*/
|
|
63
|
+
getEntry(sequence: number): LogEntry | undefined {
|
|
64
|
+
return this.entries.find(e => e.sequence === sequence);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get latest entry
|
|
69
|
+
*/
|
|
70
|
+
getLatest(): LogEntry | undefined {
|
|
71
|
+
return this.entries[this.entries.length - 1];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get current log root (latest entry hash)
|
|
76
|
+
*/
|
|
77
|
+
getLogRoot(): string | null {
|
|
78
|
+
const latest = this.getLatest();
|
|
79
|
+
return latest ? latest.entry_hash : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Verify the integrity of the log chain
|
|
84
|
+
*/
|
|
85
|
+
verify(): ChainVerificationResult {
|
|
86
|
+
const breaks: Array<{ index: number; error: string }> = [];
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < this.entries.length; i++) {
|
|
89
|
+
const entry = this.entries[i];
|
|
90
|
+
|
|
91
|
+
// Verify entry_hash is correct
|
|
92
|
+
const expectedHash = this.computeEntryHash({
|
|
93
|
+
entry_id: entry.entry_id,
|
|
94
|
+
sequence: entry.sequence,
|
|
95
|
+
timestamp: entry.timestamp,
|
|
96
|
+
receipt_hash: entry.receipt_hash,
|
|
97
|
+
prev_entry_hash: entry.prev_entry_hash,
|
|
98
|
+
anchor: entry.anchor
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (entry.entry_hash !== expectedHash) {
|
|
102
|
+
breaks.push({ index: i, error: 'ENTRY_HASH_INVALID' });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Verify chain linkage (except genesis)
|
|
107
|
+
if (i > 0) {
|
|
108
|
+
const prevEntry = this.entries[i - 1];
|
|
109
|
+
if (entry.prev_entry_hash !== prevEntry.entry_hash) {
|
|
110
|
+
breaks.push({ index: i, error: 'CHAIN_BREAK' });
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
// Genesis entry must have null prev_entry_hash
|
|
114
|
+
if (entry.prev_entry_hash !== null) {
|
|
115
|
+
breaks.push({ index: i, error: 'GENESIS_INVALID' });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Verify sequence is monotonic
|
|
120
|
+
if (entry.sequence !== i + 1) {
|
|
121
|
+
breaks.push({ index: i, error: 'SEQUENCE_GAP' });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
valid: breaks.length === 0,
|
|
127
|
+
length: this.entries.length,
|
|
128
|
+
breaks
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Mark an entry as anchored
|
|
134
|
+
*/
|
|
135
|
+
markAnchored(
|
|
136
|
+
sequence: number,
|
|
137
|
+
anchor: { type: 'hardhat' | 'ethereum' | 'external'; tx_hash: string; block_number: number }
|
|
138
|
+
): void {
|
|
139
|
+
const entry = this.entries.find(e => e.sequence === sequence);
|
|
140
|
+
if (entry) {
|
|
141
|
+
entry.anchor = {
|
|
142
|
+
...anchor,
|
|
143
|
+
anchored_at: new Date().toISOString()
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Export log to JSON Lines format
|
|
150
|
+
*/
|
|
151
|
+
exportJSONL(): string {
|
|
152
|
+
return this.entries.map(e => JSON.stringify(e)).join('\n');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Import log from JSON Lines format
|
|
157
|
+
*/
|
|
158
|
+
static fromJSONL(jsonl: string): AppendOnlyLog {
|
|
159
|
+
const log = new AppendOnlyLog();
|
|
160
|
+
const lines = jsonl.trim().split('\n').filter(Boolean);
|
|
161
|
+
|
|
162
|
+
for (const line of lines) {
|
|
163
|
+
const entry = JSON.parse(line) as LogEntry;
|
|
164
|
+
log.entries.push(entry);
|
|
165
|
+
log.sequence = Math.max(log.sequence, entry.sequence);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return log;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Export log as JSON array
|
|
173
|
+
*/
|
|
174
|
+
toJSON(): LogEntry[] {
|
|
175
|
+
return this.entries;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Import log from JSON array
|
|
180
|
+
*/
|
|
181
|
+
static fromJSON(entries: LogEntry[]): AppendOnlyLog {
|
|
182
|
+
const log = new AppendOnlyLog();
|
|
183
|
+
log.entries = [...entries];
|
|
184
|
+
log.sequence = entries.length > 0 ? entries[entries.length - 1].sequence : 0;
|
|
185
|
+
return log;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Standalone function to verify a log chain
|
|
191
|
+
*/
|
|
192
|
+
export function verifyLogChain(entries: LogEntry[]): ChainVerificationResult {
|
|
193
|
+
const log = AppendOnlyLog.fromJSON(entries);
|
|
194
|
+
return log.verify();
|
|
195
|
+
}
|
package/src/receipt.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PQC Receipt SDK - Receipt Generation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
6
|
+
import { hash, canonicalizePayload, signReceipt } from './crypto.js';
|
|
7
|
+
|
|
8
|
+
export class ReceiptGenerator {
|
|
9
|
+
constructor(keyPair) {
|
|
10
|
+
this.keyPair = keyPair;
|
|
11
|
+
this.sequence = 0;
|
|
12
|
+
this.prevReceiptHash = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get current chain state
|
|
17
|
+
*/
|
|
18
|
+
getChainState() {
|
|
19
|
+
return {
|
|
20
|
+
sequence: this.sequence,
|
|
21
|
+
prev_receipt_hash: this.prevReceiptHash
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Set chain state (for recovery/resumption)
|
|
27
|
+
*/
|
|
28
|
+
setChainState(sequence, prevReceiptHash) {
|
|
29
|
+
this.sequence = sequence;
|
|
30
|
+
this.prevReceiptHash = prevReceiptHash;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate a signed PQC receipt
|
|
35
|
+
*/
|
|
36
|
+
async generate(input) {
|
|
37
|
+
// Increment sequence
|
|
38
|
+
this.sequence++;
|
|
39
|
+
|
|
40
|
+
// Hash request and response bodies
|
|
41
|
+
const requestHash = hash(canonicalizePayload(input.requestBody));
|
|
42
|
+
const responseHash = hash(canonicalizePayload(input.responseBody));
|
|
43
|
+
|
|
44
|
+
// Build unsigned receipt
|
|
45
|
+
const unsignedReceipt = {
|
|
46
|
+
version: '1.0',
|
|
47
|
+
receipt_id: uuidv4(),
|
|
48
|
+
timestamp: new Date().toISOString(),
|
|
49
|
+
operation: {
|
|
50
|
+
type: input.type,
|
|
51
|
+
method: input.method,
|
|
52
|
+
endpoint: input.endpoint,
|
|
53
|
+
request_hash: requestHash,
|
|
54
|
+
response_hash: responseHash
|
|
55
|
+
},
|
|
56
|
+
actor: {
|
|
57
|
+
id: input.actorId,
|
|
58
|
+
org_id: input.orgId,
|
|
59
|
+
key_id: this.keyPair.keyId
|
|
60
|
+
},
|
|
61
|
+
chain: {
|
|
62
|
+
sequence: this.sequence,
|
|
63
|
+
prev_receipt_hash: this.prevReceiptHash
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Sign with ML-DSA-65
|
|
68
|
+
const { pqc_signature, receipt_hash } = await signReceipt(
|
|
69
|
+
unsignedReceipt,
|
|
70
|
+
this.keyPair.secretKey,
|
|
71
|
+
this.keyPair.publicKey
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Update chain state
|
|
75
|
+
this.prevReceiptHash = receipt_hash;
|
|
76
|
+
|
|
77
|
+
// Return complete signed receipt
|
|
78
|
+
return {
|
|
79
|
+
...unsignedReceipt,
|
|
80
|
+
pqc_signature,
|
|
81
|
+
receipt_hash
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate a genesis receipt (first in chain)
|
|
87
|
+
*/
|
|
88
|
+
async generateGenesis(orgId, actorId) {
|
|
89
|
+
this.sequence = 0;
|
|
90
|
+
this.prevReceiptHash = null;
|
|
91
|
+
|
|
92
|
+
return this.generate({
|
|
93
|
+
type: 'anchor',
|
|
94
|
+
method: 'GENESIS',
|
|
95
|
+
endpoint: '/chain/genesis',
|
|
96
|
+
requestBody: { chain_initialized: true },
|
|
97
|
+
responseBody: { status: 'genesis_created' },
|
|
98
|
+
actorId,
|
|
99
|
+
orgId
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Standalone function to create a single receipt
|
|
106
|
+
*/
|
|
107
|
+
export async function createReceipt(input, keyPair) {
|
|
108
|
+
const requestHash = hash(canonicalizePayload(input.requestBody));
|
|
109
|
+
const responseHash = hash(canonicalizePayload(input.responseBody));
|
|
110
|
+
|
|
111
|
+
const unsignedReceipt = {
|
|
112
|
+
version: '1.0',
|
|
113
|
+
receipt_id: uuidv4(),
|
|
114
|
+
timestamp: new Date().toISOString(),
|
|
115
|
+
operation: {
|
|
116
|
+
type: input.type,
|
|
117
|
+
method: input.method,
|
|
118
|
+
endpoint: input.endpoint,
|
|
119
|
+
request_hash: requestHash,
|
|
120
|
+
response_hash: responseHash
|
|
121
|
+
},
|
|
122
|
+
actor: {
|
|
123
|
+
id: input.actorId,
|
|
124
|
+
org_id: input.orgId,
|
|
125
|
+
key_id: keyPair.keyId
|
|
126
|
+
},
|
|
127
|
+
chain: input.chain
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const { pqc_signature, receipt_hash } = await signReceipt(
|
|
131
|
+
unsignedReceipt,
|
|
132
|
+
keyPair.secretKey,
|
|
133
|
+
keyPair.publicKey
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
...unsignedReceipt,
|
|
138
|
+
pqc_signature,
|
|
139
|
+
receipt_hash
|
|
140
|
+
};
|
|
141
|
+
}
|
package/src/receipt.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PQC Receipt SDK - Receipt Generation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
6
|
+
import { hash, canonicalizePayload, signReceipt } from './crypto.js';
|
|
7
|
+
import type { Receipt, CreateReceiptInput, KeyPair, Chain } from './types.js';
|
|
8
|
+
|
|
9
|
+
export class ReceiptGenerator {
|
|
10
|
+
private keyPair: KeyPair;
|
|
11
|
+
private sequence: number = 0;
|
|
12
|
+
private prevReceiptHash: string | null = null;
|
|
13
|
+
|
|
14
|
+
constructor(keyPair: KeyPair) {
|
|
15
|
+
this.keyPair = keyPair;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get current chain state
|
|
20
|
+
*/
|
|
21
|
+
getChainState(): Chain {
|
|
22
|
+
return {
|
|
23
|
+
sequence: this.sequence,
|
|
24
|
+
prev_receipt_hash: this.prevReceiptHash
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Set chain state (for recovery/resumption)
|
|
30
|
+
*/
|
|
31
|
+
setChainState(sequence: number, prevReceiptHash: string | null): void {
|
|
32
|
+
this.sequence = sequence;
|
|
33
|
+
this.prevReceiptHash = prevReceiptHash;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate a signed PQC receipt
|
|
38
|
+
*/
|
|
39
|
+
async generate(input: CreateReceiptInput): Promise<Receipt> {
|
|
40
|
+
// Increment sequence
|
|
41
|
+
this.sequence++;
|
|
42
|
+
|
|
43
|
+
// Hash request and response bodies
|
|
44
|
+
const requestHash = hash(canonicalizePayload(input.requestBody));
|
|
45
|
+
const responseHash = hash(canonicalizePayload(input.responseBody));
|
|
46
|
+
|
|
47
|
+
// Build unsigned receipt
|
|
48
|
+
const unsignedReceipt = {
|
|
49
|
+
version: '1.0' as const,
|
|
50
|
+
receipt_id: uuidv4(),
|
|
51
|
+
timestamp: new Date().toISOString(),
|
|
52
|
+
operation: {
|
|
53
|
+
type: input.type,
|
|
54
|
+
method: input.method,
|
|
55
|
+
endpoint: input.endpoint,
|
|
56
|
+
request_hash: requestHash,
|
|
57
|
+
response_hash: responseHash
|
|
58
|
+
},
|
|
59
|
+
actor: {
|
|
60
|
+
id: input.actorId,
|
|
61
|
+
org_id: input.orgId,
|
|
62
|
+
key_id: this.keyPair.keyId
|
|
63
|
+
},
|
|
64
|
+
chain: {
|
|
65
|
+
sequence: this.sequence,
|
|
66
|
+
prev_receipt_hash: this.prevReceiptHash
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Sign with ML-DSA-65
|
|
71
|
+
const { pqc_signature, receipt_hash } = await signReceipt(
|
|
72
|
+
unsignedReceipt,
|
|
73
|
+
this.keyPair.secretKey,
|
|
74
|
+
this.keyPair.publicKey
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Update chain state
|
|
78
|
+
this.prevReceiptHash = receipt_hash;
|
|
79
|
+
|
|
80
|
+
// Return complete signed receipt
|
|
81
|
+
return {
|
|
82
|
+
...unsignedReceipt,
|
|
83
|
+
pqc_signature,
|
|
84
|
+
receipt_hash
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Generate a genesis receipt (first in chain)
|
|
90
|
+
*/
|
|
91
|
+
async generateGenesis(orgId: string, actorId: string): Promise<Receipt> {
|
|
92
|
+
this.sequence = 0;
|
|
93
|
+
this.prevReceiptHash = null;
|
|
94
|
+
|
|
95
|
+
return this.generate({
|
|
96
|
+
type: 'anchor',
|
|
97
|
+
method: 'GENESIS',
|
|
98
|
+
endpoint: '/chain/genesis',
|
|
99
|
+
requestBody: { chain_initialized: true },
|
|
100
|
+
responseBody: { status: 'genesis_created' },
|
|
101
|
+
actorId,
|
|
102
|
+
orgId
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Standalone function to create a single receipt
|
|
109
|
+
* (for stateless use cases)
|
|
110
|
+
*/
|
|
111
|
+
export async function createReceipt(
|
|
112
|
+
input: CreateReceiptInput & { chain: Chain },
|
|
113
|
+
keyPair: KeyPair
|
|
114
|
+
): Promise<Receipt> {
|
|
115
|
+
const requestHash = hash(canonicalizePayload(input.requestBody));
|
|
116
|
+
const responseHash = hash(canonicalizePayload(input.responseBody));
|
|
117
|
+
|
|
118
|
+
const unsignedReceipt = {
|
|
119
|
+
version: '1.0' as const,
|
|
120
|
+
receipt_id: uuidv4(),
|
|
121
|
+
timestamp: new Date().toISOString(),
|
|
122
|
+
operation: {
|
|
123
|
+
type: input.type,
|
|
124
|
+
method: input.method,
|
|
125
|
+
endpoint: input.endpoint,
|
|
126
|
+
request_hash: requestHash,
|
|
127
|
+
response_hash: responseHash
|
|
128
|
+
},
|
|
129
|
+
actor: {
|
|
130
|
+
id: input.actorId,
|
|
131
|
+
org_id: input.orgId,
|
|
132
|
+
key_id: keyPair.keyId
|
|
133
|
+
},
|
|
134
|
+
chain: input.chain
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const { pqc_signature, receipt_hash } = await signReceipt(
|
|
138
|
+
unsignedReceipt,
|
|
139
|
+
keyPair.secretKey,
|
|
140
|
+
keyPair.publicKey
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
...unsignedReceipt,
|
|
145
|
+
pqc_signature,
|
|
146
|
+
receipt_hash
|
|
147
|
+
};
|
|
148
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PQC Receipt SDK - Type Definitions
|
|
3
|
+
* Algorithm: ML-DSA-65 (FIPS 204 / CRYSTALS-Dilithium)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface Operation {
|
|
7
|
+
type: 'intercept' | 'verify' | 'revoke' | 'anchor';
|
|
8
|
+
method: string;
|
|
9
|
+
endpoint: string;
|
|
10
|
+
request_hash: string;
|
|
11
|
+
response_hash: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Actor {
|
|
15
|
+
id: string;
|
|
16
|
+
org_id: string;
|
|
17
|
+
key_id: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Chain {
|
|
21
|
+
sequence: number;
|
|
22
|
+
prev_receipt_hash: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PQCSignature {
|
|
26
|
+
algorithm: 'ML-DSA-65';
|
|
27
|
+
public_key_id: string;
|
|
28
|
+
signature: string; // base64
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Receipt {
|
|
32
|
+
version: '1.0';
|
|
33
|
+
receipt_id: string;
|
|
34
|
+
timestamp: string;
|
|
35
|
+
operation: Operation;
|
|
36
|
+
actor: Actor;
|
|
37
|
+
chain: Chain;
|
|
38
|
+
pqc_signature: PQCSignature;
|
|
39
|
+
receipt_hash: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface LogEntry {
|
|
43
|
+
entry_id: string;
|
|
44
|
+
sequence: number;
|
|
45
|
+
timestamp: string;
|
|
46
|
+
receipt_hash: string;
|
|
47
|
+
prev_entry_hash: string | null;
|
|
48
|
+
entry_hash: string;
|
|
49
|
+
anchor: {
|
|
50
|
+
type: 'none' | 'hardhat' | 'ethereum' | 'external';
|
|
51
|
+
tx_hash?: string;
|
|
52
|
+
block_number?: number;
|
|
53
|
+
anchored_at?: string;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface KeyPair {
|
|
58
|
+
publicKey: Buffer;
|
|
59
|
+
secretKey: Buffer;
|
|
60
|
+
keyId: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface VerificationResult {
|
|
64
|
+
valid: boolean;
|
|
65
|
+
error?: 'KEY_MISMATCH' | 'SIGNATURE_INVALID' | 'HASH_MISMATCH' | 'CHAIN_BREAK' | 'SEQUENCE_GAP';
|
|
66
|
+
details?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ChainVerificationResult {
|
|
70
|
+
valid: boolean;
|
|
71
|
+
length: number;
|
|
72
|
+
breaks: Array<{
|
|
73
|
+
index: number;
|
|
74
|
+
error: string;
|
|
75
|
+
}>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface CreateReceiptInput {
|
|
79
|
+
type: Operation['type'];
|
|
80
|
+
method: string;
|
|
81
|
+
endpoint: string;
|
|
82
|
+
requestBody: unknown;
|
|
83
|
+
responseBody: unknown;
|
|
84
|
+
actorId: string;
|
|
85
|
+
orgId: string;
|
|
86
|
+
}
|
package/src/verifier.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PQC Receipt SDK - Independent Verification
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { verifySignature, hash, canonicalizePayload } from './crypto.js';
|
|
6
|
+
import { verifyLogChain } from './log.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Verify a single receipt
|
|
10
|
+
*/
|
|
11
|
+
export async function verifyReceipt(receipt, publicKey) {
|
|
12
|
+
return verifySignature(receipt, publicKey);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Verify receipt hash integrity (without signature verification)
|
|
17
|
+
*/
|
|
18
|
+
export function verifyReceiptHash(receipt) {
|
|
19
|
+
const receiptCopy = { ...receipt };
|
|
20
|
+
delete receiptCopy.receipt_hash;
|
|
21
|
+
|
|
22
|
+
const expectedHash = hash(canonicalizePayload(receiptCopy));
|
|
23
|
+
|
|
24
|
+
if (receipt.receipt_hash !== expectedHash) {
|
|
25
|
+
return { valid: false, error: 'HASH_MISMATCH' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { valid: true };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Verify a chain of receipts
|
|
33
|
+
*/
|
|
34
|
+
export async function verifyReceiptChain(receipts, publicKey) {
|
|
35
|
+
const failed = [];
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < receipts.length; i++) {
|
|
38
|
+
const receipt = receipts[i];
|
|
39
|
+
|
|
40
|
+
// Verify signature
|
|
41
|
+
const sigResult = await verifySignature(receipt, publicKey);
|
|
42
|
+
if (!sigResult.valid) {
|
|
43
|
+
failed.push({
|
|
44
|
+
index: i,
|
|
45
|
+
receipt_id: receipt.receipt_id,
|
|
46
|
+
error: sigResult.error || 'UNKNOWN'
|
|
47
|
+
});
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Verify chain linkage
|
|
52
|
+
if (i > 0) {
|
|
53
|
+
const prevReceipt = receipts[i - 1];
|
|
54
|
+
if (receipt.chain.prev_receipt_hash !== prevReceipt.receipt_hash) {
|
|
55
|
+
failed.push({
|
|
56
|
+
index: i,
|
|
57
|
+
receipt_id: receipt.receipt_id,
|
|
58
|
+
error: 'CHAIN_BREAK'
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
// First receipt should have null prev_receipt_hash
|
|
63
|
+
if (receipt.chain.prev_receipt_hash !== null) {
|
|
64
|
+
failed.push({
|
|
65
|
+
index: i,
|
|
66
|
+
receipt_id: receipt.receipt_id,
|
|
67
|
+
error: 'GENESIS_INVALID'
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Verify sequence
|
|
73
|
+
if (receipt.chain.sequence !== i + 1) {
|
|
74
|
+
failed.push({
|
|
75
|
+
index: i,
|
|
76
|
+
receipt_id: receipt.receipt_id,
|
|
77
|
+
error: 'SEQUENCE_GAP'
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
valid: failed.length === 0,
|
|
84
|
+
verified: receipts.length - failed.length,
|
|
85
|
+
failed
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Full verification: receipts + log entries
|
|
91
|
+
*/
|
|
92
|
+
export async function verifyFull(receipts, logEntries, publicKey) {
|
|
93
|
+
// Verify receipts
|
|
94
|
+
const receiptResult = await verifyReceiptChain(receipts, publicKey);
|
|
95
|
+
|
|
96
|
+
// Verify log
|
|
97
|
+
const logResult = verifyLogChain(logEntries);
|
|
98
|
+
|
|
99
|
+
// Cross-check: each log entry should reference corresponding receipt
|
|
100
|
+
const mismatches = [];
|
|
101
|
+
for (let i = 0; i < Math.min(receipts.length, logEntries.length); i++) {
|
|
102
|
+
if (logEntries[i].receipt_hash !== receipts[i].receipt_hash) {
|
|
103
|
+
mismatches.push(i);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
receipts: receiptResult,
|
|
109
|
+
log: logResult,
|
|
110
|
+
crossCheck: {
|
|
111
|
+
valid: mismatches.length === 0,
|
|
112
|
+
mismatches
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|