forked from
smokesignal.events/atproto-plc
Rust and WASM did-method-plc tools and structures
1#!/usr/bin/env node
2
3/**
4 * PLC Directory Audit Log Validator (WASM version)
5 *
6 * This is a JavaScript port of the Rust plc-audit binary that uses WASM
7 * for cryptographic operations. It fetches DID audit logs from plc.directory
8 * and validates each operation cryptographically.
9 */
10
11import { WasmDid, WasmOperation, WasmVerifyingKey } from './pkg/atproto_plc.js';
12import { parseArgs } from 'node:util';
13
14// Parse command-line arguments
15const { values, positionals } = parseArgs({
16 options: {
17 verbose: {
18 type: 'boolean',
19 short: 'v',
20 default: false,
21 },
22 quiet: {
23 type: 'boolean',
24 short: 'q',
25 default: false,
26 },
27 'plc-url': {
28 type: 'string',
29 default: 'https://plc.directory',
30 },
31 },
32 allowPositionals: true,
33});
34
35const args = {
36 did: positionals[0],
37 verbose: values.verbose,
38 quiet: values.quiet,
39 plcUrl: values['plc-url'],
40};
41
42// Validate arguments
43if (!args.did) {
44 console.error('Usage: node plc-audit.js [OPTIONS] <DID>');
45 console.error('');
46 console.error('Arguments:');
47 console.error(' <DID> The DID to audit (e.g., did:plc:ewvi7nxzyoun6zhxrhs64oiz)');
48 console.error('');
49 console.error('Options:');
50 console.error(' -v, --verbose Show verbose output including all operations');
51 console.error(' -q, --quiet Only show summary (no operation details)');
52 console.error(' --plc-url <URL> Custom PLC directory URL (default: https://plc.directory)');
53 process.exit(1);
54}
55
56/**
57 * Fetch audit log from plc.directory
58 */
59async function fetchAuditLog(plcUrl, did) {
60 const url = `${plcUrl}/${did}/log/audit`;
61
62 try {
63 const response = await fetch(url, {
64 headers: {
65 'User-Agent': 'atproto-plc-audit-wasm/0.1.0',
66 },
67 });
68
69 if (!response.ok) {
70 const text = await response.text();
71 throw new Error(`HTTP error: ${response.status} - ${text}`);
72 }
73
74 return await response.json();
75 } catch (error) {
76 throw new Error(`Failed to fetch audit log: ${error.message}`);
77 }
78}
79
80/**
81 * Main validation logic
82 */
83async function main() {
84 try {
85 // Parse and validate the DID
86 let did;
87 try {
88 did = new WasmDid(args.did);
89 } catch (error) {
90 console.error('❌ Error: Invalid DID format:', error.message);
91 console.error(' Expected format: did:plc:<24 lowercase base32 characters>');
92 process.exit(1);
93 }
94
95 if (!args.quiet) {
96 console.log('🔍 Fetching audit log for:', did.did);
97 console.log(' Source:', args.plcUrl);
98 console.log();
99 }
100
101 // Fetch the audit log
102 let auditLog;
103 try {
104 auditLog = await fetchAuditLog(args.plcUrl, did.did);
105 } catch (error) {
106 console.error('❌ Error:', error.message);
107 process.exit(1);
108 }
109
110 if (!auditLog || auditLog.length === 0) {
111 console.error('❌ Error: No operations found in audit log');
112 process.exit(1);
113 }
114
115 // Parse operations
116 const operations = auditLog.map(entry => {
117 const op = WasmOperation.fromJson(JSON.stringify(entry.operation));
118 return {
119 did: entry.did,
120 operation: op,
121 cid: entry.cid,
122 createdAt: entry.createdAt,
123 nullified: entry.nullified || false,
124 };
125 });
126
127 if (!args.quiet) {
128 console.log('📊 Audit Log Summary:');
129 console.log(' Total operations:', auditLog.length);
130 console.log(' Genesis operation:', operations[0].cid);
131 console.log(' Latest operation:', operations[operations.length - 1].cid);
132 console.log();
133 }
134
135 // Display operations if verbose
136 if (args.verbose) {
137 console.log('📋 Operations:');
138 for (let i = 0; i < operations.length; i++) {
139 const entry = operations[i];
140 const status = entry.nullified ? '❌ NULLIFIED' : '✅';
141 console.log(` [${i}] ${status} ${entry.cid} - ${entry.createdAt}`);
142
143 if (entry.operation.isGenesis()) {
144 console.log(' Type: Genesis (creates the DID)');
145 } else {
146 console.log(' Type: Update');
147 }
148
149 const prev = entry.operation.prev();
150 if (prev) {
151 console.log(' Previous:', prev);
152 }
153 }
154 console.log();
155 }
156
157 // Validate the operation chain
158 if (!args.quiet) {
159 console.log('🔐 Validating operation chain...');
160 console.log();
161 }
162
163 // Step 1: Validate chain linkage (prev references)
164 if (args.verbose) {
165 console.log('Step 1: Chain Linkage Validation');
166 console.log('================================');
167 }
168
169 for (let i = 1; i < operations.length; i++) {
170 if (operations[i].nullified) {
171 if (args.verbose) {
172 console.log(` [${i}] ⊘ Skipped (nullified)`);
173 }
174 continue;
175 }
176
177 const prevCid = operations[i - 1].cid;
178 const expectedPrev = operations[i].operation.prev();
179
180 if (args.verbose) {
181 console.log(` [${i}] Checking prev reference...`);
182 console.log(' Expected:', prevCid);
183 }
184
185 if (expectedPrev) {
186 if (args.verbose) {
187 console.log(' Actual: ', expectedPrev);
188 }
189
190 if (expectedPrev !== prevCid) {
191 console.error();
192 console.error(`❌ Validation failed: Chain linkage broken at operation ${i}`);
193 console.error(' Expected prev:', prevCid);
194 console.error(' Actual prev:', expectedPrev);
195 process.exit(1);
196 }
197
198 if (args.verbose) {
199 console.log(' ✅ Match - chain link valid');
200 }
201 } else if (i > 0) {
202 console.error();
203 console.error(`❌ Validation failed: Non-genesis operation ${i} missing prev field`);
204 process.exit(1);
205 }
206 }
207
208 if (args.verbose) {
209 console.log();
210 console.log('✅ Chain linkage validation complete');
211 console.log();
212 }
213
214 // Step 2: Validate cryptographic signatures
215 if (args.verbose) {
216 console.log('Step 2: Cryptographic Signature Validation');
217 console.log('==========================================');
218 }
219
220 let currentRotationKeys = [];
221
222 for (let i = 0; i < operations.length; i++) {
223 const entry = operations[i];
224
225 if (entry.nullified) {
226 if (args.verbose) {
227 console.log(` [${i}] ⊘ Skipped (nullified)`);
228 }
229 continue;
230 }
231
232 // For genesis operation, extract rotation keys
233 if (i === 0) {
234 if (args.verbose) {
235 console.log(` [${i}] Genesis operation - extracting rotation keys`);
236 }
237
238 const rotationKeys = entry.operation.rotationKeys();
239 if (rotationKeys) {
240 currentRotationKeys = rotationKeys;
241
242 if (args.verbose) {
243 console.log(' Rotation keys:', rotationKeys.length);
244 for (let j = 0; j < rotationKeys.length; j++) {
245 console.log(` [${j}] ${rotationKeys[j]}`);
246 }
247 console.log(' ⚠️ Genesis signature cannot be verified (bootstrapping trust)');
248 }
249 }
250 continue;
251 }
252
253 if (args.verbose) {
254 console.log(` [${i}] Validating signature...`);
255 console.log(' CID:', entry.cid);
256 console.log(' Signature:', entry.operation.signature());
257 }
258
259 // Validate signature using current rotation keys
260 if (currentRotationKeys.length > 0) {
261 if (args.verbose) {
262 console.log(' Available rotation keys:', currentRotationKeys.length);
263 for (let j = 0; j < currentRotationKeys.length; j++) {
264 console.log(` [${j}] ${currentRotationKeys[j]}`);
265 }
266 }
267
268 // Parse verifying keys
269 const verifyingKeys = [];
270 for (const keyStr of currentRotationKeys) {
271 try {
272 verifyingKeys.push(WasmVerifyingKey.fromDidKey(keyStr));
273 } catch (error) {
274 console.error(`Warning: Failed to parse rotation key: ${keyStr}`);
275 }
276 }
277
278 if (args.verbose) {
279 console.log(` Parsed verifying keys: ${verifyingKeys.length}/${currentRotationKeys.length}`);
280 }
281
282 // Try to verify with each key and track which one worked
283 try {
284 const keyIndex = entry.operation.verifyWithKeyIndex(verifyingKeys);
285
286 if (args.verbose) {
287 console.log(` ✅ Signature verified with rotation key [${keyIndex}]`);
288 console.log(` ${currentRotationKeys[keyIndex]}`);
289 }
290 } catch (error) {
291 console.error();
292 console.error(`❌ Validation failed: Invalid signature at operation ${i}`);
293 console.error(' Error:', error.message);
294 console.error(' CID:', entry.cid);
295 console.error(` Tried ${verifyingKeys.length} rotation keys, none verified the signature`);
296 process.exit(1);
297 }
298 }
299
300 // Update rotation keys if this operation changes them
301 const newRotationKeys = entry.operation.rotationKeys();
302 if (newRotationKeys) {
303 const keysChanged = JSON.stringify(newRotationKeys) !== JSON.stringify(currentRotationKeys);
304
305 if (keysChanged) {
306 if (args.verbose) {
307 console.log(' 🔄 Rotation keys updated by this operation');
308 console.log(' Old keys:', currentRotationKeys.length);
309 console.log(' New keys:', newRotationKeys.length);
310 for (let j = 0; j < newRotationKeys.length; j++) {
311 console.log(` [${j}] ${newRotationKeys[j]}`);
312 }
313 }
314 currentRotationKeys = newRotationKeys;
315 }
316 }
317 }
318
319 if (args.verbose) {
320 console.log();
321 console.log('✅ Cryptographic signature validation complete');
322 console.log();
323 }
324
325 // Build final state
326 const finalEntry = operations.filter(e => !e.nullified).pop();
327 const finalRotationKeys = finalEntry.operation.rotationKeys();
328
329 if (finalRotationKeys) {
330 if (args.quiet) {
331 console.log('✅ VALID');
332 } else {
333 console.log('✅ Validation successful!');
334 console.log();
335 console.log('📄 Final DID State:');
336 console.log(' Rotation keys:', finalRotationKeys.length);
337 for (let i = 0; i < finalRotationKeys.length; i++) {
338 console.log(` [${i}] ${finalRotationKeys[i]}`);
339 }
340 }
341 } else {
342 console.error('❌ Error: Could not extract final state');
343 process.exit(1);
344 }
345
346 } catch (error) {
347 console.error('❌ Fatal error:', error.message);
348 if (args.verbose) {
349 console.error(error.stack);
350 }
351 process.exit(1);
352 }
353}
354
355// Run the main function
356main().catch(error => {
357 console.error('❌ Unhandled error:', error);
358 process.exit(1);
359});