use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use async_trait::async_trait;
use k256::ecdsa::VerifyingKey;
use super::{handle_from_aka, is_did, verified_handle, DidDocument, DidResolveError, EndpointOverrides};
use crate::service_jwt::multibase_to_verifying_key;
use crate::sync::RepoKey;
#[async_trait]
pub trait DidKeyResolver: Send + Sync {
async fn atproto_key(&self, did: &str) -> Result<VerifyingKey, DidResolveError>;
async fn repo_key(&self, did: &str) -> Result<RepoKey, DidResolveError> {
Ok(RepoKey::K256(self.atproto_key(did).await?))
}
async fn pds_endpoint(&self, did: &str) -> Result<Option<String>, DidResolveError>;
async fn pds_fetch_endpoint(&self, did: &str) -> Result<Option<String>, DidResolveError> {
self.pds_endpoint(did).await
}
fn pds_call_origin(&self, origin: &str) -> String {
origin.trim_end_matches('/').to_owned()
}
async fn handle(&self, did: &str) -> Result<Option<String>, DidResolveError>;
}
#[derive(Default)]
pub struct InMemoryDidKeyResolver {
keys: Mutex<HashMap<String, String>>,
pds: Mutex<HashMap<String, String>>,
handles: Mutex<HashMap<String, String>>,
}
impl InMemoryDidKeyResolver {
pub fn new() -> Self {
Self::default()
}
pub fn insert_key(&self, did: impl Into<String>, multibase_key: impl Into<String>) {
self.keys.lock().unwrap().insert(did.into(), multibase_key.into());
}
pub fn insert_pds(&self, did: impl Into<String>, pds_url: impl Into<String>) {
self.pds.lock().unwrap().insert(did.into(), pds_url.into());
}
pub fn insert_handle(&self, did: impl Into<String>, handle: impl Into<String>) {
self.handles.lock().unwrap().insert(did.into(), handle.into());
}
}
#[async_trait]
impl DidKeyResolver for InMemoryDidKeyResolver {
async fn atproto_key(&self, did: &str) -> Result<VerifyingKey, DidResolveError> {
let mb = self.keys.lock().unwrap().get(did).cloned().ok_or(DidResolveError::NotFound)?;
multibase_to_verifying_key(&mb).map_err(DidResolveError::from)
}
async fn repo_key(&self, did: &str) -> Result<RepoKey, DidResolveError> {
let mb = self.keys.lock().unwrap().get(did).cloned().ok_or(DidResolveError::NotFound)?;
RepoKey::from_multikey(&mb).ok_or(DidResolveError::BadKey)
}
async fn pds_endpoint(&self, did: &str) -> Result<Option<String>, DidResolveError> {
Ok(self.pds.lock().unwrap().get(did).cloned())
}
async fn handle(&self, did: &str) -> Result<Option<String>, DidResolveError> {
Ok(self.handles.lock().unwrap().get(did).cloned())
}
}
pub struct CommonDidKeyResolver {
plc_url: String,
http: reqwest::Client,
public: reqwest::Client,
timeout: Duration,
overrides: EndpointOverrides,
nofollow: Option<reqwest::Client>,
}
impl CommonDidKeyResolver {
pub fn new(plc_url: impl Into<String>, timeout_secs: u64) -> Self {
let plc_url = plc_url.into().trim_end_matches('/').to_owned();
tracing::info!(plc_url = %plc_url, "DID resolver pinned to PLC directory");
Self {
plc_url,
http: reqwest::Client::new(),
public: public_only_client(),
timeout: Duration::from_secs(timeout_secs),
overrides: EndpointOverrides::none(),
nofollow: None,
}
}
pub fn with_endpoint_overrides(mut self, overrides: EndpointOverrides) -> Result<Self, reqwest::Error> {
if !overrides.is_empty() {
tracing::info!(overrides = %overrides.describe(), "DID resolver reads these endpoints in-network");
self.nofollow = Some(reqwest::Client::builder().redirect(reqwest::redirect::Policy::none()).build()?);
}
self.overrides = overrides;
Ok(self)
}
async fn fetch_doc(&self, http: &reqwest::Client, url: &str, must_be: Option<&str>) -> Result<DidDocument, DidResolveError> {
let fetch = async {
let resp = http
.get(url)
.header(reqwest::header::ACCEPT, "application/did+ld+json,application/json")
.send()
.await
.map_err(|_| DidResolveError::Unavailable)?;
status_failure(resp.status()).map_or(Ok(()), Err)?;
let doc: DidDocument = resp.json().await.map_err(|_| DidResolveError::NotFound)?;
if must_be.is_some_and(|did| doc.id != did) {
return Err(DidResolveError::NotFound);
}
Ok(doc)
};
tokio::time::timeout(self.timeout, fetch).await.map_err(|_| DidResolveError::Unavailable)?
}
async fn resolve_doc(&self, did: &str) -> Result<DidDocument, DidResolveError> {
if !is_did(did) {
return Err(DidResolveError::NotFound);
}
if let Some(url) = self.overrides.did_web_doc_url(did) {
let http = self.nofollow.as_ref().ok_or(DidResolveError::Unavailable)?;
return self.fetch_doc(http, &url, Some(did)).await;
}
if did.starts_with("did:plc:") {
return self.fetch_doc(&self.http, &format!("{}/{did}", self.plc_url), None).await;
}
let Some(host) = did.strip_prefix("did:web:").filter(|host| public_host(host)) else {
return Err(DidResolveError::NotFound);
};
self.fetch_doc(&self.public, &format!("https://{host}/.well-known/did.json"), Some(did)).await
}
async fn handle_did(&self, handle: &str) -> Option<String> {
if !public_host(handle) {
return None;
}
let fetch = async {
let resp = self.public.get(format!("https://{handle}/.well-known/atproto-did")).send().await.ok()?;
if !resp.status().is_success() {
return None;
}
let text = resp.text().await.ok()?;
let did = text.trim();
is_did(did).then(|| did.to_owned())
};
tokio::time::timeout(self.timeout, fetch).await.ok().flatten()
}
}
fn status_failure(status: reqwest::StatusCode) -> Option<DidResolveError> {
if status.is_success() {
None
} else if status.is_server_error() || status == reqwest::StatusCode::TOO_MANY_REQUESTS {
Some(DidResolveError::Unavailable)
} else {
Some(DidResolveError::NotFound)
}
}
const RESERVED_TLDS: [&str; 8] = ["alt", "arpa", "example", "internal", "invalid", "local", "localhost", "onion"];
fn public_host(s: &str) -> bool {
let labels: Vec<&str> = s.split('.').collect();
let tld = labels.last().copied().unwrap_or_default();
s.len() <= 253
&& labels.len() >= 2
&& labels.iter().all(|l| {
!l.is_empty() && l.len() <= 63 && l.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-') && !l.starts_with('-') && !l.ends_with('-')
})
&& tld.starts_with(|c: char| c.is_ascii_alphabetic())
&& !RESERVED_TLDS.contains(&tld.to_ascii_lowercase().as_str())
}
fn is_public_ip(ip: IpAddr) -> bool {
fn v4(ip: Ipv4Addr) -> bool {
let [a, b, c, _] = ip.octets();
!(ip.is_unspecified()
|| ip.is_loopback()
|| ip.is_private()
|| ip.is_link_local()
|| ip.is_broadcast()
|| ip.is_documentation()
|| ip.is_multicast()
|| a == 0
|| (a == 100 && (64..128).contains(&b))
|| (a == 192 && b == 0 && c == 0)
|| (a == 198 && (b == 18 || b == 19))
|| a >= 240)
}
fn v6(ip: Ipv6Addr) -> bool {
if let Some(mapped) = ip.to_ipv4_mapped() {
return v4(mapped);
}
let s = ip.segments();
if s[..6] == [0x64, 0xff9b, 0, 0, 0, 0] {
return v4(Ipv4Addr::new((s[6] >> 8) as u8, s[6] as u8, (s[7] >> 8) as u8, s[7] as u8));
}
!(ip.is_unspecified()
|| ip.is_loopback()
|| ip.is_multicast()
|| (s[0] & 0xfe00) == 0xfc00
|| (s[0] & 0xffc0) == 0xfe80
|| (s[0] == 0x2001 && s[1] == 0x0db8))
}
match ip {
IpAddr::V4(ip) => v4(ip),
IpAddr::V6(ip) => v6(ip),
}
}
struct PublicOnlyDns;
impl reqwest::dns::Resolve for PublicOnlyDns {
fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {
let host = name.as_str().to_owned();
Box::pin(async move {
let addrs: Vec<SocketAddr> = tokio::net::lookup_host((host.as_str(), 0)).await?.filter(|a| is_public_ip(a.ip())).collect();
if addrs.is_empty() {
return Err(format!("{host} resolves to no public address").into());
}
Ok(Box::new(addrs.into_iter()) as reqwest::dns::Addrs)
})
}
}
fn public_only_client() -> reqwest::Client {
let redirects = reqwest::redirect::Policy::custom(|attempt| {
let ok = attempt.previous().len() < 5 && attempt.url().scheme() == "https" && attempt.url().port().is_none() && attempt.url().domain().is_some_and(public_host);
if ok {
attempt.follow()
} else {
attempt.stop()
}
});
reqwest::Client::builder()
.dns_resolver(Arc::new(PublicOnlyDns))
.redirect(redirects)
.build()
.expect("build the public-only HTTP client")
}
#[async_trait]
impl DidKeyResolver for CommonDidKeyResolver {
async fn atproto_key(&self, did: &str) -> Result<VerifyingKey, DidResolveError> {
let doc = self.resolve_doc(did).await?;
let vm = doc.get_signing_key().ok_or(DidResolveError::NoAtprotoKey)?;
let mb = vm.public_key_multibase.as_deref().ok_or(DidResolveError::NoAtprotoKey)?;
multibase_to_verifying_key(mb).map_err(DidResolveError::from)
}
async fn repo_key(&self, did: &str) -> Result<RepoKey, DidResolveError> {
let doc = self.resolve_doc(did).await?;
let vm = doc.get_signing_key().ok_or(DidResolveError::NoAtprotoKey)?;
let mb = vm.public_key_multibase.as_deref().ok_or(DidResolveError::NoAtprotoKey)?;
RepoKey::from_multikey(mb).ok_or(DidResolveError::BadKey)
}
async fn pds_endpoint(&self, did: &str) -> Result<Option<String>, DidResolveError> {
Ok(self.resolve_doc(did).await?.get_pds_endpoint())
}
async fn pds_fetch_endpoint(&self, did: &str) -> Result<Option<String>, DidResolveError> {
Ok(self.pds_endpoint(did).await?.map(|pds| self.overrides.pds(&pds)))
}
fn pds_call_origin(&self, origin: &str) -> String {
self.overrides.pds(origin)
}
async fn handle(&self, did: &str) -> Result<Option<String>, DidResolveError> {
let doc = self.resolve_doc(did).await?;
let Some(claimed) = handle_from_aka(doc.also_known_as.as_deref()) else {
return Ok(None);
};
let resolved = self.handle_did(&claimed).await;
Ok(verified_handle(did, &claimed, resolved.as_deref()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use k256::ecdsa::SigningKey;
fn encode_did_key_multibase(vk: &VerifyingKey) -> String {
let compressed = vk.to_encoded_point(true);
let mut prefixed = vec![0xe7u8, 0x01];
prefixed.extend_from_slice(compressed.as_bytes());
format!("z{}", bs58::encode(prefixed).into_string())
}
async fn serve(app: axum::Router) -> String {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
format!("http://{addr}")
}
async fn serve_did_json(doc: serde_json::Value) -> String {
serve(axum::Router::new().route(
"/.well-known/did.json",
axum::routing::get(move || {
let doc = doc.clone();
async move { axum::Json(doc) }
}),
))
.await
}
async fn serve_status(status: u16) -> String {
let status = axum::http::StatusCode::from_u16(status).unwrap();
serve(axum::Router::new().route("/.well-known/did.json", axum::routing::get(move || async move { status }))).await
}
fn web_resolver(base: &str, timeout_secs: u64) -> CommonDidKeyResolver {
let overrides = EndpointOverrides::parse(&format!("did:web:app.example={base}")).unwrap();
CommonDidKeyResolver::new("http://127.0.0.1:9", timeout_secs).with_endpoint_overrides(overrides).unwrap()
}
fn web_doc(id: &str, pds: &str) -> serde_json::Value {
serde_json::json!({
"@context": ["https://www.w3.org/ns/did/v1"],
"id": id,
"service": [{"id": "#atproto_pds", "type": "AtprotoPersonalDataServer", "serviceEndpoint": pds}],
})
}
#[tokio::test]
async fn an_overridden_did_web_resolves_in_network_and_fetches_its_pds_in_network() {
let did = "did:web:app.example";
let base = serve_did_json(web_doc(did, "https://pds.app.example")).await;
let overrides =
EndpointOverrides::parse(&format!("https://pds.app.example=http://pds:3000 did:web:app.example={base}")).unwrap();
let r = CommonDidKeyResolver::new("http://127.0.0.1:9", 5).with_endpoint_overrides(overrides).unwrap();
assert_eq!(r.pds_endpoint(did).await, Ok(Some("https://pds.app.example".into())));
assert_eq!(r.pds_fetch_endpoint(did).await, Ok(Some("http://pds:3000".into())));
}
#[test]
fn the_in_network_client_is_built_only_when_there_are_overrides() {
let none = CommonDidKeyResolver::new("http://127.0.0.1:9", 5).with_endpoint_overrides(EndpointOverrides::none()).unwrap();
assert!(none.nofollow.is_none());
assert!(web_resolver("http://127.0.0.1:9", 5).nofollow.is_some());
}
#[tokio::test]
async fn an_overridden_did_web_must_serve_its_own_document() {
let did = "did:web:app.example";
let base = serve_did_json(web_doc("did:web:someone-else.example", "https://pds.x")).await;
let r = web_resolver(&base, 5);
assert_eq!(r.pds_endpoint(did).await, Err(DidResolveError::NotFound));
}
#[test]
fn a_pds_called_by_its_origin_is_called_in_network_when_overridden() {
let overrides = EndpointOverrides::parse("https://pds.app.example=http://pds:3000").unwrap();
let r = CommonDidKeyResolver::new("http://127.0.0.1:9", 5).with_endpoint_overrides(overrides).unwrap();
assert_eq!(r.pds_call_origin("https://pds.app.example/"), "http://pds:3000");
assert_eq!(r.pds_call_origin("https://pds.elsewhere.example"), "https://pds.elsewhere.example");
assert_eq!(InMemoryDidKeyResolver::new().pds_call_origin("https://pds.app.example/"), "https://pds.app.example");
}
#[tokio::test]
async fn without_overrides_a_fetch_uses_the_published_endpoint() {
let r = InMemoryDidKeyResolver::new();
r.insert_pds("did:plc:x", "https://pds.app.example");
assert_eq!(r.pds_fetch_endpoint("did:plc:x").await, Ok(Some("https://pds.app.example".into())));
}
#[tokio::test]
async fn an_overridden_did_web_maps_failures_like_a_directory() {
let did = "did:web:app.example";
for code in [500, 502, 503, 429] {
let r = web_resolver(&serve_status(code).await, 5);
assert_eq!(r.pds_endpoint(did).await, Err(DidResolveError::Unavailable), "{code}");
}
for code in [404, 410, 403] {
let r = web_resolver(&serve_status(code).await, 5);
assert_eq!(r.pds_endpoint(did).await, Err(DidResolveError::NotFound), "{code}");
}
let redirect = serve(axum::Router::new().route(
"/.well-known/did.json",
axum::routing::get(|| async { axum::response::Redirect::temporary("https://elsewhere.example/did.json") }),
))
.await;
assert_eq!(web_resolver(&redirect, 5).pds_endpoint(did).await, Err(DidResolveError::NotFound));
}
#[tokio::test]
async fn an_overridden_did_web_that_hangs_is_unavailable_at_the_timeout() {
let hang = serve(axum::Router::new().route(
"/.well-known/did.json",
axum::routing::get(|| async {
tokio::time::sleep(Duration::from_secs(30)).await;
"late"
}),
))
.await;
let started = std::time::Instant::now();
assert_eq!(web_resolver(&hang, 1).pds_endpoint("did:web:app.example").await, Err(DidResolveError::Unavailable));
assert!(started.elapsed() < Duration::from_secs(5), "bounded by the 1 s timeout, took {:?}", started.elapsed());
}
#[tokio::test]
async fn a_did_plc_resolves_through_plc_and_only_its_fetch_endpoint_is_rewritten() {
let did = "did:plc:ewvi7nxzyoun6zhxrhs64oiz";
let vk = *SigningKey::from_slice(&[0x11u8; 32]).unwrap().verifying_key();
let mut doc = web_doc(did, "https://pds.app.example");
doc["verificationMethod"] = serde_json::json!([{"id": format!("{did}#atproto"), "type": "Multikey", "controller": did, "publicKeyMultibase": encode_did_key_multibase(&vk)}]);
let plc = serve(axum::Router::new().route(
&format!("/{did}"),
axum::routing::get(move || {
let doc = doc.clone();
async move { axum::Json(doc) }
}),
))
.await;
let overrides = EndpointOverrides::parse("https://pds.app.example=http://pds:3000").unwrap();
let r = CommonDidKeyResolver::new(plc, 5).with_endpoint_overrides(overrides).unwrap();
assert_eq!(r.pds_endpoint(did).await, Ok(Some("https://pds.app.example".into())));
assert_eq!(r.pds_fetch_endpoint(did).await, Ok(Some("http://pds:3000".into())));
assert_eq!(r.atproto_key(did).await, Ok(vk));
assert_eq!(r.repo_key(did).await, Ok(RepoKey::K256(vk)));
assert_eq!(r.atproto_key("did:plc:nobody").await, Err(DidResolveError::NotFound), "PLC 404");
assert_eq!(r.atproto_key("not a did").await, Err(DidResolveError::NotFound));
assert_eq!(r.atproto_key("did:key:zQ3s").await, Err(DidResolveError::NotFound), "unsupported method");
}
#[tokio::test]
async fn a_directory_that_cannot_answer_is_unavailable() {
let plc = serve(axum::Router::new().fallback(|| async { axum::http::StatusCode::SERVICE_UNAVAILABLE })).await;
let r = CommonDidKeyResolver::new(plc, 5);
assert_eq!(r.atproto_key("did:plc:ewvi7nxzyoun6zhxrhs64oiz").await, Err(DidResolveError::Unavailable));
let r = CommonDidKeyResolver::new("http://127.0.0.1:9", 5);
assert_eq!(r.atproto_key("did:plc:ewvi7nxzyoun6zhxrhs64oiz").await, Err(DidResolveError::Unavailable));
}
#[test]
fn only_a_directory_that_could_not_answer_is_unavailable() {
use reqwest::StatusCode;
for code in [500, 502, 503, 429] {
assert_eq!(status_failure(StatusCode::from_u16(code).unwrap()), Some(DidResolveError::Unavailable), "{code}");
}
for code in [404, 410, 400, 403] {
assert_eq!(status_failure(StatusCode::from_u16(code).unwrap()), Some(DidResolveError::NotFound), "{code}");
}
assert_eq!(status_failure(StatusCode::OK), None);
}
#[tokio::test]
async fn a_repo_key_may_be_p256() {
let r = InMemoryDidKeyResolver::new();
r.insert_key("did:plc:p256", "zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo");
assert!(matches!(r.repo_key("did:plc:p256").await, Ok(RepoKey::P256(_))));
assert!(r.atproto_key("did:plc:p256").await.is_err());
let k = SigningKey::from_slice(&[5u8; 32]).unwrap();
r.insert_key("did:plc:k256", encode_did_key_multibase(k.verifying_key()));
assert!(matches!(r.repo_key("did:plc:k256").await, Ok(RepoKey::K256(_))));
}
#[tokio::test]
async fn unseeded_did_is_not_found() {
let r = InMemoryDidKeyResolver::new();
assert_eq!(r.atproto_key("did:plc:nobody").await, Err(DidResolveError::NotFound));
assert_eq!(r.pds_endpoint("did:plc:nobody").await, Ok(None));
assert_eq!(r.handle("did:plc:nobody").await, Ok(None));
}
#[tokio::test]
async fn seeded_did_resolves_to_the_key_and_pds() {
let vk = *SigningKey::from_slice(&[0x11u8; 32]).unwrap().verifying_key();
let r = InMemoryDidKeyResolver::new();
r.insert_key("did:plc:alice", encode_did_key_multibase(&vk));
r.insert_pds("did:plc:alice", "https://pds.example");
assert_eq!(r.atproto_key("did:plc:alice").await.unwrap(), vk);
assert_eq!(r.pds_endpoint("did:plc:alice").await.unwrap(), Some("https://pds.example".to_string()));
}
#[test]
fn handles_are_checked_before_any_lookup() {
for ok in ["alice.example.com", "alice.test", "a-1.bsky.social", "xn--bcher-kva.example.org"] {
assert!(public_host(ok), "{ok}");
}
for bad in ["alice", "-a.example.com", "a-.example.com", "a..example.com", "a b.example.com", "a.example.com/x", "a.example.com:443"] {
assert!(!public_host(bad), "{bad}");
}
}
#[test]
fn ip_literals_and_internal_names_are_not_public_hosts() {
for bad in [
"127.0.0.1",
"10.0.0.1",
"169.254.169.254",
"0x7f.1",
"1.2.3.4",
"::1",
"[::1]",
"localhost",
"pds.localhost",
"metadata.google.internal",
"printer.local",
"x.onion",
"app.example",
"x.arpa",
"x.invalid",
"x.alt",
"example.com%3A8080",
] {
assert!(!public_host(bad), "{bad}");
}
}
#[tokio::test]
async fn a_malformed_or_non_public_did_web_is_not_found_without_a_lookup() {
let r = CommonDidKeyResolver::new("http://127.0.0.1:9", 5);
for did in [
"did:web:localhost%3A3000",
"did:web:example.com%3A8443",
"did:web:example.com:user:alice",
"did:web:127.0.0.1",
"did:web:169.254.169.254",
"did:web:10.0.0.1",
"did:web:localhost",
"did:web:metadata.google.internal",
"did:web:pds",
] {
assert_eq!(r.pds_endpoint(did).await, Err(DidResolveError::NotFound), "{did}");
assert_eq!(r.atproto_key(did).await, Err(DidResolveError::NotFound), "{did}");
assert_eq!(r.handle(did).await, Err(DidResolveError::NotFound), "{did}");
}
}
#[test]
fn only_public_unicast_addresses_are_public() {
for ok in ["1.1.1.1", "8.8.8.8", "2606:4700:4700::1111", "::ffff:1.1.1.1"] {
assert!(is_public_ip(ok.parse().unwrap()), "{ok}");
}
for bad in [
"127.0.0.1",
"10.1.2.3",
"172.16.0.1",
"192.168.1.1",
"169.254.169.254",
"100.64.0.1",
"0.0.0.0",
"0.1.2.3",
"255.255.255.255",
"224.0.0.1",
"192.0.0.8",
"198.18.0.1",
"192.0.2.1",
"240.0.0.1",
"::1",
"::",
"fc00::1",
"fd12:3456::1",
"fe80::1",
"ff02::1",
"2001:db8::1",
"::ffff:127.0.0.1",
"::ffff:10.0.0.1",
"64:ff9b::a9fe:a9fe",
] {
assert!(!is_public_ip(bad.parse().unwrap()), "{bad}");
}
}
#[tokio::test]
async fn a_name_resolving_only_to_internal_addresses_is_refused() {
use reqwest::dns::Resolve;
let name: reqwest::dns::Name = "localhost".parse().unwrap();
let err = match PublicOnlyDns.resolve(name).await {
Ok(addrs) => panic!("localhost resolved to {:?}", addrs.collect::<Vec<_>>()),
Err(e) => e.to_string(),
};
assert!(err.contains("no public address"), "{err}");
}
}