Fetch User Keys - simple tool for fetching SSH keys from various sources
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}