@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/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
+ }
@@ -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
+ }