forked from
smokesignal.events/atproto-plc
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}