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}