@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/crypto.js ADDED
@@ -0,0 +1,153 @@
1
+ /**
2
+ * PQC Receipt SDK - Cryptographic Operations
3
+ * ML-DSA-65 (FIPS 204) via @noble/post-quantum
4
+ */
5
+
6
+ import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js';
7
+ import { sha3_256 } from '@noble/hashes/sha3';
8
+ import { bytesToHex } from '@noble/hashes/utils';
9
+ import canonicalize from 'canonicalize';
10
+
11
+ /**
12
+ * Generate ML-DSA-65 key pair
13
+ * @returns {Promise<{publicKey: Uint8Array, secretKey: Uint8Array, keyId: string}>}
14
+ */
15
+ export async function generateKeyPair() {
16
+ const seed = crypto.getRandomValues(new Uint8Array(32));
17
+ const keypair = ml_dsa65.keygen(seed);
18
+
19
+ return {
20
+ publicKey: keypair.publicKey,
21
+ secretKey: keypair.secretKey,
22
+ keyId: computeKeyId(keypair.publicKey)
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Compute SHA3-256 fingerprint of public key
28
+ * @param {Uint8Array} publicKey
29
+ * @returns {string}
30
+ */
31
+ export function computeKeyId(publicKey) {
32
+ return bytesToHex(sha3_256(publicKey));
33
+ }
34
+
35
+ /**
36
+ * SHA3-256 hash of any data
37
+ * @param {string|Uint8Array} data
38
+ * @returns {string}
39
+ */
40
+ export function hash(data) {
41
+ if (typeof data === 'string') {
42
+ return bytesToHex(sha3_256(new TextEncoder().encode(data)));
43
+ }
44
+ return bytesToHex(sha3_256(data));
45
+ }
46
+
47
+ /**
48
+ * Canonicalize JSON per RFC 8785 (JCS)
49
+ * @param {unknown} obj
50
+ * @returns {string}
51
+ */
52
+ export function canonicalizePayload(obj) {
53
+ if (obj === null || obj === undefined) {
54
+ return '';
55
+ }
56
+ if (typeof obj === 'string') {
57
+ return obj;
58
+ }
59
+ return canonicalize(obj) || '';
60
+ }
61
+
62
+ /**
63
+ * Build the signing payload from a receipt (excludes signature fields)
64
+ * @param {object} receipt
65
+ * @returns {string}
66
+ */
67
+ export function buildSigningPayload(receipt) {
68
+ return canonicalizePayload({
69
+ version: receipt.version,
70
+ receipt_id: receipt.receipt_id,
71
+ timestamp: receipt.timestamp,
72
+ operation: receipt.operation,
73
+ actor: receipt.actor,
74
+ chain: receipt.chain
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Sign a receipt with ML-DSA-65
80
+ * @param {object} receipt - Unsigned receipt
81
+ * @param {Uint8Array} secretKey
82
+ * @param {Uint8Array} publicKey
83
+ * @returns {Promise<{pqc_signature: object, receipt_hash: string}>}
84
+ */
85
+ export async function signReceipt(receipt, secretKey, publicKey) {
86
+ // Build canonical signing payload
87
+ const signingPayload = buildSigningPayload(receipt);
88
+ const payloadBytes = new TextEncoder().encode(signingPayload);
89
+
90
+ // Sign with ML-DSA-65: sign(message, secretKey)
91
+ const signature = ml_dsa65.sign(payloadBytes, secretKey);
92
+
93
+ const pqc_signature = {
94
+ algorithm: 'ML-DSA-65',
95
+ public_key_id: computeKeyId(publicKey),
96
+ signature: Buffer.from(signature).toString('base64')
97
+ };
98
+
99
+ // Compute final receipt hash (includes signature)
100
+ const fullReceipt = { ...receipt, pqc_signature };
101
+ const receipt_hash = hash(canonicalizePayload(fullReceipt));
102
+
103
+ return { pqc_signature, receipt_hash };
104
+ }
105
+
106
+ /**
107
+ * Verify a receipt's ML-DSA-65 signature
108
+ * @param {object} receipt
109
+ * @param {Uint8Array} publicKey
110
+ * @returns {Promise<{valid: boolean, error?: string}>}
111
+ */
112
+ export async function verifySignature(receipt, publicKey) {
113
+ // Verify key fingerprint
114
+ const expectedKeyId = computeKeyId(publicKey);
115
+ if (receipt.pqc_signature.public_key_id !== expectedKeyId) {
116
+ return { valid: false, error: 'KEY_MISMATCH' };
117
+ }
118
+
119
+ // Rebuild signing payload
120
+ const signingPayload = buildSigningPayload(receipt);
121
+ const payloadBytes = new TextEncoder().encode(signingPayload);
122
+
123
+ // Decode signature from base64
124
+ const signature = new Uint8Array(Buffer.from(receipt.pqc_signature.signature, 'base64'));
125
+
126
+ // Verify ML-DSA signature: verify(signature, message, publicKey)
127
+ try {
128
+ const isValid = ml_dsa65.verify(signature, payloadBytes, publicKey);
129
+ if (!isValid) {
130
+ return { valid: false, error: 'SIGNATURE_INVALID' };
131
+ }
132
+ } catch (e) {
133
+ return { valid: false, error: 'SIGNATURE_INVALID' };
134
+ }
135
+
136
+ // Verify receipt_hash integrity
137
+ const receiptCopy = { ...receipt };
138
+ delete receiptCopy.receipt_hash;
139
+ const expectedHash = hash(canonicalizePayload(receiptCopy));
140
+
141
+ if (receipt.receipt_hash !== expectedHash) {
142
+ return { valid: false, error: 'HASH_MISMATCH' };
143
+ }
144
+
145
+ return { valid: true };
146
+ }
147
+
148
+ // Export algorithm info
149
+ export const ALGORITHM = 'ML-DSA-65';
150
+ export const HASH_ALGORITHM = 'SHA3-256';
151
+ export const PUBLIC_KEY_SIZE = 1952;
152
+ export const SECRET_KEY_SIZE = 4032;
153
+ export const SIGNATURE_SIZE = 3309;
package/src/crypto.ts ADDED
@@ -0,0 +1,183 @@
1
+ /**
2
+ * PQC Receipt SDK - Cryptographic Operations
3
+ * ML-DSA-65 (Dilithium3) via liboqs
4
+ */
5
+
6
+ import { sha3_256 } from 'js-sha3';
7
+ import canonicalize from 'canonicalize';
8
+ import type { KeyPair, Receipt, PQCSignature } from './types.js';
9
+
10
+ // Lazy-loaded liboqs (peer dependency)
11
+ let Dilithium: any = null;
12
+
13
+ async function getDilithium() {
14
+ if (!Dilithium) {
15
+ try {
16
+ const liboqs = await import('@openforge-sh/liboqs-node');
17
+ Dilithium = liboqs.Dilithium;
18
+ } catch (e) {
19
+ throw new Error(
20
+ 'PQC library not found. Install: npm install @openforge-sh/liboqs-node'
21
+ );
22
+ }
23
+ }
24
+ return new Dilithium('Dilithium3'); // ML-DSA-65
25
+ }
26
+
27
+ /**
28
+ * Generate ML-DSA-65 key pair
29
+ */
30
+ export async function generateKeyPair(): Promise<KeyPair> {
31
+ const dilithium = await getDilithium();
32
+ const keypair = await dilithium.generateKeyPair();
33
+
34
+ return {
35
+ publicKey: keypair.publicKey,
36
+ secretKey: keypair.secretKey,
37
+ keyId: computeKeyId(keypair.publicKey)
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Compute SHA3-256 fingerprint of public key
43
+ */
44
+ export function computeKeyId(publicKey: Buffer): string {
45
+ return sha3_256(publicKey);
46
+ }
47
+
48
+ /**
49
+ * SHA3-256 hash of any data
50
+ */
51
+ export function hash(data: string | Buffer): string {
52
+ return sha3_256(data);
53
+ }
54
+
55
+ /**
56
+ * Canonicalize JSON per RFC 8785 (JCS)
57
+ */
58
+ export function canonicalizePayload(obj: unknown): string {
59
+ if (obj === null || obj === undefined) {
60
+ return '';
61
+ }
62
+ if (typeof obj === 'string') {
63
+ return obj;
64
+ }
65
+ return canonicalize(obj) || '';
66
+ }
67
+
68
+ /**
69
+ * Build the signing payload from a receipt (excludes signature fields)
70
+ */
71
+ export function buildSigningPayload(receipt: Omit<Receipt, 'pqc_signature' | 'receipt_hash'>): string {
72
+ return canonicalizePayload({
73
+ version: receipt.version,
74
+ receipt_id: receipt.receipt_id,
75
+ timestamp: receipt.timestamp,
76
+ operation: receipt.operation,
77
+ actor: receipt.actor,
78
+ chain: receipt.chain
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Sign a receipt with ML-DSA-65
84
+ */
85
+ export async function signReceipt(
86
+ receipt: Omit<Receipt, 'pqc_signature' | 'receipt_hash'>,
87
+ secretKey: Buffer,
88
+ publicKey: Buffer
89
+ ): Promise<{ pqc_signature: PQCSignature; receipt_hash: string }> {
90
+ const dilithium = await getDilithium();
91
+
92
+ // Build canonical signing payload
93
+ const signingPayload = buildSigningPayload(receipt);
94
+
95
+ // Sign with ML-DSA-65
96
+ const signature = await dilithium.sign(
97
+ Buffer.from(signingPayload, 'utf8'),
98
+ secretKey
99
+ );
100
+
101
+ const pqc_signature: PQCSignature = {
102
+ algorithm: 'ML-DSA-65',
103
+ public_key_id: computeKeyId(publicKey),
104
+ signature: signature.toString('base64')
105
+ };
106
+
107
+ // Compute final receipt hash (includes signature)
108
+ const fullReceipt = { ...receipt, pqc_signature };
109
+ const receipt_hash = hash(canonicalizePayload(fullReceipt));
110
+
111
+ return { pqc_signature, receipt_hash };
112
+ }
113
+
114
+ /**
115
+ * Verify a receipt's ML-DSA-65 signature
116
+ */
117
+ export async function verifySignature(
118
+ receipt: Receipt,
119
+ publicKey: Buffer
120
+ ): Promise<{ valid: boolean; error?: string }> {
121
+ const dilithium = await getDilithium();
122
+
123
+ // Verify key fingerprint
124
+ const expectedKeyId = computeKeyId(publicKey);
125
+ if (receipt.pqc_signature.public_key_id !== expectedKeyId) {
126
+ return { valid: false, error: 'KEY_MISMATCH' };
127
+ }
128
+
129
+ // Rebuild signing payload
130
+ const signingPayload = buildSigningPayload(receipt);
131
+
132
+ // Verify ML-DSA signature
133
+ const signature = Buffer.from(receipt.pqc_signature.signature, 'base64');
134
+ const isValid = await dilithium.verify(
135
+ Buffer.from(signingPayload, 'utf8'),
136
+ signature,
137
+ publicKey
138
+ );
139
+
140
+ if (!isValid) {
141
+ return { valid: false, error: 'SIGNATURE_INVALID' };
142
+ }
143
+
144
+ // Verify receipt_hash integrity
145
+ const receiptCopy = { ...receipt } as any;
146
+ delete receiptCopy.receipt_hash;
147
+ const expectedHash = hash(canonicalizePayload(receiptCopy));
148
+
149
+ if (receipt.receipt_hash !== expectedHash) {
150
+ return { valid: false, error: 'HASH_MISMATCH' };
151
+ }
152
+
153
+ return { valid: true };
154
+ }
155
+
156
+ /**
157
+ * Fallback: Classical RSA-PSS signing for dual-signature mode
158
+ * (Used when liboqs is unavailable)
159
+ */
160
+ export async function signWithFallback(
161
+ receipt: Omit<Receipt, 'pqc_signature' | 'receipt_hash'>,
162
+ privateKeyPem: string
163
+ ): Promise<{ pqc_signature: PQCSignature; receipt_hash: string }> {
164
+ const crypto = await import('crypto');
165
+
166
+ const signingPayload = buildSigningPayload(receipt);
167
+
168
+ const sign = crypto.createSign('RSA-SHA256');
169
+ sign.update(signingPayload);
170
+ const signature = sign.sign(privateKeyPem);
171
+
172
+ // Mark as fallback in algorithm field
173
+ const pqc_signature: PQCSignature = {
174
+ algorithm: 'ML-DSA-65', // Will be replaced when real PQC available
175
+ public_key_id: hash(privateKeyPem).slice(0, 64),
176
+ signature: signature.toString('base64')
177
+ };
178
+
179
+ const fullReceipt = { ...receipt, pqc_signature };
180
+ const receipt_hash = hash(canonicalizePayload(fullReceipt));
181
+
182
+ return { pqc_signature, receipt_hash };
183
+ }
package/src/index.js ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @finalboss/pqc-receipt-sdk
3
+ *
4
+ * Post-Quantum Cryptographic Receipt Generation SDK
5
+ * Algorithm: ML-DSA-65 (FIPS 204 / CRYSTALS-Dilithium)
6
+ *
7
+ * @example
8
+ * ```javascript
9
+ * import { generateKeyPair, ReceiptGenerator, AppendOnlyLog, verifyReceipt } from '@finalboss/pqc-receipt-sdk';
10
+ *
11
+ * // Generate PQC key pair
12
+ * const keyPair = await generateKeyPair();
13
+ *
14
+ * // Create receipt generator
15
+ * const generator = new ReceiptGenerator(keyPair);
16
+ *
17
+ * // Generate a receipt
18
+ * const receipt = await generator.generate({
19
+ * type: 'intercept',
20
+ * method: 'POST',
21
+ * endpoint: '/api/v1/verify',
22
+ * requestBody: { user_id: 'alice' },
23
+ * responseBody: { verified: true },
24
+ * actorId: 'alice',
25
+ * orgId: 'acme-corp'
26
+ * });
27
+ *
28
+ * // Verify independently
29
+ * const result = await verifyReceipt(receipt, keyPair.publicKey);
30
+ * console.log(result.valid); // true
31
+ * ```
32
+ */
33
+
34
+ // Cryptographic operations
35
+ export {
36
+ generateKeyPair,
37
+ computeKeyId,
38
+ hash,
39
+ canonicalizePayload,
40
+ signReceipt,
41
+ verifySignature,
42
+ buildSigningPayload,
43
+ ALGORITHM,
44
+ HASH_ALGORITHM,
45
+ PUBLIC_KEY_SIZE,
46
+ SECRET_KEY_SIZE,
47
+ SIGNATURE_SIZE
48
+ } from './crypto.js';
49
+
50
+ // Receipt generation
51
+ export { ReceiptGenerator, createReceipt } from './receipt.js';
52
+
53
+ // Append-only log
54
+ export { AppendOnlyLog, verifyLogChain } from './log.js';
55
+
56
+ // Verification
57
+ export {
58
+ verifyReceipt,
59
+ verifyReceiptHash,
60
+ verifyReceiptChain,
61
+ verifyFull
62
+ } from './verifier.js';
63
+
64
+ // Version
65
+ export const VERSION = '1.0.0';
package/src/index.ts ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @finalboss/pqc-receipt-sdk
3
+ *
4
+ * Post-Quantum Cryptographic Receipt Generation SDK
5
+ * Algorithm: ML-DSA-65 (FIPS 204 / CRYSTALS-Dilithium)
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { generateKeyPair, ReceiptGenerator, AppendOnlyLog, verifyReceipt } from '@finalboss/pqc-receipt-sdk';
10
+ *
11
+ * // Generate PQC key pair
12
+ * const keyPair = await generateKeyPair();
13
+ *
14
+ * // Create receipt generator
15
+ * const generator = new ReceiptGenerator(keyPair);
16
+ *
17
+ * // Generate a receipt
18
+ * const receipt = await generator.generate({
19
+ * type: 'intercept',
20
+ * method: 'POST',
21
+ * endpoint: '/api/v1/verify',
22
+ * requestBody: { user_id: 'alice' },
23
+ * responseBody: { verified: true },
24
+ * actorId: 'alice',
25
+ * orgId: 'acme-corp'
26
+ * });
27
+ *
28
+ * // Verify independently
29
+ * const result = await verifyReceipt(receipt, keyPair.publicKey);
30
+ * console.log(result.valid); // true
31
+ * ```
32
+ */
33
+
34
+ // Types
35
+ export type {
36
+ Receipt,
37
+ LogEntry,
38
+ KeyPair,
39
+ Operation,
40
+ Actor,
41
+ Chain,
42
+ PQCSignature,
43
+ VerificationResult,
44
+ ChainVerificationResult,
45
+ CreateReceiptInput
46
+ } from './types.js';
47
+
48
+ // Cryptographic operations
49
+ export {
50
+ generateKeyPair,
51
+ computeKeyId,
52
+ hash,
53
+ canonicalizePayload,
54
+ signReceipt,
55
+ verifySignature,
56
+ buildSigningPayload,
57
+ signWithFallback
58
+ } from './crypto.js';
59
+
60
+ // Receipt generation
61
+ export { ReceiptGenerator, createReceipt } from './receipt.js';
62
+
63
+ // Append-only log
64
+ export { AppendOnlyLog, verifyLogChain } from './log.js';
65
+
66
+ // Verification
67
+ export {
68
+ verifyReceipt,
69
+ verifyReceiptHash,
70
+ verifyReceiptChain,
71
+ verifyFull,
72
+ verifyAnchor
73
+ } from './verifier.js';
74
+
75
+ // Version
76
+ export const VERSION = '1.0.0';
77
+ export const ALGORITHM = 'ML-DSA-65';
78
+ export const HASH_ALGORITHM = 'SHA3-256';
package/src/log.js ADDED
@@ -0,0 +1,193 @@
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
+
8
+ export class AppendOnlyLog {
9
+ constructor() {
10
+ this.entries = [];
11
+ this.sequence = 0;
12
+ }
13
+
14
+ /**
15
+ * Append a receipt to the log
16
+ */
17
+ append(receipt) {
18
+ this.sequence++;
19
+
20
+ const prevEntry = this.entries[this.entries.length - 1];
21
+ const prevEntryHash = prevEntry ? prevEntry.entry_hash : null;
22
+
23
+ const entry = {
24
+ entry_id: uuidv4(),
25
+ sequence: this.sequence,
26
+ timestamp: new Date().toISOString(),
27
+ receipt_hash: receipt.receipt_hash,
28
+ prev_entry_hash: prevEntryHash,
29
+ entry_hash: '', // Computed below
30
+ anchor: { type: 'none' }
31
+ };
32
+
33
+ // Compute entry hash
34
+ entry.entry_hash = this.computeEntryHash(entry);
35
+
36
+ this.entries.push(entry);
37
+ return entry;
38
+ }
39
+
40
+ /**
41
+ * Compute hash of a log entry
42
+ */
43
+ computeEntryHash(entry) {
44
+ const preimage = [
45
+ entry.sequence.toString(),
46
+ entry.receipt_hash,
47
+ entry.prev_entry_hash || 'GENESIS',
48
+ entry.timestamp
49
+ ].join('|');
50
+
51
+ return hash(preimage);
52
+ }
53
+
54
+ /**
55
+ * Get all entries
56
+ */
57
+ getEntries() {
58
+ return [...this.entries];
59
+ }
60
+
61
+ /**
62
+ * Get entry by sequence number
63
+ */
64
+ getEntry(sequence) {
65
+ return this.entries.find(e => e.sequence === sequence);
66
+ }
67
+
68
+ /**
69
+ * Get latest entry
70
+ */
71
+ getLatest() {
72
+ return this.entries[this.entries.length - 1];
73
+ }
74
+
75
+ /**
76
+ * Get current log root (latest entry hash)
77
+ */
78
+ getLogRoot() {
79
+ const latest = this.getLatest();
80
+ return latest ? latest.entry_hash : null;
81
+ }
82
+
83
+ /**
84
+ * Verify the integrity of the log chain
85
+ */
86
+ verify() {
87
+ const breaks = [];
88
+
89
+ for (let i = 0; i < this.entries.length; i++) {
90
+ const entry = this.entries[i];
91
+
92
+ // Verify entry_hash is correct
93
+ const expectedHash = this.computeEntryHash({
94
+ entry_id: entry.entry_id,
95
+ sequence: entry.sequence,
96
+ timestamp: entry.timestamp,
97
+ receipt_hash: entry.receipt_hash,
98
+ prev_entry_hash: entry.prev_entry_hash,
99
+ anchor: entry.anchor
100
+ });
101
+
102
+ if (entry.entry_hash !== expectedHash) {
103
+ breaks.push({ index: i, error: 'ENTRY_HASH_INVALID' });
104
+ continue;
105
+ }
106
+
107
+ // Verify chain linkage (except genesis)
108
+ if (i > 0) {
109
+ const prevEntry = this.entries[i - 1];
110
+ if (entry.prev_entry_hash !== prevEntry.entry_hash) {
111
+ breaks.push({ index: i, error: 'CHAIN_BREAK' });
112
+ }
113
+ } else {
114
+ // Genesis entry must have null prev_entry_hash
115
+ if (entry.prev_entry_hash !== null) {
116
+ breaks.push({ index: i, error: 'GENESIS_INVALID' });
117
+ }
118
+ }
119
+
120
+ // Verify sequence is monotonic
121
+ if (entry.sequence !== i + 1) {
122
+ breaks.push({ index: i, error: 'SEQUENCE_GAP' });
123
+ }
124
+ }
125
+
126
+ return {
127
+ valid: breaks.length === 0,
128
+ length: this.entries.length,
129
+ breaks
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Mark an entry as anchored
135
+ */
136
+ markAnchored(sequence, anchor) {
137
+ const entry = this.entries.find(e => e.sequence === sequence);
138
+ if (entry) {
139
+ entry.anchor = {
140
+ ...anchor,
141
+ anchored_at: new Date().toISOString()
142
+ };
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Export log to JSON Lines format
148
+ */
149
+ exportJSONL() {
150
+ return this.entries.map(e => JSON.stringify(e)).join('\n');
151
+ }
152
+
153
+ /**
154
+ * Import log from JSON Lines format
155
+ */
156
+ static fromJSONL(jsonl) {
157
+ const log = new AppendOnlyLog();
158
+ const lines = jsonl.trim().split('\n').filter(Boolean);
159
+
160
+ for (const line of lines) {
161
+ const entry = JSON.parse(line);
162
+ log.entries.push(entry);
163
+ log.sequence = Math.max(log.sequence, entry.sequence);
164
+ }
165
+
166
+ return log;
167
+ }
168
+
169
+ /**
170
+ * Export log as JSON array
171
+ */
172
+ toJSON() {
173
+ return this.entries;
174
+ }
175
+
176
+ /**
177
+ * Import log from JSON array
178
+ */
179
+ static fromJSON(entries) {
180
+ const log = new AppendOnlyLog();
181
+ log.entries = [...entries];
182
+ log.sequence = entries.length > 0 ? entries[entries.length - 1].sequence : 0;
183
+ return log;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Standalone function to verify a log chain
189
+ */
190
+ export function verifyLogChain(entries) {
191
+ const log = AppendOnlyLog.fromJSON(entries);
192
+ return log.verify();
193
+ }