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