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