Rust and WASM did-method-plc tools and structures
at main 11 kB view raw
1//! DID document structures and parsing 2 3use crate::did::Did; 4use crate::error::{PlcError, Result}; 5use serde::{Deserialize, Serialize}; 6use std::collections::HashMap; 7 8/// Maximum number of verification methods allowed 9pub const MAX_VERIFICATION_METHODS: usize = 10; 10 11/// Internal PLC state format 12/// 13/// This represents the internal state of a did:plc document as stored 14/// in the PLC directory. 15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 16pub struct PlcState { 17 /// Rotation keys (1-5 did:key strings) 18 #[serde(rename = "rotationKeys")] 19 pub rotation_keys: Vec<String>, 20 21 /// Verification methods (max 10 entries) 22 #[serde(rename = "verificationMethods")] 23 pub verification_methods: HashMap<String, String>, 24 25 /// Also-known-as URIs 26 #[serde(rename = "alsoKnownAs")] 27 pub also_known_as: Vec<String>, 28 29 /// Service endpoints 30 pub services: HashMap<String, ServiceEndpoint>, 31} 32 33impl PlcState { 34 /// Create a new empty PLC state 35 pub fn new() -> Self { 36 Self { 37 rotation_keys: Vec::new(), 38 verification_methods: HashMap::new(), 39 also_known_as: Vec::new(), 40 services: HashMap::new(), 41 } 42 } 43 44 /// Validate this PLC state according to the specification 45 /// 46 /// # Errors 47 /// 48 /// Returns errors if: 49 /// - Rotation keys count is not 1-5 50 /// - Rotation keys contain duplicates 51 /// - Verification methods exceed 10 entries 52 pub fn validate(&self) -> Result<()> { 53 // Validate rotation keys (1-5 required, no duplicates) 54 if self.rotation_keys.is_empty() { 55 return Err(PlcError::InvalidRotationKeys( 56 "At least one rotation key is required".to_string(), 57 )); 58 } 59 60 if self.rotation_keys.len() > 5 { 61 return Err(PlcError::TooManyEntries { 62 field: "rotation_keys".to_string(), 63 max: 5, 64 actual: self.rotation_keys.len(), 65 }); 66 } 67 68 // Check for duplicate rotation keys 69 let mut seen = std::collections::HashSet::new(); 70 for key in &self.rotation_keys { 71 if !seen.insert(key) { 72 return Err(PlcError::DuplicateEntry { 73 field: "rotation_keys".to_string(), 74 value: key.clone(), 75 }); 76 } 77 } 78 79 // Validate all rotation keys are valid did:key format 80 for key in &self.rotation_keys { 81 if !key.starts_with("did:key:") { 82 return Err(PlcError::InvalidRotationKeys(format!( 83 "Rotation key must be in did:key format: {}", 84 key 85 ))); 86 } 87 } 88 89 // Validate verification methods (max 10) 90 if self.verification_methods.len() > MAX_VERIFICATION_METHODS { 91 return Err(PlcError::TooManyEntries { 92 field: "verification_methods".to_string(), 93 max: MAX_VERIFICATION_METHODS, 94 actual: self.verification_methods.len(), 95 }); 96 } 97 98 // Validate all verification methods are valid did:key format 99 for (name, key) in &self.verification_methods { 100 if !key.starts_with("did:key:") { 101 return Err(PlcError::InvalidVerificationMethods(format!( 102 "Verification method '{}' must be in did:key format: {}", 103 name, key 104 ))); 105 } 106 } 107 108 Ok(()) 109 } 110 111 /// Convert this PLC state to a W3C DID document 112 pub fn to_did_document(&self, did: &Did) -> DidDocument { 113 DidDocument::from_plc_state(did.clone(), self.clone()) 114 } 115} 116 117impl Default for PlcState { 118 fn default() -> Self { 119 Self::new() 120 } 121} 122 123/// Service endpoint definition 124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 125pub struct ServiceEndpoint { 126 /// Service type (e.g., "AtprotoPersonalDataServer") 127 #[serde(rename = "type")] 128 pub service_type: String, 129 130 /// Service endpoint URL 131 pub endpoint: String, 132} 133 134impl ServiceEndpoint { 135 /// Create a new service endpoint 136 pub fn new(service_type: String, endpoint: String) -> Self { 137 Self { 138 service_type, 139 endpoint, 140 } 141 } 142 143 /// Validate this service endpoint 144 pub fn validate(&self) -> Result<()> { 145 if self.service_type.is_empty() { 146 return Err(PlcError::InvalidService( 147 "Service type cannot be empty".to_string(), 148 )); 149 } 150 151 if self.endpoint.is_empty() { 152 return Err(PlcError::InvalidService( 153 "Service endpoint cannot be empty".to_string(), 154 )); 155 } 156 157 Ok(()) 158 } 159} 160 161/// W3C DID Document format 162/// 163/// This represents a DID document in the W3C standard format. 164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 165pub struct DidDocument { 166 /// The DID this document describes 167 pub id: Did, 168 169 /// JSON-LD context 170 #[serde(rename = "@context")] 171 pub context: Vec<String>, 172 173 /// Verification methods 174 #[serde(rename = "verificationMethod")] 175 pub verification_method: Vec<VerificationMethod>, 176 177 /// Also-known-as URIs 178 #[serde(rename = "alsoKnownAs", skip_serializing_if = "Vec::is_empty", default)] 179 pub also_known_as: Vec<String>, 180 181 /// Services 182 #[serde(skip_serializing_if = "Vec::is_empty", default)] 183 pub service: Vec<Service>, 184} 185 186impl DidDocument { 187 /// Create a DID document from PLC state 188 pub fn from_plc_state(did: Did, state: PlcState) -> Self { 189 let mut verification_methods = Vec::new(); 190 191 // Add verification methods 192 for (id, controller) in &state.verification_methods { 193 verification_methods.push(VerificationMethod { 194 id: format!("{}#{}", did, id), 195 method_type: "Multikey".to_string(), 196 controller: did.to_string(), 197 public_key_multibase: controller.clone(), 198 }); 199 } 200 201 // Add services 202 let services: Vec<Service> = state 203 .services 204 .iter() 205 .map(|(id, endpoint)| Service { 206 id: format!("{}#{}", did, id), 207 service_type: endpoint.service_type.clone(), 208 service_endpoint: endpoint.endpoint.clone(), 209 }) 210 .collect(); 211 212 Self { 213 id: did, 214 context: vec![ 215 "https://www.w3.org/ns/did/v1".to_string(), 216 "https://w3id.org/security/multikey/v1".to_string(), 217 ], 218 verification_method: verification_methods, 219 also_known_as: state.also_known_as.clone(), 220 service: services, 221 } 222 } 223 224 /// Validate this DID document 225 pub fn validate(&self) -> Result<()> { 226 // Convert to PLC state and validate 227 let plc_state = self.to_plc_state()?; 228 plc_state.validate() 229 } 230 231 /// Convert this DID document to PLC state 232 pub fn to_plc_state(&self) -> Result<PlcState> { 233 let mut verification_methods = HashMap::new(); 234 235 for vm in &self.verification_method { 236 // Extract the fragment ID (after '#') 237 let id = vm 238 .id 239 .rsplit('#') 240 .next() 241 .ok_or_else(|| { 242 PlcError::InvalidVerificationMethods(format!( 243 "Invalid verification method ID: {}", 244 vm.id 245 )) 246 })? 247 .to_string(); 248 249 verification_methods.insert(id, vm.public_key_multibase.clone()); 250 } 251 252 let mut services = HashMap::new(); 253 for svc in &self.service { 254 let id = svc 255 .id 256 .rsplit('#') 257 .next() 258 .ok_or_else(|| PlcError::InvalidService(format!("Invalid service ID: {}", svc.id)))? 259 .to_string(); 260 261 services.insert( 262 id, 263 ServiceEndpoint { 264 service_type: svc.service_type.clone(), 265 endpoint: svc.service_endpoint.clone(), 266 }, 267 ); 268 } 269 270 Ok(PlcState { 271 rotation_keys: Vec::new(), // Not stored in DID document 272 verification_methods, 273 also_known_as: self.also_known_as.clone(), 274 services, 275 }) 276 } 277} 278 279/// Verification method in W3C format 280#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 281pub struct VerificationMethod { 282 /// Verification method ID (e.g., "did:plc:xyz#atproto") 283 pub id: String, 284 285 /// Method type (e.g., "Multikey") 286 #[serde(rename = "type")] 287 pub method_type: String, 288 289 /// Controller DID 290 pub controller: String, 291 292 /// Public key in multibase format 293 #[serde(rename = "publicKeyMultibase")] 294 pub public_key_multibase: String, 295} 296 297/// Service in W3C format 298#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 299pub struct Service { 300 /// Service ID (e.g., "did:plc:xyz#atproto_pds") 301 pub id: String, 302 303 /// Service type 304 #[serde(rename = "type")] 305 pub service_type: String, 306 307 /// Service endpoint URL 308 #[serde(rename = "serviceEndpoint")] 309 pub service_endpoint: String, 310} 311 312#[cfg(test)] 313mod tests { 314 use super::*; 315 316 #[test] 317 fn test_plc_state_validation() { 318 let mut state = PlcState::new(); 319 320 // Empty state should fail (no rotation keys) 321 assert!(state.validate().is_err()); 322 323 // Add a rotation key 324 state.rotation_keys.push("did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N".to_string()); 325 assert!(state.validate().is_ok()); 326 327 // Add duplicate rotation key 328 state.rotation_keys.push("did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N".to_string()); 329 assert!(state.validate().is_err()); 330 } 331 332 #[test] 333 fn test_service_endpoint() { 334 let endpoint = ServiceEndpoint::new( 335 "AtprotoPersonalDataServer".to_string(), 336 "https://pds.example.com".to_string(), 337 ); 338 assert!(endpoint.validate().is_ok()); 339 340 let empty_type = ServiceEndpoint::new(String::new(), "https://example.com".to_string()); 341 assert!(empty_type.validate().is_err()); 342 } 343 344 #[test] 345 fn test_did_document_conversion() { 346 let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz").unwrap(); 347 let mut state = PlcState::new(); 348 state.rotation_keys.push("did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N".to_string()); 349 state.verification_methods.insert( 350 "atproto".to_string(), 351 "did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169".to_string(), 352 ); 353 354 let doc = state.to_did_document(&did); 355 assert_eq!(doc.id, did); 356 assert_eq!(doc.verification_method.len(), 1); 357 assert_eq!(doc.verification_method[0].id, "did:plc:ewvi7nxzyoun6zhxrhs64oiz#atproto"); 358 } 359}