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}