mod common; use cid::Cid; use common::*; use ipld_core::ipld::Ipld; use jacquard::types::{integer::LimitedU32, string::Tid}; use jacquard_repo::commit::Commit; use k256::ecdsa::SigningKey; use reqwest::StatusCode; use serde_json::json; use sha2::{Digest, Sha256}; use sqlx::PgPool; use std::collections::BTreeMap; use std::str::FromStr; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; fn make_cid(data: &[u8]) -> Cid { let mut hasher = Sha256::new(); hasher.update(data); let hash = hasher.finalize(); let multihash = multihash::Multihash::wrap(0x12, &hash).unwrap(); Cid::new_v1(0x71, multihash) } fn write_varint(buf: &mut Vec, mut value: u64) { loop { let mut byte = (value & 0x7F) as u8; value >>= 7; if value != 0 { byte |= 0x80; } buf.push(byte); if value == 0 { break; } } } fn encode_car_block(cid: &Cid, data: &[u8]) -> Vec { let cid_bytes = cid.to_bytes(); let mut result = Vec::new(); write_varint(&mut result, (cid_bytes.len() + data.len()) as u64); result.extend_from_slice(&cid_bytes); result.extend_from_slice(data); result } fn get_multikey_from_signing_key(signing_key: &SigningKey) -> String { let public_key = signing_key.verifying_key(); let compressed = public_key.to_sec1_bytes(); fn encode_uvarint(mut x: u64) -> Vec { let mut out = Vec::new(); while x >= 0x80 { out.push(((x as u8) & 0x7F) | 0x80); x >>= 7; } out.push(x as u8); out } let mut buf = encode_uvarint(0xE7); buf.extend_from_slice(&compressed); multibase::encode(multibase::Base::Base58Btc, buf) } fn create_did_document( did: &str, handle: &str, signing_key: &SigningKey, pds_endpoint: &str, ) -> serde_json::Value { let multikey = get_multikey_from_signing_key(signing_key); json!({ "@context": [ "https://www.w3.org/ns/did/v1", "https://w3id.org/security/multikey/v1" ], "id": did, "alsoKnownAs": [format!("at://{}", handle)], "verificationMethod": [{ "id": format!("{}#atproto", did), "type": "Multikey", "controller": did, "publicKeyMultibase": multikey }], "service": [{ "id": "#atproto_pds", "type": "AtprotoPersonalDataServer", "serviceEndpoint": pds_endpoint }] }) } fn create_signed_commit(did: &str, data_cid: &Cid, signing_key: &SigningKey) -> (Vec, Cid) { let rev = Tid::now(LimitedU32::MIN); let did = jacquard::types::string::Did::new(did).expect("valid DID"); let unsigned = Commit::new_unsigned(did, *data_cid, rev, None); let signed = unsigned.sign(signing_key).expect("signing failed"); let signed_bytes = signed.to_cbor().expect("serialization failed"); let cid = make_cid(&signed_bytes); (signed_bytes, cid) } fn create_mst_node(entries: Vec<(String, Cid)>) -> (Vec, Cid) { let ipld_entries: Vec = entries .into_iter() .map(|(key, value_cid)| { Ipld::Map(BTreeMap::from([ ("k".to_string(), Ipld::Bytes(key.into_bytes())), ("v".to_string(), Ipld::Link(value_cid)), ("p".to_string(), Ipld::Integer(0)), ])) }) .collect(); let node = Ipld::Map(BTreeMap::from([( "e".to_string(), Ipld::List(ipld_entries), )])); let bytes = serde_ipld_dagcbor::to_vec(&node).unwrap(); let cid = make_cid(&bytes); (bytes, cid) } fn create_record() -> (Vec, Cid) { let record = Ipld::Map(BTreeMap::from([ ( "$type".to_string(), Ipld::String("app.bsky.feed.post".to_string()), ), ( "text".to_string(), Ipld::String("Test post for verification".to_string()), ), ( "createdAt".to_string(), Ipld::String("2024-01-01T00:00:00Z".to_string()), ), ])); let bytes = serde_ipld_dagcbor::to_vec(&record).unwrap(); let cid = make_cid(&bytes); (bytes, cid) } fn build_car_with_signature(did: &str, signing_key: &SigningKey) -> (Vec, Cid) { let (record_bytes, record_cid) = create_record(); let (mst_bytes, mst_cid) = create_mst_node(vec![("app.bsky.feed.post/test123".to_string(), record_cid)]); let (commit_bytes, commit_cid) = create_signed_commit(did, &mst_cid, signing_key); let header = iroh_car::CarHeader::new_v1(vec![commit_cid]); let header_bytes = header.encode().unwrap(); let mut car = Vec::new(); write_varint(&mut car, header_bytes.len() as u64); car.extend_from_slice(&header_bytes); car.extend(encode_car_block(&commit_cid, &commit_bytes)); car.extend(encode_car_block(&mst_cid, &mst_bytes)); car.extend(encode_car_block(&record_cid, &record_bytes)); (car, commit_cid) } async fn setup_mock_plc_directory(did: &str, did_doc: serde_json::Value) -> MockServer { let mock_server = MockServer::start().await; let did_encoded = urlencoding::encode(did); let did_path = format!("/{}", did_encoded); Mock::given(method("GET")) .and(path(did_path)) .respond_with(ResponseTemplate::new(200).set_body_json(did_doc)) .mount(&mock_server) .await; mock_server } async fn get_user_signing_key(did: &str) -> Option> { let db_url = get_db_connection_string().await; let pool = PgPool::connect(&db_url).await.ok()?; let row = sqlx::query!( r#" SELECT k.key_bytes, k.encryption_version FROM user_keys k JOIN users u ON k.user_id = u.id WHERE u.did = $1 "#, did ) .fetch_optional(&pool) .await .ok()??; tranquil_pds::config::decrypt_key(&row.key_bytes, row.encryption_version).ok() } #[tokio::test] #[ignore = "requires exclusive env var access; run with: cargo test test_import_with_valid_signature_and_mock_plc -- --ignored --test-threads=1"] async fn test_import_with_valid_signature_and_mock_plc() { let client = client(); let (token, did) = create_account_and_login(&client).await; let key_bytes = get_user_signing_key(&did) .await .expect("Failed to get user signing key"); let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key"); let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); let pds_endpoint = format!("https://{}", hostname); let handle = did.split(':').last().unwrap_or("user"); let did_doc = create_did_document(&did, handle, &signing_key, &pds_endpoint); let mock_plc = setup_mock_plc_directory(&did, did_doc).await; unsafe { std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri()); std::env::remove_var("SKIP_IMPORT_VERIFICATION"); } let (car_bytes, _root_cid) = build_car_with_signature(&did, &signing_key); let import_res = client .post(format!( "{}/xrpc/com.atproto.repo.importRepo", base_url().await )) .bearer_auth(&token) .header("Content-Type", "application/vnd.ipld.car") .body(car_bytes) .send() .await .expect("Import request failed"); let status = import_res.status(); let body: serde_json::Value = import_res.json().await.unwrap_or(json!({})); unsafe { std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); } assert_eq!( status, StatusCode::OK, "Import with valid signature should succeed. Response: {:?}", body ); } #[tokio::test] #[ignore = "requires exclusive env var access; run with: cargo test test_import_with_wrong_signing_key_fails -- --ignored --test-threads=1"] async fn test_import_with_wrong_signing_key_fails() { let client = client(); let (token, did) = create_account_and_login(&client).await; let wrong_signing_key = SigningKey::random(&mut rand::thread_rng()); let key_bytes = get_user_signing_key(&did) .await .expect("Failed to get user signing key"); let correct_signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key"); let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); let pds_endpoint = format!("https://{}", hostname); let handle = did.split(':').last().unwrap_or("user"); let did_doc = create_did_document(&did, handle, &correct_signing_key, &pds_endpoint); let mock_plc = setup_mock_plc_directory(&did, did_doc).await; unsafe { std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri()); std::env::remove_var("SKIP_IMPORT_VERIFICATION"); } let (car_bytes, _root_cid) = build_car_with_signature(&did, &wrong_signing_key); let import_res = client .post(format!( "{}/xrpc/com.atproto.repo.importRepo", base_url().await )) .bearer_auth(&token) .header("Content-Type", "application/vnd.ipld.car") .body(car_bytes) .send() .await .expect("Import request failed"); let status = import_res.status(); let body: serde_json::Value = import_res.json().await.unwrap_or(json!({})); unsafe { std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); } assert_eq!( status, StatusCode::BAD_REQUEST, "Import with wrong signature should fail. Response: {:?}", body ); assert!( body["error"] == "InvalidSignature" || body["message"].as_str().unwrap_or("").contains("signature"), "Error should mention signature: {:?}", body ); } #[tokio::test] #[ignore = "requires exclusive env var access; run with: cargo test test_import_with_did_mismatch_fails -- --ignored --test-threads=1"] async fn test_import_with_did_mismatch_fails() { let client = client(); let (token, did) = create_account_and_login(&client).await; let key_bytes = get_user_signing_key(&did) .await .expect("Failed to get user signing key"); let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key"); let wrong_did = "did:plc:wrongdidthatdoesnotmatch"; let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); let pds_endpoint = format!("https://{}", hostname); let handle = did.split(':').last().unwrap_or("user"); let did_doc = create_did_document(&did, handle, &signing_key, &pds_endpoint); let mock_plc = setup_mock_plc_directory(&did, did_doc).await; unsafe { std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri()); std::env::remove_var("SKIP_IMPORT_VERIFICATION"); } let (car_bytes, _root_cid) = build_car_with_signature(wrong_did, &signing_key); let import_res = client .post(format!( "{}/xrpc/com.atproto.repo.importRepo", base_url().await )) .bearer_auth(&token) .header("Content-Type", "application/vnd.ipld.car") .body(car_bytes) .send() .await .expect("Import request failed"); let status = import_res.status(); let body: serde_json::Value = import_res.json().await.unwrap_or(json!({})); unsafe { std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); } assert_eq!( status, StatusCode::FORBIDDEN, "Import with DID mismatch should be forbidden. Response: {:?}", body ); } #[tokio::test] #[ignore = "requires exclusive env var access; run with: cargo test test_import_with_plc_resolution_failure -- --ignored --test-threads=1"] async fn test_import_with_plc_resolution_failure() { let client = client(); let (token, did) = create_account_and_login(&client).await; let key_bytes = get_user_signing_key(&did) .await .expect("Failed to get user signing key"); let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key"); let mock_plc = MockServer::start().await; let did_encoded = urlencoding::encode(&did); let did_path = format!("/{}", did_encoded); Mock::given(method("GET")) .and(path(did_path)) .respond_with(ResponseTemplate::new(404)) .mount(&mock_plc) .await; unsafe { std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri()); std::env::remove_var("SKIP_IMPORT_VERIFICATION"); } let (car_bytes, _root_cid) = build_car_with_signature(&did, &signing_key); let import_res = client .post(format!( "{}/xrpc/com.atproto.repo.importRepo", base_url().await )) .bearer_auth(&token) .header("Content-Type", "application/vnd.ipld.car") .body(car_bytes) .send() .await .expect("Import request failed"); let status = import_res.status(); let body: serde_json::Value = import_res.json().await.unwrap_or(json!({})); unsafe { std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); } assert_eq!( status, StatusCode::BAD_REQUEST, "Import with PLC resolution failure should fail. Response: {:?}", body ); } #[tokio::test] #[ignore = "requires exclusive env var access; run with: cargo test test_import_with_no_signing_key_in_did_doc -- --ignored --test-threads=1"] async fn test_import_with_no_signing_key_in_did_doc() { let client = client(); let (token, did) = create_account_and_login(&client).await; let key_bytes = get_user_signing_key(&did) .await .expect("Failed to get user signing key"); let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key"); let handle = did.split(':').last().unwrap_or("user"); let did_doc_without_key = json!({ "@context": ["https://www.w3.org/ns/did/v1"], "id": did, "alsoKnownAs": [format!("at://{}", handle)], "verificationMethod": [], "service": [] }); let mock_plc = setup_mock_plc_directory(&did, did_doc_without_key).await; unsafe { std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri()); std::env::remove_var("SKIP_IMPORT_VERIFICATION"); } let (car_bytes, _root_cid) = build_car_with_signature(&did, &signing_key); let import_res = client .post(format!( "{}/xrpc/com.atproto.repo.importRepo", base_url().await )) .bearer_auth(&token) .header("Content-Type", "application/vnd.ipld.car") .body(car_bytes) .send() .await .expect("Import request failed"); let status = import_res.status(); let body: serde_json::Value = import_res.json().await.unwrap_or(json!({})); unsafe { std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); } assert_eq!( status, StatusCode::BAD_REQUEST, "Import with missing signing key should fail. Response: {:?}", body ); }