this repo has no description
1use base32::Alphabet; 2use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 3use k256::ecdsa::{SigningKey, Signature, signature::Signer}; 4use reqwest::Client; 5use serde::{Deserialize, Serialize}; 6use serde_json::{json, Value}; 7use sha2::{Digest, Sha256}; 8use std::collections::HashMap; 9use std::time::Duration; 10use thiserror::Error; 11#[derive(Error, Debug)] 12pub enum PlcError { 13 #[error("HTTP request failed: {0}")] 14 Http(#[from] reqwest::Error), 15 #[error("Invalid response: {0}")] 16 InvalidResponse(String), 17 #[error("DID not found")] 18 NotFound, 19 #[error("DID is tombstoned")] 20 Tombstoned, 21 #[error("Serialization error: {0}")] 22 Serialization(String), 23 #[error("Signing error: {0}")] 24 Signing(String), 25 #[error("Request timeout")] 26 Timeout, 27 #[error("Service unavailable (circuit breaker open)")] 28 CircuitBreakerOpen, 29} 30#[derive(Debug, Clone, Serialize, Deserialize)] 31pub struct PlcOperation { 32 #[serde(rename = "type")] 33 pub op_type: String, 34 #[serde(rename = "rotationKeys")] 35 pub rotation_keys: Vec<String>, 36 #[serde(rename = "verificationMethods")] 37 pub verification_methods: HashMap<String, String>, 38 #[serde(rename = "alsoKnownAs")] 39 pub also_known_as: Vec<String>, 40 pub services: HashMap<String, PlcService>, 41 pub prev: Option<String>, 42 #[serde(skip_serializing_if = "Option::is_none")] 43 pub sig: Option<String>, 44} 45#[derive(Debug, Clone, Serialize, Deserialize)] 46pub struct PlcService { 47 #[serde(rename = "type")] 48 pub service_type: String, 49 pub endpoint: String, 50} 51#[derive(Debug, Clone, Serialize, Deserialize)] 52pub struct PlcTombstone { 53 #[serde(rename = "type")] 54 pub op_type: String, 55 pub prev: String, 56 #[serde(skip_serializing_if = "Option::is_none")] 57 pub sig: Option<String>, 58} 59#[derive(Debug, Clone, Serialize, Deserialize)] 60#[serde(untagged)] 61pub enum PlcOpOrTombstone { 62 Operation(PlcOperation), 63 Tombstone(PlcTombstone), 64} 65impl PlcOpOrTombstone { 66 pub fn is_tombstone(&self) -> bool { 67 match self { 68 PlcOpOrTombstone::Tombstone(_) => true, 69 PlcOpOrTombstone::Operation(op) => op.op_type == "plc_tombstone", 70 } 71 } 72} 73pub struct PlcClient { 74 base_url: String, 75 client: Client, 76} 77impl PlcClient { 78 pub fn new(base_url: Option<String>) -> Self { 79 let base_url = base_url.unwrap_or_else(|| { 80 std::env::var("PLC_DIRECTORY_URL") 81 .unwrap_or_else(|_| "https://plc.directory".to_string()) 82 }); 83 let timeout_secs: u64 = std::env::var("PLC_TIMEOUT_SECS") 84 .ok() 85 .and_then(|v| v.parse().ok()) 86 .unwrap_or(10); 87 let connect_timeout_secs: u64 = std::env::var("PLC_CONNECT_TIMEOUT_SECS") 88 .ok() 89 .and_then(|v| v.parse().ok()) 90 .unwrap_or(5); 91 let client = Client::builder() 92 .timeout(Duration::from_secs(timeout_secs)) 93 .connect_timeout(Duration::from_secs(connect_timeout_secs)) 94 .pool_max_idle_per_host(5) 95 .build() 96 .unwrap_or_else(|_| Client::new()); 97 Self { 98 base_url, 99 client, 100 } 101 } 102 fn encode_did(did: &str) -> String { 103 urlencoding::encode(did).to_string() 104 } 105 pub async fn get_document(&self, did: &str) -> Result<Value, PlcError> { 106 let url = format!("{}/{}", self.base_url, Self::encode_did(did)); 107 let response = self.client.get(&url).send().await?; 108 if response.status() == reqwest::StatusCode::NOT_FOUND { 109 return Err(PlcError::NotFound); 110 } 111 if !response.status().is_success() { 112 let status = response.status(); 113 let body = response.text().await.unwrap_or_default(); 114 return Err(PlcError::InvalidResponse(format!( 115 "HTTP {}: {}", 116 status, body 117 ))); 118 } 119 response.json().await.map_err(|e| PlcError::InvalidResponse(e.to_string())) 120 } 121 pub async fn get_document_data(&self, did: &str) -> Result<Value, PlcError> { 122 let url = format!("{}/{}/data", self.base_url, Self::encode_did(did)); 123 let response = self.client.get(&url).send().await?; 124 if response.status() == reqwest::StatusCode::NOT_FOUND { 125 return Err(PlcError::NotFound); 126 } 127 if !response.status().is_success() { 128 let status = response.status(); 129 let body = response.text().await.unwrap_or_default(); 130 return Err(PlcError::InvalidResponse(format!( 131 "HTTP {}: {}", 132 status, body 133 ))); 134 } 135 response.json().await.map_err(|e| PlcError::InvalidResponse(e.to_string())) 136 } 137 pub async fn get_last_op(&self, did: &str) -> Result<PlcOpOrTombstone, PlcError> { 138 let url = format!("{}/{}/log/last", self.base_url, Self::encode_did(did)); 139 let response = self.client.get(&url).send().await?; 140 if response.status() == reqwest::StatusCode::NOT_FOUND { 141 return Err(PlcError::NotFound); 142 } 143 if !response.status().is_success() { 144 let status = response.status(); 145 let body = response.text().await.unwrap_or_default(); 146 return Err(PlcError::InvalidResponse(format!( 147 "HTTP {}: {}", 148 status, body 149 ))); 150 } 151 response.json().await.map_err(|e| PlcError::InvalidResponse(e.to_string())) 152 } 153 pub async fn get_audit_log(&self, did: &str) -> Result<Vec<Value>, PlcError> { 154 let url = format!("{}/{}/log/audit", self.base_url, Self::encode_did(did)); 155 let response = self.client.get(&url).send().await?; 156 if response.status() == reqwest::StatusCode::NOT_FOUND { 157 return Err(PlcError::NotFound); 158 } 159 if !response.status().is_success() { 160 let status = response.status(); 161 let body = response.text().await.unwrap_or_default(); 162 return Err(PlcError::InvalidResponse(format!( 163 "HTTP {}: {}", 164 status, body 165 ))); 166 } 167 response.json().await.map_err(|e| PlcError::InvalidResponse(e.to_string())) 168 } 169 pub async fn send_operation(&self, did: &str, operation: &Value) -> Result<(), PlcError> { 170 let url = format!("{}/{}", self.base_url, Self::encode_did(did)); 171 let response = self.client 172 .post(&url) 173 .json(operation) 174 .send() 175 .await?; 176 if !response.status().is_success() { 177 let status = response.status(); 178 let body = response.text().await.unwrap_or_default(); 179 return Err(PlcError::InvalidResponse(format!( 180 "HTTP {}: {}", 181 status, body 182 ))); 183 } 184 Ok(()) 185 } 186} 187pub fn cid_for_cbor(value: &Value) -> Result<String, PlcError> { 188 let cbor_bytes = serde_ipld_dagcbor::to_vec(value) 189 .map_err(|e| PlcError::Serialization(e.to_string()))?; 190 let mut hasher = Sha256::new(); 191 hasher.update(&cbor_bytes); 192 let hash = hasher.finalize(); 193 let multihash = multihash::Multihash::wrap(0x12, &hash) 194 .map_err(|e| PlcError::Serialization(e.to_string()))?; 195 let cid = cid::Cid::new_v1(0x71, multihash); 196 Ok(cid.to_string()) 197} 198pub fn sign_operation( 199 operation: &Value, 200 signing_key: &SigningKey, 201) -> Result<Value, PlcError> { 202 let mut op = operation.clone(); 203 if let Some(obj) = op.as_object_mut() { 204 obj.remove("sig"); 205 } 206 let cbor_bytes = serde_ipld_dagcbor::to_vec(&op) 207 .map_err(|e| PlcError::Serialization(e.to_string()))?; 208 let signature: Signature = signing_key.sign(&cbor_bytes); 209 let sig_bytes = signature.to_bytes(); 210 let sig_b64 = URL_SAFE_NO_PAD.encode(sig_bytes); 211 if let Some(obj) = op.as_object_mut() { 212 obj.insert("sig".to_string(), json!(sig_b64)); 213 } 214 Ok(op) 215} 216pub fn create_update_op( 217 last_op: &PlcOpOrTombstone, 218 rotation_keys: Option<Vec<String>>, 219 verification_methods: Option<HashMap<String, String>>, 220 also_known_as: Option<Vec<String>>, 221 services: Option<HashMap<String, PlcService>>, 222) -> Result<Value, PlcError> { 223 let prev_value = match last_op { 224 PlcOpOrTombstone::Operation(op) => serde_json::to_value(op) 225 .map_err(|e| PlcError::Serialization(e.to_string()))?, 226 PlcOpOrTombstone::Tombstone(t) => serde_json::to_value(t) 227 .map_err(|e| PlcError::Serialization(e.to_string()))?, 228 }; 229 let prev_cid = cid_for_cbor(&prev_value)?; 230 let (base_rotation_keys, base_verification_methods, base_also_known_as, base_services) = 231 match last_op { 232 PlcOpOrTombstone::Operation(op) => ( 233 op.rotation_keys.clone(), 234 op.verification_methods.clone(), 235 op.also_known_as.clone(), 236 op.services.clone(), 237 ), 238 PlcOpOrTombstone::Tombstone(_) => { 239 return Err(PlcError::Tombstoned); 240 } 241 }; 242 let new_op = PlcOperation { 243 op_type: "plc_operation".to_string(), 244 rotation_keys: rotation_keys.unwrap_or(base_rotation_keys), 245 verification_methods: verification_methods.unwrap_or(base_verification_methods), 246 also_known_as: also_known_as.unwrap_or(base_also_known_as), 247 services: services.unwrap_or(base_services), 248 prev: Some(prev_cid), 249 sig: None, 250 }; 251 serde_json::to_value(new_op).map_err(|e| PlcError::Serialization(e.to_string())) 252} 253pub fn signing_key_to_did_key(signing_key: &SigningKey) -> String { 254 let verifying_key = signing_key.verifying_key(); 255 let point = verifying_key.to_encoded_point(true); 256 let compressed_bytes = point.as_bytes(); 257 let mut prefixed = vec![0xe7, 0x01]; 258 prefixed.extend_from_slice(compressed_bytes); 259 let encoded = multibase::encode(multibase::Base::Base58Btc, &prefixed); 260 format!("did:key:{}", encoded) 261} 262pub struct GenesisResult { 263 pub did: String, 264 pub signed_operation: Value, 265} 266pub fn create_genesis_operation( 267 signing_key: &SigningKey, 268 rotation_key: &str, 269 handle: &str, 270 pds_endpoint: &str, 271) -> Result<GenesisResult, PlcError> { 272 let signing_did_key = signing_key_to_did_key(signing_key); 273 let mut verification_methods = HashMap::new(); 274 verification_methods.insert("atproto".to_string(), signing_did_key.clone()); 275 let mut services = HashMap::new(); 276 services.insert( 277 "atproto_pds".to_string(), 278 PlcService { 279 service_type: "AtprotoPersonalDataServer".to_string(), 280 endpoint: pds_endpoint.to_string(), 281 }, 282 ); 283 let genesis_op = PlcOperation { 284 op_type: "plc_operation".to_string(), 285 rotation_keys: vec![rotation_key.to_string()], 286 verification_methods, 287 also_known_as: vec![format!("at://{}", handle)], 288 services, 289 prev: None, 290 sig: None, 291 }; 292 let genesis_value = serde_json::to_value(&genesis_op) 293 .map_err(|e| PlcError::Serialization(e.to_string()))?; 294 let signed_op = sign_operation(&genesis_value, signing_key)?; 295 let did = did_for_genesis_op(&signed_op)?; 296 Ok(GenesisResult { 297 did, 298 signed_operation: signed_op, 299 }) 300} 301pub fn did_for_genesis_op(signed_op: &Value) -> Result<String, PlcError> { 302 let cbor_bytes = serde_ipld_dagcbor::to_vec(signed_op) 303 .map_err(|e| PlcError::Serialization(e.to_string()))?; 304 let mut hasher = Sha256::new(); 305 hasher.update(&cbor_bytes); 306 let hash = hasher.finalize(); 307 let encoded = base32::encode(Alphabet::Rfc4648Lower { padding: false }, &hash); 308 let truncated = &encoded[..24]; 309 Ok(format!("did:plc:{}", truncated)) 310} 311pub fn validate_plc_operation(op: &Value) -> Result<(), PlcError> { 312 let obj = op.as_object() 313 .ok_or_else(|| PlcError::InvalidResponse("Operation must be an object".to_string()))?; 314 let op_type = obj.get("type") 315 .and_then(|v| v.as_str()) 316 .ok_or_else(|| PlcError::InvalidResponse("Missing type field".to_string()))?; 317 if op_type != "plc_operation" && op_type != "plc_tombstone" { 318 return Err(PlcError::InvalidResponse(format!("Invalid type: {}", op_type))); 319 } 320 if op_type == "plc_operation" { 321 if obj.get("rotationKeys").is_none() { 322 return Err(PlcError::InvalidResponse("Missing rotationKeys".to_string())); 323 } 324 if obj.get("verificationMethods").is_none() { 325 return Err(PlcError::InvalidResponse("Missing verificationMethods".to_string())); 326 } 327 if obj.get("alsoKnownAs").is_none() { 328 return Err(PlcError::InvalidResponse("Missing alsoKnownAs".to_string())); 329 } 330 if obj.get("services").is_none() { 331 return Err(PlcError::InvalidResponse("Missing services".to_string())); 332 } 333 } 334 if obj.get("sig").is_none() { 335 return Err(PlcError::InvalidResponse("Missing sig".to_string())); 336 } 337 Ok(()) 338} 339pub struct PlcValidationContext { 340 pub server_rotation_key: String, 341 pub expected_signing_key: String, 342 pub expected_handle: String, 343 pub expected_pds_endpoint: String, 344} 345pub fn validate_plc_operation_for_submission( 346 op: &Value, 347 ctx: &PlcValidationContext, 348) -> Result<(), PlcError> { 349 validate_plc_operation(op)?; 350 let obj = op.as_object() 351 .ok_or_else(|| PlcError::InvalidResponse("Operation must be an object".to_string()))?; 352 let op_type = obj.get("type") 353 .and_then(|v| v.as_str()) 354 .unwrap_or(""); 355 if op_type != "plc_operation" { 356 return Ok(()); 357 } 358 let rotation_keys = obj.get("rotationKeys") 359 .and_then(|v| v.as_array()) 360 .ok_or_else(|| PlcError::InvalidResponse("rotationKeys must be an array".to_string()))?; 361 let rotation_key_strings: Vec<&str> = rotation_keys 362 .iter() 363 .filter_map(|v| v.as_str()) 364 .collect(); 365 if !rotation_key_strings.contains(&ctx.server_rotation_key.as_str()) { 366 return Err(PlcError::InvalidResponse( 367 "Rotation keys do not include server's rotation key".to_string() 368 )); 369 } 370 let verification_methods = obj.get("verificationMethods") 371 .and_then(|v| v.as_object()) 372 .ok_or_else(|| PlcError::InvalidResponse("verificationMethods must be an object".to_string()))?; 373 if let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str()) { 374 if atproto_key != ctx.expected_signing_key { 375 return Err(PlcError::InvalidResponse("Incorrect signing key".to_string())); 376 } 377 } 378 let also_known_as = obj.get("alsoKnownAs") 379 .and_then(|v| v.as_array()) 380 .ok_or_else(|| PlcError::InvalidResponse("alsoKnownAs must be an array".to_string()))?; 381 let expected_handle_uri = format!("at://{}", ctx.expected_handle); 382 let has_correct_handle = also_known_as 383 .iter() 384 .filter_map(|v| v.as_str()) 385 .any(|s| s == expected_handle_uri); 386 if !has_correct_handle && !also_known_as.is_empty() { 387 return Err(PlcError::InvalidResponse( 388 "Incorrect handle in alsoKnownAs".to_string() 389 )); 390 } 391 let services = obj.get("services") 392 .and_then(|v| v.as_object()) 393 .ok_or_else(|| PlcError::InvalidResponse("services must be an object".to_string()))?; 394 if let Some(pds_service) = services.get("atproto_pds").and_then(|v| v.as_object()) { 395 let service_type = pds_service.get("type").and_then(|v| v.as_str()).unwrap_or(""); 396 if service_type != "AtprotoPersonalDataServer" { 397 return Err(PlcError::InvalidResponse( 398 "Incorrect type on atproto_pds service".to_string() 399 )); 400 } 401 let endpoint = pds_service.get("endpoint").and_then(|v| v.as_str()).unwrap_or(""); 402 if endpoint != ctx.expected_pds_endpoint { 403 return Err(PlcError::InvalidResponse( 404 "Incorrect endpoint on atproto_pds service".to_string() 405 )); 406 } 407 } 408 Ok(()) 409} 410pub fn verify_operation_signature( 411 op: &Value, 412 rotation_keys: &[String], 413) -> Result<bool, PlcError> { 414 let obj = op.as_object() 415 .ok_or_else(|| PlcError::InvalidResponse("Operation must be an object".to_string()))?; 416 let sig_b64 = obj.get("sig") 417 .and_then(|v| v.as_str()) 418 .ok_or_else(|| PlcError::InvalidResponse("Missing sig".to_string()))?; 419 let sig_bytes = URL_SAFE_NO_PAD 420 .decode(sig_b64) 421 .map_err(|e| PlcError::InvalidResponse(format!("Invalid signature encoding: {}", e)))?; 422 let signature = Signature::from_slice(&sig_bytes) 423 .map_err(|e| PlcError::InvalidResponse(format!("Invalid signature format: {}", e)))?; 424 let mut unsigned_op = op.clone(); 425 if let Some(unsigned_obj) = unsigned_op.as_object_mut() { 426 unsigned_obj.remove("sig"); 427 } 428 let cbor_bytes = serde_ipld_dagcbor::to_vec(&unsigned_op) 429 .map_err(|e| PlcError::Serialization(e.to_string()))?; 430 for key_did in rotation_keys { 431 if let Ok(true) = verify_signature_with_did_key(key_did, &cbor_bytes, &signature) { 432 return Ok(true); 433 } 434 } 435 Ok(false) 436} 437fn verify_signature_with_did_key( 438 did_key: &str, 439 message: &[u8], 440 signature: &Signature, 441) -> Result<bool, PlcError> { 442 use k256::ecdsa::{VerifyingKey, signature::Verifier}; 443 if !did_key.starts_with("did:key:z") { 444 return Err(PlcError::InvalidResponse("Invalid did:key format".to_string())); 445 } 446 let multibase_part = &did_key[8..]; 447 let (_, decoded) = multibase::decode(multibase_part) 448 .map_err(|e| PlcError::InvalidResponse(format!("Failed to decode did:key: {}", e)))?; 449 if decoded.len() < 2 { 450 return Err(PlcError::InvalidResponse("Invalid did:key data".to_string())); 451 } 452 let (codec, key_bytes) = if decoded[0] == 0xe7 && decoded[1] == 0x01 { 453 (0xe701u16, &decoded[2..]) 454 } else { 455 return Err(PlcError::InvalidResponse("Unsupported key type in did:key".to_string())); 456 }; 457 if codec != 0xe701 { 458 return Err(PlcError::InvalidResponse("Only secp256k1 keys are supported".to_string())); 459 } 460 let verifying_key = VerifyingKey::from_sec1_bytes(key_bytes) 461 .map_err(|e| PlcError::InvalidResponse(format!("Invalid public key: {}", e)))?; 462 Ok(verifying_key.verify(message, signature).is_ok()) 463} 464#[cfg(test)] 465mod tests { 466 use super::*; 467 #[test] 468 fn test_signing_key_to_did_key() { 469 let key = SigningKey::random(&mut rand::thread_rng()); 470 let did_key = signing_key_to_did_key(&key); 471 assert!(did_key.starts_with("did:key:z")); 472 } 473 #[test] 474 fn test_cid_for_cbor() { 475 let value = json!({ 476 "test": "data", 477 "number": 42 478 }); 479 let cid = cid_for_cbor(&value).unwrap(); 480 assert!(cid.starts_with("bafyrei")); 481 } 482 #[test] 483 fn test_sign_operation() { 484 let key = SigningKey::random(&mut rand::thread_rng()); 485 let op = json!({ 486 "type": "plc_operation", 487 "rotationKeys": [], 488 "verificationMethods": {}, 489 "alsoKnownAs": [], 490 "services": {}, 491 "prev": null 492 }); 493 let signed = sign_operation(&op, &key).unwrap(); 494 assert!(signed.get("sig").is_some()); 495 } 496}