// SPDX-FileCopyrightText: 2024 Łukasz Niemier <#@hauleth.dev> // SPDX-FileCopyrightText: 2025 Łukasz Niemier <#@hauleth.dev> // // SPDX-License-Identifier: EUPL-1.2 use serde::Deserialize; use simple_eyre::eyre::Result; use ssh_key::PublicKey; use std::process::Command; mod atproto; mod helpers; pub use atproto::ATProto; pub trait Fetch: std::fmt::Debug { fn fetch(&self) -> Result>; } #[derive(Debug, Deserialize)] #[serde(rename_all = "lowercase")] #[non_exhaustive] pub enum Source { Raw(Raw), Hosts(Hosts), Http(Http), Github(String), Sourcehut(String), Gitlab(String), Codeberg(String), #[serde(deserialize_with = "helpers::string_or_struct")] Tangled(ATProto), } impl Fetch for Source { fn fetch(&self) -> Result> { match *self { Source::Raw(ref raw) => raw.fetch(), Source::Hosts(ref raw) => raw.fetch(), Source::Http(ref raw) => raw.fetch(), Source::Github(ref user) => Http { url: format!("https://github.com/{user}.keys"), } .fetch(), Source::Sourcehut(ref user) => Http { url: format!("https://meta.sr.ht/{}.keys", normalize_sourcehut(user)), } .fetch(), Source::Gitlab(ref user) => Http { url: format!("https://gitlab.com/{user}.keys"), } .fetch(), Source::Codeberg(ref user) => Http { url: format!("https://codeberg.org/{user}.keys"), } .fetch(), Source::Tangled(ref atproto) => atproto.fetch(), } } } fn normalize_sourcehut(s: &str) -> std::borrow::Cow<'_, str> { if s.starts_with("~") { s.into() } else { format!("~{s}").into() } } #[derive(Debug, Deserialize)] pub struct Raw(Box<[PublicKey]>); impl Fetch for Raw { fn fetch(&self) -> Result> { Ok(self.0.clone().into()) } } #[derive(Debug, Deserialize)] pub struct Hosts(pub Box<[String]>); impl Fetch for Hosts { fn fetch(&self) -> Result> { // TODO: Check if we can do it in-process instead of shelling out to `ssh-keyscan` let result = Command::new("ssh-keyscan").args(&self.0).output().unwrap(); std::str::from_utf8(&result.stdout)? .trim() .split('\n') .map(str::trim) // Remove comments .filter(|line| !line.starts_with("#")) // Ignore first column as it contain hostname which is not // needed there .map(|line| line.split_once(' ').unwrap().1) .map(|k| PublicKey::from_openssh(k).map_err(Into::into)) .collect() } } #[derive(Debug, Deserialize)] pub struct Http { pub url: String, } impl Fetch for Http { fn fetch(&self) -> Result> { use ureq::tls::{RootCerts, TlsConfig, TlsProvider}; let config = ureq::Agent::config_builder() .tls_config( TlsConfig::builder() .root_certs(RootCerts::PlatformVerifier) .provider(TlsProvider::NativeTls) .build(), ) .build(); let agent = config.new_agent(); agent.get(&self.url) .call()? .body_mut() .read_to_string()? .trim() .split('\n') .map(|s| PublicKey::from_openssh(s).map_err(Into::into)) .collect() } }