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
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_signing_key_to_did_key() {
328 let key = SigningKey::random(&mut rand::thread_rng());
329 let did_key = signing_key_to_did_key(&key);
330 assert!(did_key.starts_with("did:key:z"));
331 }
332
333 #[test]
334 fn test_cid_for_cbor() {
335 let value = json!({
336 "test": "data",
337 "number": 42
338 });
339 let cid = cid_for_cbor(&value).unwrap();
340 assert!(cid.starts_with("bafyrei"));
341 }
342
343 #[test]
344 fn test_sign_operation() {
345 let key = SigningKey::random(&mut rand::thread_rng());
346 let op = json!({
347 "type": "plc_operation",
348 "rotationKeys": [],
349 "verificationMethods": {},
350 "alsoKnownAs": [],
351 "services": {},
352 "prev": null
353 });
354
355 let signed = sign_operation(&op, &key).unwrap();
356 assert!(signed.get("sig").is_some());
357 }
358}