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}