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