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