this repo has no description
1use bytes::Bytes; 2use cid::Cid; 3use jacquard::common::IntoStatic; 4use jacquard::common::types::crypto::PublicKey; 5use jacquard::common::types::did_doc::DidDocument; 6use jacquard_repo::commit::Commit; 7use reqwest::Client; 8use std::collections::HashMap; 9use thiserror::Error; 10use tracing::{debug, warn}; 11 12#[derive(Error, Debug)] 13pub enum VerifyError { 14 #[error("Invalid commit: {0}")] 15 InvalidCommit(String), 16 #[error("DID mismatch: commit has {commit_did}, expected {expected_did}")] 17 DidMismatch { 18 commit_did: String, 19 expected_did: String, 20 }, 21 #[error("Failed to resolve DID: {0}")] 22 DidResolutionFailed(String), 23 #[error("No signing key found in DID document")] 24 NoSigningKey, 25 #[error("Invalid signature")] 26 InvalidSignature, 27 #[error("MST validation failed: {0}")] 28 MstValidationFailed(String), 29 #[error("Block not found: {0}")] 30 BlockNotFound(String), 31 #[error("Invalid CBOR: {0}")] 32 InvalidCbor(String), 33} 34 35pub struct CarVerifier { 36 http_client: Client, 37} 38 39impl Default for CarVerifier { 40 fn default() -> Self { 41 Self::new() 42 } 43} 44 45impl CarVerifier { 46 pub fn new() -> Self { 47 Self { 48 http_client: Client::builder() 49 .timeout(std::time::Duration::from_secs(10)) 50 .build() 51 .unwrap_or_default(), 52 } 53 } 54 55 pub async fn verify_car( 56 &self, 57 expected_did: &str, 58 root_cid: &Cid, 59 blocks: &HashMap<Cid, Bytes>, 60 ) -> Result<VerifiedCar, VerifyError> { 61 let root_block = blocks 62 .get(root_cid) 63 .ok_or_else(|| VerifyError::BlockNotFound(root_cid.to_string()))?; 64 let commit = 65 Commit::from_cbor(root_block).map_err(|e| VerifyError::InvalidCommit(e.to_string()))?; 66 let commit_did = commit.did().as_str(); 67 if commit_did != expected_did { 68 return Err(VerifyError::DidMismatch { 69 commit_did: commit_did.to_string(), 70 expected_did: expected_did.to_string(), 71 }); 72 } 73 let pubkey = self.resolve_did_signing_key(commit_did).await?; 74 commit 75 .verify(&pubkey) 76 .map_err(|_| VerifyError::InvalidSignature)?; 77 debug!("Commit signature verified for DID {}", commit_did); 78 let data_cid = commit.data(); 79 self.verify_mst_structure(data_cid, blocks)?; 80 debug!("MST structure verified for DID {}", commit_did); 81 Ok(VerifiedCar { 82 did: commit_did.to_string(), 83 rev: commit.rev().to_string(), 84 data_cid: *data_cid, 85 prev: commit.prev().cloned(), 86 }) 87 } 88 89 pub fn verify_car_structure_only( 90 &self, 91 expected_did: &str, 92 root_cid: &Cid, 93 blocks: &HashMap<Cid, Bytes>, 94 ) -> Result<VerifiedCar, VerifyError> { 95 let root_block = blocks 96 .get(root_cid) 97 .ok_or_else(|| VerifyError::BlockNotFound(root_cid.to_string()))?; 98 let commit = 99 Commit::from_cbor(root_block).map_err(|e| VerifyError::InvalidCommit(e.to_string()))?; 100 let commit_did = commit.did().as_str(); 101 if commit_did != expected_did { 102 return Err(VerifyError::DidMismatch { 103 commit_did: commit_did.to_string(), 104 expected_did: expected_did.to_string(), 105 }); 106 } 107 let data_cid = commit.data(); 108 self.verify_mst_structure(data_cid, blocks)?; 109 debug!( 110 "MST structure verified for DID {} (signature verification skipped for migration)", 111 commit_did 112 ); 113 Ok(VerifiedCar { 114 did: commit_did.to_string(), 115 rev: commit.rev().to_string(), 116 data_cid: *data_cid, 117 prev: commit.prev().cloned(), 118 }) 119 } 120 121 async fn resolve_did_signing_key(&self, did: &str) -> Result<PublicKey<'static>, VerifyError> { 122 let did_doc = self.resolve_did_document(did).await?; 123 did_doc 124 .atproto_public_key() 125 .map_err(|e| VerifyError::DidResolutionFailed(e.to_string()))? 126 .ok_or(VerifyError::NoSigningKey) 127 } 128 129 async fn resolve_did_document(&self, did: &str) -> Result<DidDocument<'static>, VerifyError> { 130 if did.starts_with("did:plc:") { 131 self.resolve_plc_did(did).await 132 } else if did.starts_with("did:web:") { 133 self.resolve_web_did(did).await 134 } else { 135 Err(VerifyError::DidResolutionFailed(format!( 136 "Unsupported DID method: {}", 137 did 138 ))) 139 } 140 } 141 142 async fn resolve_plc_did(&self, did: &str) -> Result<DidDocument<'static>, VerifyError> { 143 let plc_url = std::env::var("PLC_DIRECTORY_URL") 144 .unwrap_or_else(|_| "https://plc.directory".to_string()); 145 let url = format!("{}/{}", plc_url, urlencoding::encode(did)); 146 let response = self 147 .http_client 148 .get(&url) 149 .send() 150 .await 151 .map_err(|e| VerifyError::DidResolutionFailed(e.to_string()))?; 152 if !response.status().is_success() { 153 return Err(VerifyError::DidResolutionFailed(format!( 154 "PLC directory returned {}", 155 response.status() 156 ))); 157 } 158 let body = response 159 .text() 160 .await 161 .map_err(|e| VerifyError::DidResolutionFailed(e.to_string()))?; 162 let doc: DidDocument<'_> = serde_json::from_str(&body) 163 .map_err(|e| VerifyError::DidResolutionFailed(e.to_string()))?; 164 Ok(doc.into_static()) 165 } 166 167 async fn resolve_web_did(&self, did: &str) -> Result<DidDocument<'static>, VerifyError> { 168 let domain = did.strip_prefix("did:web:").ok_or_else(|| { 169 VerifyError::DidResolutionFailed("Invalid did:web format".to_string()) 170 })?; 171 let domain_decoded = urlencoding::decode(domain) 172 .map_err(|e| VerifyError::DidResolutionFailed(e.to_string()))?; 173 let url = format!("https://{}/.well-known/did.json", domain_decoded); 174 let response = self 175 .http_client 176 .get(&url) 177 .send() 178 .await 179 .map_err(|e| VerifyError::DidResolutionFailed(e.to_string()))?; 180 if !response.status().is_success() { 181 return Err(VerifyError::DidResolutionFailed(format!( 182 "did:web resolution returned {}", 183 response.status() 184 ))); 185 } 186 let body = response 187 .text() 188 .await 189 .map_err(|e| VerifyError::DidResolutionFailed(e.to_string()))?; 190 let doc: DidDocument<'_> = serde_json::from_str(&body) 191 .map_err(|e| VerifyError::DidResolutionFailed(e.to_string()))?; 192 Ok(doc.into_static()) 193 } 194 195 fn verify_mst_structure( 196 &self, 197 data_cid: &Cid, 198 blocks: &HashMap<Cid, Bytes>, 199 ) -> Result<(), VerifyError> { 200 use ipld_core::ipld::Ipld; 201 202 let mut stack = vec![*data_cid]; 203 let mut visited = std::collections::HashSet::new(); 204 let mut node_count = 0; 205 const MAX_NODES: usize = 100_000; 206 while let Some(cid) = stack.pop() { 207 if visited.contains(&cid) { 208 continue; 209 } 210 visited.insert(cid); 211 node_count += 1; 212 if node_count > MAX_NODES { 213 return Err(VerifyError::MstValidationFailed( 214 "MST exceeds maximum node count".to_string(), 215 )); 216 } 217 let block = blocks 218 .get(&cid) 219 .ok_or_else(|| VerifyError::BlockNotFound(cid.to_string()))?; 220 let node: Ipld = serde_ipld_dagcbor::from_slice(block) 221 .map_err(|e| VerifyError::InvalidCbor(e.to_string()))?; 222 if let Ipld::Map(ref obj) = node { 223 if let Some(Ipld::Link(left_cid)) = obj.get("l") { 224 if !blocks.contains_key(left_cid) { 225 return Err(VerifyError::BlockNotFound(format!( 226 "MST left pointer {} not in CAR", 227 left_cid 228 ))); 229 } 230 stack.push(*left_cid); 231 } 232 if let Some(Ipld::List(entries)) = obj.get("e") { 233 let mut last_full_key: Vec<u8> = Vec::new(); 234 for entry in entries { 235 if let Ipld::Map(entry_obj) = entry { 236 let prefix_len = entry_obj 237 .get("p") 238 .and_then(|p| match p { 239 Ipld::Integer(i) => Some(*i as usize), 240 _ => None, 241 }) 242 .unwrap_or(0); 243 let key_suffix = entry_obj.get("k").and_then(|k| match k { 244 Ipld::Bytes(b) => Some(b.clone()), 245 Ipld::String(s) => Some(s.as_bytes().to_vec()), 246 _ => None, 247 }); 248 if let Some(suffix) = key_suffix { 249 let mut full_key = Vec::new(); 250 if prefix_len > 0 && prefix_len <= last_full_key.len() { 251 full_key.extend_from_slice(&last_full_key[..prefix_len]); 252 } 253 full_key.extend_from_slice(&suffix); 254 if !last_full_key.is_empty() && full_key <= last_full_key { 255 return Err(VerifyError::MstValidationFailed( 256 "MST keys not in sorted order".to_string(), 257 )); 258 } 259 last_full_key = full_key; 260 } 261 if let Some(Ipld::Link(tree_cid)) = entry_obj.get("t") { 262 if !blocks.contains_key(tree_cid) { 263 return Err(VerifyError::BlockNotFound(format!( 264 "MST subtree {} not in CAR", 265 tree_cid 266 ))); 267 } 268 stack.push(*tree_cid); 269 } 270 if let Some(Ipld::Link(value_cid)) = entry_obj.get("v") 271 && !blocks.contains_key(value_cid) 272 { 273 warn!( 274 "Record block {} referenced in MST not in CAR (may be expected for partial export)", 275 value_cid 276 ); 277 } 278 } 279 } 280 } 281 } 282 } 283 debug!( 284 "MST validation complete: {} nodes, {} blocks visited", 285 node_count, 286 visited.len() 287 ); 288 Ok(()) 289 } 290} 291 292#[derive(Debug, Clone)] 293pub struct VerifiedCar { 294 pub did: String, 295 pub rev: String, 296 pub data_cid: Cid, 297 pub prev: Option<Cid>, 298} 299 300#[cfg(test)] 301#[path = "verify_tests.rs"] 302mod tests;