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}