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
@@ -39,7 +39,13 @@ pub fn issue_access_jwt(
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
let exp = now + 3600;
|
||||
let claims = JwtClaims {
|
||||
iss: format!("did:web:{}", cfg.pds_public_url.trim_start_matches("http://").trim_start_matches("https://")),
|
||||
// Same derivation as `describeServer` and `/.well-known/did.json`
|
||||
// (`AppConfig::pds_did`), so a verifier can take `iss`, resolve
|
||||
// the did:web document and arrive at the key this token is
|
||||
// signed with. The previous inline version dropped the
|
||||
// percent-encoding of the port, producing an `iss` that no
|
||||
// did:web resolver could follow.
|
||||
iss: cfg.pds_did(),
|
||||
sub: did.to_string(),
|
||||
aud: "did:web:appview.maarcadetweet.local".into(),
|
||||
iat: now,
|
||||
|
||||
@@ -73,6 +73,7 @@ pub fn router(state: AppState) -> Router {
|
||||
Router::new()
|
||||
.route("/", get(root))
|
||||
.route("/healthz", get(healthz))
|
||||
.route("/.well-known/did.json", get(did_document))
|
||||
.route(
|
||||
"/xrpc/com.atproto.server.describeServer",
|
||||
get(describe_server),
|
||||
@@ -163,9 +164,69 @@ async fn healthz() -> Json<serde_json::Value> {
|
||||
Json(json!({ "ok": true }))
|
||||
}
|
||||
|
||||
/// `GET /.well-known/did.json` — the PDS's own DID document.
|
||||
///
|
||||
/// This is how the AppView (and any other relying party) learns the
|
||||
/// P-256 public key that the access JWTs in
|
||||
/// `Authorization: Bearer …` are signed with. Without it the AppView
|
||||
/// could not verify a token at all, and the only alternative would be
|
||||
/// shipping `PDS_JWT_SECRET` to a second service — a private signing
|
||||
/// key crossing a service boundary, for a check that needs nothing but
|
||||
/// the public half.
|
||||
///
|
||||
/// Nothing in this response is secret. `publicKeyMultibase` is the
|
||||
/// uncompressed P-256 point derived from `PDS_JWT_SECRET` by
|
||||
/// [`jwt_issuer::server_p256_public_multibase`]; the secret itself
|
||||
/// never leaves this process.
|
||||
///
|
||||
/// The document id is [`AppConfig::pds_did`], i.e. it follows
|
||||
/// `PDS_PUBLIC_URL` — so a `did:web:` resolver that starts from the DID,
|
||||
/// rebuilds the URL and fetches this path lands back here rather than at
|
||||
/// some other host's document.
|
||||
async fn did_document(State(state): State<AppState>) -> Result<Json<serde_json::Value>, (axum::http::StatusCode, Json<serde_json::Value>)> {
|
||||
let did = state.cfg.pds_did();
|
||||
let public_multibase = jwt_issuer::server_p256_public_multibase(&state.cfg).map_err(|e| {
|
||||
// A malformed `PDS_JWT_SECRET` is the one way this fails, and
|
||||
// it is exactly the failure that also breaks every token this
|
||||
// server issues — surface it instead of publishing a document
|
||||
// with a missing key.
|
||||
(
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({
|
||||
"error": "InternalServerError",
|
||||
"message": format!("server key unavailable: {e}"),
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
Ok(Json(json!({
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/did/v1",
|
||||
"https://w3id.org/security/multikey/v1",
|
||||
],
|
||||
"id": did,
|
||||
"verificationMethod": [{
|
||||
// `#atproto` is the fragment AT Proto uses for a repo's
|
||||
// signing key; we reuse it for the server key so a generic
|
||||
// did:web consumer finds it in the usual place.
|
||||
"id": format!("{did}#atproto"),
|
||||
"type": "Multikey",
|
||||
"controller": did,
|
||||
"publicKeyMultibase": public_multibase,
|
||||
}],
|
||||
"service": [{
|
||||
"id": "#atproto_pds",
|
||||
"type": "AtprotoPersonalDataServer",
|
||||
"serviceEndpoint": state.cfg.pds_public_url,
|
||||
}],
|
||||
})))
|
||||
}
|
||||
|
||||
async fn describe_server(State(state): State<AppState>) -> Json<DescribeServerResp> {
|
||||
Json(DescribeServerResp {
|
||||
did: "did:web:pds.maarcadetweet.local".into(),
|
||||
// Derived from `PDS_PUBLIC_URL`, never hardcoded — see
|
||||
// `AppConfig::pds_did`. The same value ids the document at
|
||||
// `/.well-known/did.json`.
|
||||
did: state.cfg.pds_did(),
|
||||
available_user_domains: vec![state
|
||||
.cfg
|
||||
.pds_handle_dns_zone
|
||||
|
||||
Reference in New Issue
Block a user