Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
5.2 kB · 151 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152//! Bidirectional handle verification.//!//! OAuth returns a DID. The DID document advertises a handle in//! `alsoKnownAs`, but the account controls that document, so read alone it is//! a claim anybody can make about any name. The binding only holds if the//! handle also resolves *back* to the DID through the mechanism the domain//! owner controls (docs/identity-and-sessions.md). atrium's handle resolver//! is exactly that forward resolution; this module adds only the comparison.
use atrium_api::did_doc::DidDocument;use atrium_api::types::string::{Did, Handle};use atrium_common::resolver::Resolver;
/// The verified handle, or None for unverified — which the caller stores as/// such and the UI shows next to the word "unverified". Any resolution/// failure is unverified rather than an error: for some hosting setups that/// is the honest answer, not a fault (docs/local-dev.md).////// Generic over the two resolvers rather than naming the concrete ones, so/// the comparison this module exists for can be tested against fakes without/// a DNS or HTTPS round trip. atrium still stops at this module's door: the/// bounds are its traits and its types.pub async fn verify_handle<D, H>(did_resolver: &D, handle_resolver: &H, did: &Did) -> Option<String>where D: Resolver<Input = Did, Output = DidDocument>, H: Resolver<Input = Handle, Output = Did>,{ let document = match did_resolver.resolve(did).await { Ok(document) => document, Err(_) => { tracing::warn!("handle verification: DID document fetch failed"); return None; } };
let claimed = document .also_known_as .as_ref()? .iter() .find_map(|aka| aka.strip_prefix("at://"))? .to_owned(); let handle = Handle::new(claimed).ok()?;
match handle_resolver.resolve(&handle).await { Ok(resolved) if resolved == *did => Some(handle.as_str().to_owned()), Ok(_) => { tracing::warn!("handle verification: handle resolves to a different DID"); None } Err(_) => { tracing::warn!("handle verification: handle does not resolve"); None } }}
#[cfg(test)]mod tests { use super::*;
struct Unreachable;
/// Answers with whatever the test set up, so each half of the round trip /// can fail on its own. struct Fake<I: ?Sized, O>(Result<O, Unreachable>, std::marker::PhantomData<fn(&I)>);
impl<I: ?Sized, O> Fake<I, O> { fn ok(value: O) -> Self { Fake(Ok(value), std::marker::PhantomData) } fn err() -> Self { Fake(Err(Unreachable), std::marker::PhantomData) } }
impl<I: ?Sized + Sync, O: Clone + Send + Sync> Resolver for Fake<I, O> { type Input = I; type Output = O; type Error = Unreachable;
async fn resolve(&self, _input: &I) -> Result<O, Unreachable> { self.0.as_ref().cloned().map_err(|_| Unreachable) } }
fn did() -> Did { Did::new("did:plc:abc".to_owned()).unwrap() }
fn document(also_known_as: Option<Vec<&str>>) -> DidDocument { DidDocument { context: None, id: did().as_str().to_owned(), also_known_as: also_known_as.map(|aka| aka.into_iter().map(str::to_owned).collect()), verification_method: None, service: None, } }
#[tokio::test] async fn both_directions_agree() { let handle = verify_handle( &Fake::ok(document(Some(vec!["at://someone.example"]))), &Fake::ok(did()), &did(), ) .await; assert_eq!(handle.as_deref(), Some("someone.example")); }
#[tokio::test] async fn a_handle_pointing_at_another_did_is_unverified() { // The whole point of the second direction: the DID document is // written by the account, so this is someone claiming a name they do // not own. let other = Did::new("did:plc:somebody-else".to_owned()).unwrap(); let handle = verify_handle( &Fake::ok(document(Some(vec!["at://someone.example"]))), &Fake::ok(other), &did(), ) .await; assert_eq!(handle, None); }
#[tokio::test] async fn a_handle_that_does_not_resolve_is_unverified() { let handle = verify_handle( &Fake::ok(document(Some(vec!["at://someone.example"]))), &Fake::<Handle, Did>::err(), &did(), ) .await; assert_eq!(handle, None); }
#[tokio::test] async fn an_unreachable_did_document_is_unverified() { let handle = verify_handle(&Fake::<Did, DidDocument>::err(), &Fake::ok(did()), &did()).await; assert_eq!(handle, None); }
#[tokio::test] async fn a_document_claiming_no_handle_is_unverified() { for aka in [None, Some(vec![]), Some(vec!["https://example.com"])] { let handle = verify_handle(&Fake::ok(document(aka)), &Fake::ok(did()), &did()).await; assert_eq!(handle, None); } }}