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