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}