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