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