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}