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});