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) => Http {
41 url: format!("https://github.com/{user}.keys"),
42 }
43 .fetch(),
44 Source::Sourcehut(ref user) => Http {
45 url: format!("https://meta.sr.ht/{user}.keys"),
46 }
47 .fetch(),
48 Source::Gitlab(ref user) => Http {
49 url: format!("https://gitlab.com/{user}.keys"),
50 }
51 .fetch(),
52 Source::Codeberg(ref user) => Http {
53 url: format!("https://codeberg.org/{user}.keys"),
54 }
55 .fetch(),
56 Source::Tangled(ref atproto) => atproto.fetch(),
57 }
58 }
59}
60
61#[derive(Debug, Deserialize)]
62pub struct Raw(Box<[PublicKey]>);
63
64impl Fetch for Raw {
65 fn fetch(&self) -> Vec<PublicKey> {
66 self.0.clone().into()
67 }
68}
69
70#[derive(Debug, Deserialize)]
71pub struct Hosts(pub Box<[String]>);
72
73impl Fetch for Hosts {
74 fn fetch(&self) -> Vec<PublicKey> {
75 // TODO: Check if we can do it in-process instead of shelling out to `ssh-keyscan`
76 let result = Command::new("ssh-keyscan").args(&self.0).output().unwrap();
77
78 std::str::from_utf8(&result.stdout)
79 .unwrap()
80 .trim()
81 .split('\n')
82 .map(str::trim)
83 // Remove comments
84 .filter(|line| !line.starts_with("#"))
85 // Ignore first column as it contain hostname which is not
86 // needed there
87 .map(|line| line.split_once(' ').unwrap().1)
88 .map(|k| PublicKey::from_openssh(&k).unwrap())
89 .collect()
90 }
91}
92
93#[derive(Debug, Deserialize)]
94pub struct Http {
95 pub url: String,
96}
97
98impl Fetch for Http {
99 fn fetch(&self) -> Vec<PublicKey> {
100 ureq::get(&self.url)
101 .call()
102 .unwrap()
103 .body_mut()
104 .read_to_string()
105 .unwrap()
106 .trim()
107 .split('\n')
108 .map(|s| PublicKey::from_openssh(s).unwrap())
109 .collect()
110 }
111}