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