use anyhow::Context; use ssh_key::{Certificate, PublicKey}; use std::{path::{Path, PathBuf}, fmt::Debug}; use tokio::fs; use tracing::{trace, instrument}; pub async fn read_certs( ca: &PublicKey, path: impl AsRef, ) -> anyhow::Result> { if !path.as_ref().exists() { return Ok(Vec::new()); } read_dir(path.as_ref().join(ca_dir(ca))).await } #[instrument] pub async fn read_dir(path: impl AsRef + Debug) -> anyhow::Result> { let mut dir = fs::read_dir(path.as_ref()) .await .with_context(|| format!("read certs dir '{:?}'", path.as_ref()))?; let mut certs = Vec::new(); while let Some(entry) = dir.next_entry().await? { //TODO: investigate why path().ends_with doesn't work if !entry .file_name() .into_string() .unwrap() .ends_with("-cert.pub") { trace!( "skipped {:?} due to missing '-cert.pub' extension", entry.path() ); continue; } let contents = fs::read(&entry.path()) .await .with_context(|| format!("read {:?}", entry.path()))?; let string_repr = parse_utf8(contents)?; let cert = Certificate::from_openssh(&string_repr) .with_context(|| format!("parse {:?} as openssh certificate", entry.path()))?; certs.push(cert); } Ok(certs) } fn parse_utf8(bytes: Vec) -> anyhow::Result { String::from_utf8(bytes).context("invalid utf-8") } pub async fn read_pubkey(path: impl AsRef) -> anyhow::Result { let contents = fs::read(&path) .await .with_context(|| format!("read {:?}", path.as_ref()))?; let string_repr = parse_utf8(contents)?; PublicKey::from_openssh(&string_repr) .with_context(|| format!("parse '{}' as public key", string_repr)) } fn ca_dir(ca: &PublicKey) -> String { ca.comment().to_string() } #[instrument] fn cert_path(ca: &PublicKey, identifier: &str) -> String { let _ca_fingerprint = ca.fingerprint(Default::default()); format!("{}/{}-cert.pub", ca_dir(ca), identifier) } #[instrument] pub async fn store_cert( cert_dir: impl AsRef + Debug, ca: &PublicKey, cert: &Certificate, ) -> anyhow::Result { // TODO: proper store let path = cert_dir.as_ref().join(cert_path(&ca, cert.key_id())); if let Some(parent) = path.parent() { fs::create_dir_all(parent).await?; } fs::write(&path, cert.to_openssh().context("encode cert")?).await?; Ok(path) } pub async fn load_cert( cert_dir: impl AsRef, ca: &PublicKey, identifier: &str, ) -> anyhow::Result> { let path = cert_dir.as_ref().join(cert_path(ca, identifier)); if !path.exists() { trace!("no certificate at {:?}", path); return Ok(None); } let contents = fs::read(&path) .await .with_context(|| format!("read {:?}", &path))?; let string_repr = parse_utf8(contents)?; Ok(Some(Certificate::from_openssh(&string_repr).with_context( || format!("parse {:?} as openssh certificate", &path), )?)) }