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.2.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 * Detect if there are fork points in the audit log 82 */ 83function detectForks(operations) { 84 const prevCounts = new Map(); 85 86 for (const entry of operations) { 87 const prev = entry.operation.prev(); 88 if (prev) { 89 prevCounts.set(prev, (prevCounts.get(prev) || 0) + 1); 90 } 91 } 92 93 // If any prev CID is referenced by more than one operation, there's a fork 94 return Array.from(prevCounts.values()).some(count => count > 1); 95} 96 97/** 98 * Build a list of indices that form the canonical chain 99 */ 100function buildCanonicalChainIndices(operations) { 101 // Build a map of prev CID to operations 102 const prevToIndices = new Map(); 103 104 for (let i = 0; i < operations.length; i++) { 105 const prev = operations[i].operation.prev(); 106 if (prev) { 107 if (!prevToIndices.has(prev)) { 108 prevToIndices.set(prev, []); 109 } 110 prevToIndices.get(prev).push(i); 111 } 112 } 113 114 // Start from genesis and follow the canonical chain 115 const canonical = []; 116 117 // Find genesis (first operation) 118 if (operations.length === 0) { 119 return canonical; 120 } 121 122 canonical.push(0); 123 let currentCid = operations[0].cid; 124 125 // Follow the chain, preferring non-nullified operations 126 while (true) { 127 const indices = prevToIndices.get(currentCid); 128 if (!indices || indices.length === 0) { 129 break; 130 } 131 132 // Find the first non-nullified operation 133 const nextIdx = indices.find(idx => !operations[idx].nullified); 134 if (nextIdx !== undefined) { 135 canonical.push(nextIdx); 136 currentCid = operations[nextIdx].cid; 137 } else { 138 // All operations at this point are nullified - try to find any operation 139 if (indices.length > 0) { 140 canonical.push(indices[0]); 141 currentCid = operations[indices[0]].cid; 142 } else { 143 break; 144 } 145 } 146 } 147 148 return canonical; 149} 150 151/** 152 * Display the final state after validation 153 */ 154function displayFinalState(finalEntry, rawEntry) { 155 const rotationKeys = finalEntry.operation.rotationKeys(); 156 157 if (!rotationKeys) { 158 console.error('❌ Error: Could not extract final state'); 159 process.exit(1); 160 } 161 162 if (args.quiet) { 163 console.log('✅ VALID'); 164 } else { 165 console.log('✅ Validation successful!'); 166 console.log(); 167 console.log('📄 Final DID State:'); 168 console.log(' Rotation keys:', rotationKeys.length); 169 for (let i = 0; i < rotationKeys.length; i++) { 170 console.log(` [${i}] ${rotationKeys[i]}`); 171 } 172 console.log(); 173 174 // Extract additional state from the raw operation 175 const op = rawEntry.operation; 176 if (op.verificationMethods) { 177 const vmKeys = Object.keys(op.verificationMethods); 178 console.log(' Verification methods:', vmKeys.length); 179 for (const name of vmKeys) { 180 console.log(` ${name}: ${op.verificationMethods[name]}`); 181 } 182 console.log(); 183 } 184 185 if (op.alsoKnownAs && op.alsoKnownAs.length > 0) { 186 console.log(' Also known as:', op.alsoKnownAs.length); 187 for (const uri of op.alsoKnownAs) { 188 console.log(` - ${uri}`); 189 } 190 console.log(); 191 } 192 193 if (op.services) { 194 const serviceNames = Object.keys(op.services); 195 if (serviceNames.length > 0) { 196 console.log(' Services:', serviceNames.length); 197 for (const name of serviceNames) { 198 const service = op.services[name]; 199 console.log(` ${name}: ${service.endpoint} (${service.type})`); 200 } 201 } 202 } 203 } 204} 205 206/** 207 * Main validation logic 208 */ 209async function main() { 210 try { 211 // Parse and validate the DID 212 let did; 213 try { 214 did = new WasmDid(args.did); 215 } catch (error) { 216 console.error('❌ Error: Invalid DID format:', error.message); 217 console.error(' Expected format: did:plc:<24 lowercase base32 characters>'); 218 process.exit(1); 219 } 220 221 if (!args.quiet) { 222 console.log('🔍 Fetching audit log for:', did.did); 223 console.log(' Source:', args.plcUrl); 224 console.log(); 225 } 226 227 // Fetch the audit log 228 let auditLog; 229 try { 230 auditLog = await fetchAuditLog(args.plcUrl, did.did); 231 } catch (error) { 232 console.error('❌ Error:', error.message); 233 process.exit(1); 234 } 235 236 if (!auditLog || auditLog.length === 0) { 237 console.error('❌ Error: No operations found in audit log'); 238 process.exit(1); 239 } 240 241 // Parse operations 242 const operations = auditLog.map(entry => { 243 const op = WasmOperation.fromJson(JSON.stringify(entry.operation)); 244 return { 245 did: entry.did, 246 operation: op, 247 cid: entry.cid, 248 createdAt: entry.createdAt, 249 nullified: entry.nullified || false, 250 }; 251 }); 252 253 if (!args.quiet) { 254 console.log('📊 Audit Log Summary:'); 255 console.log(' Total operations:', auditLog.length); 256 console.log(' Genesis operation:', operations[0].cid); 257 console.log(' Latest operation:', operations[operations.length - 1].cid); 258 console.log(); 259 } 260 261 // Display operations if verbose 262 if (args.verbose) { 263 console.log('📋 Operations:'); 264 for (let i = 0; i < operations.length; i++) { 265 const entry = operations[i]; 266 const status = entry.nullified ? '❌ NULLIFIED' : '✅'; 267 console.log(` [${i}] ${status} ${entry.cid} - ${entry.createdAt}`); 268 269 if (entry.operation.isGenesis()) { 270 console.log(' Type: Genesis (creates the DID)'); 271 } else { 272 console.log(' Type: Update'); 273 } 274 275 const prev = entry.operation.prev(); 276 if (prev) { 277 console.log(' Previous:', prev); 278 } 279 } 280 console.log(); 281 } 282 283 // Detect forks and build canonical chain 284 if (!args.quiet) { 285 console.log('🔐 Analyzing operation chain...'); 286 console.log(); 287 } 288 289 // Detect fork points and nullified operations 290 const hasForks = detectForks(operations); 291 const hasNullified = operations.some(e => e.nullified); 292 293 if (hasForks || hasNullified) { 294 if (!args.quiet) { 295 if (hasForks) { 296 console.log('⚠️ Fork detected - multiple operations reference the same prev CID'); 297 } 298 if (hasNullified) { 299 console.log('⚠️ Nullified operations detected - will validate canonical chain only'); 300 } 301 console.log(); 302 } 303 304 // Build canonical chain 305 if (args.verbose) { 306 console.log('Step 1: Fork Resolution & Canonical Chain Building'); 307 console.log('==================================================='); 308 } 309 310 const canonicalIndices = buildCanonicalChainIndices(operations); 311 312 if (args.verbose) { 313 console.log(' ✅ Fork resolution complete'); 314 console.log(' ✅ Canonical chain identified'); 315 console.log(); 316 317 console.log('Canonical Chain Operations:'); 318 console.log('==========================='); 319 320 for (const idx of canonicalIndices) { 321 const entry = operations[idx]; 322 console.log(` [${idx}] ✅ ${entry.cid} - ${entry.createdAt}`); 323 } 324 console.log(); 325 326 if (hasNullified) { 327 console.log('Nullified/Rejected Operations:'); 328 console.log('=============================='); 329 for (let i = 0; i < operations.length; i++) { 330 const entry = operations[i]; 331 if (entry.nullified && !canonicalIndices.includes(i)) { 332 console.log(` [${i}] ❌ ${entry.cid} - ${entry.createdAt} (nullified)`); 333 const prev = entry.operation.prev(); 334 if (prev) { 335 console.log(' Referenced:', prev); 336 } 337 } 338 } 339 console.log(); 340 } 341 } 342 343 // Validate signatures along canonical chain 344 if (args.verbose) { 345 console.log('Step 2: Cryptographic Signature Validation'); 346 console.log('=========================================='); 347 } 348 349 let currentRotationKeys = []; 350 351 for (const idx of canonicalIndices) { 352 const entry = operations[idx]; 353 354 // For genesis operation, extract rotation keys 355 if (idx === 0) { 356 if (args.verbose) { 357 console.log(` [${idx}] Genesis operation - extracting rotation keys`); 358 } 359 360 const rotationKeys = entry.operation.rotationKeys(); 361 if (rotationKeys) { 362 currentRotationKeys = rotationKeys; 363 364 if (args.verbose) { 365 console.log(' Rotation keys:', rotationKeys.length); 366 for (let j = 0; j < rotationKeys.length; j++) { 367 console.log(` [${j}] ${rotationKeys[j]}`); 368 } 369 console.log(' ⚠️ Genesis signature cannot be verified (bootstrapping trust)'); 370 } 371 } 372 continue; 373 } 374 375 if (args.verbose) { 376 console.log(` [${idx}] Validating signature...`); 377 console.log(' CID:', entry.cid); 378 console.log(' Signature:', entry.operation.signature()); 379 } 380 381 // Validate signature using current rotation keys 382 if (currentRotationKeys.length > 0) { 383 if (args.verbose) { 384 console.log(' Available rotation keys:', currentRotationKeys.length); 385 for (let j = 0; j < currentRotationKeys.length; j++) { 386 console.log(` [${j}] ${currentRotationKeys[j]}`); 387 } 388 } 389 390 // Parse verifying keys 391 const verifyingKeys = []; 392 for (const keyStr of currentRotationKeys) { 393 try { 394 verifyingKeys.push(WasmVerifyingKey.fromDidKey(keyStr)); 395 } catch (error) { 396 console.error(`Warning: Failed to parse rotation key: ${keyStr}`); 397 } 398 } 399 400 if (args.verbose) { 401 console.log(` Parsed verifying keys: ${verifyingKeys.length}/${currentRotationKeys.length}`); 402 } 403 404 // Try to verify with each key and track which one worked 405 try { 406 const keyIndex = entry.operation.verifyWithKeyIndex(verifyingKeys); 407 408 if (args.verbose) { 409 console.log(` ✅ Signature verified with rotation key [${keyIndex}]`); 410 console.log(` ${currentRotationKeys[keyIndex]}`); 411 } 412 } catch (error) { 413 console.error(); 414 console.error(`❌ Validation failed: Invalid signature at operation ${idx}`); 415 console.error(' Error:', error.message); 416 console.error(' CID:', entry.cid); 417 console.error(` Tried ${verifyingKeys.length} rotation keys, none verified the signature`); 418 process.exit(1); 419 } 420 } 421 422 // Update rotation keys if this operation changes them 423 const newRotationKeys = entry.operation.rotationKeys(); 424 if (newRotationKeys) { 425 const keysChanged = JSON.stringify(newRotationKeys) !== JSON.stringify(currentRotationKeys); 426 427 if (keysChanged) { 428 if (args.verbose) { 429 console.log(' 🔄 Rotation keys updated by this operation'); 430 console.log(' Old keys:', currentRotationKeys.length); 431 console.log(' New keys:', newRotationKeys.length); 432 for (let j = 0; j < newRotationKeys.length; j++) { 433 console.log(` [${j}] ${newRotationKeys[j]}`); 434 } 435 } 436 currentRotationKeys = newRotationKeys; 437 } 438 } 439 } 440 441 if (args.verbose) { 442 console.log(); 443 console.log('✅ Cryptographic signature validation complete'); 444 console.log(); 445 } 446 447 // Build final state 448 const finalIdx = canonicalIndices[canonicalIndices.length - 1]; 449 const finalEntry = operations[finalIdx]; 450 const finalRawEntry = auditLog[finalIdx]; 451 displayFinalState(finalEntry, finalRawEntry); 452 return; 453 } 454 455 // Simple linear chain validation (no forks or nullified operations) 456 if (args.verbose) { 457 console.log('Step 1: Linear Chain Validation'); 458 console.log('================================'); 459 } 460 461 for (let i = 1; i < operations.length; i++) { 462 const prevCid = operations[i - 1].cid; 463 const expectedPrev = operations[i].operation.prev(); 464 465 if (args.verbose) { 466 console.log(` [${i}] Checking prev reference...`); 467 console.log(' Expected:', prevCid); 468 } 469 470 if (expectedPrev) { 471 if (args.verbose) { 472 console.log(' Actual: ', expectedPrev); 473 } 474 475 if (expectedPrev !== prevCid) { 476 console.error(); 477 console.error(`❌ Validation failed: Chain linkage broken at operation ${i}`); 478 console.error(' Expected prev:', prevCid); 479 console.error(' Actual prev:', expectedPrev); 480 process.exit(1); 481 } 482 483 if (args.verbose) { 484 console.log(' ✅ Match - chain link valid'); 485 } 486 } else if (i > 0) { 487 console.error(); 488 console.error(`❌ Validation failed: Non-genesis operation ${i} missing prev field`); 489 process.exit(1); 490 } 491 } 492 493 if (args.verbose) { 494 console.log(); 495 console.log('✅ Chain linkage validation complete'); 496 console.log(); 497 } 498 499 // Step 2: Validate cryptographic signatures 500 if (args.verbose) { 501 console.log('Step 2: Cryptographic Signature Validation'); 502 console.log('=========================================='); 503 } 504 505 let currentRotationKeys = []; 506 507 for (let i = 0; i < operations.length; i++) { 508 const entry = operations[i]; 509 510 if (entry.nullified) { 511 if (args.verbose) { 512 console.log(` [${i}] ⊘ Skipped (nullified)`); 513 } 514 continue; 515 } 516 517 // For genesis operation, extract rotation keys 518 if (i === 0) { 519 if (args.verbose) { 520 console.log(` [${i}] Genesis operation - extracting rotation keys`); 521 } 522 523 const rotationKeys = entry.operation.rotationKeys(); 524 if (rotationKeys) { 525 currentRotationKeys = rotationKeys; 526 527 if (args.verbose) { 528 console.log(' Rotation keys:', rotationKeys.length); 529 for (let j = 0; j < rotationKeys.length; j++) { 530 console.log(` [${j}] ${rotationKeys[j]}`); 531 } 532 console.log(' ⚠️ Genesis signature cannot be verified (bootstrapping trust)'); 533 } 534 } 535 continue; 536 } 537 538 if (args.verbose) { 539 console.log(` [${i}] Validating signature...`); 540 console.log(' CID:', entry.cid); 541 console.log(' Signature:', entry.operation.signature()); 542 } 543 544 // Validate signature using current rotation keys 545 if (currentRotationKeys.length > 0) { 546 if (args.verbose) { 547 console.log(' Available rotation keys:', currentRotationKeys.length); 548 for (let j = 0; j < currentRotationKeys.length; j++) { 549 console.log(` [${j}] ${currentRotationKeys[j]}`); 550 } 551 } 552 553 // Parse verifying keys 554 const verifyingKeys = []; 555 for (const keyStr of currentRotationKeys) { 556 try { 557 verifyingKeys.push(WasmVerifyingKey.fromDidKey(keyStr)); 558 } catch (error) { 559 console.error(`Warning: Failed to parse rotation key: ${keyStr}`); 560 } 561 } 562 563 if (args.verbose) { 564 console.log(` Parsed verifying keys: ${verifyingKeys.length}/${currentRotationKeys.length}`); 565 } 566 567 // Try to verify with each key and track which one worked 568 try { 569 const keyIndex = entry.operation.verifyWithKeyIndex(verifyingKeys); 570 571 if (args.verbose) { 572 console.log(` ✅ Signature verified with rotation key [${keyIndex}]`); 573 console.log(` ${currentRotationKeys[keyIndex]}`); 574 } 575 } catch (error) { 576 console.error(); 577 console.error(`❌ Validation failed: Invalid signature at operation ${i}`); 578 console.error(' Error:', error.message); 579 console.error(' CID:', entry.cid); 580 console.error(` Tried ${verifyingKeys.length} rotation keys, none verified the signature`); 581 process.exit(1); 582 } 583 } 584 585 // Update rotation keys if this operation changes them 586 const newRotationKeys = entry.operation.rotationKeys(); 587 if (newRotationKeys) { 588 const keysChanged = JSON.stringify(newRotationKeys) !== JSON.stringify(currentRotationKeys); 589 590 if (keysChanged) { 591 if (args.verbose) { 592 console.log(' 🔄 Rotation keys updated by this operation'); 593 console.log(' Old keys:', currentRotationKeys.length); 594 console.log(' New keys:', newRotationKeys.length); 595 for (let j = 0; j < newRotationKeys.length; j++) { 596 console.log(` [${j}] ${newRotationKeys[j]}`); 597 } 598 } 599 currentRotationKeys = newRotationKeys; 600 } 601 } 602 } 603 604 if (args.verbose) { 605 console.log(); 606 console.log('✅ Cryptographic signature validation complete'); 607 console.log(); 608 } 609 610 // Build final state 611 const finalEntry = operations[operations.length - 1]; 612 const finalRawEntry = auditLog[auditLog.length - 1]; 613 displayFinalState(finalEntry, finalRawEntry); 614 615 } catch (error) { 616 console.error('❌ Fatal error:', error.message); 617 if (args.verbose) { 618 console.error(error.stack); 619 } 620 process.exit(1); 621 } 622} 623 624// Run the main function 625main().catch(error => { 626 console.error('❌ Unhandled error:', error); 627 process.exit(1); 628});