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