chainproof 0.1.0 → 0.1.1
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/README.md +233 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# chainproof
|
|
2
|
+
|
|
3
|
+
Hash-chained, Ed25519-signed append-only logs for TypeScript. Tamper-evident audit trails as a reusable primitive.
|
|
4
|
+
|
|
5
|
+
## Why chainproof?
|
|
6
|
+
|
|
7
|
+
Any system that needs a provable, tamper-evident history of events — audit logs, receipt chains, compliance trails, change tracking — needs three things:
|
|
8
|
+
|
|
9
|
+
1. **Hash linking** — each entry commits to the previous one, so deletions and reordering are detectable
|
|
10
|
+
2. **Signatures** — each entry is cryptographically signed, so forgery requires the private key
|
|
11
|
+
3. **Verification** — anyone with the public key can verify the entire chain without trusting the writer
|
|
12
|
+
|
|
13
|
+
chainproof gives you all three in a single library. No database required — just append entries and verify.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install chainproof
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { ChainLog, generateKeyPair } from 'chainproof'
|
|
25
|
+
|
|
26
|
+
// Generate Ed25519 key pair
|
|
27
|
+
const keyPair = generateKeyPair().unwrap()
|
|
28
|
+
|
|
29
|
+
// Create a chain and append entries
|
|
30
|
+
const chain = new ChainLog(keyPair)
|
|
31
|
+
|
|
32
|
+
chain.append({ tool: 'Edit', file: 'server.ts', user: 'alice' })
|
|
33
|
+
chain.append({ tool: 'Bash', command: 'pnpm test', user: 'alice' })
|
|
34
|
+
chain.append({ tool: 'Deploy', target: 'production', user: 'bob' })
|
|
35
|
+
|
|
36
|
+
// Verify the entire chain
|
|
37
|
+
const result = chain.verifyIntegrity()
|
|
38
|
+
console.log(result)
|
|
39
|
+
// { valid: true, chainLength: 3, lastValidSeq: 2, message: 'Chain verified: 3 entries' }
|
|
40
|
+
|
|
41
|
+
// Tamper with an entry — verification catches it
|
|
42
|
+
const tampered = chain.entries.map((e, i) =>
|
|
43
|
+
i === 0 ? { ...e, data: { tool: 'FORGED', file: 'evil.ts', user: 'alice' } } : e
|
|
44
|
+
)
|
|
45
|
+
const check = ChainLog.verify(tampered, keyPair.publicKey)
|
|
46
|
+
console.log(check.valid) // false
|
|
47
|
+
console.log(check.message) // 'Seq 0: invalid signature'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## How It Works
|
|
51
|
+
|
|
52
|
+
Each entry in the chain contains:
|
|
53
|
+
|
|
54
|
+
| Field | Description |
|
|
55
|
+
|-------|-------------|
|
|
56
|
+
| `id` | UUIDv7 (time-ordered, RFC 9562) |
|
|
57
|
+
| `seq` | Monotonic sequence number |
|
|
58
|
+
| `timestamp` | Unix timestamp |
|
|
59
|
+
| `data` | Your payload (generic `T`) |
|
|
60
|
+
| `dataHash` | SHA-256 of canonical JSON of `data` |
|
|
61
|
+
| `prevHash` | SHA-256 of previous entry's canonical bytes |
|
|
62
|
+
| `signature` | Ed25519 signature (base64) |
|
|
63
|
+
|
|
64
|
+
**Genesis**: The first entry links to `SHA-256("chainproof:genesis")`.
|
|
65
|
+
|
|
66
|
+
**Canonical form**: Entries are serialized with sorted keys and the `signature` field excluded before hashing and signing. This ensures deterministic verification regardless of JSON key ordering.
|
|
67
|
+
|
|
68
|
+
**Tamper detection**:
|
|
69
|
+
- Modify any field → signature verification fails
|
|
70
|
+
- Delete an entry → next entry's `prevHash` no longer matches
|
|
71
|
+
- Reorder entries → hash chain breaks
|
|
72
|
+
- Forge an entry → requires the private key
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
### Chain Operations
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { ChainLog, generateKeyPair } from 'chainproof'
|
|
80
|
+
|
|
81
|
+
const keyPair = generateKeyPair().unwrap()
|
|
82
|
+
const chain = new ChainLog<{ action: string; user: string }>(keyPair)
|
|
83
|
+
|
|
84
|
+
// Append returns Result<ChainEntry<T>, ChainError>
|
|
85
|
+
const entry = chain.append({ action: 'create', user: 'alice' })
|
|
86
|
+
if (entry.isOk()) {
|
|
87
|
+
console.log(entry.value.id) // '019d1263-...'
|
|
88
|
+
console.log(entry.value.seq) // 0
|
|
89
|
+
console.log(entry.value.signature) // 'base64...'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Access entries
|
|
93
|
+
console.log(chain.length) // 1
|
|
94
|
+
console.log(chain.entries) // readonly ChainEntry<T>[]
|
|
95
|
+
console.log(chain.lastHash) // SHA-256 of last entry
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Verification
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// Verify using the chain's own key pair
|
|
102
|
+
const result = chain.verifyIntegrity()
|
|
103
|
+
|
|
104
|
+
// Or verify externally with just the public key
|
|
105
|
+
const result = ChainLog.verify(entries, publicKey)
|
|
106
|
+
|
|
107
|
+
// Result shape
|
|
108
|
+
// { valid: boolean, chainLength: number, lastValidSeq: number, message: string }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### JSONL Serialization
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// Serialize to JSONL (one JSON entry per line)
|
|
115
|
+
const jsonl = chain.toJsonl()
|
|
116
|
+
|
|
117
|
+
// Restore from JSONL
|
|
118
|
+
const restored = ChainLog.fromJsonl(jsonl, keyPair)
|
|
119
|
+
if (restored.isOk()) {
|
|
120
|
+
console.log(restored.value.length) // same as original
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### File Storage
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { appendToFile, readFromFile, saveChainToFile, loadChainFromFile } from 'chainproof'
|
|
128
|
+
|
|
129
|
+
// Append entries one at a time (ideal for live logging)
|
|
130
|
+
const entry = chain.append({ action: 'deploy' }).unwrap()
|
|
131
|
+
await appendToFile('/var/log/audit.jsonl', entry)
|
|
132
|
+
|
|
133
|
+
// Read entries from file
|
|
134
|
+
const entries = await readFromFile('/var/log/audit.jsonl')
|
|
135
|
+
|
|
136
|
+
// Save/load entire chain
|
|
137
|
+
await saveChainToFile('/var/log/audit.jsonl', chain)
|
|
138
|
+
const loaded = await loadChainFromFile('/var/log/audit.jsonl', keyPair)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Key Management
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import {
|
|
145
|
+
generateKeyPair,
|
|
146
|
+
saveKeyPair,
|
|
147
|
+
loadKeyPair,
|
|
148
|
+
exportPublicKey,
|
|
149
|
+
importPublicKey
|
|
150
|
+
} from 'chainproof'
|
|
151
|
+
|
|
152
|
+
// Generate and save keys (private key gets 0o600 permissions)
|
|
153
|
+
const keyPair = generateKeyPair().unwrap()
|
|
154
|
+
await saveKeyPair(keyPair, '/etc/myapp/keys')
|
|
155
|
+
// Creates: /etc/myapp/keys/chain.key (private, 0o600)
|
|
156
|
+
// /etc/myapp/keys/chain.pub (public, 0o644)
|
|
157
|
+
|
|
158
|
+
// Load keys from disk
|
|
159
|
+
const loaded = await loadKeyPair('/etc/myapp/keys')
|
|
160
|
+
|
|
161
|
+
// Export public key for external verifiers
|
|
162
|
+
const pem = exportPublicKey(keyPair.publicKey)
|
|
163
|
+
// Share this PEM — anyone can verify the chain without the private key
|
|
164
|
+
|
|
165
|
+
// Import a public key for verification
|
|
166
|
+
const pubKey = importPublicKey(pem).unwrap()
|
|
167
|
+
const result = ChainLog.verify(entries, pubKey)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Low-Level API
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { sha256, sign, verify, canonicalJson, createEntry, entryHash, genesisHash, uuid7 } from 'chainproof'
|
|
174
|
+
|
|
175
|
+
// SHA-256 hashing
|
|
176
|
+
sha256('hello') // '2cf24dba...'
|
|
177
|
+
|
|
178
|
+
// Canonical JSON (sorted keys, deterministic)
|
|
179
|
+
canonicalJson({ z: 1, a: 2 }) // '{"a":2,"z":1}'
|
|
180
|
+
|
|
181
|
+
// Ed25519 sign/verify
|
|
182
|
+
const sig = sign(keyPair.privateKey, 'data').unwrap()
|
|
183
|
+
const valid = verify(keyPair.publicKey, 'data', sig).unwrap() // true
|
|
184
|
+
|
|
185
|
+
// UUIDv7 (time-ordered)
|
|
186
|
+
uuid7() // '019d1263-7a2b-7c58-...'
|
|
187
|
+
|
|
188
|
+
// Manual entry creation
|
|
189
|
+
const entry = createEntry({ action: 'test' }, 0, genesisHash(), keyPair)
|
|
190
|
+
const hash = entryHash(entry.unwrap())
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Error Handling
|
|
194
|
+
|
|
195
|
+
All fallible operations return `Result<T, ChainError>` or `ResultAsync<T, ChainError>` from [@valencets/resultkit](https://github.com/valencets/resultkit).
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
interface ChainError {
|
|
199
|
+
readonly code: ChainErrorCode
|
|
200
|
+
readonly message: string
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Error codes: INVALID_KEY, SIGN_FAILED, VERIFY_FAILED, CHAIN_BROKEN, IO_FAILED, PARSE_FAILED
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Cryptographic Primitives
|
|
207
|
+
|
|
208
|
+
| Primitive | Purpose | Implementation |
|
|
209
|
+
|-----------|---------|----------------|
|
|
210
|
+
| **SHA-256** | Hash chain links, data hashing | `node:crypto` (built-in) |
|
|
211
|
+
| **Ed25519** | Entry signing and verification | `node:crypto` (built-in) |
|
|
212
|
+
| **UUIDv7** | Time-ordered entry IDs | Custom (RFC 9562) |
|
|
213
|
+
| **Base64** | Signature encoding | Built-in |
|
|
214
|
+
|
|
215
|
+
No native addons. No npm crypto dependencies. Just `node:crypto`.
|
|
216
|
+
|
|
217
|
+
## Design Principles
|
|
218
|
+
|
|
219
|
+
1. **Append-only** — entries are never modified or deleted
|
|
220
|
+
2. **Externally verifiable** — anyone with the public key can verify (no shared secret)
|
|
221
|
+
3. **Deterministic** — canonical JSON serialization ensures consistent hashing
|
|
222
|
+
4. **Minimal** — one runtime dependency (`@valencets/resultkit`)
|
|
223
|
+
5. **Generic** — `ChainLog<T>` works with any serializable payload type
|
|
224
|
+
|
|
225
|
+
## Requirements
|
|
226
|
+
|
|
227
|
+
- Node.js >= 22
|
|
228
|
+
- TypeScript >= 5.9
|
|
229
|
+
- ESM only (`"type": "module"`)
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT
|
package/package.json
CHANGED