this repo has no description

Check did:web on account create

Changed files
+105 -7
src
tests
+1 -1
TODO.md
··· 20 - [x] Initialize user repository (Root commit). 21 - [x] Return access JWT and DID. 22 - [x] Create DID for new user (did:web). 23 - - [ ] Implement all TODOs regarding did:webs. 24 - [x] Session Management 25 - [x] Implement `com.atproto.server.createSession` (Login). 26 - [x] Implement `com.atproto.server.getSession`. ··· 138 - [ ] Implement CAR (Content Addressable Archive) encoding/decoding. 139 - [ ] Validation 140 - [ ] DID PLC Operations (Sign rotation keys). 141
··· 20 - [x] Initialize user repository (Root commit). 21 - [x] Return access JWT and DID. 22 - [x] Create DID for new user (did:web). 23 - [x] Session Management 24 - [x] Implement `com.atproto.server.createSession` (Login). 25 - [x] Implement `com.atproto.server.getSession`. ··· 137 - [ ] Implement CAR (Content Addressable Archive) encoding/decoding. 138 - [ ] Validation 139 - [ ] DID PLC Operations (Sign rotation keys). 140 + - [ ] Fix any remaining TODOs in the code, everywhere, full stop. 141
+74 -4
src/api/identity.rs
··· 16 use k256::SecretKey; 17 use rand::rngs::OsRng; 18 use base64::Engine; 19 20 #[derive(Deserialize)] 21 pub struct CreateAccountInput { ··· 50 format!("did:plc:{}", uuid::Uuid::new_v4()) 51 } else { 52 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 53 - let _expected_prefix = format!("did:web:{}", hostname); 54 - 55 - // TODO: should verify we are the authority for it if it matches our hostname. 56 - // TODO: if it's an external did:web, we should technically verify ownership via ServiceAuth, but skipping for now. 57 d.clone() 58 } 59 } else { ··· 352 }] 353 })).into_response() 354 }
··· 16 use k256::SecretKey; 17 use rand::rngs::OsRng; 18 use base64::Engine; 19 + use reqwest; 20 21 #[derive(Deserialize)] 22 pub struct CreateAccountInput { ··· 51 format!("did:plc:{}", uuid::Uuid::new_v4()) 52 } else { 53 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 54 + if let Err(e) = verify_did_web(d, &hostname, &input.handle).await { 55 + return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidDid", "message": e}))).into_response(); 56 + } 57 d.clone() 58 } 59 } else { ··· 352 }] 353 })).into_response() 354 } 355 + 356 + async fn verify_did_web(did: &str, hostname: &str, handle: &str) -> Result<(), String> { 357 + let expected_prefix = if hostname.contains(':') { 358 + format!("did:web:{}", hostname.replace(':', "%3A")) 359 + } else { 360 + format!("did:web:{}", hostname) 361 + }; 362 + 363 + if did.starts_with(&expected_prefix) { 364 + let suffix = &did[expected_prefix.len()..]; 365 + let expected_suffix = format!(":u:{}", handle); 366 + if suffix == expected_suffix { 367 + Ok(()) 368 + } else { 369 + Err(format!("Invalid DID path for this PDS. Expected {}", expected_suffix)) 370 + } 371 + } else { 372 + let parts: Vec<&str> = did.split(':').collect(); 373 + if parts.len() < 3 || parts[0] != "did" || parts[1] != "web" { 374 + return Err("Invalid did:web format".into()); 375 + } 376 + 377 + let domain_segment = parts[2]; 378 + let domain = domain_segment.replace("%3A", ":"); 379 + 380 + let scheme = if domain.starts_with("localhost") || domain.starts_with("127.0.0.1") { 381 + "http" 382 + } else { 383 + "https" 384 + }; 385 + 386 + let url = if parts.len() == 3 { 387 + format!("{}://{}/.well-known/did.json", scheme, domain) 388 + } else { 389 + let path = parts[3..].join("/"); 390 + format!("{}://{}/{}/did.json", scheme, domain, path) 391 + }; 392 + 393 + let client = reqwest::Client::builder() 394 + .timeout(std::time::Duration::from_secs(5)) 395 + .build() 396 + .map_err(|e| format!("Failed to create client: {}", e))?; 397 + 398 + let resp = client.get(&url).send().await 399 + .map_err(|e| format!("Failed to fetch DID doc: {}", e))?; 400 + 401 + if !resp.status().is_success() { 402 + return Err(format!("Failed to fetch DID doc: HTTP {}", resp.status())); 403 + } 404 + 405 + let doc: serde_json::Value = resp.json().await 406 + .map_err(|e| format!("Failed to parse DID doc: {}", e))?; 407 + 408 + let services = doc["service"].as_array() 409 + .ok_or("No services found in DID doc")?; 410 + 411 + let pds_endpoint = format!("https://{}", hostname); 412 + 413 + let has_valid_service = services.iter().any(|s| { 414 + s["type"] == "AtprotoPersonalDataServer" && 415 + s["serviceEndpoint"] == pds_endpoint 416 + }); 417 + 418 + if has_valid_service { 419 + Ok(()) 420 + } else { 421 + Err(format!("DID document does not list this PDS ({}) as AtprotoPersonalDataServer", pds_endpoint)) 422 + } 423 + } 424 + }
+30 -2
tests/identity.rs
··· 2 use common::*; 3 use reqwest::StatusCode; 4 use serde_json::{json, Value}; 5 6 // #[tokio::test] 7 // async fn test_resolve_handle() { ··· 36 async fn test_create_did_web_account_and_resolve() { 37 let client = client(); 38 39 let handle = format!("webuser_{}", uuid::Uuid::new_v4()); 40 41 - let did = format!("did:web:example.com:u:{}", handle); 42 43 let payload = json!({ 44 "handle": handle, ··· 53 .await 54 .expect("Failed to send request"); 55 56 - assert_eq!(res.status(), StatusCode::OK); 57 let body: Value = res.json().await.expect("createAccount response was not JSON"); 58 assert_eq!(body["did"], did); 59
··· 2 use common::*; 3 use reqwest::StatusCode; 4 use serde_json::{json, Value}; 5 + use wiremock::{MockServer, Mock, ResponseTemplate}; 6 + use wiremock::matchers::{method, path}; 7 8 // #[tokio::test] 9 // async fn test_resolve_handle() { ··· 38 async fn test_create_did_web_account_and_resolve() { 39 let client = client(); 40 41 + let mock_server = MockServer::start().await; 42 + let mock_uri = mock_server.uri(); 43 + let mock_addr = mock_uri.trim_start_matches("http://"); 44 + 45 + let did = format!("did:web:{}", mock_addr.replace(":", "%3A")); 46 + 47 let handle = format!("webuser_{}", uuid::Uuid::new_v4()); 48 49 + let pds_endpoint = "https://localhost"; 50 + 51 + let did_doc = json!({ 52 + "@context": ["https://www.w3.org/ns/did/v1"], 53 + "id": did, 54 + "service": [{ 55 + "id": "#atproto_pds", 56 + "type": "AtprotoPersonalDataServer", 57 + "serviceEndpoint": pds_endpoint 58 + }] 59 + }); 60 + 61 + Mock::given(method("GET")) 62 + .and(path("/.well-known/did.json")) 63 + .respond_with(ResponseTemplate::new(200).set_body_json(did_doc)) 64 + .mount(&mock_server) 65 + .await; 66 67 let payload = json!({ 68 "handle": handle, ··· 77 .await 78 .expect("Failed to send request"); 79 80 + if res.status() != StatusCode::OK { 81 + let status = res.status(); 82 + let body: Value = res.json().await.unwrap_or(json!({"error": "could not parse body"})); 83 + panic!("createAccount failed with status {}: {:?}", status, body); 84 + } 85 let body: Value = res.json().await.expect("createAccount response was not JSON"); 86 assert_eq!(body["did"], did); 87