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