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