Rust and WASM did-method-plc tools and structures
1//! PLC Directory Audit Log Validator 2//! 3//! This binary fetches DID audit logs from plc.directory and validates 4//! each operation cryptographically to ensure the chain is valid. 5 6use atproto_plc::{Did, Operation, OperationChainValidator, PlcState}; 7use clap::Parser; 8use reqwest::blocking::Client; 9use serde::Deserialize; 10use std::process; 11 12/// Command-line arguments 13#[derive(Parser, Debug)] 14#[command( 15 name = "plc-audit", 16 about = "Validate DID audit logs from plc.directory", 17 long_about = "Fetches and validates the complete audit log for a did:plc identifier from plc.directory" 18)] 19struct Args { 20 /// The DID to audit (e.g., did:plc:ewvi7nxzyoun6zhxrhs64oiz) 21 #[arg(value_name = "DID")] 22 did: String, 23 24 /// Show verbose output including all operations 25 #[arg(short, long)] 26 verbose: bool, 27 28 /// Only show summary (no operation details) 29 #[arg(short, long)] 30 quiet: bool, 31 32 /// Custom PLC directory URL (default: https://plc.directory) 33 #[arg(long, default_value = "https://plc.directory")] 34 plc_url: String, 35} 36 37/// Audit log response from plc.directory 38#[derive(Debug, Deserialize)] 39struct AuditLogEntry { 40 /// The DID this operation is for 41 #[allow(dead_code)] 42 did: String, 43 44 /// The operation itself 45 operation: Operation, 46 47 /// CID of this operation 48 cid: String, 49 50 /// Timestamp when this operation was created 51 #[serde(rename = "createdAt")] 52 created_at: String, 53 54 /// Nullified flag (if this operation was invalidated) 55 #[serde(default)] 56 nullified: bool, 57} 58 59fn main() { 60 let args = Args::parse(); 61 62 // Parse and validate the DID 63 let did = match Did::parse(&args.did) { 64 Ok(did) => did, 65 Err(e) => { 66 eprintln!("❌ Error: Invalid DID format: {}", e); 67 eprintln!(" Expected format: did:plc:<24 lowercase base32 characters>"); 68 process::exit(1); 69 } 70 }; 71 72 if !args.quiet { 73 println!("🔍 Fetching audit log for: {}", did); 74 println!(" Source: {}", args.plc_url); 75 println!(); 76 } 77 78 // Fetch the audit log 79 let audit_log = match fetch_audit_log(&args.plc_url, &did) { 80 Ok(log) => log, 81 Err(e) => { 82 eprintln!("❌ Error: Failed to fetch audit log: {}", e); 83 process::exit(1); 84 } 85 }; 86 87 if audit_log.is_empty() { 88 eprintln!("❌ Error: No operations found in audit log"); 89 process::exit(1); 90 } 91 92 if !args.quiet { 93 println!("📊 Audit Log Summary:"); 94 println!(" Total operations: {}", audit_log.len()); 95 println!(" Genesis operation: {}", audit_log[0].cid); 96 println!(" Latest operation: {}", audit_log.last().unwrap().cid); 97 println!(); 98 } 99 100 // Display operations if verbose 101 if args.verbose { 102 println!("📋 Operations:"); 103 for (i, entry) in audit_log.iter().enumerate() { 104 let status = if entry.nullified { "❌ NULLIFIED" } else { "" }; 105 println!( 106 " [{}] {} {} - {}", 107 i, 108 status, 109 entry.cid, 110 entry.created_at 111 ); 112 if entry.operation.is_genesis() { 113 println!(" Type: Genesis (creates the DID)"); 114 } else { 115 println!(" Type: Update"); 116 } 117 if let Some(prev) = entry.operation.prev() { 118 println!(" Previous: {}", prev); 119 } 120 } 121 println!(); 122 } 123 124 // Detect forks and build canonical chain 125 if !args.quiet { 126 println!("🔐 Analyzing operation chain..."); 127 println!(); 128 } 129 130 // Detect fork points and nullified operations 131 let has_forks = detect_forks(&audit_log); 132 let has_nullified = audit_log.iter().any(|e| e.nullified); 133 134 if has_forks || has_nullified { 135 if !args.quiet { 136 if has_forks { 137 println!("⚠️ Fork detected - multiple operations reference the same prev CID"); 138 } 139 if has_nullified { 140 println!("⚠️ Nullified operations detected - will validate canonical chain only"); 141 } 142 println!(); 143 } 144 145 // Use fork resolution to build canonical chain 146 if args.verbose { 147 println!("Step 1: Fork Resolution & Canonical Chain Building"); 148 println!("==================================================="); 149 } 150 151 // Build operations and timestamps for fork resolution 152 let operations: Vec<_> = audit_log.iter().map(|e| e.operation.clone()).collect(); 153 let timestamps: Vec<_> = audit_log 154 .iter() 155 .map(|e| { 156 e.created_at 157 .parse::<chrono::DateTime<chrono::Utc>>() 158 .unwrap_or_else(|_| chrono::Utc::now()) 159 }) 160 .collect(); 161 162 // Resolve forks and get canonical chain 163 match OperationChainValidator::validate_chain_with_forks(&operations, &timestamps) { 164 Ok(final_state) => { 165 if args.verbose { 166 println!(" ✅ Fork resolution complete"); 167 println!(" ✅ Canonical chain validated successfully"); 168 println!(); 169 170 // Show which operations are in the canonical chain 171 println!("Canonical Chain Operations:"); 172 println!("==========================="); 173 174 // Build the canonical chain by following non-nullified operations 175 let canonical_indices = build_canonical_chain_indices(&audit_log); 176 177 for idx in &canonical_indices { 178 let entry = &audit_log[*idx]; 179 println!(" [{}] ✅ {} - {}", idx, entry.cid, entry.created_at); 180 } 181 println!(); 182 183 if has_nullified { 184 println!("Nullified/Rejected Operations:"); 185 println!("=============================="); 186 for (i, entry) in audit_log.iter().enumerate() { 187 if entry.nullified && !canonical_indices.contains(&i) { 188 println!(" [{}] ❌ {} - {} (nullified)", i, entry.cid, entry.created_at); 189 if let Some(prev) = entry.operation.prev() { 190 println!(" Referenced: {}", prev); 191 } 192 } 193 } 194 println!(); 195 } 196 } 197 198 // Display final state 199 display_final_state(&final_state, args.quiet); 200 return; 201 } 202 Err(e) => { 203 eprintln!(); 204 eprintln!("❌ Validation failed: {}", e); 205 process::exit(1); 206 } 207 } 208 } 209 210 // Simple linear chain validation (no forks or nullified operations) 211 if args.verbose { 212 println!("Step 1: Linear Chain Validation"); 213 println!("================================"); 214 } 215 216 for i in 1..audit_log.len() { 217 let prev_cid = audit_log[i - 1].cid.clone(); 218 let expected_prev = audit_log[i].operation.prev(); 219 220 if args.verbose { 221 println!(" [{}] Checking prev reference...", i); 222 println!(" Expected: {}", prev_cid); 223 } 224 225 if let Some(actual_prev) = expected_prev { 226 if args.verbose { 227 println!(" Actual: {}", actual_prev); 228 } 229 230 if actual_prev != prev_cid { 231 eprintln!(); 232 eprintln!("❌ Validation failed: Chain linkage broken at operation {}", i); 233 eprintln!(" Expected prev: {}", prev_cid); 234 eprintln!(" Actual prev: {}", actual_prev); 235 process::exit(1); 236 } 237 238 if args.verbose { 239 println!(" ✅ Match - chain link valid"); 240 } 241 } else if i > 0 { 242 eprintln!(); 243 eprintln!("❌ Validation failed: Non-genesis operation {} missing prev field", i); 244 process::exit(1); 245 } 246 } 247 248 if args.verbose { 249 println!(); 250 println!("✅ Chain linkage validation complete"); 251 println!(); 252 } 253 254 // Step 2: Validate cryptographic signatures 255 if args.verbose { 256 println!("Step 2: Cryptographic Signature Validation"); 257 println!("=========================================="); 258 } 259 260 let mut current_rotation_keys: Vec<String> = Vec::new(); 261 262 for (i, entry) in audit_log.iter().enumerate() { 263 if entry.nullified { 264 if args.verbose { 265 println!(" [{}] ⊘ Skipped (nullified)", i); 266 } 267 continue; 268 } 269 270 // For genesis operation, we can't validate signature without rotation keys 271 // But we can extract them and validate subsequent operations 272 if i == 0 { 273 if args.verbose { 274 println!(" [{}] Genesis operation - extracting rotation keys", i); 275 } 276 277 if let Some(rotation_keys) = entry.operation.rotation_keys() { 278 current_rotation_keys = rotation_keys.to_vec(); 279 280 if args.verbose { 281 println!(" Rotation keys: {}", rotation_keys.len()); 282 for (j, key) in rotation_keys.iter().enumerate() { 283 println!(" [{}] {}", j, key); 284 } 285 println!(" ⚠️ Genesis signature cannot be verified (bootstrapping trust)"); 286 } 287 } 288 continue; 289 } 290 291 if args.verbose { 292 println!(" [{}] Validating signature...", i); 293 println!(" CID: {}", entry.cid); 294 println!(" Signature: {}", entry.operation.signature()); 295 } 296 297 // Validate signature using current rotation keys 298 if !current_rotation_keys.is_empty() { 299 use atproto_plc::VerifyingKey; 300 301 if args.verbose { 302 println!(" Available rotation keys: {}", current_rotation_keys.len()); 303 for (j, key) in current_rotation_keys.iter().enumerate() { 304 println!(" [{}] {}", j, key); 305 } 306 } 307 308 let verifying_keys: Vec<VerifyingKey> = current_rotation_keys 309 .iter() 310 .filter_map(|k| VerifyingKey::from_did_key(k).ok()) 311 .collect(); 312 313 if args.verbose { 314 println!(" Parsed verifying keys: {}/{}", verifying_keys.len(), current_rotation_keys.len()); 315 } 316 317 // Try to verify with each key and track which one worked 318 let mut verified = false; 319 let mut verification_key_index = None; 320 321 for (j, key) in verifying_keys.iter().enumerate() { 322 if entry.operation.verify(&[*key]).is_ok() { 323 verified = true; 324 verification_key_index = Some(j); 325 break; 326 } 327 } 328 329 if !verified { 330 // Final attempt with all keys (for comprehensive error) 331 if let Err(e) = entry.operation.verify(&verifying_keys) { 332 eprintln!(); 333 eprintln!("❌ Validation failed: Invalid signature at operation {}", i); 334 eprintln!(" Error: {}", e); 335 eprintln!(" CID: {}", entry.cid); 336 eprintln!(" Tried {} rotation keys, none verified the signature", verifying_keys.len()); 337 process::exit(1); 338 } 339 } 340 341 if args.verbose { 342 if let Some(key_idx) = verification_key_index { 343 println!(" ✅ Signature verified with rotation key [{}]", key_idx); 344 println!(" {}", current_rotation_keys[key_idx]); 345 } else { 346 println!(" ✅ Signature verified"); 347 } 348 } 349 } 350 351 // Update rotation keys if this operation changes them 352 if let Some(new_rotation_keys) = entry.operation.rotation_keys() { 353 if new_rotation_keys != &current_rotation_keys { 354 if args.verbose { 355 println!(" 🔄 Rotation keys updated by this operation"); 356 println!(" Old keys: {}", current_rotation_keys.len()); 357 println!(" New keys: {}", new_rotation_keys.len()); 358 for (j, key) in new_rotation_keys.iter().enumerate() { 359 println!(" [{}] {}", j, key); 360 } 361 } 362 current_rotation_keys = new_rotation_keys.to_vec(); 363 } 364 } 365 } 366 367 if args.verbose { 368 println!(); 369 println!("✅ Cryptographic signature validation complete"); 370 println!(); 371 } 372 373 // Build final state 374 let final_entry = audit_log.last().unwrap(); 375 if let Some(_rotation_keys) = final_entry.operation.rotation_keys() { 376 let final_state = match &final_entry.operation { 377 Operation::PlcOperation { 378 rotation_keys, 379 verification_methods, 380 also_known_as, 381 services, 382 .. 383 } => { 384 PlcState { 385 rotation_keys: rotation_keys.clone(), 386 verification_methods: verification_methods.clone(), 387 also_known_as: also_known_as.clone(), 388 services: services.clone(), 389 } 390 } 391 _ => { 392 PlcState::new() 393 } 394 }; 395 396 display_final_state(&final_state, args.quiet); 397 } else { 398 eprintln!("❌ Error: Could not extract final state"); 399 process::exit(1); 400 } 401} 402 403/// Detect if there are fork points in the audit log 404fn detect_forks(audit_log: &[AuditLogEntry]) -> bool { 405 use std::collections::HashMap; 406 407 let mut prev_counts: HashMap<String, usize> = HashMap::new(); 408 409 for entry in audit_log { 410 if let Some(prev) = entry.operation.prev() { 411 *prev_counts.entry(prev.to_string()).or_insert(0) += 1; 412 } 413 } 414 415 // If any prev CID is referenced by more than one operation, there's a fork 416 prev_counts.values().any(|&count| count > 1) 417} 418 419/// Build a list of indices that form the canonical chain 420fn build_canonical_chain_indices(audit_log: &[AuditLogEntry]) -> Vec<usize> { 421 use std::collections::HashMap; 422 423 // Build a map of prev CID to operations 424 let mut prev_to_indices: HashMap<String, Vec<usize>> = HashMap::new(); 425 426 for (i, entry) in audit_log.iter().enumerate() { 427 if let Some(prev) = entry.operation.prev() { 428 prev_to_indices 429 .entry(prev.to_string()) 430 .or_default() 431 .push(i); 432 } 433 } 434 435 // Start from genesis and follow the canonical chain 436 let mut canonical = Vec::new(); 437 438 // Find genesis (first operation) 439 let genesis = match audit_log.first() { 440 Some(g) => g, 441 None => return canonical, 442 }; 443 444 canonical.push(0); 445 let mut current_cid = genesis.cid.clone(); 446 447 // Follow the chain, preferring non-nullified operations 448 loop { 449 if let Some(indices) = prev_to_indices.get(&current_cid) { 450 // Find the first non-nullified operation 451 if let Some(&next_idx) = indices.iter().find(|&&idx| !audit_log[idx].nullified) { 452 canonical.push(next_idx); 453 current_cid = audit_log[next_idx].cid.clone(); 454 } else { 455 // All operations at this point are nullified - try to find any operation 456 if let Some(&next_idx) = indices.first() { 457 canonical.push(next_idx); 458 current_cid = audit_log[next_idx].cid.clone(); 459 } else { 460 break; 461 } 462 } 463 } else { 464 // No more operations 465 break; 466 } 467 } 468 469 canonical 470} 471 472/// Display the final state after validation 473fn display_final_state(final_state: &PlcState, quiet: bool) { 474 if quiet { 475 println!("✅ VALID"); 476 } else { 477 println!("✅ Validation successful!"); 478 println!(); 479 println!("📄 Final DID State:"); 480 println!(" Rotation keys: {}", final_state.rotation_keys.len()); 481 for (i, key) in final_state.rotation_keys.iter().enumerate() { 482 println!(" [{}] {}", i, key); 483 } 484 println!(); 485 println!(" Verification methods: {}", final_state.verification_methods.len()); 486 for (name, key) in &final_state.verification_methods { 487 println!(" {}: {}", name, key); 488 } 489 println!(); 490 if !final_state.also_known_as.is_empty() { 491 println!(" Also known as: {}", final_state.also_known_as.len()); 492 for uri in &final_state.also_known_as { 493 println!(" - {}", uri); 494 } 495 println!(); 496 } 497 if !final_state.services.is_empty() { 498 println!(" Services: {}", final_state.services.len()); 499 for (name, service) in &final_state.services { 500 println!(" {}: {} ({})", name, service.endpoint, service.service_type); 501 } 502 } 503 } 504} 505 506/// Fetch the audit log for a DID from plc.directory 507fn fetch_audit_log(plc_url: &str, did: &Did) -> Result<Vec<AuditLogEntry>, Box<dyn std::error::Error>> { 508 let url = format!("{}/{}/log/audit", plc_url, did); 509 510 let client = Client::builder() 511 .user_agent("atproto-plc-audit/0.2.0") 512 .timeout(std::time::Duration::from_secs(30)) 513 .build()?; 514 515 let response = client.get(&url).send()?; 516 517 if !response.status().is_success() { 518 return Err(format!( 519 "HTTP error: {} - {}", 520 response.status(), 521 response.text().unwrap_or_default() 522 ) 523 .into()); 524 } 525 526 let audit_log: Vec<AuditLogEntry> = response.json()?; 527 Ok(audit_log) 528}