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}