this repo has no description
1mod common; 2use common::*; 3 4use cid::Cid; 5use ipld_core::ipld::Ipld; 6use jacquard::types::{integer::LimitedU32, string::Tid}; 7use k256::ecdsa::{signature::Signer, Signature, SigningKey}; 8use reqwest::StatusCode; 9use serde_json::json; 10use sha2::{Digest, Sha256}; 11use sqlx::PgPool; 12use std::collections::BTreeMap; 13use wiremock::matchers::{method, path}; 14use wiremock::{Mock, MockServer, ResponseTemplate}; 15 16fn make_cid(data: &[u8]) -> Cid { 17 let mut hasher = Sha256::new(); 18 hasher.update(data); 19 let hash = hasher.finalize(); 20 let multihash = multihash::Multihash::wrap(0x12, &hash).unwrap(); 21 Cid::new_v1(0x71, multihash) 22} 23 24fn write_varint(buf: &mut Vec<u8>, mut value: u64) { 25 loop { 26 let mut byte = (value & 0x7F) as u8; 27 value >>= 7; 28 if value != 0 { 29 byte |= 0x80; 30 } 31 buf.push(byte); 32 if value == 0 { 33 break; 34 } 35 } 36} 37 38fn encode_car_block(cid: &Cid, data: &[u8]) -> Vec<u8> { 39 let cid_bytes = cid.to_bytes(); 40 let mut result = Vec::new(); 41 write_varint(&mut result, (cid_bytes.len() + data.len()) as u64); 42 result.extend_from_slice(&cid_bytes); 43 result.extend_from_slice(data); 44 result 45} 46 47fn get_multikey_from_signing_key(signing_key: &SigningKey) -> String { 48 let public_key = signing_key.verifying_key(); 49 let compressed = public_key.to_sec1_bytes(); 50 51 fn encode_uvarint(mut x: u64) -> Vec<u8> { 52 let mut out = Vec::new(); 53 while x >= 0x80 { 54 out.push(((x as u8) & 0x7F) | 0x80); 55 x >>= 7; 56 } 57 out.push(x as u8); 58 out 59 } 60 61 let mut buf = encode_uvarint(0xE7); 62 buf.extend_from_slice(&compressed); 63 multibase::encode(multibase::Base::Base58Btc, buf) 64} 65 66fn create_did_document(did: &str, handle: &str, signing_key: &SigningKey, pds_endpoint: &str) -> serde_json::Value { 67 let multikey = get_multikey_from_signing_key(signing_key); 68 69 json!({ 70 "@context": [ 71 "https://www.w3.org/ns/did/v1", 72 "https://w3id.org/security/multikey/v1" 73 ], 74 "id": did, 75 "alsoKnownAs": [format!("at://{}", handle)], 76 "verificationMethod": [{ 77 "id": format!("{}#atproto", did), 78 "type": "Multikey", 79 "controller": did, 80 "publicKeyMultibase": multikey 81 }], 82 "service": [{ 83 "id": "#atproto_pds", 84 "type": "AtprotoPersonalDataServer", 85 "serviceEndpoint": pds_endpoint 86 }] 87 }) 88} 89 90fn create_signed_commit( 91 did: &str, 92 data_cid: &Cid, 93 signing_key: &SigningKey, 94) -> (Vec<u8>, Cid) { 95 let rev = Tid::now(LimitedU32::MIN).to_string(); 96 97 let unsigned = Ipld::Map(BTreeMap::from([ 98 ("data".to_string(), Ipld::Link(*data_cid)), 99 ("did".to_string(), Ipld::String(did.to_string())), 100 ("prev".to_string(), Ipld::Null), 101 ("rev".to_string(), Ipld::String(rev.clone())), 102 ("sig".to_string(), Ipld::Bytes(vec![])), 103 ("version".to_string(), Ipld::Integer(3)), 104 ])); 105 106 let unsigned_bytes = serde_ipld_dagcbor::to_vec(&unsigned).unwrap(); 107 108 let signature: Signature = signing_key.sign(&unsigned_bytes); 109 let sig_bytes = signature.to_bytes().to_vec(); 110 111 let signed = Ipld::Map(BTreeMap::from([ 112 ("data".to_string(), Ipld::Link(*data_cid)), 113 ("did".to_string(), Ipld::String(did.to_string())), 114 ("prev".to_string(), Ipld::Null), 115 ("rev".to_string(), Ipld::String(rev)), 116 ("sig".to_string(), Ipld::Bytes(sig_bytes)), 117 ("version".to_string(), Ipld::Integer(3)), 118 ])); 119 120 let signed_bytes = serde_ipld_dagcbor::to_vec(&signed).unwrap(); 121 let cid = make_cid(&signed_bytes); 122 123 (signed_bytes, cid) 124} 125 126fn create_mst_node(entries: Vec<(String, Cid)>) -> (Vec<u8>, Cid) { 127 let ipld_entries: Vec<Ipld> = entries 128 .into_iter() 129 .map(|(key, value_cid)| { 130 Ipld::Map(BTreeMap::from([ 131 ("k".to_string(), Ipld::Bytes(key.into_bytes())), 132 ("v".to_string(), Ipld::Link(value_cid)), 133 ("p".to_string(), Ipld::Integer(0)), 134 ])) 135 }) 136 .collect(); 137 138 let node = Ipld::Map(BTreeMap::from([ 139 ("e".to_string(), Ipld::List(ipld_entries)), 140 ])); 141 142 let bytes = serde_ipld_dagcbor::to_vec(&node).unwrap(); 143 let cid = make_cid(&bytes); 144 (bytes, cid) 145} 146 147fn create_record() -> (Vec<u8>, Cid) { 148 let record = Ipld::Map(BTreeMap::from([ 149 ("$type".to_string(), Ipld::String("app.bsky.feed.post".to_string())), 150 ("text".to_string(), Ipld::String("Test post for verification".to_string())), 151 ("createdAt".to_string(), Ipld::String("2024-01-01T00:00:00Z".to_string())), 152 ])); 153 154 let bytes = serde_ipld_dagcbor::to_vec(&record).unwrap(); 155 let cid = make_cid(&bytes); 156 (bytes, cid) 157} 158 159fn build_car_with_signature( 160 did: &str, 161 signing_key: &SigningKey, 162) -> (Vec<u8>, Cid) { 163 let (record_bytes, record_cid) = create_record(); 164 165 let (mst_bytes, mst_cid) = create_mst_node(vec![ 166 ("app.bsky.feed.post/test123".to_string(), record_cid), 167 ]); 168 169 let (commit_bytes, commit_cid) = create_signed_commit(did, &mst_cid, signing_key); 170 171 let header = iroh_car::CarHeader::new_v1(vec![commit_cid]); 172 let header_bytes = header.encode().unwrap(); 173 174 let mut car = Vec::new(); 175 write_varint(&mut car, header_bytes.len() as u64); 176 car.extend_from_slice(&header_bytes); 177 car.extend(encode_car_block(&commit_cid, &commit_bytes)); 178 car.extend(encode_car_block(&mst_cid, &mst_bytes)); 179 car.extend(encode_car_block(&record_cid, &record_bytes)); 180 181 (car, commit_cid) 182} 183 184async fn setup_mock_plc_directory(did: &str, did_doc: serde_json::Value) -> MockServer { 185 let mock_server = MockServer::start().await; 186 187 let did_encoded = urlencoding::encode(did); 188 let did_path = format!("/{}", did_encoded); 189 190 Mock::given(method("GET")) 191 .and(path(did_path)) 192 .respond_with(ResponseTemplate::new(200).set_body_json(did_doc)) 193 .mount(&mock_server) 194 .await; 195 196 mock_server 197} 198 199async fn get_user_signing_key(did: &str) -> Option<Vec<u8>> { 200 let db_url = get_db_connection_string().await; 201 let pool = PgPool::connect(&db_url).await.ok()?; 202 203 let row = sqlx::query!( 204 r#" 205 SELECT k.key_bytes, k.encryption_version 206 FROM user_keys k 207 JOIN users u ON k.user_id = u.id 208 WHERE u.did = $1 209 "#, 210 did 211 ) 212 .fetch_optional(&pool) 213 .await 214 .ok()??; 215 216 bspds::config::decrypt_key(&row.key_bytes, row.encryption_version).ok() 217} 218 219#[tokio::test] 220#[ignore = "requires exclusive env var access; run with: cargo test test_import_with_valid_signature_and_mock_plc -- --ignored --test-threads=1"] 221async fn test_import_with_valid_signature_and_mock_plc() { 222 let client = client(); 223 let (token, did) = create_account_and_login(&client).await; 224 225 let key_bytes = get_user_signing_key(&did).await 226 .expect("Failed to get user signing key"); 227 let signing_key = SigningKey::from_slice(&key_bytes) 228 .expect("Failed to create signing key"); 229 230 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 231 let pds_endpoint = format!("https://{}", hostname); 232 233 let handle = did.split(':').last().unwrap_or("user"); 234 let did_doc = create_did_document(&did, handle, &signing_key, &pds_endpoint); 235 236 let mock_plc = setup_mock_plc_directory(&did, did_doc).await; 237 238 unsafe { 239 std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri()); 240 std::env::remove_var("SKIP_IMPORT_VERIFICATION"); 241 } 242 243 let (car_bytes, _root_cid) = build_car_with_signature(&did, &signing_key); 244 245 let import_res = client 246 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await)) 247 .bearer_auth(&token) 248 .header("Content-Type", "application/vnd.ipld.car") 249 .body(car_bytes) 250 .send() 251 .await 252 .expect("Import request failed"); 253 254 let status = import_res.status(); 255 let body: serde_json::Value = import_res.json().await.unwrap_or(json!({})); 256 257 unsafe { 258 std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); 259 } 260 261 assert_eq!( 262 status, 263 StatusCode::OK, 264 "Import with valid signature should succeed. Response: {:?}", 265 body 266 ); 267} 268 269#[tokio::test] 270#[ignore = "requires exclusive env var access; run with: cargo test test_import_with_wrong_signing_key_fails -- --ignored --test-threads=1"] 271async fn test_import_with_wrong_signing_key_fails() { 272 let client = client(); 273 let (token, did) = create_account_and_login(&client).await; 274 275 let wrong_signing_key = SigningKey::random(&mut rand::thread_rng()); 276 277 let key_bytes = get_user_signing_key(&did).await 278 .expect("Failed to get user signing key"); 279 let correct_signing_key = SigningKey::from_slice(&key_bytes) 280 .expect("Failed to create signing key"); 281 282 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 283 let pds_endpoint = format!("https://{}", hostname); 284 285 let handle = did.split(':').last().unwrap_or("user"); 286 let did_doc = create_did_document(&did, handle, &correct_signing_key, &pds_endpoint); 287 288 let mock_plc = setup_mock_plc_directory(&did, did_doc).await; 289 290 unsafe { 291 std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri()); 292 std::env::remove_var("SKIP_IMPORT_VERIFICATION"); 293 } 294 295 let (car_bytes, _root_cid) = build_car_with_signature(&did, &wrong_signing_key); 296 297 let import_res = client 298 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await)) 299 .bearer_auth(&token) 300 .header("Content-Type", "application/vnd.ipld.car") 301 .body(car_bytes) 302 .send() 303 .await 304 .expect("Import request failed"); 305 306 let status = import_res.status(); 307 let body: serde_json::Value = import_res.json().await.unwrap_or(json!({})); 308 309 unsafe { 310 std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); 311 } 312 313 assert_eq!( 314 status, 315 StatusCode::BAD_REQUEST, 316 "Import with wrong signature should fail. Response: {:?}", 317 body 318 ); 319 assert!( 320 body["error"] == "InvalidSignature" || body["message"].as_str().unwrap_or("").contains("signature"), 321 "Error should mention signature: {:?}", 322 body 323 ); 324} 325 326#[tokio::test] 327#[ignore = "requires exclusive env var access; run with: cargo test test_import_with_did_mismatch_fails -- --ignored --test-threads=1"] 328async fn test_import_with_did_mismatch_fails() { 329 let client = client(); 330 let (token, did) = create_account_and_login(&client).await; 331 332 let key_bytes = get_user_signing_key(&did).await 333 .expect("Failed to get user signing key"); 334 let signing_key = SigningKey::from_slice(&key_bytes) 335 .expect("Failed to create signing key"); 336 337 let wrong_did = "did:plc:wrongdidthatdoesnotmatch"; 338 339 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 340 let pds_endpoint = format!("https://{}", hostname); 341 342 let handle = did.split(':').last().unwrap_or("user"); 343 let did_doc = create_did_document(&did, handle, &signing_key, &pds_endpoint); 344 345 let mock_plc = setup_mock_plc_directory(&did, did_doc).await; 346 347 unsafe { 348 std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri()); 349 std::env::remove_var("SKIP_IMPORT_VERIFICATION"); 350 } 351 352 let (car_bytes, _root_cid) = build_car_with_signature(wrong_did, &signing_key); 353 354 let import_res = client 355 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await)) 356 .bearer_auth(&token) 357 .header("Content-Type", "application/vnd.ipld.car") 358 .body(car_bytes) 359 .send() 360 .await 361 .expect("Import request failed"); 362 363 let status = import_res.status(); 364 let body: serde_json::Value = import_res.json().await.unwrap_or(json!({})); 365 366 unsafe { 367 std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); 368 } 369 370 assert_eq!( 371 status, 372 StatusCode::FORBIDDEN, 373 "Import with DID mismatch should be forbidden. Response: {:?}", 374 body 375 ); 376} 377 378#[tokio::test] 379#[ignore = "requires exclusive env var access; run with: cargo test test_import_with_plc_resolution_failure -- --ignored --test-threads=1"] 380async fn test_import_with_plc_resolution_failure() { 381 let client = client(); 382 let (token, did) = create_account_and_login(&client).await; 383 384 let key_bytes = get_user_signing_key(&did).await 385 .expect("Failed to get user signing key"); 386 let signing_key = SigningKey::from_slice(&key_bytes) 387 .expect("Failed to create signing key"); 388 389 let mock_plc = MockServer::start().await; 390 391 let did_encoded = urlencoding::encode(&did); 392 let did_path = format!("/{}", did_encoded); 393 Mock::given(method("GET")) 394 .and(path(did_path)) 395 .respond_with(ResponseTemplate::new(404)) 396 .mount(&mock_plc) 397 .await; 398 399 unsafe { 400 std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri()); 401 std::env::remove_var("SKIP_IMPORT_VERIFICATION"); 402 } 403 404 let (car_bytes, _root_cid) = build_car_with_signature(&did, &signing_key); 405 406 let import_res = client 407 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await)) 408 .bearer_auth(&token) 409 .header("Content-Type", "application/vnd.ipld.car") 410 .body(car_bytes) 411 .send() 412 .await 413 .expect("Import request failed"); 414 415 let status = import_res.status(); 416 let body: serde_json::Value = import_res.json().await.unwrap_or(json!({})); 417 418 unsafe { 419 std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); 420 } 421 422 assert_eq!( 423 status, 424 StatusCode::BAD_REQUEST, 425 "Import with PLC resolution failure should fail. Response: {:?}", 426 body 427 ); 428} 429 430#[tokio::test] 431#[ignore = "requires exclusive env var access; run with: cargo test test_import_with_no_signing_key_in_did_doc -- --ignored --test-threads=1"] 432async fn test_import_with_no_signing_key_in_did_doc() { 433 let client = client(); 434 let (token, did) = create_account_and_login(&client).await; 435 436 let key_bytes = get_user_signing_key(&did).await 437 .expect("Failed to get user signing key"); 438 let signing_key = SigningKey::from_slice(&key_bytes) 439 .expect("Failed to create signing key"); 440 441 let handle = did.split(':').last().unwrap_or("user"); 442 let did_doc_without_key = json!({ 443 "@context": ["https://www.w3.org/ns/did/v1"], 444 "id": did, 445 "alsoKnownAs": [format!("at://{}", handle)], 446 "verificationMethod": [], 447 "service": [] 448 }); 449 450 let mock_plc = setup_mock_plc_directory(&did, did_doc_without_key).await; 451 452 unsafe { 453 std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri()); 454 std::env::remove_var("SKIP_IMPORT_VERIFICATION"); 455 } 456 457 let (car_bytes, _root_cid) = build_car_with_signature(&did, &signing_key); 458 459 let import_res = client 460 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await)) 461 .bearer_auth(&token) 462 .header("Content-Type", "application/vnd.ipld.car") 463 .body(car_bytes) 464 .send() 465 .await 466 .expect("Import request failed"); 467 468 let status = import_res.status(); 469 let body: serde_json::Value = import_res.json().await.unwrap_or(json!({})); 470 471 unsafe { 472 std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); 473 } 474 475 assert_eq!( 476 status, 477 StatusCode::BAD_REQUEST, 478 "Import with missing signing key should fail. Response: {:?}", 479 body 480 ); 481}