+1
-1
TODO.md
+1
-1
TODO.md
···
20
20
- [x] Initialize user repository (Root commit).
21
21
- [x] Return access JWT and DID.
22
22
- [x] Create DID for new user (did:web).
23
-
- [ ] Implement all TODOs regarding did:webs.
24
23
- [x] Session Management
25
24
- [x] Implement `com.atproto.server.createSession` (Login).
26
25
- [x] Implement `com.atproto.server.getSession`.
···
138
137
- [ ] Implement CAR (Content Addressable Archive) encoding/decoding.
139
138
- [ ] Validation
140
139
- [ ] DID PLC Operations (Sign rotation keys).
140
+
- [ ] Fix any remaining TODOs in the code, everywhere, full stop.
141
141
+74
-4
src/api/identity.rs
+74
-4
src/api/identity.rs
···
16
16
use k256::SecretKey;
17
17
use rand::rngs::OsRng;
18
18
use base64::Engine;
19
+
use reqwest;
19
20
20
21
#[derive(Deserialize)]
21
22
pub struct CreateAccountInput {
···
50
51
format!("did:plc:{}", uuid::Uuid::new_v4())
51
52
} else {
52
53
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.
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
57
d.clone()
58
58
}
59
59
} else {
···
352
352
}]
353
353
})).into_response()
354
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
+30
-2
tests/identity.rs
···
2
2
use common::*;
3
3
use reqwest::StatusCode;
4
4
use serde_json::{json, Value};
5
+
use wiremock::{MockServer, Mock, ResponseTemplate};
6
+
use wiremock::matchers::{method, path};
5
7
6
8
// #[tokio::test]
7
9
// async fn test_resolve_handle() {
···
36
38
async fn test_create_did_web_account_and_resolve() {
37
39
let client = client();
38
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
+
39
47
let handle = format!("webuser_{}", uuid::Uuid::new_v4());
40
48
41
-
let did = format!("did:web:example.com:u:{}", handle);
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;
42
66
43
67
let payload = json!({
44
68
"handle": handle,
···
53
77
.await
54
78
.expect("Failed to send request");
55
79
56
-
assert_eq!(res.status(), StatusCode::OK);
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
+
}
57
85
let body: Value = res.json().await.expect("createAccount response was not JSON");
58
86
assert_eq!(body["did"], did);
59
87