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