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