Something went wrong. Try again.
Identities for entities did.bot
agent llm did
Something went wrong. Try again.
11 kB · 257 lines
Rust
at commit 18ba4fe0
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258//! The DID document that backs an agent account.//!//! Only the parts atproto reads are modelled. A resolver following the spec//! looks for exactly three things — the `id`, the `alsoKnownAs` handle, the//! `#atproto` signing key and the `#atproto_pds` service entry — and ignores//! the rest of the W3C surface, so this crate emits those and nothing more.
use serde::{Deserialize, Serialize};
use crate::did::AgentDid;
/// JSON-LD contexts a spec-compliant atproto DID document declares.pub const DEFAULT_CONTEXT: [&str; 3] = [ "https://www.w3.org/ns/did/v1", "https://w3id.org/security/multikey/v1", "https://w3id.org/security/suites/secp256k1-2019/v1",];
/// Fragment identifying the atproto signing key within a DID document.pub const ATPROTO_KEY_FRAGMENT: &str = "#atproto";
/// Fragment identifying the account's PDS within a DID document.pub const ATPROTO_PDS_FRAGMENT: &str = "#atproto_pds";
/// Fragment identifying the key a host itself holds, beside the `#atproto`/// key its PDS signs commits with.////// A host mints its own keypair before it has an identity, and the account/// it is later reserved holds a server-side signing key like every other/// account; the two are different keys for different jobs. The `#atproto`/// key is what atproto readers look for and is never this one. This entry/// is what an operator's `bot.did.operator` claim binds for a host, and what/// the host can later prove possession of.pub const NODE_KEY_FRAGMENT: &str = "#node";
/// The `type` atproto requires on the signing key's verification method.pub const MULTIKEY_TYPE: &str = "Multikey";
/// The `type` atproto requires on the PDS service entry.pub const PDS_SERVICE_TYPE: &str = "AtprotoPersonalDataServer";
/// A DID document, reduced to the fields atproto consumes.#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct DidDocument { /// JSON-LD contexts. Serialized first, and under its literal `@context` /// name, which is why it is renamed rather than left to the camel-case /// rule that covers every other field here. #[serde(rename = "@context")] pub context: Vec<String>, /// The DID this document describes. A resolver must check it against the /// DID it asked for; see [`mod@crate::resolve`]. pub id: String, /// Claimed handles, as `at://` URIs. Ordered: the first syntactically /// valid entry is the claimed handle. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub also_known_as: Vec<String>, /// Public keys. The atproto signing key is the entry whose `id` ends in /// `#atproto`. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub verification_method: Vec<VerificationMethod>, /// Service endpoints. The PDS is the entry whose `id` ends in /// `#atproto_pds`. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub service: Vec<Service>,}
/// One public key in a DID document.#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct VerificationMethod { /// Fully qualified or relative fragment, e.g. `did:web:x.example#atproto`. pub id: String, /// Always [`MULTIKEY_TYPE`] for the atproto signing key. #[serde(rename = "type")] pub type_: String, /// The DID that controls this key. pub controller: String, /// The public key, multibase-encoded. pub public_key_multibase: String,}
/// One service endpoint in a DID document.#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct Service { /// Fragment naming the service, e.g. `#atproto_pds`. pub id: String, /// Always [`PDS_SERVICE_TYPE`] for a personal data server. #[serde(rename = "type")] pub type_: String, /// Scheme, hostname and optional port. No path, query or fragment. pub service_endpoint: String,}
impl DidDocument { /// Builds the document for an agent account. /// /// `public_key_multibase` is a `&str` on purpose: this crate has no /// opinion on how the key was generated, and taking a key type here would /// make every consumer of DID documents depend on the key crate. /// /// Fragment ids are emitted fully qualified. The spec asks readers to /// accept both forms, and the qualified one survives being copied out of /// the document into some other context without losing its subject. pub fn for_account( did: &AgentDid, public_key_multibase: &str, handle: &str, pds_endpoint: &str, ) -> Self { let did_str = did.as_str(); Self { context: DEFAULT_CONTEXT.iter().map(|c| (*c).to_owned()).collect(), id: did_str.to_owned(), also_known_as: vec![format!("at://{handle}")], verification_method: vec![VerificationMethod { id: format!("{did_str}{ATPROTO_KEY_FRAGMENT}"), type_: MULTIKEY_TYPE.to_owned(), controller: did_str.to_owned(), public_key_multibase: public_key_multibase.to_owned(), }], service: vec![Service { id: format!("{did_str}{ATPROTO_PDS_FRAGMENT}"), type_: PDS_SERVICE_TYPE.to_owned(), service_endpoint: pds_endpoint.to_owned(), }], } }
/// Builds the document for the server itself. /// /// A service document, not an account one: no signing key and no handle, /// because the server holds neither on its own behalf. It exists so that /// the DID a vouch record names resolves to something that says where the /// server is, which is the whole of what a reader needs from it. pub fn for_service(did: &str, pds_endpoint: &str) -> Self { Self { context: DEFAULT_CONTEXT.iter().map(|c| (*c).to_owned()).collect(), id: did.to_owned(), also_known_as: Vec::new(), verification_method: Vec::new(), service: vec![Service { id: format!("{did}{ATPROTO_PDS_FRAGMENT}"), type_: PDS_SERVICE_TYPE.to_owned(), service_endpoint: pds_endpoint.to_owned(), }], } }
/// The first claimed handle, with the `at://` prefix stripped. /// /// "First *valid* entry" is the spec's rule and now the implementation's: /// `alsoKnownAs` is an ordered list a DID controller writes, it may hold /// entries in other URI schemes, and it is not this crate's to trust. An /// entry that is not a syntactically valid handle is skipped rather than /// returned, so a document whose first entry is `at://not a handle` and /// whose second is real resolves to the real one instead of to garbage a /// caller would then try to look up. pub fn handle(&self) -> Option<&str> { self.also_known_as .iter() .filter_map(|aka| aka.strip_prefix("at://")) .find(|handle| crate::handle::validate(handle).is_ok()) }
/// Whether this document claims `handle` as its own. /// /// The other half of atproto's bidirectional rule. A handle resolves to a /// DID, and that is only half an answer: the DID's document has to claim /// the handle back, or what has been resolved is somebody asserting a name /// they do not hold. A resolver that stops after the first half will file /// records under whichever account was quickest to claim the name. /// /// Only the *first valid* `alsoKnownAs` entry counts, which is [`handle`] /// and the specification's own rule. Deliberately not "any entry mentions /// it": `alsoKnownAs` is an ordered list its controller writes, so a /// document could list a real handle second and have this answer yes to a /// name no resolver would ever return for it. /// /// Compared case-insensitively, because a handle is a hostname; see /// [`crate::handle::normalize`]. /// /// [`handle`]: Self::handle pub fn claims_handle(&self, handle: &str) -> bool { self.handle() .is_some_and(|claimed| claimed.eq_ignore_ascii_case(handle)) }
/// Whether `id` is this document's own `fragment` entry. /// /// A DID URL is a DID plus a fragment, and the DID half is what says /// whose entry it is. `ends_with` alone does not read it, so a document /// for `did:web:victim.example` could list /// `did:web:attacker.example#atproto` and have it answered as the /// victim's signing key — a well-formed entry that does not belong to /// the subject, accepted because the only field that says so was never /// looked at. The relative form (`#atproto`, no DID at all) is the other /// spelling the spec asks readers to accept, and it can only mean the /// document's own subject. fn owns_fragment(&self, id: &str, fragment: &str) -> bool { matches!(id.strip_suffix(fragment), Some(base) if base.is_empty() || base == self.id) }
/// Adds the key a host itself holds, under [`NODE_KEY_FRAGMENT`]. /// /// Appended after the signing key rather than replacing it: a reader /// that takes the first `#atproto` entry still finds the PDS's key, and /// a reader asking for the node key finds this one by fragment. #[must_use] pub fn with_node_key(mut self, public_key_multibase: &str) -> Self { self.verification_method.push(VerificationMethod { id: format!("{}{NODE_KEY_FRAGMENT}", self.id), type_: MULTIKEY_TYPE.to_owned(), controller: self.id.clone(), public_key_multibase: public_key_multibase.to_owned(), }); self }
/// The multibase key a host holds itself, the document's own `#node` /// entry of the correct type; see [`NODE_KEY_FRAGMENT`]. pub fn node_key_multibase(&self) -> Option<&str> { self.verification_method .iter() .find(|vm| self.owns_fragment(&vm.id, NODE_KEY_FRAGMENT) && vm.type_ == MULTIKEY_TYPE) .map(|vm| vm.public_key_multibase.as_str()) }
/// The multibase signing key, taking the first `#atproto` entry of the /// correct type and ignoring any others, as the spec directs. /// /// The entry must also be this document's own: an `id` whose DID half is /// this document's `id`, or absent entirely. pub fn signing_key_multibase(&self) -> Option<&str> { self.verification_method .iter() .find(|vm| { self.owns_fragment(&vm.id, ATPROTO_KEY_FRAGMENT) && vm.type_ == MULTIKEY_TYPE }) .map(|vm| vm.public_key_multibase.as_str()) }
/// The PDS endpoint, taking the first `#atproto_pds` entry of the correct /// type and ignoring any others. /// /// The entry must also be this document's own: an `id` whose DID half is /// this document's `id`, or absent entirely. pub fn pds_endpoint(&self) -> Option<&str> { self.service .iter() .find(|s| { self.owns_fragment(&s.id, ATPROTO_PDS_FRAGMENT) && s.type_ == PDS_SERVICE_TYPE }) .map(|s| s.service_endpoint.as_str()) }}