Fetch User Keys - simple tool for fetching SSH keys from various sources
at master 5.2 kB view raw
1// SPDX-FileCopyrightText: 2025 Łukasz Niemier <#@hauleth.dev> 2// 3// SPDX-License-Identifier: EUPL-1.2 4 5use std::fmt; 6use std::str::FromStr; 7 8use super::helpers; 9 10use serde::Deserialize; 11use simple_eyre::eyre::Result; 12use ssh_key::PublicKey; 13 14#[derive(Debug)] 15pub struct Did { 16 method: String, 17 id: String, 18} 19 20impl FromStr for Did { 21 type Err = (); 22 23 fn from_str(input: &str) -> Result<Self, ()> { 24 if !input.starts_with("did:") { 25 return Err(()); 26 } 27 if input.ends_with(":") { 28 return Err(()); 29 } 30 31 let chunks: Box<[_]> = input.splitn(3, ":").collect(); 32 33 if chunks.len() != 3 { 34 return Err(()); 35 } 36 37 Ok(Did { 38 method: chunks[1].into(), 39 id: chunks[2].into(), 40 }) 41 } 42} 43 44impl fmt::Display for Did { 45 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 46 write!(f, "did:{}:{}", self.method, self.id) 47 } 48} 49 50#[derive(Debug)] 51pub struct Handle(String); 52 53impl fmt::Display for Handle { 54 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 55 write!(f, "{}", self.0) 56 } 57} 58 59impl FromStr for Handle { 60 type Err = (); 61 62 fn from_str(input: &str) -> Result<Self, ()> { 63 if input.len() > 253 { 64 return Err(()); 65 } 66 67 let input = { 68 let mut input = input.to_ascii_lowercase(); 69 if input.starts_with("@") { 70 input.remove(0); 71 } 72 73 input 74 }; 75 76 let segments: Box<[_]> = input.split('.').collect(); 77 78 if segments.len() < 2 { 79 return Err(()); 80 } 81 82 if !segments.iter().all(|&s| legal_segment(s)) { 83 return Err(()); 84 } 85 86 if segments.last().unwrap().as_bytes()[0].is_ascii_digit() { 87 return Err(()); 88 } 89 90 Ok(Handle(input)) 91 } 92} 93 94pub struct InvalidHandle(String); 95 96impl fmt::Display for InvalidHandle { 97 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 98 write!(f, "Invalid handle: {}", self.0) 99 } 100} 101 102#[derive(Debug)] 103pub enum Identifier { 104 Did(Did), 105 Handle(Handle), 106} 107 108impl FromStr for Identifier { 109 type Err = InvalidHandle; 110 111 fn from_str(input: &str) -> Result<Self, Self::Err> { 112 input 113 .parse() 114 .map(Identifier::Did) 115 .or_else(|_| input.parse().map(Identifier::Handle)) 116 .map_err(|_| InvalidHandle(input.into())) 117 } 118} 119 120impl fmt::Display for Identifier { 121 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 122 match *self { 123 Identifier::Did(ref did) => write!(f, "{}", did), 124 Identifier::Handle(ref handle) => write!(f, "{}", handle), 125 } 126 } 127} 128 129#[derive(Debug, Deserialize)] 130pub struct ATProto { 131 #[serde(default = "default_atproto")] 132 pub host: String, 133 #[serde(deserialize_with = "helpers::from_str")] 134 pub handle: Identifier, 135} 136 137impl FromStr for ATProto { 138 type Err = InvalidHandle; 139 140 fn from_str(input: &str) -> Result<Self, InvalidHandle> { 141 Ok(ATProto { 142 host: default_atproto(), 143 handle: input.parse().map_err(|_| InvalidHandle(input.into()))?, 144 }) 145 } 146} 147 148fn legal_segment(segment: &str) -> bool { 149 let bytes = segment.as_bytes(); 150 !segment.is_empty() 151 && bytes.iter().all(|&b| allowed_byte(b)) 152 && *bytes.first().unwrap() != b'-' 153 && *bytes.last().unwrap() != b'-' 154} 155 156fn allowed_byte(c: u8) -> bool { 157 c.is_ascii_digit() || c.is_ascii_lowercase() || c == b'-' 158} 159 160fn default_atproto() -> String { 161 "https://bsky.social/".into() 162} 163 164mod resp { 165 use serde::Deserialize; 166 use ssh_key::PublicKey; 167 168 #[derive(Debug, Deserialize)] 169 pub struct Resp { 170 pub records: Box<[Record]>, 171 } 172 173 #[derive(Debug, Deserialize)] 174 pub struct Record { 175 value: Value, 176 } 177 178 #[derive(Debug, Deserialize)] 179 pub struct Value { 180 key: String, 181 } 182 183 impl TryFrom<&Record> for PublicKey { 184 type Error = ssh_key::Error; 185 186 fn try_from(val: &Record) -> ssh_key::Result<PublicKey> { 187 PublicKey::from_openssh(&val.value.key) 188 } 189 } 190} 191 192impl super::Fetch for ATProto { 193 fn fetch(&self) -> Result<Vec<PublicKey>> { 194 let url = format!( 195 "{host}/xrpc/com.atproto.repo.listRecords?repo={handle}&collection=sh.tangled.publicKey", 196 host = self.host.trim_end_matches('/'), 197 handle = self.handle 198 ); 199 200 use ureq::tls::{RootCerts, TlsConfig, TlsProvider}; 201 202 let config = ureq::Agent::config_builder() 203 .tls_config( 204 TlsConfig::builder() 205 .root_certs(RootCerts::PlatformVerifier) 206 .provider(TlsProvider::NativeTls) 207 .build(), 208 ) 209 .build(); 210 211 let agent = config.new_agent(); 212 213 let data = agent 214 .get(&url.to_string()) 215 .call()? 216 .body_mut() 217 .read_to_string()?; 218 219 let decoded: resp::Resp = serde_json::from_str(&data)?; 220 221 decoded 222 .records 223 .iter() 224 .map(|val| val.try_into().map_err(Into::into)) 225 .collect() 226 } 227}