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