Rust and WASM did-method-plc tools and structures
at main 11 kB view raw
1//! Builder pattern for creating did:plc identifiers 2 3use crate::crypto::SigningKey; 4use crate::did::Did; 5use crate::document::ServiceEndpoint; 6use crate::encoding::{base32_encode, dag_cbor_encode, sha256}; 7use crate::error::{PlcError, Result}; 8use crate::operations::{Operation, UnsignedOperation}; 9use crate::validation::{ 10 validate_also_known_as, validate_rotation_keys, validate_services, 11 validate_verification_methods, 12}; 13use std::collections::HashMap; 14 15/// Builder for creating new did:plc identifiers 16/// 17/// # Examples 18/// 19/// ``` 20/// use atproto_plc::{DidBuilder, SigningKey, ServiceEndpoint}; 21/// 22/// let rotation_key = SigningKey::generate_p256(); 23/// let signing_key = SigningKey::generate_k256(); 24/// 25/// let (did, operation, keys) = DidBuilder::new() 26/// .add_rotation_key(rotation_key) 27/// .add_verification_method("atproto".into(), signing_key) 28/// .add_also_known_as("at://alice.example.com".into()) 29/// .add_service( 30/// "atproto_pds".into(), 31/// ServiceEndpoint::new( 32/// "AtprotoPersonalDataServer".into(), 33/// "https://pds.example.com".into(), 34/// ), 35/// ) 36/// .build()?; 37/// 38/// println!("Created DID: {}", did); 39/// # Ok::<(), atproto_plc::PlcError>(()) 40/// ``` 41pub struct DidBuilder { 42 rotation_keys: Vec<SigningKey>, 43 verification_methods: HashMap<String, SigningKey>, 44 also_known_as: Vec<String>, 45 services: HashMap<String, ServiceEndpoint>, 46} 47 48impl DidBuilder { 49 /// Create a new DID builder 50 pub fn new() -> Self { 51 Self { 52 rotation_keys: Vec::new(), 53 verification_methods: HashMap::new(), 54 also_known_as: Vec::new(), 55 services: HashMap::new(), 56 } 57 } 58 59 /// Add a rotation key (1-5 required, no duplicates) 60 /// 61 /// Rotation keys are used to sign operations and can be used to recover 62 /// control of the DID within a 72-hour window. 63 /// 64 /// # Examples 65 /// 66 /// ``` 67 /// use atproto_plc::{DidBuilder, SigningKey}; 68 /// 69 /// let key = SigningKey::generate_p256(); 70 /// let builder = DidBuilder::new().add_rotation_key(key); 71 /// ``` 72 pub fn add_rotation_key(mut self, key: SigningKey) -> Self { 73 self.rotation_keys.push(key); 74 self 75 } 76 77 /// Add a verification method (max 10) 78 /// 79 /// Verification methods are cryptographic keys used for authentication 80 /// and signing. In ATProto, these are typically used for signing posts 81 /// and other records. 82 /// 83 /// # Examples 84 /// 85 /// ``` 86 /// use atproto_plc::{DidBuilder, SigningKey}; 87 /// 88 /// let key = SigningKey::generate_k256(); 89 /// let builder = DidBuilder::new() 90 /// .add_verification_method("atproto".into(), key); 91 /// ``` 92 pub fn add_verification_method(mut self, name: String, key: SigningKey) -> Self { 93 self.verification_methods.insert(name, key); 94 self 95 } 96 97 /// Add an also-known-as URI 98 /// 99 /// Also-known-as URIs are alternate identifiers for the same entity. 100 /// In ATProto, this is typically the user's handle. 101 /// 102 /// # Examples 103 /// 104 /// ``` 105 /// use atproto_plc::DidBuilder; 106 /// 107 /// let builder = DidBuilder::new() 108 /// .add_also_known_as("at://alice.bsky.social".into()); 109 /// ``` 110 pub fn add_also_known_as(mut self, uri: String) -> Self { 111 self.also_known_as.push(uri); 112 self 113 } 114 115 /// Add a service endpoint 116 /// 117 /// Services are endpoints that provide functionality for the DID. 118 /// In ATProto, this is typically the Personal Data Server (PDS). 119 /// 120 /// # Examples 121 /// 122 /// ``` 123 /// use atproto_plc::{DidBuilder, ServiceEndpoint}; 124 /// 125 /// let builder = DidBuilder::new() 126 /// .add_service( 127 /// "atproto_pds".into(), 128 /// ServiceEndpoint::new( 129 /// "AtprotoPersonalDataServer".into(), 130 /// "https://pds.example.com".into(), 131 /// ), 132 /// ); 133 /// ``` 134 pub fn add_service(mut self, name: String, endpoint: ServiceEndpoint) -> Self { 135 self.services.insert(name, endpoint); 136 self 137 } 138 139 /// Build and sign the genesis operation, returning the DID, operation, and keys 140 /// 141 /// This method: 142 /// 1. Validates all inputs 143 /// 2. Creates an unsigned genesis operation 144 /// 3. Signs it with the first rotation key 145 /// 4. Derives the DID from the signed operation's hash 146 /// 5. Returns the DID, signed operation, and all keys for safekeeping 147 /// 148 /// # Errors 149 /// 150 /// Returns errors if: 151 /// - No rotation keys provided 152 /// - Too many rotation keys (>5) 153 /// - Too many verification methods (>10) 154 /// - Invalid URIs or service endpoints 155 /// - Signing fails 156 /// 157 /// # Examples 158 /// 159 /// ``` 160 /// use atproto_plc::{DidBuilder, SigningKey}; 161 /// 162 /// let rotation_key = SigningKey::generate_p256(); 163 /// 164 /// let (did, operation, keys) = DidBuilder::new() 165 /// .add_rotation_key(rotation_key) 166 /// .build()?; 167 /// 168 /// assert!(did.as_str().starts_with("did:plc:")); 169 /// # Ok::<(), atproto_plc::PlcError>(()) 170 /// ``` 171 pub fn build(self) -> Result<(Did, Operation, BuilderKeys)> { 172 // Validate inputs 173 if self.rotation_keys.is_empty() { 174 return Err(PlcError::InvalidRotationKeys( 175 "At least one rotation key is required".to_string(), 176 )); 177 } 178 179 // Convert keys to did:key format for validation 180 let rotation_key_strings: Vec<String> = 181 self.rotation_keys.iter().map(|k| k.to_did_key()).collect(); 182 183 let verification_method_strings: HashMap<String, String> = self 184 .verification_methods 185 .iter() 186 .map(|(name, key)| (name.clone(), key.to_did_key())) 187 .collect(); 188 189 // Validate all fields 190 validate_rotation_keys(&rotation_key_strings)?; 191 validate_verification_methods(&verification_method_strings)?; 192 validate_also_known_as(&self.also_known_as)?; 193 validate_services(&self.services)?; 194 195 // Create unsigned genesis operation 196 let unsigned = UnsignedOperation::PlcOperation { 197 rotation_keys: rotation_key_strings, 198 verification_methods: verification_method_strings, 199 also_known_as: self.also_known_as, 200 services: self.services, 201 prev: None, // Genesis has no previous operation 202 }; 203 204 // Sign with the first rotation key 205 let signed = unsigned.sign(&self.rotation_keys[0])?; 206 207 // Derive DID from the signed operation 208 let did = Self::derive_did(&signed)?; 209 210 // Collect all keys to return 211 let keys = BuilderKeys { 212 rotation_keys: self.rotation_keys, 213 verification_methods: self.verification_methods, 214 }; 215 216 Ok((did, signed, keys)) 217 } 218 219 /// Derive a DID from a signed genesis operation 220 /// 221 /// The DID is derived by: 222 /// 1. Computing the CID of the signed operation 223 /// 2. Taking the SHA-256 hash of the operation 224 /// 3. Base32-encoding the hash 225 /// 4. Taking the first 24 characters 226 fn derive_did(operation: &Operation) -> Result<Did> { 227 // Get the CID of the operation 228 let _cid = operation.cid()?; 229 230 // The DID is derived from the CID by taking the hash portion 231 // For simplicity, we'll hash the entire serialized operation 232 let serialized = 233 dag_cbor_encode(operation).map_err(|e| PlcError::DagCborError(e.to_string()))?; 234 235 let hash = sha256(&serialized); 236 let encoded = base32_encode(&hash); 237 238 // Take first 24 characters for the DID identifier 239 let identifier = &encoded[..24.min(encoded.len())]; 240 241 Did::from_identifier(identifier) 242 } 243} 244 245impl Default for DidBuilder { 246 fn default() -> Self { 247 Self::new() 248 } 249} 250 251/// Keys returned from the builder 252/// 253/// These should be stored securely by the application. 254/// The rotation keys can be used to update or recover the DID. 255/// The verification methods are used for signing application data. 256pub struct BuilderKeys { 257 /// Rotation keys (private keys) 258 pub rotation_keys: Vec<SigningKey>, 259 260 /// Verification method keys (private keys) 261 pub verification_methods: HashMap<String, SigningKey>, 262} 263 264impl BuilderKeys { 265 /// Get a rotation key by index 266 pub fn rotation_key(&self, index: usize) -> Option<&SigningKey> { 267 self.rotation_keys.get(index) 268 } 269 270 /// Get a verification method key by name 271 pub fn verification_method(&self, name: &str) -> Option<&SigningKey> { 272 self.verification_methods.get(name) 273 } 274 275 /// Get the primary rotation key (first one) 276 pub fn primary_rotation_key(&self) -> Option<&SigningKey> { 277 self.rotation_key(0) 278 } 279} 280 281#[cfg(test)] 282mod tests { 283 use super::*; 284 285 #[test] 286 fn test_builder_basic() { 287 let rotation_key = SigningKey::generate_p256(); 288 289 let (did, operation, keys) = DidBuilder::new() 290 .add_rotation_key(rotation_key) 291 .build() 292 .unwrap(); 293 294 assert!(did.as_str().starts_with("did:plc:")); 295 assert!(operation.is_genesis()); 296 assert_eq!(keys.rotation_keys.len(), 1); 297 } 298 299 #[test] 300 fn test_builder_with_verification_methods() { 301 let rotation_key = SigningKey::generate_p256(); 302 let signing_key = SigningKey::generate_k256(); 303 304 let (did, _, keys) = DidBuilder::new() 305 .add_rotation_key(rotation_key) 306 .add_verification_method("atproto".into(), signing_key) 307 .build() 308 .unwrap(); 309 310 assert!(did.as_str().starts_with("did:plc:")); 311 assert_eq!(keys.verification_methods.len(), 1); 312 assert!(keys.verification_method("atproto").is_some()); 313 } 314 315 #[test] 316 fn test_builder_with_services() { 317 let rotation_key = SigningKey::generate_p256(); 318 319 let (did, _, _) = DidBuilder::new() 320 .add_rotation_key(rotation_key) 321 .add_service( 322 "atproto_pds".into(), 323 ServiceEndpoint::new( 324 "AtprotoPersonalDataServer".into(), 325 "https://pds.example.com".into(), 326 ), 327 ) 328 .build() 329 .unwrap(); 330 331 assert!(did.as_str().starts_with("did:plc:")); 332 } 333 334 #[test] 335 fn test_builder_no_rotation_keys() { 336 let result = DidBuilder::new().build(); 337 assert!(result.is_err()); 338 } 339 340 #[test] 341 fn test_builder_too_many_rotation_keys() { 342 let mut builder = DidBuilder::new(); 343 344 for _ in 0..6 { 345 builder = builder.add_rotation_key(SigningKey::generate_p256()); 346 } 347 348 assert!(builder.build().is_err()); 349 } 350 351 #[test] 352 fn test_builder_keys_access() { 353 let rotation_key = SigningKey::generate_p256(); 354 let signing_key = SigningKey::generate_k256(); 355 356 let (_, _, keys) = DidBuilder::new() 357 .add_rotation_key(rotation_key) 358 .add_verification_method("atproto".into(), signing_key) 359 .build() 360 .unwrap(); 361 362 assert!(keys.primary_rotation_key().is_some()); 363 assert!(keys.verification_method("atproto").is_some()); 364 assert!(keys.verification_method("nonexistent").is_none()); 365 } 366}