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