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