this repo has no description
1use bytes::Bytes; 2use cid::Cid; 3use jacquard::common::types::crypto::PublicKey; 4use jacquard::common::types::did_doc::DidDocument; 5use jacquard::common::IntoStatic; 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 65 let commit = Commit::from_cbor(root_block) 66 .map_err(|e| VerifyError::InvalidCommit(e.to_string()))?; 67 68 let commit_did = commit.did().as_str(); 69 if commit_did != expected_did { 70 return Err(VerifyError::DidMismatch { 71 commit_did: commit_did.to_string(), 72 expected_did: expected_did.to_string(), 73 }); 74 } 75 76 let pubkey = self.resolve_did_signing_key(commit_did).await?; 77 78 commit 79 .verify(&pubkey) 80 .map_err(|_| VerifyError::InvalidSignature)?; 81 82 debug!("Commit signature verified for DID {}", commit_did); 83 84 let data_cid = commit.data(); 85 self.verify_mst_structure(data_cid, blocks)?; 86 87 debug!("MST structure verified for DID {}", commit_did); 88 89 Ok(VerifiedCar { 90 did: commit_did.to_string(), 91 rev: commit.rev().to_string(), 92 data_cid: *data_cid, 93 prev: commit.prev().cloned(), 94 }) 95 } 96 97 async fn resolve_did_signing_key(&self, did: &str) -> Result<PublicKey<'static>, VerifyError> { 98 let did_doc = self.resolve_did_document(did).await?; 99 100 did_doc 101 .atproto_public_key() 102 .map_err(|e| VerifyError::DidResolutionFailed(e.to_string()))? 103 .ok_or(VerifyError::NoSigningKey) 104 } 105 106 async fn resolve_did_document(&self, did: &str) -> Result<DidDocument<'static>, VerifyError> { 107 if did.starts_with("did:plc:") { 108 self.resolve_plc_did(did).await 109 } else if did.starts_with("did:web:") { 110 self.resolve_web_did(did).await 111 } else { 112 Err(VerifyError::DidResolutionFailed(format!( 113 "Unsupported DID method: {}", 114 did 115 ))) 116 } 117 } 118 119 async fn resolve_plc_did(&self, did: &str) -> Result<DidDocument<'static>, VerifyError> { 120 let plc_url = std::env::var("PLC_DIRECTORY_URL") 121 .unwrap_or_else(|_| "https://plc.directory".to_string()); 122 let url = format!("{}/{}", plc_url, urlencoding::encode(did)); 123 124 let response = self 125 .http_client 126 .get(&url) 127 .send() 128 .await 129 .map_err(|e| VerifyError::DidResolutionFailed(e.to_string()))?; 130 131 if !response.status().is_success() { 132 return Err(VerifyError::DidResolutionFailed(format!( 133 "PLC directory returned {}", 134 response.status() 135 ))); 136 } 137 138 let body = response 139 .text() 140 .await 141 .map_err(|e| VerifyError::DidResolutionFailed(e.to_string()))?; 142 143 let doc: DidDocument<'_> = serde_json::from_str(&body) 144 .map_err(|e| VerifyError::DidResolutionFailed(e.to_string()))?; 145 146 Ok(doc.into_static()) 147 } 148 149 async fn resolve_web_did(&self, did: &str) -> Result<DidDocument<'static>, VerifyError> { 150 let domain = did 151 .strip_prefix("did:web:") 152 .ok_or_else(|| VerifyError::DidResolutionFailed("Invalid did:web format".to_string()))?; 153 154 let domain_decoded = urlencoding::decode(domain) 155 .map_err(|e| VerifyError::DidResolutionFailed(e.to_string()))?; 156 157 let url = if domain_decoded.contains(':') || domain_decoded.contains('/') { 158 format!("https://{}/.well-known/did.json", domain_decoded) 159 } else { 160 format!("https://{}/.well-known/did.json", domain_decoded) 161 }; 162 163 let response = self 164 .http_client 165 .get(&url) 166 .send() 167 .await 168 .map_err(|e| VerifyError::DidResolutionFailed(e.to_string()))?; 169 170 if !response.status().is_success() { 171 return Err(VerifyError::DidResolutionFailed(format!( 172 "did:web resolution returned {}", 173 response.status() 174 ))); 175 } 176 177 let body = response 178 .text() 179 .await 180 .map_err(|e| VerifyError::DidResolutionFailed(e.to_string()))?; 181 182 let doc: DidDocument<'_> = serde_json::from_str(&body) 183 .map_err(|e| VerifyError::DidResolutionFailed(e.to_string()))?; 184 185 Ok(doc.into_static()) 186 } 187 188 fn verify_mst_structure( 189 &self, 190 data_cid: &Cid, 191 blocks: &HashMap<Cid, Bytes>, 192 ) -> Result<(), VerifyError> { 193 use ipld_core::ipld::Ipld; 194 195 let mut stack = vec![*data_cid]; 196 let mut visited = std::collections::HashSet::new(); 197 let mut node_count = 0; 198 const MAX_NODES: usize = 100_000; 199 200 while let Some(cid) = stack.pop() { 201 if visited.contains(&cid) { 202 continue; 203 } 204 visited.insert(cid); 205 node_count += 1; 206 207 if node_count > MAX_NODES { 208 return Err(VerifyError::MstValidationFailed( 209 "MST exceeds maximum node count".to_string(), 210 )); 211 } 212 213 let block = blocks 214 .get(&cid) 215 .ok_or_else(|| VerifyError::BlockNotFound(cid.to_string()))?; 216 217 let node: Ipld = serde_ipld_dagcbor::from_slice(block) 218 .map_err(|e| VerifyError::InvalidCbor(e.to_string()))?; 219 220 if let Ipld::Map(ref obj) = node { 221 if let Some(Ipld::Link(left_cid)) = obj.get("l") { 222 if !blocks.contains_key(left_cid) { 223 return Err(VerifyError::BlockNotFound(format!( 224 "MST left pointer {} not in CAR", 225 left_cid 226 ))); 227 } 228 stack.push(*left_cid); 229 } 230 231 if let Some(Ipld::List(entries)) = obj.get("e") { 232 let mut last_full_key: Vec<u8> = Vec::new(); 233 234 for entry in entries { 235 if let Ipld::Map(entry_obj) = entry { 236 let prefix_len = entry_obj.get("p").and_then(|p| match p { 237 Ipld::Integer(i) => Some(*i as usize), 238 _ => None, 239 }).unwrap_or(0); 240 241 let key_suffix = entry_obj.get("k").and_then(|k| match k { 242 Ipld::Bytes(b) => Some(b.clone()), 243 Ipld::String(s) => Some(s.as_bytes().to_vec()), 244 _ => None, 245 }); 246 247 if let Some(suffix) = key_suffix { 248 let mut full_key = Vec::new(); 249 if prefix_len > 0 && prefix_len <= last_full_key.len() { 250 full_key.extend_from_slice(&last_full_key[..prefix_len]); 251 } 252 full_key.extend_from_slice(&suffix); 253 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 262 if let Some(Ipld::Link(tree_cid)) = entry_obj.get("t") { 263 if !blocks.contains_key(tree_cid) { 264 return Err(VerifyError::BlockNotFound(format!( 265 "MST subtree {} not in CAR", 266 tree_cid 267 ))); 268 } 269 stack.push(*tree_cid); 270 } 271 272 if let Some(Ipld::Link(value_cid)) = entry_obj.get("v") { 273 if !blocks.contains_key(value_cid) { 274 warn!( 275 "Record block {} referenced in MST not in CAR (may be expected for partial export)", 276 value_cid 277 ); 278 } 279 } 280 } 281 } 282 } 283 } 284 } 285 286 debug!( 287 "MST validation complete: {} nodes, {} blocks visited", 288 node_count, 289 visited.len() 290 ); 291 292 Ok(()) 293 } 294} 295 296#[derive(Debug, Clone)] 297pub struct VerifiedCar { 298 pub did: String, 299 pub rev: String, 300 pub data_cid: Cid, 301 pub prev: Option<Cid>, 302} 303 304#[cfg(test)] 305mod tests { 306 use super::*; 307 use sha2::{Digest, Sha256}; 308 309 fn make_cid(data: &[u8]) -> Cid { 310 let mut hasher = Sha256::new(); 311 hasher.update(data); 312 let hash = hasher.finalize(); 313 let multihash = multihash::Multihash::wrap(0x12, &hash).unwrap(); 314 Cid::new_v1(0x71, multihash) 315 } 316 317 #[test] 318 fn test_verifier_creation() { 319 let _verifier = CarVerifier::new(); 320 } 321 322 #[test] 323 fn test_verify_error_display() { 324 let err = VerifyError::DidMismatch { 325 commit_did: "did:plc:abc".to_string(), 326 expected_did: "did:plc:xyz".to_string(), 327 }; 328 assert!(err.to_string().contains("did:plc:abc")); 329 assert!(err.to_string().contains("did:plc:xyz")); 330 331 let err = VerifyError::InvalidSignature; 332 assert!(err.to_string().contains("signature")); 333 334 let err = VerifyError::NoSigningKey; 335 assert!(err.to_string().contains("signing key")); 336 337 let err = VerifyError::MstValidationFailed("test error".to_string()); 338 assert!(err.to_string().contains("test error")); 339 } 340 341 #[test] 342 fn test_mst_validation_missing_root_block() { 343 let verifier = CarVerifier::new(); 344 let blocks: HashMap<Cid, Bytes> = HashMap::new(); 345 346 let fake_cid = make_cid(b"fake data"); 347 let result = verifier.verify_mst_structure(&fake_cid, &blocks); 348 349 assert!(result.is_err()); 350 let err = result.unwrap_err(); 351 assert!(matches!(err, VerifyError::BlockNotFound(_))); 352 } 353 354 #[test] 355 fn test_mst_validation_invalid_cbor() { 356 let verifier = CarVerifier::new(); 357 358 let bad_cbor = Bytes::from(vec![0xFF, 0xFF, 0xFF]); 359 let cid = make_cid(&bad_cbor); 360 361 let mut blocks = HashMap::new(); 362 blocks.insert(cid, bad_cbor); 363 364 let result = verifier.verify_mst_structure(&cid, &blocks); 365 366 assert!(result.is_err()); 367 let err = result.unwrap_err(); 368 assert!(matches!(err, VerifyError::InvalidCbor(_))); 369 } 370 371 #[test] 372 fn test_mst_validation_empty_node() { 373 let verifier = CarVerifier::new(); 374 375 let empty_node = serde_ipld_dagcbor::to_vec(&serde_json::json!({ 376 "e": [] 377 })).unwrap(); 378 let cid = make_cid(&empty_node); 379 380 let mut blocks = HashMap::new(); 381 blocks.insert(cid, Bytes::from(empty_node)); 382 383 let result = verifier.verify_mst_structure(&cid, &blocks); 384 assert!(result.is_ok()); 385 } 386 387 #[test] 388 fn test_mst_validation_missing_left_pointer() { 389 use ipld_core::ipld::Ipld; 390 391 let verifier = CarVerifier::new(); 392 393 let missing_left_cid = make_cid(b"missing left"); 394 let node = Ipld::Map(std::collections::BTreeMap::from([ 395 ("l".to_string(), Ipld::Link(missing_left_cid)), 396 ("e".to_string(), Ipld::List(vec![])), 397 ])); 398 let node_bytes = serde_ipld_dagcbor::to_vec(&node).unwrap(); 399 let cid = make_cid(&node_bytes); 400 401 let mut blocks = HashMap::new(); 402 blocks.insert(cid, Bytes::from(node_bytes)); 403 404 let result = verifier.verify_mst_structure(&cid, &blocks); 405 406 assert!(result.is_err()); 407 let err = result.unwrap_err(); 408 assert!(matches!(err, VerifyError::BlockNotFound(_))); 409 assert!(err.to_string().contains("left pointer")); 410 } 411 412 #[test] 413 fn test_mst_validation_missing_subtree() { 414 use ipld_core::ipld::Ipld; 415 416 let verifier = CarVerifier::new(); 417 418 let missing_subtree_cid = make_cid(b"missing subtree"); 419 let record_cid = make_cid(b"record"); 420 421 let entry = Ipld::Map(std::collections::BTreeMap::from([ 422 ("k".to_string(), Ipld::Bytes(b"key1".to_vec())), 423 ("v".to_string(), Ipld::Link(record_cid)), 424 ("p".to_string(), Ipld::Integer(0)), 425 ("t".to_string(), Ipld::Link(missing_subtree_cid)), 426 ])); 427 428 let node = Ipld::Map(std::collections::BTreeMap::from([ 429 ("e".to_string(), Ipld::List(vec![entry])), 430 ])); 431 let node_bytes = serde_ipld_dagcbor::to_vec(&node).unwrap(); 432 let cid = make_cid(&node_bytes); 433 434 let mut blocks = HashMap::new(); 435 blocks.insert(cid, Bytes::from(node_bytes)); 436 437 let result = verifier.verify_mst_structure(&cid, &blocks); 438 439 assert!(result.is_err()); 440 let err = result.unwrap_err(); 441 assert!(matches!(err, VerifyError::BlockNotFound(_))); 442 assert!(err.to_string().contains("subtree")); 443 } 444 445 #[test] 446 fn test_mst_validation_unsorted_keys() { 447 use ipld_core::ipld::Ipld; 448 449 let verifier = CarVerifier::new(); 450 451 let record_cid = make_cid(b"record"); 452 453 let entry1 = Ipld::Map(std::collections::BTreeMap::from([ 454 ("k".to_string(), Ipld::Bytes(b"zzz".to_vec())), 455 ("v".to_string(), Ipld::Link(record_cid)), 456 ("p".to_string(), Ipld::Integer(0)), 457 ])); 458 459 let entry2 = Ipld::Map(std::collections::BTreeMap::from([ 460 ("k".to_string(), Ipld::Bytes(b"aaa".to_vec())), 461 ("v".to_string(), Ipld::Link(record_cid)), 462 ("p".to_string(), Ipld::Integer(0)), 463 ])); 464 465 let node = Ipld::Map(std::collections::BTreeMap::from([ 466 ("e".to_string(), Ipld::List(vec![entry1, entry2])), 467 ])); 468 let node_bytes = serde_ipld_dagcbor::to_vec(&node).unwrap(); 469 let cid = make_cid(&node_bytes); 470 471 let mut blocks = HashMap::new(); 472 blocks.insert(cid, Bytes::from(node_bytes)); 473 474 let result = verifier.verify_mst_structure(&cid, &blocks); 475 476 assert!(result.is_err()); 477 let err = result.unwrap_err(); 478 assert!(matches!(err, VerifyError::MstValidationFailed(_))); 479 assert!(err.to_string().contains("sorted")); 480 } 481 482 #[test] 483 fn test_mst_validation_sorted_keys_ok() { 484 use ipld_core::ipld::Ipld; 485 486 let verifier = CarVerifier::new(); 487 488 let record_cid = make_cid(b"record"); 489 490 let entry1 = Ipld::Map(std::collections::BTreeMap::from([ 491 ("k".to_string(), Ipld::Bytes(b"aaa".to_vec())), 492 ("v".to_string(), Ipld::Link(record_cid)), 493 ("p".to_string(), Ipld::Integer(0)), 494 ])); 495 496 let entry2 = Ipld::Map(std::collections::BTreeMap::from([ 497 ("k".to_string(), Ipld::Bytes(b"bbb".to_vec())), 498 ("v".to_string(), Ipld::Link(record_cid)), 499 ("p".to_string(), Ipld::Integer(0)), 500 ])); 501 502 let entry3 = Ipld::Map(std::collections::BTreeMap::from([ 503 ("k".to_string(), Ipld::Bytes(b"zzz".to_vec())), 504 ("v".to_string(), Ipld::Link(record_cid)), 505 ("p".to_string(), Ipld::Integer(0)), 506 ])); 507 508 let node = Ipld::Map(std::collections::BTreeMap::from([ 509 ("e".to_string(), Ipld::List(vec![entry1, entry2, entry3])), 510 ])); 511 let node_bytes = serde_ipld_dagcbor::to_vec(&node).unwrap(); 512 let cid = make_cid(&node_bytes); 513 514 let mut blocks = HashMap::new(); 515 blocks.insert(cid, Bytes::from(node_bytes)); 516 517 let result = verifier.verify_mst_structure(&cid, &blocks); 518 assert!(result.is_ok()); 519 } 520 521 #[test] 522 fn test_mst_validation_with_valid_left_pointer() { 523 use ipld_core::ipld::Ipld; 524 525 let verifier = CarVerifier::new(); 526 527 let left_node = Ipld::Map(std::collections::BTreeMap::from([ 528 ("e".to_string(), Ipld::List(vec![])), 529 ])); 530 let left_node_bytes = serde_ipld_dagcbor::to_vec(&left_node).unwrap(); 531 let left_cid = make_cid(&left_node_bytes); 532 533 let root_node = Ipld::Map(std::collections::BTreeMap::from([ 534 ("l".to_string(), Ipld::Link(left_cid)), 535 ("e".to_string(), Ipld::List(vec![])), 536 ])); 537 let root_node_bytes = serde_ipld_dagcbor::to_vec(&root_node).unwrap(); 538 let root_cid = make_cid(&root_node_bytes); 539 540 let mut blocks = HashMap::new(); 541 blocks.insert(root_cid, Bytes::from(root_node_bytes)); 542 blocks.insert(left_cid, Bytes::from(left_node_bytes)); 543 544 let result = verifier.verify_mst_structure(&root_cid, &blocks); 545 assert!(result.is_ok()); 546 } 547 548 #[test] 549 fn test_mst_validation_cycle_detection() { 550 let verifier = CarVerifier::new(); 551 552 let node = serde_ipld_dagcbor::to_vec(&serde_json::json!({ 553 "e": [] 554 })).unwrap(); 555 let cid = make_cid(&node); 556 557 let mut blocks = HashMap::new(); 558 blocks.insert(cid, Bytes::from(node)); 559 560 let result = verifier.verify_mst_structure(&cid, &blocks); 561 assert!(result.is_ok()); 562 } 563 564 #[tokio::test] 565 async fn test_unsupported_did_method() { 566 let verifier = CarVerifier::new(); 567 let result = verifier.resolve_did_document("did:unknown:test").await; 568 569 assert!(result.is_err()); 570 let err = result.unwrap_err(); 571 assert!(matches!(err, VerifyError::DidResolutionFailed(_))); 572 assert!(err.to_string().contains("Unsupported")); 573 } 574 575 #[test] 576 fn test_mst_validation_with_prefix_compression() { 577 use ipld_core::ipld::Ipld; 578 579 let verifier = CarVerifier::new(); 580 let record_cid = make_cid(b"record"); 581 582 let entry1 = Ipld::Map(std::collections::BTreeMap::from([ 583 ("k".to_string(), Ipld::Bytes(b"app.bsky.feed.post/abc".to_vec())), 584 ("v".to_string(), Ipld::Link(record_cid)), 585 ("p".to_string(), Ipld::Integer(0)), 586 ])); 587 588 let entry2 = Ipld::Map(std::collections::BTreeMap::from([ 589 ("k".to_string(), Ipld::Bytes(b"def".to_vec())), 590 ("v".to_string(), Ipld::Link(record_cid)), 591 ("p".to_string(), Ipld::Integer(19)), 592 ])); 593 594 let entry3 = Ipld::Map(std::collections::BTreeMap::from([ 595 ("k".to_string(), Ipld::Bytes(b"xyz".to_vec())), 596 ("v".to_string(), Ipld::Link(record_cid)), 597 ("p".to_string(), Ipld::Integer(19)), 598 ])); 599 600 let node = Ipld::Map(std::collections::BTreeMap::from([ 601 ("e".to_string(), Ipld::List(vec![entry1, entry2, entry3])), 602 ])); 603 let node_bytes = serde_ipld_dagcbor::to_vec(&node).unwrap(); 604 let cid = make_cid(&node_bytes); 605 606 let mut blocks = HashMap::new(); 607 blocks.insert(cid, Bytes::from(node_bytes)); 608 609 let result = verifier.verify_mst_structure(&cid, &blocks); 610 assert!(result.is_ok(), "Prefix-compressed keys should be validated correctly"); 611 } 612 613 #[test] 614 fn test_mst_validation_prefix_compression_unsorted() { 615 use ipld_core::ipld::Ipld; 616 617 let verifier = CarVerifier::new(); 618 let record_cid = make_cid(b"record"); 619 620 let entry1 = Ipld::Map(std::collections::BTreeMap::from([ 621 ("k".to_string(), Ipld::Bytes(b"app.bsky.feed.post/xyz".to_vec())), 622 ("v".to_string(), Ipld::Link(record_cid)), 623 ("p".to_string(), Ipld::Integer(0)), 624 ])); 625 626 let entry2 = Ipld::Map(std::collections::BTreeMap::from([ 627 ("k".to_string(), Ipld::Bytes(b"abc".to_vec())), 628 ("v".to_string(), Ipld::Link(record_cid)), 629 ("p".to_string(), Ipld::Integer(19)), 630 ])); 631 632 let node = Ipld::Map(std::collections::BTreeMap::from([ 633 ("e".to_string(), Ipld::List(vec![entry1, entry2])), 634 ])); 635 let node_bytes = serde_ipld_dagcbor::to_vec(&node).unwrap(); 636 let cid = make_cid(&node_bytes); 637 638 let mut blocks = HashMap::new(); 639 blocks.insert(cid, Bytes::from(node_bytes)); 640 641 let result = verifier.verify_mst_structure(&cid, &blocks); 642 assert!(result.is_err(), "Unsorted prefix-compressed keys should fail validation"); 643 let err = result.unwrap_err(); 644 assert!(matches!(err, VerifyError::MstValidationFailed(_))); 645 } 646}