this repo has no description
1mod common;
2use base64::engine::general_purpose::URL_SAFE_NO_PAD;
3use base64::Engine;
4use common::*;
5use k256::ecdsa::{SigningKey, signature::Signer};
6use reqwest::StatusCode;
7use serde_json::{Value, json};
8use wiremock::matchers::{method, path};
9use wiremock::{Mock, MockServer, ResponseTemplate};
10
11#[tokio::test]
12async fn test_create_self_hosted_did_web() {
13 let client = client();
14 let handle = format!("selfweb_{}", uuid::Uuid::new_v4());
15 let payload = json!({
16 "handle": handle,
17 "email": format!("{}@example.com", handle),
18 "password": "Testpass123!",
19 "didType": "web"
20 });
21 let res = client
22 .post(format!(
23 "{}/xrpc/com.atproto.server.createAccount",
24 base_url().await
25 ))
26 .json(&payload)
27 .send()
28 .await
29 .expect("Failed to send request");
30 if res.status() != StatusCode::OK {
31 let body: Value = res.json().await.unwrap_or(json!({"error": "parse failed"}));
32 panic!("createAccount failed: {:?}", body);
33 }
34 let body: Value = res.json().await.expect("Response was not JSON");
35 let did = body["did"].as_str().expect("No DID in response");
36 assert!(
37 did.starts_with("did:web:"),
38 "DID should start with did:web:, got: {}",
39 did
40 );
41 assert!(
42 did.contains(&handle),
43 "DID should contain handle {}, got: {}",
44 handle,
45 did
46 );
47 assert!(
48 !did.contains(":u:"),
49 "Self-hosted did:web should use subdomain format (no :u:), got: {}",
50 did
51 );
52 let jwt = verify_new_account(&client, did).await;
53 let res = client
54 .get(format!("{}/u/{}/did.json", base_url().await, handle))
55 .send()
56 .await
57 .expect("Failed to fetch DID doc via path");
58 assert_eq!(
59 res.status(),
60 StatusCode::OK,
61 "Self-hosted did:web should have DID doc served by PDS (via path for backwards compat)"
62 );
63 let doc: Value = res.json().await.expect("DID doc was not JSON");
64 assert_eq!(doc["id"], did);
65 assert!(
66 doc["verificationMethod"][0]["publicKeyMultibase"].is_string(),
67 "DID doc should have publicKeyMultibase"
68 );
69 let res = client
70 .post(format!(
71 "{}/xrpc/com.atproto.repo.createRecord",
72 base_url().await
73 ))
74 .bearer_auth(&jwt)
75 .json(&json!({
76 "repo": did,
77 "collection": "app.bsky.feed.post",
78 "record": {
79 "$type": "app.bsky.feed.post",
80 "text": "Hello from did:web!",
81 "createdAt": chrono::Utc::now().to_rfc3339()
82 }
83 }))
84 .send()
85 .await
86 .expect("Failed to create post");
87 assert_eq!(
88 res.status(),
89 StatusCode::OK,
90 "Self-hosted did:web account should be able to create records"
91 );
92}
93
94#[tokio::test]
95async fn test_external_did_web_no_local_doc() {
96 let client = client();
97 let mock_server = MockServer::start().await;
98 let mock_uri = mock_server.uri();
99 let mock_addr = mock_uri.trim_start_matches("http://");
100 let did = format!("did:web:{}", mock_addr.replace(":", "%3A"));
101 let handle = format!("extweb_{}", uuid::Uuid::new_v4());
102 let pds_endpoint = base_url().await.replace("http://", "https://");
103
104 let reserve_res = client
105 .post(format!(
106 "{}/xrpc/com.atproto.server.reserveSigningKey",
107 base_url().await
108 ))
109 .json(&json!({ "did": did }))
110 .send()
111 .await
112 .expect("Failed to reserve signing key");
113 assert_eq!(reserve_res.status(), StatusCode::OK);
114 let reserve_body: Value = reserve_res.json().await.expect("Response was not JSON");
115 let signing_key = reserve_body["signingKey"]
116 .as_str()
117 .expect("No signingKey returned");
118 let public_key_multibase = signing_key
119 .strip_prefix("did:key:")
120 .expect("signingKey should start with did:key:");
121
122 let did_doc = json!({
123 "@context": ["https://www.w3.org/ns/did/v1"],
124 "id": did,
125 "verificationMethod": [{
126 "id": format!("{}#atproto", did),
127 "type": "Multikey",
128 "controller": did,
129 "publicKeyMultibase": public_key_multibase
130 }],
131 "service": [{
132 "id": "#atproto_pds",
133 "type": "AtprotoPersonalDataServer",
134 "serviceEndpoint": pds_endpoint
135 }]
136 });
137 Mock::given(method("GET"))
138 .and(path("/.well-known/did.json"))
139 .respond_with(ResponseTemplate::new(200).set_body_json(did_doc))
140 .mount(&mock_server)
141 .await;
142 let payload = json!({
143 "handle": handle,
144 "email": format!("{}@example.com", handle),
145 "password": "Testpass123!",
146 "didType": "web-external",
147 "did": did,
148 "signingKey": signing_key
149 });
150 let res = client
151 .post(format!(
152 "{}/xrpc/com.atproto.server.createAccount",
153 base_url().await
154 ))
155 .json(&payload)
156 .send()
157 .await
158 .expect("Failed to send request");
159 if res.status() != StatusCode::OK {
160 let body: Value = res.json().await.unwrap_or(json!({"error": "parse failed"}));
161 panic!("createAccount failed: {:?}", body);
162 }
163 let res = client
164 .get(format!("{}/u/{}/did.json", base_url().await, handle))
165 .send()
166 .await
167 .expect("Failed to fetch DID doc");
168 assert_eq!(
169 res.status(),
170 StatusCode::NOT_FOUND,
171 "External did:web should NOT have DID doc served by PDS"
172 );
173 let body: Value = res.json().await.expect("Response was not JSON");
174 assert!(
175 body["message"].as_str().unwrap_or("").contains("External"),
176 "Error message should indicate external did:web"
177 );
178}
179
180#[tokio::test]
181async fn test_plc_operations_blocked_for_did_web() {
182 let client = client();
183 let handle = format!("plcblock_{}", uuid::Uuid::new_v4());
184 let payload = json!({
185 "handle": handle,
186 "email": format!("{}@example.com", handle),
187 "password": "Testpass123!",
188 "didType": "web"
189 });
190 let res = client
191 .post(format!(
192 "{}/xrpc/com.atproto.server.createAccount",
193 base_url().await
194 ))
195 .json(&payload)
196 .send()
197 .await
198 .expect("Failed to send request");
199 assert_eq!(res.status(), StatusCode::OK);
200 let body: Value = res.json().await.expect("Response was not JSON");
201 let did = body["did"].as_str().expect("No DID").to_string();
202 let jwt = verify_new_account(&client, &did).await;
203 let res = client
204 .post(format!(
205 "{}/xrpc/com.atproto.identity.signPlcOperation",
206 base_url().await
207 ))
208 .bearer_auth(&jwt)
209 .json(&json!({
210 "token": "fake-token"
211 }))
212 .send()
213 .await
214 .expect("Failed to send request");
215 assert_eq!(
216 res.status(),
217 StatusCode::BAD_REQUEST,
218 "signPlcOperation should be blocked for did:web users"
219 );
220 let body: Value = res.json().await.expect("Response was not JSON");
221 assert!(
222 body["message"].as_str().unwrap_or("").contains("did:plc"),
223 "Error should mention did:plc: {:?}",
224 body
225 );
226 let res = client
227 .post(format!(
228 "{}/xrpc/com.atproto.identity.submitPlcOperation",
229 base_url().await
230 ))
231 .bearer_auth(&jwt)
232 .json(&json!({
233 "operation": {}
234 }))
235 .send()
236 .await
237 .expect("Failed to send request");
238 assert_eq!(
239 res.status(),
240 StatusCode::BAD_REQUEST,
241 "submitPlcOperation should be blocked for did:web users"
242 );
243}
244
245#[tokio::test]
246async fn test_get_recommended_did_credentials_no_rotation_keys_for_did_web() {
247 let client = client();
248 let handle = format!("creds_{}", uuid::Uuid::new_v4());
249 let payload = json!({
250 "handle": handle,
251 "email": format!("{}@example.com", handle),
252 "password": "Testpass123!",
253 "didType": "web"
254 });
255 let res = client
256 .post(format!(
257 "{}/xrpc/com.atproto.server.createAccount",
258 base_url().await
259 ))
260 .json(&payload)
261 .send()
262 .await
263 .expect("Failed to send request");
264 assert_eq!(res.status(), StatusCode::OK);
265 let body: Value = res.json().await.expect("Response was not JSON");
266 let did = body["did"].as_str().expect("No DID").to_string();
267 let jwt = verify_new_account(&client, &did).await;
268 let res = client
269 .get(format!(
270 "{}/xrpc/com.atproto.identity.getRecommendedDidCredentials",
271 base_url().await
272 ))
273 .bearer_auth(&jwt)
274 .send()
275 .await
276 .expect("Failed to send request");
277 assert_eq!(res.status(), StatusCode::OK);
278 let body: Value = res.json().await.expect("Response was not JSON");
279 let rotation_keys = body["rotationKeys"]
280 .as_array()
281 .expect("rotationKeys should be an array");
282 assert!(
283 rotation_keys.is_empty(),
284 "did:web should have no rotation keys, got: {:?}",
285 rotation_keys
286 );
287 assert!(
288 body["verificationMethods"].is_object(),
289 "verificationMethods should be present"
290 );
291 assert!(body["services"].is_object(), "services should be present");
292}
293
294#[tokio::test]
295async fn test_did_plc_still_works_with_did_type_param() {
296 let client = client();
297 let handle = format!("plctype_{}", uuid::Uuid::new_v4());
298 let payload = json!({
299 "handle": handle,
300 "email": format!("{}@example.com", handle),
301 "password": "Testpass123!",
302 "didType": "plc"
303 });
304 let res = client
305 .post(format!(
306 "{}/xrpc/com.atproto.server.createAccount",
307 base_url().await
308 ))
309 .json(&payload)
310 .send()
311 .await
312 .expect("Failed to send request");
313 assert_eq!(res.status(), StatusCode::OK);
314 let body: Value = res.json().await.expect("Response was not JSON");
315 let did = body["did"].as_str().expect("No DID").to_string();
316 assert!(
317 did.starts_with("did:plc:"),
318 "DID with didType=plc should be did:plc:, got: {}",
319 did
320 );
321}
322
323#[tokio::test]
324async fn test_external_did_web_requires_did_field() {
325 let client = client();
326 let handle = format!("nodid_{}", uuid::Uuid::new_v4());
327 let payload = json!({
328 "handle": handle,
329 "email": format!("{}@example.com", handle),
330 "password": "Testpass123!",
331 "didType": "web-external"
332 });
333 let res = client
334 .post(format!(
335 "{}/xrpc/com.atproto.server.createAccount",
336 base_url().await
337 ))
338 .json(&payload)
339 .send()
340 .await
341 .expect("Failed to send request");
342 assert_eq!(
343 res.status(),
344 StatusCode::BAD_REQUEST,
345 "web-external without did should fail"
346 );
347 let body: Value = res.json().await.expect("Response was not JSON");
348 assert!(
349 body["message"].as_str().unwrap_or("").contains("did"),
350 "Error should mention did field is required: {:?}",
351 body
352 );
353}
354
355fn signing_key_to_multibase(signing_key: &SigningKey) -> String {
356 let verifying_key = signing_key.verifying_key();
357 let compressed = verifying_key.to_sec1_bytes();
358 let mut multicodec = vec![0xe7, 0x01];
359 multicodec.extend_from_slice(&compressed);
360 multibase::encode(multibase::Base::Base58Btc, &multicodec)
361}
362
363fn create_service_jwt(signing_key: &SigningKey, did: &str, aud: &str) -> String {
364 let header = json!({"alg": "ES256K", "typ": "jwt"});
365 let now = chrono::Utc::now().timestamp() as usize;
366 let claims = json!({
367 "iss": did,
368 "sub": did,
369 "aud": aud,
370 "exp": now + 300,
371 "iat": now,
372 "lxm": "com.atproto.server.createAccount",
373 "jti": uuid::Uuid::new_v4().to_string()
374 });
375 let header_b64 = URL_SAFE_NO_PAD.encode(header.to_string());
376 let claims_b64 = URL_SAFE_NO_PAD.encode(claims.to_string());
377 let message = format!("{}.{}", header_b64, claims_b64);
378 let signature: k256::ecdsa::Signature = signing_key.sign(message.as_bytes());
379 let sig_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
380 format!("{}.{}", message, sig_b64)
381}
382
383#[tokio::test]
384async fn test_did_web_byod_flow() {
385 let client = client();
386 let mock_server = MockServer::start().await;
387 let mock_uri = mock_server.uri();
388 let mock_addr = mock_uri.trim_start_matches("http://");
389 let unique_id = uuid::Uuid::new_v4().to_string().replace("-", "");
390 let did = format!("did:web:{}:byod:{}", mock_addr.replace(":", "%3A"), unique_id);
391 let handle = format!("byod_{}", uuid::Uuid::new_v4());
392 let pds_endpoint = base_url().await.replace("http://", "https://");
393 let pds_did = format!(
394 "did:web:{}",
395 pds_endpoint.trim_start_matches("https://")
396 );
397
398 let temp_key = SigningKey::random(&mut rand::thread_rng());
399 let public_key_multibase = signing_key_to_multibase(&temp_key);
400
401 let did_doc = json!({
402 "@context": ["https://www.w3.org/ns/did/v1"],
403 "id": did,
404 "verificationMethod": [{
405 "id": format!("{}#atproto", did),
406 "type": "Multikey",
407 "controller": did,
408 "publicKeyMultibase": public_key_multibase
409 }],
410 "service": [{
411 "id": "#atproto_pds",
412 "type": "AtprotoPersonalDataServer",
413 "serviceEndpoint": pds_endpoint
414 }]
415 });
416 Mock::given(method("GET"))
417 .and(path(format!("/byod/{}/did.json", unique_id)))
418 .respond_with(ResponseTemplate::new(200).set_body_json(&did_doc))
419 .mount(&mock_server)
420 .await;
421
422 let service_jwt = create_service_jwt(&temp_key, &did, &pds_did);
423 let payload = json!({
424 "handle": handle,
425 "email": format!("{}@example.com", handle),
426 "password": "Testpass123!",
427 "did": did
428 });
429 let res = client
430 .post(format!(
431 "{}/xrpc/com.atproto.server.createAccount",
432 base_url().await
433 ))
434 .header("Authorization", format!("Bearer {}", service_jwt))
435 .json(&payload)
436 .send()
437 .await
438 .expect("Failed to send request");
439 if res.status() != StatusCode::OK {
440 let body: Value = res.json().await.unwrap_or(json!({"error": "parse failed"}));
441 panic!("createAccount BYOD failed: {:?}", body);
442 }
443 let body: Value = res.json().await.expect("Response was not JSON");
444 let returned_did = body["did"].as_str().expect("No DID in response");
445 assert_eq!(returned_did, did, "Returned DID should match requested DID");
446 let access_jwt = body["accessJwt"]
447 .as_str()
448 .expect("No accessJwt in response");
449
450 let res = client
451 .get(format!(
452 "{}/xrpc/com.atproto.server.checkAccountStatus",
453 base_url().await
454 ))
455 .bearer_auth(access_jwt)
456 .send()
457 .await
458 .expect("Failed to check account status");
459 assert_eq!(res.status(), StatusCode::OK);
460 let status: Value = res.json().await.expect("Response was not JSON");
461 assert_eq!(
462 status["activated"], false,
463 "BYOD account should be deactivated initially"
464 );
465
466 let res = client
467 .get(format!(
468 "{}/xrpc/com.atproto.identity.getRecommendedDidCredentials",
469 base_url().await
470 ))
471 .bearer_auth(access_jwt)
472 .send()
473 .await
474 .expect("Failed to get recommended credentials");
475 assert_eq!(res.status(), StatusCode::OK);
476 let creds: Value = res.json().await.expect("Response was not JSON");
477 assert!(
478 creds["verificationMethods"]["atproto"].is_string(),
479 "Should return PDS signing key"
480 );
481 let pds_signing_key = creds["verificationMethods"]["atproto"]
482 .as_str()
483 .expect("No atproto verification method");
484 assert!(
485 pds_signing_key.starts_with("did:key:"),
486 "PDS signing key should be did:key format"
487 );
488
489 let res = client
490 .post(format!(
491 "{}/xrpc/com.atproto.server.activateAccount",
492 base_url().await
493 ))
494 .bearer_auth(access_jwt)
495 .send()
496 .await
497 .expect("Failed to activate account");
498 assert_eq!(
499 res.status(),
500 StatusCode::OK,
501 "activateAccount should succeed"
502 );
503
504 let res = client
505 .get(format!(
506 "{}/xrpc/com.atproto.server.checkAccountStatus",
507 base_url().await
508 ))
509 .bearer_auth(access_jwt)
510 .send()
511 .await
512 .expect("Failed to check account status");
513 assert_eq!(res.status(), StatusCode::OK);
514 let status: Value = res.json().await.expect("Response was not JSON");
515 assert_eq!(
516 status["activated"], true,
517 "Account should be activated after activateAccount call"
518 );
519
520 let res = client
521 .post(format!(
522 "{}/xrpc/com.atproto.repo.createRecord",
523 base_url().await
524 ))
525 .bearer_auth(access_jwt)
526 .json(&json!({
527 "repo": did,
528 "collection": "app.bsky.feed.post",
529 "record": {
530 "$type": "app.bsky.feed.post",
531 "text": "Hello from BYOD did:web!",
532 "createdAt": chrono::Utc::now().to_rfc3339()
533 }
534 }))
535 .send()
536 .await
537 .expect("Failed to create post");
538 assert_eq!(
539 res.status(),
540 StatusCode::OK,
541 "Activated BYOD account should be able to create records"
542 );
543}