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