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