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