Rust and WASM did-method-plc tools and structures
1//! Operation types for did:plc (genesis, update, tombstone) 2 3use crate::crypto::{SigningKey, VerifyingKey}; 4use crate::document::ServiceEndpoint; 5use crate::encoding::{base64url_decode, compute_cid, dag_cbor_encode}; 6use crate::error::{PlcError, Result}; 7use serde::{Deserialize, Serialize}; 8use std::collections::HashMap; 9 10/// Represents a PLC operation (genesis, update, or tombstone) 11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 12#[serde(tag = "type")] 13pub enum Operation { 14 /// Standard PLC operation (genesis or update) 15 #[serde(rename = "plc_operation")] 16 PlcOperation { 17 /// Rotation keys (1-5 did:key strings) 18 #[serde(rename = "rotationKeys")] 19 rotation_keys: Vec<String>, 20 21 /// Verification methods (max 10 entries) 22 #[serde(rename = "verificationMethods")] 23 verification_methods: HashMap<String, String>, 24 25 /// Also-known-as URIs 26 #[serde(rename = "alsoKnownAs")] 27 also_known_as: Vec<String>, 28 29 /// Service endpoints 30 services: HashMap<String, ServiceEndpoint>, 31 32 /// Previous operation CID (null for genesis) 33 prev: Option<String>, 34 35 /// Base64url-encoded signature 36 sig: String, 37 }, 38 39 /// Tombstone operation (marks DID as deleted) 40 #[serde(rename = "plc_tombstone")] 41 PlcTombstone { 42 /// Previous operation CID (never null for tombstone) 43 prev: String, 44 45 /// Base64url-encoded signature 46 sig: String, 47 }, 48 49 /// Legacy create operation (for backwards compatibility) 50 #[serde(rename = "create")] 51 LegacyCreate { 52 /// Signing key (did:key format) 53 #[serde(rename = "signingKey")] 54 signing_key: String, 55 56 /// Recovery key (did:key format) 57 #[serde(rename = "recoveryKey")] 58 recovery_key: String, 59 60 /// Handle (e.g., "alice.bsky.social") 61 handle: String, 62 63 /// Service endpoint URL 64 service: String, 65 66 /// Previous operation CID 67 prev: Option<String>, 68 69 /// Base64url-encoded signature 70 sig: String, 71 }, 72} 73 74impl Operation { 75 /// Create a new unsigned genesis operation 76 pub fn new_genesis( 77 rotation_keys: Vec<String>, 78 verification_methods: HashMap<String, String>, 79 also_known_as: Vec<String>, 80 services: HashMap<String, ServiceEndpoint>, 81 ) -> UnsignedOperation { 82 UnsignedOperation::PlcOperation { 83 rotation_keys, 84 verification_methods, 85 also_known_as, 86 services, 87 prev: None, 88 } 89 } 90 91 /// Create a new unsigned update operation 92 pub fn new_update( 93 rotation_keys: Vec<String>, 94 verification_methods: HashMap<String, String>, 95 also_known_as: Vec<String>, 96 services: HashMap<String, ServiceEndpoint>, 97 prev: String, 98 ) -> UnsignedOperation { 99 UnsignedOperation::PlcOperation { 100 rotation_keys, 101 verification_methods, 102 also_known_as, 103 services, 104 prev: Some(prev), 105 } 106 } 107 108 /// Create a new unsigned tombstone operation 109 pub fn new_tombstone(prev: String) -> UnsignedOperation { 110 UnsignedOperation::PlcTombstone { prev } 111 } 112 113 /// Get the previous operation CID, if any 114 pub fn prev(&self) -> Option<&str> { 115 match self { 116 Operation::PlcOperation { prev, .. } => prev.as_deref(), 117 Operation::PlcTombstone { prev, .. } => Some(prev), 118 Operation::LegacyCreate { prev, .. } => prev.as_deref(), 119 } 120 } 121 122 /// Get the signature as a base64url string 123 pub fn signature(&self) -> &str { 124 match self { 125 Operation::PlcOperation { sig, .. } => sig, 126 Operation::PlcTombstone { sig, .. } => sig, 127 Operation::LegacyCreate { sig, .. } => sig, 128 } 129 } 130 131 /// Check if this is a genesis operation (prev is None) 132 pub fn is_genesis(&self) -> bool { 133 self.prev().is_none() 134 } 135 136 /// Compute the CID of this operation 137 /// 138 /// # Errors 139 /// 140 /// Returns an error if DAG-CBOR encoding fails 141 pub fn cid(&self) -> Result<String> { 142 let encoded = dag_cbor_encode(self)?; 143 compute_cid(&encoded) 144 } 145 146 /// Verify the signature on this operation using the provided rotation keys 147 /// 148 /// # Errors 149 /// 150 /// Returns `PlcError::SignatureVerificationFailed` if verification fails 151 pub fn verify(&self, rotation_keys: &[VerifyingKey]) -> Result<()> { 152 if rotation_keys.is_empty() { 153 return Err(PlcError::InvalidRotationKeys( 154 "At least one rotation key is required for verification".to_string(), 155 )); 156 } 157 158 // Get the unsigned operation data 159 let unsigned_data = self.unsigned_data()?; 160 161 // Decode signature 162 let signature = base64url_decode(self.signature())?; 163 164 // Try to verify with each rotation key 165 let mut last_error = None; 166 for key in rotation_keys { 167 match key.verify(&unsigned_data, &signature) { 168 Ok(_) => return Ok(()), // Success! 169 Err(e) => last_error = Some(e), 170 } 171 } 172 173 // If we get here, none of the keys verified the signature 174 Err(last_error.unwrap_or(PlcError::SignatureVerificationFailed)) 175 } 176 177 /// Get the unsigned data that was signed 178 fn unsigned_data(&self) -> Result<Vec<u8>> { 179 let unsigned = match self { 180 Operation::PlcOperation { 181 rotation_keys, 182 verification_methods, 183 also_known_as, 184 services, 185 prev, 186 .. 187 } => UnsignedOperation::PlcOperation { 188 rotation_keys: rotation_keys.clone(), 189 verification_methods: verification_methods.clone(), 190 also_known_as: also_known_as.clone(), 191 services: services.clone(), 192 prev: prev.clone(), 193 }, 194 Operation::PlcTombstone { prev, .. } => UnsignedOperation::PlcTombstone { 195 prev: prev.clone(), 196 }, 197 Operation::LegacyCreate { 198 signing_key, 199 recovery_key, 200 handle, 201 service, 202 prev, 203 .. 204 } => UnsignedOperation::LegacyCreate { 205 signing_key: signing_key.clone(), 206 recovery_key: recovery_key.clone(), 207 handle: handle.clone(), 208 service: service.clone(), 209 prev: prev.clone(), 210 }, 211 }; 212 213 dag_cbor_encode(&unsigned) 214 } 215 216 /// Get the rotation keys from this operation, if any 217 pub fn rotation_keys(&self) -> Option<&[String]> { 218 match self { 219 Operation::PlcOperation { rotation_keys, .. } => Some(rotation_keys), 220 _ => None, 221 } 222 } 223} 224 225/// An unsigned operation that needs to be signed 226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 227#[serde(tag = "type")] 228pub enum UnsignedOperation { 229 /// Standard PLC operation (genesis or update) 230 #[serde(rename = "plc_operation")] 231 PlcOperation { 232 /// Rotation keys for signing future operations 233 #[serde(rename = "rotationKeys")] 234 rotation_keys: Vec<String>, 235 236 /// Verification methods for authentication 237 #[serde(rename = "verificationMethods")] 238 verification_methods: HashMap<String, String>, 239 240 /// Also-known-as URIs (aliases) 241 #[serde(rename = "alsoKnownAs")] 242 also_known_as: Vec<String>, 243 244 /// Service endpoints 245 services: HashMap<String, ServiceEndpoint>, 246 247 /// CID of previous operation (None for genesis) 248 prev: Option<String>, 249 }, 250 251 /// Tombstone operation 252 #[serde(rename = "plc_tombstone")] 253 PlcTombstone { 254 /// CID of previous operation 255 prev: String, 256 }, 257 258 /// Legacy create operation 259 #[serde(rename = "create")] 260 LegacyCreate { 261 /// Signing key for the DID 262 #[serde(rename = "signingKey")] 263 signing_key: String, 264 265 /// Recovery key for the DID 266 #[serde(rename = "recoveryKey")] 267 recovery_key: String, 268 269 /// Handle for the DID 270 handle: String, 271 272 /// Service endpoint 273 service: String, 274 275 /// CID of previous operation (None for genesis) 276 prev: Option<String>, 277 }, 278} 279 280impl UnsignedOperation { 281 /// Sign this operation with the provided signing key 282 /// 283 /// # Errors 284 /// 285 /// Returns an error if signing or encoding fails 286 pub fn sign(self, key: &SigningKey) -> Result<Operation> { 287 // Serialize to DAG-CBOR 288 let data = dag_cbor_encode(&self)?; 289 290 // Sign the data 291 let signature = key.sign_base64url(&data)?; 292 293 // Create the signed operation 294 let operation = match self { 295 UnsignedOperation::PlcOperation { 296 rotation_keys, 297 verification_methods, 298 also_known_as, 299 services, 300 prev, 301 } => Operation::PlcOperation { 302 rotation_keys, 303 verification_methods, 304 also_known_as, 305 services, 306 prev, 307 sig: signature, 308 }, 309 UnsignedOperation::PlcTombstone { prev } => Operation::PlcTombstone { 310 prev, 311 sig: signature, 312 }, 313 UnsignedOperation::LegacyCreate { 314 signing_key, 315 recovery_key, 316 handle, 317 service, 318 prev, 319 } => Operation::LegacyCreate { 320 signing_key, 321 recovery_key, 322 handle, 323 service, 324 prev, 325 sig: signature, 326 }, 327 }; 328 329 Ok(operation) 330 } 331 332 /// Compute the CID of this unsigned operation 333 /// 334 /// This is used to derive the DID from the genesis operation 335 pub fn cid(&self) -> Result<String> { 336 let encoded = dag_cbor_encode(self)?; 337 compute_cid(&encoded) 338 } 339} 340 341#[cfg(test)] 342mod tests { 343 use super::*; 344 use crate::crypto::SigningKey; 345 346 #[test] 347 fn test_genesis_operation() { 348 let key = SigningKey::generate_p256(); 349 let did_key = key.to_did_key(); 350 351 let unsigned = Operation::new_genesis( 352 vec![did_key.clone()], 353 HashMap::new(), 354 vec![], 355 HashMap::new(), 356 ); 357 358 let signed = unsigned.sign(&key).unwrap(); 359 assert!(signed.is_genesis()); 360 assert_eq!(signed.prev(), None); 361 } 362 363 #[test] 364 fn test_update_operation() { 365 let key = SigningKey::generate_p256(); 366 let did_key = key.to_did_key(); 367 368 let unsigned = Operation::new_update( 369 vec![did_key], 370 HashMap::new(), 371 vec![], 372 HashMap::new(), 373 "bafyreib2rxk3rybk3aobmv5msrxগত7h4b4kfzxx4wxltyqu7e7vgq".to_string(), 374 ); 375 376 let signed = unsigned.sign(&key).unwrap(); 377 assert!(!signed.is_genesis()); 378 assert!(signed.prev().is_some()); 379 } 380 381 #[test] 382 fn test_tombstone_operation() { 383 let key = SigningKey::generate_p256(); 384 385 let unsigned = Operation::new_tombstone( 386 "bafyreib2rxk3rybk3aobmv5msrxhgt7h4b4kfzxx4wxltyqu7e7vgq".to_string(), 387 ); 388 389 let signed = unsigned.sign(&key).unwrap(); 390 assert!(!signed.is_genesis()); 391 assert!(signed.prev().is_some()); 392 } 393 394 #[test] 395 fn test_sign_and_verify() { 396 let key = SigningKey::generate_p256(); 397 let did_key = key.to_did_key(); 398 399 let unsigned = Operation::new_genesis( 400 vec![did_key.clone()], 401 HashMap::new(), 402 vec![], 403 HashMap::new(), 404 ); 405 406 let signed = unsigned.sign(&key).unwrap(); 407 408 // Parse the rotation key to get verifying key 409 let verifying_key = VerifyingKey::from_did_key(&did_key).unwrap(); 410 411 // Verify should succeed 412 assert!(signed.verify(&[verifying_key]).is_ok()); 413 414 // Verify with wrong key should fail 415 let wrong_key = SigningKey::generate_p256(); 416 let wrong_verifying_key = wrong_key.verifying_key(); 417 assert!(signed.verify(&[wrong_verifying_key]).is_err()); 418 } 419 420 #[test] 421 fn test_operation_cid() { 422 let key = SigningKey::generate_p256(); 423 let did_key = key.to_did_key(); 424 425 let unsigned = Operation::new_genesis( 426 vec![did_key], 427 HashMap::new(), 428 vec![], 429 HashMap::new(), 430 ); 431 432 let signed = unsigned.sign(&key).unwrap(); 433 let cid = signed.cid().unwrap(); 434 435 // CID should start with 'b' (CIDv1 in base32) 436 assert!(cid.starts_with('b')); 437 } 438}