I've been saying "PDSes seem easy enough, they're what, some CRUD to a db? I can do that in my sleep". well i'm sleeping rn so let's go
at main 22 kB view raw
1mod common; 2use base64::Engine; 3use base64::engine::general_purpose::URL_SAFE_NO_PAD; 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!("sw{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 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!("xw{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 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!("pb{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 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!("cr{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 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!("pt{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 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!("nd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 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!( 391 "did:web:{}:byod:{}", 392 mock_addr.replace(":", "%3A"), 393 unique_id 394 ); 395 let handle = format!("by{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 396 let pds_endpoint = base_url().await.replace("http://", "https://"); 397 let pds_did = format!("did:web:{}", pds_endpoint.trim_start_matches("https://")); 398 399 let temp_key = SigningKey::random(&mut rand::thread_rng()); 400 let public_key_multibase = signing_key_to_multibase(&temp_key); 401 402 let did_doc = json!({ 403 "@context": ["https://www.w3.org/ns/did/v1"], 404 "id": did, 405 "verificationMethod": [{ 406 "id": format!("{}#atproto", did), 407 "type": "Multikey", 408 "controller": did, 409 "publicKeyMultibase": public_key_multibase 410 }], 411 "service": [{ 412 "id": "#atproto_pds", 413 "type": "AtprotoPersonalDataServer", 414 "serviceEndpoint": pds_endpoint 415 }] 416 }); 417 Mock::given(method("GET")) 418 .and(path(format!("/byod/{}/did.json", unique_id))) 419 .respond_with(ResponseTemplate::new(200).set_body_json(&did_doc)) 420 .mount(&mock_server) 421 .await; 422 423 let service_jwt = create_service_jwt(&temp_key, &did, &pds_did); 424 let payload = json!({ 425 "handle": handle, 426 "email": format!("{}@example.com", handle), 427 "password": "Testpass123!", 428 "did": did 429 }); 430 let res = client 431 .post(format!( 432 "{}/xrpc/com.atproto.server.createAccount", 433 base_url().await 434 )) 435 .header("Authorization", format!("Bearer {}", service_jwt)) 436 .json(&payload) 437 .send() 438 .await 439 .expect("Failed to send request"); 440 if res.status() != StatusCode::OK { 441 let body: Value = res.json().await.unwrap_or(json!({"error": "parse failed"})); 442 panic!("createAccount BYOD failed: {:?}", body); 443 } 444 let body: Value = res.json().await.expect("Response was not JSON"); 445 let returned_did = body["did"].as_str().expect("No DID in response"); 446 assert_eq!(returned_did, did, "Returned DID should match requested DID"); 447 assert_eq!( 448 body["verificationRequired"], true, 449 "BYOD accounts should require verification" 450 ); 451 452 let access_jwt = common::verify_new_account(&client, returned_did).await; 453 454 let res = client 455 .get(format!( 456 "{}/xrpc/com.atproto.server.checkAccountStatus", 457 base_url().await 458 )) 459 .bearer_auth(&access_jwt) 460 .send() 461 .await 462 .expect("Failed to check account status"); 463 assert_eq!(res.status(), StatusCode::OK); 464 let status: Value = res.json().await.expect("Response was not JSON"); 465 assert_eq!( 466 status["activated"], false, 467 "BYOD account should be deactivated initially" 468 ); 469 470 let res = client 471 .get(format!( 472 "{}/xrpc/com.atproto.identity.getRecommendedDidCredentials", 473 base_url().await 474 )) 475 .bearer_auth(&access_jwt) 476 .send() 477 .await 478 .expect("Failed to get recommended credentials"); 479 assert_eq!(res.status(), StatusCode::OK); 480 let creds: Value = res.json().await.expect("Response was not JSON"); 481 assert!( 482 creds["verificationMethods"]["atproto"].is_string(), 483 "Should return PDS signing key" 484 ); 485 let pds_signing_key = creds["verificationMethods"]["atproto"] 486 .as_str() 487 .expect("No atproto verification method"); 488 assert!( 489 pds_signing_key.starts_with("did:key:"), 490 "PDS signing key should be did:key format" 491 ); 492 493 let res = client 494 .post(format!( 495 "{}/xrpc/com.atproto.server.activateAccount", 496 base_url().await 497 )) 498 .bearer_auth(&access_jwt) 499 .send() 500 .await 501 .expect("Failed to activate account"); 502 assert_eq!( 503 res.status(), 504 StatusCode::OK, 505 "activateAccount should succeed" 506 ); 507 508 let res = client 509 .get(format!( 510 "{}/xrpc/com.atproto.server.checkAccountStatus", 511 base_url().await 512 )) 513 .bearer_auth(&access_jwt) 514 .send() 515 .await 516 .expect("Failed to check account status"); 517 assert_eq!(res.status(), StatusCode::OK); 518 let status: Value = res.json().await.expect("Response was not JSON"); 519 assert_eq!( 520 status["activated"], true, 521 "Account should be activated after activateAccount call" 522 ); 523 524 let res = client 525 .post(format!( 526 "{}/xrpc/com.atproto.repo.createRecord", 527 base_url().await 528 )) 529 .bearer_auth(&access_jwt) 530 .json(&json!({ 531 "repo": did, 532 "collection": "app.bsky.feed.post", 533 "record": { 534 "$type": "app.bsky.feed.post", 535 "text": "Hello from BYOD did:web!", 536 "createdAt": chrono::Utc::now().to_rfc3339() 537 } 538 })) 539 .send() 540 .await 541 .expect("Failed to create post"); 542 assert_eq!( 543 res.status(), 544 StatusCode::OK, 545 "Activated BYOD account should be able to create records" 546 ); 547} 548 549#[tokio::test] 550async fn test_did_web_can_edit_did_document() { 551 let client = client(); 552 let base = base_url().await; 553 let handle = format!("doc{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 554 let payload = json!({ 555 "handle": handle, 556 "email": format!("{}@example.com", handle), 557 "password": "Testpass123!", 558 "didType": "web" 559 }); 560 let res = client 561 .post(format!("{}/xrpc/com.atproto.server.createAccount", base)) 562 .json(&payload) 563 .send() 564 .await 565 .expect("Failed to send request"); 566 assert_eq!(res.status(), StatusCode::OK); 567 let body: Value = res.json().await.expect("Response was not JSON"); 568 let did = body["did"].as_str().expect("No DID").to_string(); 569 let jwt = verify_new_account(&client, &did).await; 570 let res = client 571 .get(format!("{}/xrpc/_account.getDidDocument", base)) 572 .bearer_auth(&jwt) 573 .send() 574 .await 575 .expect("Failed to send request"); 576 assert_eq!(res.status(), StatusCode::OK); 577 let body: Value = res.json().await.expect("Response was not JSON"); 578 assert!( 579 body["didDocument"].is_object(), 580 "Should return DID document" 581 ); 582 assert_eq!( 583 body["didDocument"]["id"], did, 584 "DID document should have correct id" 585 ); 586 let res = client 587 .post(format!("{}/xrpc/_account.updateDidDocument", base)) 588 .bearer_auth(&jwt) 589 .json(&json!({ 590 "alsoKnownAs": ["at://custom.handle.test"] 591 })) 592 .send() 593 .await 594 .expect("Failed to send request"); 595 assert_eq!( 596 res.status(), 597 StatusCode::OK, 598 "Non-migrated did:web user should be able to update DID document" 599 ); 600 let body: Value = res.json().await.expect("Response was not JSON"); 601 assert!(body["success"].as_bool().unwrap_or(false)); 602 let also_known_as = body["didDocument"]["alsoKnownAs"] 603 .as_array() 604 .expect("alsoKnownAs should be array"); 605 assert!( 606 also_known_as 607 .iter() 608 .any(|v| v.as_str() == Some("at://custom.handle.test")), 609 "alsoKnownAs should contain custom entry" 610 ); 611} 612 613#[tokio::test] 614async fn test_deactivate_account_basic() { 615 let client = client(); 616 let base = base_url().await; 617 let handle = format!("dea{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 618 let payload = json!({ 619 "handle": handle, 620 "email": format!("{}@example.com", handle), 621 "password": "Testpass123!", 622 "didType": "web" 623 }); 624 let res = client 625 .post(format!("{}/xrpc/com.atproto.server.createAccount", base)) 626 .json(&payload) 627 .send() 628 .await 629 .expect("Failed to send request"); 630 assert_eq!(res.status(), StatusCode::OK); 631 let body: Value = res.json().await.expect("Response was not JSON"); 632 let did = body["did"].as_str().expect("No DID").to_string(); 633 let jwt = verify_new_account(&client, &did).await; 634 let res = client 635 .post(format!( 636 "{}/xrpc/com.atproto.server.deactivateAccount", 637 base 638 )) 639 .bearer_auth(&jwt) 640 .json(&json!({})) 641 .send() 642 .await 643 .expect("Failed to send request"); 644 assert_eq!(res.status(), StatusCode::OK); 645 let res = client 646 .get(format!("{}/xrpc/com.atproto.server.getSession", base)) 647 .bearer_auth(&jwt) 648 .send() 649 .await 650 .expect("Failed to send request"); 651 assert_eq!(res.status(), StatusCode::OK); 652 let body: Value = res.json().await.expect("Response was not JSON"); 653 assert_eq!(body["active"], false, "Account should be deactivated"); 654 assert_eq!( 655 body["status"], "deactivated", 656 "Status should be 'deactivated'" 657 ); 658}