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] 220async fn test_import_with_valid_signature_and_mock_plc() { 221 let client = client(); 222 let (token, did) = create_account_and_login(&client).await; 223 224 let key_bytes = get_user_signing_key(&did).await 225 .expect("Failed to get user signing key"); 226 let signing_key = SigningKey::from_slice(&key_bytes) 227 .expect("Failed to create signing key"); 228 229 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 230 let pds_endpoint = format!("https://{}", hostname); 231 232 let handle = did.split(':').last().unwrap_or("user"); 233 let did_doc = create_did_document(&did, handle, &signing_key, &pds_endpoint); 234 235 let mock_plc = setup_mock_plc_directory(&did, did_doc).await; 236 237 unsafe { 238 std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri()); 239 std::env::remove_var("SKIP_IMPORT_VERIFICATION"); 240 } 241 242 let (car_bytes, _root_cid) = build_car_with_signature(&did, &signing_key); 243 244 let import_res = client 245 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await)) 246 .bearer_auth(&token) 247 .header("Content-Type", "application/vnd.ipld.car") 248 .body(car_bytes) 249 .send() 250 .await 251 .expect("Import request failed"); 252 253 let status = import_res.status(); 254 let body: serde_json::Value = import_res.json().await.unwrap_or(json!({})); 255 256 unsafe { 257 std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); 258 } 259 260 assert_eq!( 261 status, 262 StatusCode::OK, 263 "Import with valid signature should succeed. Response: {:?}", 264 body 265 ); 266} 267 268#[tokio::test] 269async fn test_import_with_wrong_signing_key_fails() { 270 let client = client(); 271 let (token, did) = create_account_and_login(&client).await; 272 273 let wrong_signing_key = SigningKey::random(&mut rand::thread_rng()); 274 275 let key_bytes = get_user_signing_key(&did).await 276 .expect("Failed to get user signing key"); 277 let correct_signing_key = SigningKey::from_slice(&key_bytes) 278 .expect("Failed to create signing key"); 279 280 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 281 let pds_endpoint = format!("https://{}", hostname); 282 283 let handle = did.split(':').last().unwrap_or("user"); 284 let did_doc = create_did_document(&did, handle, &correct_signing_key, &pds_endpoint); 285 286 let mock_plc = setup_mock_plc_directory(&did, did_doc).await; 287 288 unsafe { 289 std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri()); 290 std::env::remove_var("SKIP_IMPORT_VERIFICATION"); 291 } 292 293 let (car_bytes, _root_cid) = build_car_with_signature(&did, &wrong_signing_key); 294 295 let import_res = client 296 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await)) 297 .bearer_auth(&token) 298 .header("Content-Type", "application/vnd.ipld.car") 299 .body(car_bytes) 300 .send() 301 .await 302 .expect("Import request failed"); 303 304 let status = import_res.status(); 305 let body: serde_json::Value = import_res.json().await.unwrap_or(json!({})); 306 307 unsafe { 308 std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); 309 } 310 311 assert_eq!( 312 status, 313 StatusCode::BAD_REQUEST, 314 "Import with wrong signature should fail. Response: {:?}", 315 body 316 ); 317 assert!( 318 body["error"] == "InvalidSignature" || body["message"].as_str().unwrap_or("").contains("signature"), 319 "Error should mention signature: {:?}", 320 body 321 ); 322} 323 324#[tokio::test] 325async fn test_import_with_did_mismatch_fails() { 326 let client = client(); 327 let (token, did) = create_account_and_login(&client).await; 328 329 let key_bytes = get_user_signing_key(&did).await 330 .expect("Failed to get user signing key"); 331 let signing_key = SigningKey::from_slice(&key_bytes) 332 .expect("Failed to create signing key"); 333 334 let wrong_did = "did:plc:wrongdidthatdoesnotmatch"; 335 336 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 337 let pds_endpoint = format!("https://{}", hostname); 338 339 let handle = did.split(':').last().unwrap_or("user"); 340 let did_doc = create_did_document(&did, handle, &signing_key, &pds_endpoint); 341 342 let mock_plc = setup_mock_plc_directory(&did, did_doc).await; 343 344 unsafe { 345 std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri()); 346 std::env::remove_var("SKIP_IMPORT_VERIFICATION"); 347 } 348 349 let (car_bytes, _root_cid) = build_car_with_signature(wrong_did, &signing_key); 350 351 let import_res = client 352 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await)) 353 .bearer_auth(&token) 354 .header("Content-Type", "application/vnd.ipld.car") 355 .body(car_bytes) 356 .send() 357 .await 358 .expect("Import request failed"); 359 360 let status = import_res.status(); 361 let body: serde_json::Value = import_res.json().await.unwrap_or(json!({})); 362 363 unsafe { 364 std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); 365 } 366 367 assert_eq!( 368 status, 369 StatusCode::FORBIDDEN, 370 "Import with DID mismatch should be forbidden. Response: {:?}", 371 body 372 ); 373} 374 375#[tokio::test] 376async fn test_import_with_plc_resolution_failure() { 377 let client = client(); 378 let (token, did) = create_account_and_login(&client).await; 379 380 let key_bytes = get_user_signing_key(&did).await 381 .expect("Failed to get user signing key"); 382 let signing_key = SigningKey::from_slice(&key_bytes) 383 .expect("Failed to create signing key"); 384 385 let mock_plc = MockServer::start().await; 386 387 let did_encoded = urlencoding::encode(&did); 388 let did_path = format!("/{}", did_encoded); 389 Mock::given(method("GET")) 390 .and(path(did_path)) 391 .respond_with(ResponseTemplate::new(404)) 392 .mount(&mock_plc) 393 .await; 394 395 unsafe { 396 std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri()); 397 std::env::remove_var("SKIP_IMPORT_VERIFICATION"); 398 } 399 400 let (car_bytes, _root_cid) = build_car_with_signature(&did, &signing_key); 401 402 let import_res = client 403 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await)) 404 .bearer_auth(&token) 405 .header("Content-Type", "application/vnd.ipld.car") 406 .body(car_bytes) 407 .send() 408 .await 409 .expect("Import request failed"); 410 411 let status = import_res.status(); 412 let body: serde_json::Value = import_res.json().await.unwrap_or(json!({})); 413 414 unsafe { 415 std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); 416 } 417 418 assert_eq!( 419 status, 420 StatusCode::BAD_REQUEST, 421 "Import with PLC resolution failure should fail. Response: {:?}", 422 body 423 ); 424} 425 426#[tokio::test] 427async fn test_import_with_no_signing_key_in_did_doc() { 428 let client = client(); 429 let (token, did) = create_account_and_login(&client).await; 430 431 let key_bytes = get_user_signing_key(&did).await 432 .expect("Failed to get user signing key"); 433 let signing_key = SigningKey::from_slice(&key_bytes) 434 .expect("Failed to create signing key"); 435 436 let handle = did.split(':').last().unwrap_or("user"); 437 let did_doc_without_key = json!({ 438 "@context": ["https://www.w3.org/ns/did/v1"], 439 "id": did, 440 "alsoKnownAs": [format!("at://{}", handle)], 441 "verificationMethod": [], 442 "service": [] 443 }); 444 445 let mock_plc = setup_mock_plc_directory(&did, did_doc_without_key).await; 446 447 unsafe { 448 std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri()); 449 std::env::remove_var("SKIP_IMPORT_VERIFICATION"); 450 } 451 452 let (car_bytes, _root_cid) = build_car_with_signature(&did, &signing_key); 453 454 let import_res = client 455 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await)) 456 .bearer_auth(&token) 457 .header("Content-Type", "application/vnd.ipld.car") 458 .body(car_bytes) 459 .send() 460 .await 461 .expect("Import request failed"); 462 463 let status = import_res.status(); 464 let body: serde_json::Value = import_res.json().await.unwrap_or(json!({})); 465 466 unsafe { 467 std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); 468 } 469 470 assert_eq!( 471 status, 472 StatusCode::BAD_REQUEST, 473 "Import with missing signing key should fail. Response: {:?}", 474 body 475 ); 476}