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]
220async fn test_import_with_valid_signature_and_mock_plc() {
221 let client = client();
222 let (token, did) = create_account_and_login(&client).await;
223
224 let key_bytes = get_user_signing_key(&did).await
225 .expect("Failed to get user signing key");
226 let signing_key = SigningKey::from_slice(&key_bytes)
227 .expect("Failed to create signing key");
228
229 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
230 let pds_endpoint = format!("https://{}", hostname);
231
232 let handle = did.split(':').last().unwrap_or("user");
233 let did_doc = create_did_document(&did, handle, &signing_key, &pds_endpoint);
234
235 let mock_plc = setup_mock_plc_directory(&did, did_doc).await;
236
237 unsafe {
238 std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri());
239 std::env::remove_var("SKIP_IMPORT_VERIFICATION");
240 }
241
242 let (car_bytes, _root_cid) = build_car_with_signature(&did, &signing_key);
243
244 let import_res = client
245 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
246 .bearer_auth(&token)
247 .header("Content-Type", "application/vnd.ipld.car")
248 .body(car_bytes)
249 .send()
250 .await
251 .expect("Import request failed");
252
253 let status = import_res.status();
254 let body: serde_json::Value = import_res.json().await.unwrap_or(json!({}));
255
256 unsafe {
257 std::env::set_var("SKIP_IMPORT_VERIFICATION", "true");
258 }
259
260 assert_eq!(
261 status,
262 StatusCode::OK,
263 "Import with valid signature should succeed. Response: {:?}",
264 body
265 );
266}
267
268#[tokio::test]
269async fn test_import_with_wrong_signing_key_fails() {
270 let client = client();
271 let (token, did) = create_account_and_login(&client).await;
272
273 let wrong_signing_key = SigningKey::random(&mut rand::thread_rng());
274
275 let key_bytes = get_user_signing_key(&did).await
276 .expect("Failed to get user signing key");
277 let correct_signing_key = SigningKey::from_slice(&key_bytes)
278 .expect("Failed to create signing key");
279
280 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
281 let pds_endpoint = format!("https://{}", hostname);
282
283 let handle = did.split(':').last().unwrap_or("user");
284 let did_doc = create_did_document(&did, handle, &correct_signing_key, &pds_endpoint);
285
286 let mock_plc = setup_mock_plc_directory(&did, did_doc).await;
287
288 unsafe {
289 std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri());
290 std::env::remove_var("SKIP_IMPORT_VERIFICATION");
291 }
292
293 let (car_bytes, _root_cid) = build_car_with_signature(&did, &wrong_signing_key);
294
295 let import_res = client
296 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
297 .bearer_auth(&token)
298 .header("Content-Type", "application/vnd.ipld.car")
299 .body(car_bytes)
300 .send()
301 .await
302 .expect("Import request failed");
303
304 let status = import_res.status();
305 let body: serde_json::Value = import_res.json().await.unwrap_or(json!({}));
306
307 unsafe {
308 std::env::set_var("SKIP_IMPORT_VERIFICATION", "true");
309 }
310
311 assert_eq!(
312 status,
313 StatusCode::BAD_REQUEST,
314 "Import with wrong signature should fail. Response: {:?}",
315 body
316 );
317 assert!(
318 body["error"] == "InvalidSignature" || body["message"].as_str().unwrap_or("").contains("signature"),
319 "Error should mention signature: {:?}",
320 body
321 );
322}
323
324#[tokio::test]
325async fn test_import_with_did_mismatch_fails() {
326 let client = client();
327 let (token, did) = create_account_and_login(&client).await;
328
329 let key_bytes = get_user_signing_key(&did).await
330 .expect("Failed to get user signing key");
331 let signing_key = SigningKey::from_slice(&key_bytes)
332 .expect("Failed to create signing key");
333
334 let wrong_did = "did:plc:wrongdidthatdoesnotmatch";
335
336 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
337 let pds_endpoint = format!("https://{}", hostname);
338
339 let handle = did.split(':').last().unwrap_or("user");
340 let did_doc = create_did_document(&did, handle, &signing_key, &pds_endpoint);
341
342 let mock_plc = setup_mock_plc_directory(&did, did_doc).await;
343
344 unsafe {
345 std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri());
346 std::env::remove_var("SKIP_IMPORT_VERIFICATION");
347 }
348
349 let (car_bytes, _root_cid) = build_car_with_signature(wrong_did, &signing_key);
350
351 let import_res = client
352 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
353 .bearer_auth(&token)
354 .header("Content-Type", "application/vnd.ipld.car")
355 .body(car_bytes)
356 .send()
357 .await
358 .expect("Import request failed");
359
360 let status = import_res.status();
361 let body: serde_json::Value = import_res.json().await.unwrap_or(json!({}));
362
363 unsafe {
364 std::env::set_var("SKIP_IMPORT_VERIFICATION", "true");
365 }
366
367 assert_eq!(
368 status,
369 StatusCode::FORBIDDEN,
370 "Import with DID mismatch should be forbidden. Response: {:?}",
371 body
372 );
373}
374
375#[tokio::test]
376async fn test_import_with_plc_resolution_failure() {
377 let client = client();
378 let (token, did) = create_account_and_login(&client).await;
379
380 let key_bytes = get_user_signing_key(&did).await
381 .expect("Failed to get user signing key");
382 let signing_key = SigningKey::from_slice(&key_bytes)
383 .expect("Failed to create signing key");
384
385 let mock_plc = MockServer::start().await;
386
387 let did_encoded = urlencoding::encode(&did);
388 let did_path = format!("/{}", did_encoded);
389 Mock::given(method("GET"))
390 .and(path(did_path))
391 .respond_with(ResponseTemplate::new(404))
392 .mount(&mock_plc)
393 .await;
394
395 unsafe {
396 std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri());
397 std::env::remove_var("SKIP_IMPORT_VERIFICATION");
398 }
399
400 let (car_bytes, _root_cid) = build_car_with_signature(&did, &signing_key);
401
402 let import_res = client
403 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
404 .bearer_auth(&token)
405 .header("Content-Type", "application/vnd.ipld.car")
406 .body(car_bytes)
407 .send()
408 .await
409 .expect("Import request failed");
410
411 let status = import_res.status();
412 let body: serde_json::Value = import_res.json().await.unwrap_or(json!({}));
413
414 unsafe {
415 std::env::set_var("SKIP_IMPORT_VERIFICATION", "true");
416 }
417
418 assert_eq!(
419 status,
420 StatusCode::BAD_REQUEST,
421 "Import with PLC resolution failure should fail. Response: {:?}",
422 body
423 );
424}
425
426#[tokio::test]
427async fn test_import_with_no_signing_key_in_did_doc() {
428 let client = client();
429 let (token, did) = create_account_and_login(&client).await;
430
431 let key_bytes = get_user_signing_key(&did).await
432 .expect("Failed to get user signing key");
433 let signing_key = SigningKey::from_slice(&key_bytes)
434 .expect("Failed to create signing key");
435
436 let handle = did.split(':').last().unwrap_or("user");
437 let did_doc_without_key = json!({
438 "@context": ["https://www.w3.org/ns/did/v1"],
439 "id": did,
440 "alsoKnownAs": [format!("at://{}", handle)],
441 "verificationMethod": [],
442 "service": []
443 });
444
445 let mock_plc = setup_mock_plc_directory(&did, did_doc_without_key).await;
446
447 unsafe {
448 std::env::set_var("PLC_DIRECTORY_URL", mock_plc.uri());
449 std::env::remove_var("SKIP_IMPORT_VERIFICATION");
450 }
451
452 let (car_bytes, _root_cid) = build_car_with_signature(&did, &signing_key);
453
454 let import_res = client
455 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
456 .bearer_auth(&token)
457 .header("Content-Type", "application/vnd.ipld.car")
458 .body(car_bytes)
459 .send()
460 .await
461 .expect("Import request failed");
462
463 let status = import_res.status();
464 let body: serde_json::Value = import_res.json().await.unwrap_or(json!({}));
465
466 unsafe {
467 std::env::set_var("SKIP_IMPORT_VERIFICATION", "true");
468 }
469
470 assert_eq!(
471 status,
472 StatusCode::BAD_REQUEST,
473 "Import with missing signing key should fail. Response: {:?}",
474 body
475 );
476}