//! Operation types for did:plc (genesis, update, tombstone) use crate::crypto::{SigningKey, VerifyingKey}; use crate::document::ServiceEndpoint; use crate::encoding::{base64url_decode, compute_cid, dag_cbor_encode}; use crate::error::{PlcError, Result}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// Represents a PLC operation (genesis, update, or tombstone) #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type")] pub enum Operation { /// Standard PLC operation (genesis or update) #[serde(rename = "plc_operation")] PlcOperation { /// Rotation keys (1-5 did:key strings) #[serde(rename = "rotationKeys")] rotation_keys: Vec, /// Verification methods (max 10 entries) #[serde(rename = "verificationMethods")] verification_methods: HashMap, /// Also-known-as URIs #[serde(rename = "alsoKnownAs")] also_known_as: Vec, /// Service endpoints services: HashMap, /// Previous operation CID (null for genesis) #[serde(skip_serializing_if = "Option::is_none")] prev: Option, /// Base64url-encoded signature sig: String, }, /// Tombstone operation (marks DID as deleted) #[serde(rename = "plc_tombstone")] PlcTombstone { /// Previous operation CID (never null for tombstone) prev: String, /// Base64url-encoded signature sig: String, }, /// Legacy create operation (for backwards compatibility) #[serde(rename = "create")] LegacyCreate { /// Signing key (did:key format) #[serde(rename = "signingKey")] signing_key: String, /// Recovery key (did:key format) #[serde(rename = "recoveryKey")] recovery_key: String, /// Handle (e.g., "alice.bsky.social") handle: String, /// Service endpoint URL service: String, /// Previous operation CID #[serde(skip_serializing_if = "Option::is_none")] prev: Option, /// Base64url-encoded signature sig: String, }, } impl Operation { /// Create a new unsigned genesis operation pub fn new_genesis( rotation_keys: Vec, verification_methods: HashMap, also_known_as: Vec, services: HashMap, ) -> UnsignedOperation { UnsignedOperation::PlcOperation { rotation_keys, verification_methods, also_known_as, services, prev: None, } } /// Create a new unsigned update operation pub fn new_update( rotation_keys: Vec, verification_methods: HashMap, also_known_as: Vec, services: HashMap, prev: String, ) -> UnsignedOperation { UnsignedOperation::PlcOperation { rotation_keys, verification_methods, also_known_as, services, prev: Some(prev), } } /// Create a new unsigned tombstone operation pub fn new_tombstone(prev: String) -> UnsignedOperation { UnsignedOperation::PlcTombstone { prev } } /// Get the previous operation CID, if any pub fn prev(&self) -> Option<&str> { match self { Operation::PlcOperation { prev, .. } => prev.as_deref(), Operation::PlcTombstone { prev, .. } => Some(prev), Operation::LegacyCreate { prev, .. } => prev.as_deref(), } } /// Get the signature as a base64url string pub fn signature(&self) -> &str { match self { Operation::PlcOperation { sig, .. } => sig, Operation::PlcTombstone { sig, .. } => sig, Operation::LegacyCreate { sig, .. } => sig, } } /// Check if this is a genesis operation (prev is None) pub fn is_genesis(&self) -> bool { self.prev().is_none() } /// Compute the CID of this operation /// /// # Errors /// /// Returns an error if DAG-CBOR encoding fails pub fn cid(&self) -> Result { let encoded = dag_cbor_encode(self)?; compute_cid(&encoded) } /// Verify the signature on this operation using the provided rotation keys /// /// # Errors /// /// Returns `PlcError::SignatureVerificationFailed` if verification fails pub fn verify(&self, rotation_keys: &[VerifyingKey]) -> Result<()> { if rotation_keys.is_empty() { return Err(PlcError::InvalidRotationKeys( "At least one rotation key is required for verification".to_string(), )); } // Get the unsigned operation data let unsigned_data = self.unsigned_data()?; // Decode signature let signature = base64url_decode(self.signature())?; // Try to verify with each rotation key let mut last_error = None; for key in rotation_keys { match key.verify(&unsigned_data, &signature) { Ok(_) => return Ok(()), // Success! Err(e) => last_error = Some(e), } } // If we get here, none of the keys verified the signature Err(last_error.unwrap_or(PlcError::SignatureVerificationFailed)) } /// Get the unsigned data that was signed fn unsigned_data(&self) -> Result> { let unsigned = match self { Operation::PlcOperation { rotation_keys, verification_methods, also_known_as, services, prev, .. } => UnsignedOperation::PlcOperation { rotation_keys: rotation_keys.clone(), verification_methods: verification_methods.clone(), also_known_as: also_known_as.clone(), services: services.clone(), prev: prev.clone(), }, Operation::PlcTombstone { prev, .. } => UnsignedOperation::PlcTombstone { prev: prev.clone(), }, Operation::LegacyCreate { signing_key, recovery_key, handle, service, prev, .. } => UnsignedOperation::LegacyCreate { signing_key: signing_key.clone(), recovery_key: recovery_key.clone(), handle: handle.clone(), service: service.clone(), prev: prev.clone(), }, }; dag_cbor_encode(&unsigned) } /// Get the rotation keys from this operation, if any pub fn rotation_keys(&self) -> Option<&[String]> { match self { Operation::PlcOperation { rotation_keys, .. } => Some(rotation_keys), _ => None, } } } /// An unsigned operation that needs to be signed #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type")] pub enum UnsignedOperation { /// Standard PLC operation (genesis or update) #[serde(rename = "plc_operation")] PlcOperation { /// Rotation keys for signing future operations #[serde(rename = "rotationKeys")] rotation_keys: Vec, /// Verification methods for authentication #[serde(rename = "verificationMethods")] verification_methods: HashMap, /// Also-known-as URIs (aliases) #[serde(rename = "alsoKnownAs")] also_known_as: Vec, /// Service endpoints services: HashMap, /// CID of previous operation (None for genesis) #[serde(skip_serializing_if = "Option::is_none")] prev: Option, }, /// Tombstone operation #[serde(rename = "plc_tombstone")] PlcTombstone { /// CID of previous operation prev: String, }, /// Legacy create operation #[serde(rename = "create")] LegacyCreate { /// Signing key for the DID #[serde(rename = "signingKey")] signing_key: String, /// Recovery key for the DID #[serde(rename = "recoveryKey")] recovery_key: String, /// Handle for the DID handle: String, /// Service endpoint service: String, /// CID of previous operation (None for genesis) #[serde(skip_serializing_if = "Option::is_none")] prev: Option, }, } impl UnsignedOperation { /// Sign this operation with the provided signing key /// /// # Errors /// /// Returns an error if signing or encoding fails pub fn sign(self, key: &SigningKey) -> Result { // Serialize to DAG-CBOR let data = dag_cbor_encode(&self)?; // Sign the data let signature = key.sign_base64url(&data)?; // Create the signed operation let operation = match self { UnsignedOperation::PlcOperation { rotation_keys, verification_methods, also_known_as, services, prev, } => Operation::PlcOperation { rotation_keys, verification_methods, also_known_as, services, prev, sig: signature, }, UnsignedOperation::PlcTombstone { prev } => Operation::PlcTombstone { prev, sig: signature, }, UnsignedOperation::LegacyCreate { signing_key, recovery_key, handle, service, prev, } => Operation::LegacyCreate { signing_key, recovery_key, handle, service, prev, sig: signature, }, }; Ok(operation) } /// Compute the CID of this unsigned operation /// /// This is used to derive the DID from the genesis operation pub fn cid(&self) -> Result { let encoded = dag_cbor_encode(self)?; compute_cid(&encoded) } } #[cfg(test)] mod tests { use super::*; use crate::crypto::SigningKey; #[test] fn test_genesis_operation() { let key = SigningKey::generate_p256(); let did_key = key.to_did_key(); let unsigned = Operation::new_genesis( vec![did_key.clone()], HashMap::new(), vec![], HashMap::new(), ); let signed = unsigned.sign(&key).unwrap(); assert!(signed.is_genesis()); assert_eq!(signed.prev(), None); } #[test] fn test_update_operation() { let key = SigningKey::generate_p256(); let did_key = key.to_did_key(); let unsigned = Operation::new_update( vec![did_key], HashMap::new(), vec![], HashMap::new(), "bafyreib2rxk3rybk3aobmv5msrxগত7h4b4kfzxx4wxltyqu7e7vgq".to_string(), ); let signed = unsigned.sign(&key).unwrap(); assert!(!signed.is_genesis()); assert!(signed.prev().is_some()); } #[test] fn test_tombstone_operation() { let key = SigningKey::generate_p256(); let unsigned = Operation::new_tombstone( "bafyreib2rxk3rybk3aobmv5msrxhgt7h4b4kfzxx4wxltyqu7e7vgq".to_string(), ); let signed = unsigned.sign(&key).unwrap(); assert!(!signed.is_genesis()); assert!(signed.prev().is_some()); } #[test] fn test_sign_and_verify() { let key = SigningKey::generate_p256(); let did_key = key.to_did_key(); let unsigned = Operation::new_genesis( vec![did_key.clone()], HashMap::new(), vec![], HashMap::new(), ); let signed = unsigned.sign(&key).unwrap(); // Parse the rotation key to get verifying key let verifying_key = VerifyingKey::from_did_key(&did_key).unwrap(); // Verify should succeed assert!(signed.verify(&[verifying_key]).is_ok()); // Verify with wrong key should fail let wrong_key = SigningKey::generate_p256(); let wrong_verifying_key = wrong_key.verifying_key(); assert!(signed.verify(&[wrong_verifying_key]).is_err()); } #[test] fn test_operation_cid() { let key = SigningKey::generate_p256(); let did_key = key.to_did_key(); let unsigned = Operation::new_genesis( vec![did_key], HashMap::new(), vec![], HashMap::new(), ); let signed = unsigned.sign(&key).unwrap(); let cid = signed.cid().unwrap(); // CID should start with 'b' (CIDv1 in base32) assert!(cid.starts_with('b')); } }