Rust and WASM did-method-plc tools and structures
1//! Validation logic for operations and operation chains 2 3use crate::crypto::VerifyingKey; 4use crate::document::{PlcState, MAX_VERIFICATION_METHODS}; 5use crate::error::{PlcError, Result}; 6use crate::operations::Operation; 7use chrono::{DateTime, Duration, Utc}; 8 9/// Recovery window duration (72 hours) 10const RECOVERY_WINDOW_HOURS: i64 = 72; 11 12/// Operation chain validator 13pub struct OperationChainValidator; 14 15impl OperationChainValidator { 16 /// Validate a complete operation chain and return the final state 17 /// 18 /// # Errors 19 /// 20 /// Returns errors if: 21 /// - Chain is empty 22 /// - First operation is not genesis 23 /// - Any operation has invalid prev reference 24 /// - Any signature is invalid 25 /// - Any operation violates constraints 26 pub fn validate_chain(operations: &[Operation]) -> Result<PlcState> { 27 if operations.is_empty() { 28 return Err(PlcError::EmptyChain); 29 } 30 31 // First operation must be genesis 32 if !operations[0].is_genesis() { 33 return Err(PlcError::FirstOperationNotGenesis); 34 } 35 36 let mut current_state = PlcState::new(); 37 let mut prev_cid: Option<String> = None; 38 39 for (i, operation) in operations.iter().enumerate() { 40 // Verify prev field matches expected CID 41 if i == 0 { 42 // Genesis operation must have prev = None 43 if operation.prev().is_some() { 44 return Err(PlcError::InvalidPrev( 45 "Genesis operation must have prev = null".to_string(), 46 )); 47 } 48 } else { 49 // Non-genesis operations must reference previous CID 50 let expected_prev = prev_cid.as_ref().ok_or_else(|| { 51 PlcError::ChainValidationFailed("Missing previous CID".to_string()) 52 })?; 53 54 let actual_prev = operation.prev().ok_or_else(|| { 55 PlcError::InvalidPrev("Non-genesis operation must have prev field".to_string()) 56 })?; 57 58 if actual_prev != expected_prev { 59 return Err(PlcError::InvalidPrev(format!( 60 "Expected prev = {}, got {}", 61 expected_prev, actual_prev 62 ))); 63 } 64 } 65 66 // Verify signature using current rotation keys 67 if !current_state.rotation_keys.is_empty() { 68 let verifying_keys: Result<Vec<VerifyingKey>> = current_state 69 .rotation_keys 70 .iter() 71 .map(|k| VerifyingKey::from_did_key(k)) 72 .collect(); 73 74 let verifying_keys = verifying_keys?; 75 operation.verify(&verifying_keys)?; 76 } else if i > 0 { 77 // After genesis, we must have rotation keys 78 return Err(PlcError::InvalidRotationKeys( 79 "No rotation keys available for verification".to_string(), 80 )); 81 } 82 83 // Apply operation to state 84 match operation { 85 Operation::PlcOperation { 86 rotation_keys, 87 verification_methods, 88 also_known_as, 89 services, 90 .. 91 } => { 92 current_state.rotation_keys = rotation_keys.clone(); 93 current_state.verification_methods = verification_methods.clone(); 94 current_state.also_known_as = also_known_as.clone(); 95 current_state.services = services.clone(); 96 97 // Validate the state 98 current_state.validate()?; 99 } 100 Operation::PlcTombstone { .. } => { 101 // Tombstone marks the DID as deleted 102 // Clear all state 103 current_state = PlcState::new(); 104 } 105 Operation::LegacyCreate { .. } => { 106 // Legacy create format - convert to modern format 107 // This is for backwards compatibility 108 return Err(PlcError::InvalidOperationType( 109 "Legacy create operations not fully supported".to_string(), 110 )); 111 } 112 } 113 114 // Update prev CID for next iteration 115 prev_cid = Some(operation.cid()?); 116 } 117 118 Ok(current_state) 119 } 120 121 /// Validate a chain with fork resolution 122 /// 123 /// This handles the recovery mechanism where operations signed by higher-priority 124 /// rotation keys can invalidate later operations if submitted within 72 hours. 125 pub fn validate_chain_with_forks( 126 operations: &[Operation], 127 timestamps: &[DateTime<Utc>], 128 ) -> Result<PlcState> { 129 if operations.len() != timestamps.len() { 130 return Err(PlcError::ChainValidationFailed( 131 "Operations and timestamps length mismatch".to_string(), 132 )); 133 } 134 135 // For now, we do basic validation without fork resolution 136 // Full fork resolution would require tracking all possible forks 137 // and selecting the canonical chain based on rotation key priority 138 Self::validate_chain(operations) 139 } 140 141 /// Check if an operation is within the recovery window relative to another operation 142 /// 143 /// Returns true if the time difference is less than 72 hours 144 pub fn is_within_recovery_window( 145 fork_timestamp: DateTime<Utc>, 146 current_timestamp: DateTime<Utc>, 147 ) -> bool { 148 let diff = current_timestamp - fork_timestamp; 149 diff < Duration::hours(RECOVERY_WINDOW_HOURS) && diff >= Duration::zero() 150 } 151} 152 153/// Validate rotation keys 154/// 155/// # Errors 156/// 157/// Returns errors if: 158/// - Not 1-5 keys 159/// - Contains duplicates 160/// - Invalid did:key format 161/// - Unsupported key type 162pub fn validate_rotation_keys(keys: &[String]) -> Result<()> { 163 if keys.is_empty() { 164 return Err(PlcError::InvalidRotationKeys( 165 "At least one rotation key is required".to_string(), 166 )); 167 } 168 169 if keys.len() > 5 { 170 return Err(PlcError::TooManyEntries { 171 field: "rotation_keys".to_string(), 172 max: 5, 173 actual: keys.len(), 174 }); 175 } 176 177 // Check for duplicates 178 let mut seen = std::collections::HashSet::new(); 179 for key in keys { 180 if !seen.insert(key) { 181 return Err(PlcError::DuplicateEntry { 182 field: "rotation_keys".to_string(), 183 value: key.clone(), 184 }); 185 } 186 187 // Validate format 188 if !key.starts_with("did:key:") { 189 return Err(PlcError::InvalidRotationKeys(format!( 190 "Rotation key must be in did:key format: {}", 191 key 192 ))); 193 } 194 195 // Try to parse to ensure it's valid 196 VerifyingKey::from_did_key(key)?; 197 } 198 199 Ok(()) 200} 201 202/// Validate verification methods 203/// 204/// # Errors 205/// 206/// Returns errors if: 207/// - More than 10 methods 208/// - Invalid did:key format 209pub fn validate_verification_methods( 210 methods: &std::collections::HashMap<String, String>, 211) -> Result<()> { 212 if methods.len() > MAX_VERIFICATION_METHODS { 213 return Err(PlcError::TooManyEntries { 214 field: "verification_methods".to_string(), 215 max: MAX_VERIFICATION_METHODS, 216 actual: methods.len(), 217 }); 218 } 219 220 for (name, key) in methods { 221 if !key.starts_with("did:key:") { 222 return Err(PlcError::InvalidVerificationMethods(format!( 223 "Verification method '{}' must be in did:key format: {}", 224 name, key 225 ))); 226 } 227 228 // Try to parse to ensure it's valid 229 VerifyingKey::from_did_key(key)?; 230 } 231 232 Ok(()) 233} 234 235/// Validate also-known-as URIs 236/// 237/// # Errors 238/// 239/// Returns errors if any URI is invalid 240pub fn validate_also_known_as(uris: &[String]) -> Result<()> { 241 for uri in uris { 242 if uri.is_empty() { 243 return Err(PlcError::InvalidAlsoKnownAs( 244 "URI cannot be empty".to_string(), 245 )); 246 } 247 248 // Basic URI validation - should start with a scheme 249 if !uri.contains(':') { 250 return Err(PlcError::InvalidAlsoKnownAs(format!( 251 "URI must contain a scheme: {}", 252 uri 253 ))); 254 } 255 } 256 257 Ok(()) 258} 259 260/// Validate service endpoints 261/// 262/// # Errors 263/// 264/// Returns errors if any service is invalid 265pub fn validate_services( 266 services: &std::collections::HashMap<String, crate::document::ServiceEndpoint>, 267) -> Result<()> { 268 for (name, service) in services { 269 if name.is_empty() { 270 return Err(PlcError::InvalidService( 271 "Service name cannot be empty".to_string(), 272 )); 273 } 274 275 service.validate()?; 276 } 277 278 Ok(()) 279} 280 281#[cfg(test)] 282mod tests { 283 use super::*; 284 use crate::crypto::SigningKey; 285 use crate::document::ServiceEndpoint; 286 use std::collections::HashMap; 287 288 #[test] 289 fn test_validate_rotation_keys() { 290 let key1 = SigningKey::generate_p256(); 291 let key2 = SigningKey::generate_k256(); 292 293 let keys = vec![key1.to_did_key(), key2.to_did_key()]; 294 assert!(validate_rotation_keys(&keys).is_ok()); 295 296 // Empty keys 297 assert!(validate_rotation_keys(&[]).is_err()); 298 299 // Too many keys 300 let many_keys: Vec<String> = (0..6).map(|_| SigningKey::generate_p256().to_did_key()).collect(); 301 assert!(validate_rotation_keys(&many_keys).is_err()); 302 303 // Duplicate keys 304 let dup_key = key1.to_did_key(); 305 let dup_keys = vec![dup_key.clone(), dup_key]; 306 assert!(validate_rotation_keys(&dup_keys).is_err()); 307 } 308 309 #[test] 310 fn test_validate_verification_methods() { 311 let mut methods = HashMap::new(); 312 let key = SigningKey::generate_p256(); 313 methods.insert("atproto".to_string(), key.to_did_key()); 314 315 assert!(validate_verification_methods(&methods).is_ok()); 316 317 // Too many methods 318 let mut many_methods = HashMap::new(); 319 for i in 0..11 { 320 let key = SigningKey::generate_p256(); 321 many_methods.insert(format!("key{}", i), key.to_did_key()); 322 } 323 assert!(validate_verification_methods(&many_methods).is_err()); 324 } 325 326 #[test] 327 fn test_validate_also_known_as() { 328 let uris = vec![ 329 "at://alice.example.com".to_string(), 330 "https://example.com".to_string(), 331 ]; 332 assert!(validate_also_known_as(&uris).is_ok()); 333 334 // Empty URI 335 assert!(validate_also_known_as(&[String::new()]).is_err()); 336 337 // Invalid URI (no scheme) 338 assert!(validate_also_known_as(&["not-a-uri".to_string()]).is_err()); 339 } 340 341 #[test] 342 fn test_recovery_window() { 343 let base = Utc::now(); 344 let within = base + Duration::hours(24); 345 let outside = base + Duration::hours(100); 346 347 assert!(OperationChainValidator::is_within_recovery_window(base, within)); 348 assert!(!OperationChainValidator::is_within_recovery_window(base, outside)); 349 } 350 351 #[test] 352 fn test_validate_chain_genesis() { 353 let key = SigningKey::generate_p256(); 354 let did_key = key.to_did_key(); 355 356 let unsigned = Operation::new_genesis( 357 vec![did_key], 358 HashMap::new(), 359 vec![], 360 HashMap::new(), 361 ); 362 363 let signed = unsigned.sign(&key).unwrap(); 364 365 // Single genesis operation should validate 366 let state = OperationChainValidator::validate_chain(&[signed]).unwrap(); 367 assert_eq!(state.rotation_keys.len(), 1); 368 } 369 370 #[test] 371 fn test_validate_chain_empty() { 372 assert!(OperationChainValidator::validate_chain(&[]).is_err()); 373 } 374}