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