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