feat(pds): DID-Dokument unter /.well-known/did.json ausliefern
Die AppView soll die Access-Tokens der PDS prüfen können, ohne dass PDS_JWT_SECRET den PDS-Prozess verlässt. Verifiziert wird ES256 mit dem *öffentlichen* Teil des P-256-Schlüssels — den veröffentlicht die PDS jetzt als verificationMethod (Multikey) in ihrem DID-Dokument. Damit fällt auch die hartkodierte Service-DID: describeServer gab stur did:web:pds.maarcadetweet.local zurück, unabhängig von PDS_PUBLIC_URL. Beide Endpoints leiten sie jetzt aus einer Quelle ab (AppConfig::pds_did(), did:web-Regel mit %3A-kodiertem Port). Der `iss` des Access-Tokens baute die DID zuvor ohne Port-Kodierung zusammen — also in einer Form, der kein did:web-Resolver folgen könnte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
This commit is contained in:
co-authored by
Claude Opus 5
parent
ec8fe187fe
commit
786a892658
@@ -38,11 +38,86 @@ async fn describe_server() {
|
||||
.json()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(r["did"].is_string());
|
||||
// The DID is derived from `PDS_PUBLIC_URL`, not hardcoded — so we
|
||||
// assert the *shape* (any deployment must produce a did:web) and
|
||||
// leave the exact value to `at_shared::config`'s unit tests.
|
||||
let did = r["did"].as_str().expect("describeServer must return a did");
|
||||
assert!(did.starts_with("did:web:"), "did = {did}");
|
||||
assert!(r["available_user_domains"].is_array());
|
||||
assert_eq!(r["invite_code_required"], json!(false));
|
||||
}
|
||||
|
||||
/// `GET /.well-known/did.json` — the document the AppView fetches to
|
||||
/// learn the key our access tokens are signed with.
|
||||
///
|
||||
/// Two properties matter beyond "it returns JSON": the document's `id`
|
||||
/// must be the same DID `describeServer` advertises (otherwise a client
|
||||
/// that trusts one and resolves the other ends up at a different
|
||||
/// identity), and it must carry a usable `publicKeyMultibase`.
|
||||
#[tokio::test]
|
||||
async fn did_document_publishes_the_server_key() {
|
||||
if !wait_for_pds().await {
|
||||
eprintln!("pds not running, skipping");
|
||||
return;
|
||||
}
|
||||
let c = client().await;
|
||||
let doc: Value = c
|
||||
.get(format!("{}/.well-known/did.json", PDS_URL))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let id = doc["id"].as_str().expect("did document needs an id");
|
||||
assert!(id.starts_with("did:web:"), "id = {id}");
|
||||
|
||||
let described: Value = c
|
||||
.get(format!("{}/xrpc/com.atproto.server.describeServer", PDS_URL))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
described["did"].as_str().unwrap(),
|
||||
id,
|
||||
"describeServer and the did document must name the same identity"
|
||||
);
|
||||
|
||||
let vm = &doc["verificationMethod"][0];
|
||||
assert_eq!(vm["type"], json!("Multikey"));
|
||||
assert_eq!(vm["controller"], json!(id));
|
||||
assert_eq!(vm["id"], json!(format!("{id}#atproto")));
|
||||
let key = vm["publicKeyMultibase"]
|
||||
.as_str()
|
||||
.expect("verificationMethod needs publicKeyMultibase");
|
||||
// base58-btc multibase — the `z` prefix the AppView's decoder wants.
|
||||
assert!(key.starts_with('z'), "key = {key}");
|
||||
|
||||
// And it really is the key our tokens verify against: mint a
|
||||
// session and check the access JWT against the published key.
|
||||
let handle = format!("didjson_{}.maarcadetweet.local", uuid::Uuid::new_v4().simple());
|
||||
let acc: Value = c
|
||||
.post(format!("{}/xrpc/com.atproto.server.createAccount", PDS_URL))
|
||||
.json(&json!({"handle": handle, "password": "hunter2hunter2"}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json()
|
||||
.await
|
||||
.unwrap();
|
||||
let jwt = acc["access_jwt"].as_str().expect("access_jwt");
|
||||
let claims = at_crypto::jwt::verify_jwt(jwt, key)
|
||||
.expect("access token must verify against the published key");
|
||||
assert_eq!(claims.sub, acc["did"].as_str().unwrap());
|
||||
assert_eq!(claims.scope.as_deref(), Some("com.atproto.access"));
|
||||
// `iss` is the same did:web the document identifies.
|
||||
assert_eq!(claims.iss, id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_account_session_refresh_resolve() {
|
||||
if !wait_for_pds().await {
|
||||
|
||||
Reference in New Issue
Block a user