Alternative ATProto PDS implementation
at oauth 2.6 kB view raw
1//! DID utilities. 2 3use anyhow::{Context as _, Result, bail}; 4use atrium_api::types::string::Did; 5use serde::{Deserialize, Serialize}; 6use url::Url; 7 8use crate::serve::Client; 9 10/// URL whitelist for DID document resolution. 11const ALLOWED_URLS: &[&str] = &["bsky.app", "bsky.chat"]; 12 13#[expect( 14 clippy::arbitrary_source_item_ordering, 15 reason = "serialized data might be structured" 16)] 17#[derive(Clone, Debug, Serialize, Deserialize)] 18#[serde(rename_all = "camelCase")] 19/// DID verification method. 20pub(crate) struct DidVerificationMethod { 21 pub id: String, 22 #[serde(rename = "type")] 23 pub ty: String, 24 pub controller: String, 25 pub public_key_multibase: String, 26} 27 28#[expect( 29 clippy::arbitrary_source_item_ordering, 30 reason = "serialized data might be structured" 31)] 32#[derive(Clone, Debug, Serialize, Deserialize)] 33#[serde(rename_all = "camelCase")] 34pub(crate) struct DidService { 35 pub id: String, 36 #[serde(rename = "type")] 37 pub ty: String, 38 pub service_endpoint: Url, 39} 40 41#[expect( 42 clippy::arbitrary_source_item_ordering, 43 reason = "serialized data might be structured" 44)] 45#[derive(Clone, Debug, Serialize, Deserialize)] 46#[serde(rename_all = "camelCase")] 47/// DID document. 48pub(crate) struct DidDocument { 49 #[serde(rename = "@context", skip_serializing_if = "Vec::is_empty")] 50 pub context: Vec<Url>, 51 pub id: Did, 52 #[serde(skip_serializing_if = "Vec::is_empty", default)] 53 pub verification_method: Vec<DidVerificationMethod>, 54 pub service: Vec<DidService>, 55} 56 57/// Resolve a DID document using the specified reqwest client. 58pub(crate) async fn resolve(client: &Client, did: Did) -> Result<DidDocument> { 59 let url = match did.method() { 60 "did:web" => { 61 // N.B: This is a potentially hostile operation, so we are only going to allow 62 // certain URLs for now. 63 let host = did 64 .as_str() 65 .strip_prefix("did:web:") 66 .context("invalid DID format")?; 67 68 if !ALLOWED_URLS.iter().any(|u| host.ends_with(u)) { 69 bail!("forbidden URL {host}"); 70 } 71 72 format!("https://{host}/.well-known/did.json") 73 } 74 "did:plc" => { 75 format!("https://plc.directory/{}", did.as_str()) 76 } 77 m => bail!("unknown did method {m}"), 78 } 79 .parse::<Url>() 80 .context("failed to resolve DID URL")?; 81 82 client 83 .get(url) 84 .send() 85 .await 86 .context("failed to fetch DID document")? 87 .json() 88 .await 89 .context("failed to decode DID document") 90}