Fetch User Keys - simple tool for fetching SSH keys from various sources
1// SPDX-FileCopyrightText: 2024 Łukasz Niemier <#@hauleth.dev>
2// SPDX-FileCopyrightText: 2025 Łukasz Niemier <#@hauleth.dev>
3//
4// SPDX-License-Identifier: EUPL-1.2
5
6use serde::Deserialize;
7use ssh_key::PublicKey;
8use std::process::Command;
9
10mod atproto;
11mod helpers;
12
13pub use atproto::ATProto;
14
15pub trait Fetch: std::fmt::Debug {
16 fn fetch(&self) -> Vec<PublicKey>;
17}
18
19#[derive(Debug, Deserialize)]
20#[serde(rename_all = "lowercase")]
21#[non_exhaustive]
22pub enum Source {
23 Raw(Raw),
24 Hosts(Hosts),
25 Http(Http),
26 Github(String),
27 Sourcehut(String),
28 Gitlab(String),
29 Codeberg(String),
30 #[serde(deserialize_with = "helpers::string_or_struct")]
31 Tangled(ATProto),
32}
33
34impl Fetch for Source {
35 fn fetch(&self) -> Vec<PublicKey> {
36 match *self {
37 Source::Raw(ref raw) => raw.fetch(),
38 Source::Hosts(ref raw) => raw.fetch(),
39 Source::Http(ref raw) => raw.fetch(),
40 Source::Github(ref user) => {
41 Http {
42 url: format!("https://github.com/{user}.keys"),
43 }
44 .fetch()
45 }
46 Source::Sourcehut(ref user) => {
47 Http {
48 url: format!("https://meta.sr.ht/{user}.keys"),
49 }
50 .fetch()
51 }
52 Source::Gitlab(ref user) => {
53 Http {
54 url: format!("https://gitlab.com/{user}.keys"),
55 }
56 .fetch()
57 }
58 Source::Codeberg(ref user) => {
59 Http {
60 url: format!("https://codeberg.org/{user}.keys"),
61 }
62 .fetch()
63 }
64 Source::Tangled(ref atproto) => atproto.fetch(),
65 }
66 }
67}
68
69#[derive(Debug, Deserialize)]
70pub struct Raw(Box<[PublicKey]>);
71
72impl Fetch for Raw {
73 fn fetch(&self) -> Vec<PublicKey> {
74 self.0.clone().into()
75 }
76}
77
78#[derive(Debug, Deserialize)]
79pub struct Hosts(pub Box<[String]>);
80
81impl Fetch for Hosts {
82 fn fetch(&self) -> Vec<PublicKey> {
83 // TODO: Check if we can do it in-process instead of shelling out to `ssh-keyscan`
84 let result = Command::new("ssh-keyscan")
85 .args(&self.0)
86 .output()
87 .unwrap();
88
89 std::str::from_utf8(&result.stdout)
90 .unwrap()
91 .trim()
92 .split('\n')
93 .map(str::trim)
94 // Remove comments
95 .filter(|line| !line.starts_with("#"))
96 .map(|line| {
97 // Ignore first column as it contain hostname which is not
98 // needed there
99 line.split(' ')
100 .skip(1)
101 .collect::<Box<[_]>>()
102 .join(" ")
103 .to_owned()
104 })
105 .map(|k| PublicKey::from_openssh(&k).unwrap())
106 .collect()
107 }
108}
109
110#[derive(Debug, Deserialize)]
111pub struct Http {
112 pub url: String,
113}
114
115impl Fetch for Http {
116 fn fetch(&self) -> Vec<PublicKey> {
117 ureq::get(&self.url)
118 .call()
119 .unwrap()
120 .body_mut()
121 .read_to_string()
122 .unwrap()
123 .trim()
124 .split('\n')
125 .map(|s| PublicKey::from_openssh(s).unwrap())
126 .collect()
127 }
128}