This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-ssh / src / identity.rs
4.4 kB 152 lines
1use std::net::IpAddr; 2use std::sync::Arc; 3 4use knot_atproto::ClaimedKeys; 5use knot_index::{Coverage, Resolved}; 6use knot_runtime::{Clock, HttpTransport}; 7use knot_types::{AccountDid, OfferedKey, OwnerRef}; 8 9use crate::SshState; 10 11#[derive(Clone)] 12pub(crate) enum Credential { 13 Identified(AccountDid), 14 Offered(OfferedKey), 15} 16 17pub(crate) struct Asserted { 18 claim: OwnerRef, 19 outcome: Claimed, 20} 21 22enum Claimed { 23 Publishes { 24 did: AccountDid, 25 keys: Vec<OfferedKey>, 26 }, 27 Unreadable, 28} 29 30pub(crate) enum Verdict { 31 Identified(AccountDid), 32 Offered, 33 Refused, 34} 35 36pub(crate) async fn verify<H: HttpTransport, C: Clock>( 37 state: &Arc<SshState<H, C>>, 38 claim: Option<OwnerRef>, 39 key: &OfferedKey, 40 peer: Option<IpAddr>, 41 asserted: &mut Option<Asserted>, 42) -> Verdict { 43 match claim { 44 Some(claim) => match against_claim(state, claim, key, peer, asserted).await { 45 Some(verdict) => verdict, 46 None => against_key_set(state, key, peer), 47 }, 48 None => against_key_set(state, key, peer), 49 } 50} 51 52fn against_key_set<H: HttpTransport, C: Clock>( 53 state: &Arc<SshState<H, C>>, 54 key: &OfferedKey, 55 peer: Option<IpAddr>, 56) -> Verdict { 57 let now = state.atproto.now().seconds(); 58 match ( 59 state.index.owner_of_key(key, now), 60 state.index.keys().coverage(), 61 ) { 62 (Resolved::Ready(Some(_)), _) => Verdict::Offered, 63 (_, Coverage::Warming) => Verdict::Offered, 64 (_, Coverage::Ready) => match state.index.keys().any_unheld() { 65 true => Verdict::Offered, 66 false => { 67 if miss_worth_a_reread(state, peer) { 68 state.index.keys().note_miss(); 69 } 70 Verdict::Refused 71 } 72 }, 73 } 74} 75 76fn miss_worth_a_reread<H: HttpTransport, C: Clock>( 77 state: &Arc<SshState<H, C>>, 78 peer: Option<IpAddr>, 79) -> bool { 80 peer.is_none_or(|peer| state.miss_pace.reserve_now(&peer, state.atproto.now())) 81} 82 83async fn against_claim<H: HttpTransport, C: Clock>( 84 state: &Arc<SshState<H, C>>, 85 claim: OwnerRef, 86 key: &OfferedKey, 87 peer: Option<IpAddr>, 88 asserted: &mut Option<Asserted>, 89) -> Option<Verdict> { 90 let known = match asserted.take().filter(|known| known.claim == claim) { 91 Some(known) => known, 92 None => Asserted { 93 outcome: resolve_claim(state, &claim, peer).await, 94 claim, 95 }, 96 }; 97 let verdict = match &known.outcome { 98 Claimed::Unreadable => None, 99 Claimed::Publishes { did, keys } if keys.contains(key) => { 100 Some(Verdict::Identified(did.clone())) 101 } 102 Claimed::Publishes { did, keys } => { 103 tracing::debug!( 104 ?peer, 105 did = did.as_str(), 106 published = keys.len(), 107 "ssh auth refused a key the asserted account doesn't publish" 108 ); 109 Some(Verdict::Refused) 110 } 111 }; 112 *asserted = Some(known); 113 verdict 114} 115 116async fn resolve_claim<H: HttpTransport, C: Clock>( 117 state: &Arc<SshState<H, C>>, 118 claim: &OwnerRef, 119 peer: Option<IpAddr>, 120) -> Claimed { 121 let Ok(_peer_guard) = state.lookup_peers.admit(peer, state.atproto.now()) else { 122 tracing::debug!(?peer, "ssh auth couldn't check a claim, peer budget spent"); 123 return Claimed::Unreadable; 124 }; 125 let did = match claim { 126 OwnerRef::Did(did) => AccountDid::from(did.clone()), 127 OwnerRef::Handle(handle) => match state.atproto.resolve_handle_to_did(handle).await { 128 Ok(did) => did, 129 Err(error) => { 130 tracing::warn!( 131 ?peer, 132 handle = handle.as_str(), 133 %error, 134 "ssh auth couldn't resolve the handle in the login name" 135 ); 136 return Claimed::Unreadable; 137 } 138 }, 139 }; 140 match state.atproto.claimed_pubkeys(&did).await { 141 ClaimedKeys::Published(keys) => Claimed::Publishes { did, keys }, 142 ClaimedKeys::Unread(error) => { 143 tracing::warn!( 144 ?peer, 145 did = did.as_str(), 146 %error, 147 "ssh auth couldn't read the asserted account's published keys" 148 ); 149 Claimed::Unreadable 150 } 151 } 152}