diff --git a/client/src/client.rs b/client/src/client.rs index 81f4940..738533f 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -116,11 +116,13 @@ async fn fetch( args: ClientArgs { api, interactive }, }: FetchArgs, ) -> anyhow::Result<()> { - let certs = read_dir(&cert_dir).await?; + let certs = read_certs_dir(&cert_dir).await?; + let publics_keys = read_pubkey_dir(&cert_dir).await?; let client = reqwest::Client::new(); let threshold_exp = min_delta.and_then(|min_delta| { SystemTime::now().checked_add(Duration::from_secs(60 * 60 * 24 * min_delta as u64)) }); + let standalone_certs = publics_keys.into_iter().map(|(name, key)| ) let updates = certs .into_iter() .filter(|cert| { diff --git a/common/src/certs.rs b/common/src/certs.rs index 67cff7b..5dd761c 100644 --- a/common/src/certs.rs +++ b/common/src/certs.rs @@ -24,11 +24,11 @@ pub async fn read_certs( if !ca_dir.exists() { return Ok(Vec::new()); } - read_dir(&ca_dir).await + read_certs_dir(&ca_dir).await } #[instrument] -pub async fn read_dir(path: impl AsRef + Debug) -> anyhow::Result> { +pub async fn read_certs_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()))?; @@ -55,6 +55,31 @@ pub async fn read_dir(path: impl AsRef + Debug) -> anyhow::Result + Debug) -> anyhow::Result> { + let mut dir = fs::read_dir(path.as_ref()) + .await + .with_context(|| format!("read certs dir '{:?}'", path.as_ref()))?; + let mut pubs = Vec::new(); + while let Some(entry) = dir.next_entry().await? { + //TODO: investigate why path().ends_with doesn't work + let file_name = entry.file_name().into_string().unwrap(); + if !file_name.ends_with(".pub") || file_name.ends_with("-cert.pub") + { + trace!( + "skipped {:?} due to missing '.pub' extension", + entry.path() + ); + continue; + } + let cert = load_public_key(entry.path()).await?; + if let Some(cert) = cert { + pubs.push(cert); + } + } + Ok(pubs) + +} + fn parse_utf8(bytes: Vec) -> anyhow::Result { String::from_utf8(bytes).context("invalid utf-8") } @@ -122,3 +147,16 @@ pub async fn load_cert(file: impl AsRef + Debug) -> anyhow::Result + Debug) -> anyhow::Result> { + let contents = match fs::read(&file).await { + Ok(contents) => contents, + Err(e) if e.kind() == ErrorKind::NotFound => return Ok(None), + Err(e) => return Err(e).with_context(|| format!("read {:?}", &file)), + }; + let string_repr = parse_utf8(contents)?; + Ok(Some(PublicKey::from_openssh(&string_repr).with_context( + || format!("parse {:?} as openssh public key", &file), + )?)) + +} diff --git a/common/src/routes.rs b/common/src/routes.rs index 3b4599e..274349d 100644 --- a/common/src/routes.rs +++ b/common/src/routes.rs @@ -1,24 +1,39 @@ + + use axum_extra::routing::TypedPath; -use serde::Deserialize; + +use serde::{Deserialize, Serialize}; +use ssh_key::Fingerprint; #[derive(TypedPath, Deserialize)] #[typed_path("/certs")] pub struct CertList; #[derive(TypedPath, Deserialize)] -#[typed_path("/certs/:identifier")] +#[typed_path("/cert/:identifier")] pub struct GetCert { pub identifier: String, } #[derive(TypedPath, Deserialize)] -#[typed_path("/certs/:identifier/info")] +#[typed_path("/certs/:pubkey_hash")] +pub struct GetCertsPubkey { + pub pubkey_hash: Fingerprint, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct CertIds { + pub ids: Vec +} + +#[derive(TypedPath, Deserialize)] +#[typed_path("/cert/:identifier/info")] pub struct GetCertInfo { pub identifier: String, } #[derive(TypedPath, Deserialize)] -#[typed_path("/certs/:identifier")] +#[typed_path("/cert/:identifier")] pub struct PostCertInfo { pub identifier: String, } diff --git a/server/src/api.rs b/server/src/api.rs index 2b1e99d..679fab4 100644 --- a/server/src/api.rs +++ b/server/src/api.rs @@ -9,6 +9,7 @@ use std::time::SystemTime; use anyhow::Context; use axum::body; use axum::extract::{Query, State}; +use chrono::Duration; use ssh_cert_dist_common::*; use axum::{http::StatusCode, response::IntoResponse, Json, Router}; @@ -291,12 +292,12 @@ struct CertInfo { impl From<&Certificate> for CertInfo { fn from(cert: &Certificate) -> Self { - let validity = cert.valid_after_time().duration_since(cert.valid_before_time()).unwrap(); + let validity = cert.valid_before_time().duration_since(cert.valid_after_time()).unwrap_or(Duration::zero().to_std().unwrap()); let validity_days = validity.as_secs() / ((60*60) * 24); let host_key = if cert.cert_type().is_host() { " -h" } else { "" }; - let opts = cert.critical_options().iter().map(|(opt, val)| if val.is_empty() { opt.clone() } else { format!("{opt}={val}") }).map(|arg| format!("-O {arg}")).join(" "); + let opts = cert.critical_options().iter().map(|(opt, val)| if val.is_empty() { opt.clone() } else { format!("{opt}={val}") }).map(|arg| format!("-O {arg}")).collect::>().join(" "); let renew_command = format!("ssh-keygen -s ./ca_key {host_key} -I {} -n {} -V {validity_days}d {opts}", cert.key_id(), cert.valid_principals().join(",")); CertInfo { principals: cert.valid_principals().to_vec(),