diff --git a/Cargo.lock b/Cargo.lock index 503d462..b959b14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1520,6 +1520,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-escape" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" + [[package]] name = "signature" version = "2.0.0" @@ -1634,6 +1640,7 @@ dependencies = [ "jwt-compact", "rand", "serde", + "shell-escape", "ssh-cert-dist-common", "ssh-key", "tempfile", diff --git a/server/Cargo.toml b/server/Cargo.toml index b213a4e..07ee978 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -32,6 +32,7 @@ tower-http = { version = "0.3.4", features = ["map-request-body", "trace"] } tracing = { version = "0.1.37", features = ["release_max_level_debug"] } tracing-subscriber = "0.3.16" ssh-cert-dist-common = { path = "../common" } +shell-escape = "0.1.5" [dev-dependencies] tempfile = "3.3.0" diff --git a/server/src/api.rs b/server/src/api.rs index 7d88cb3..f4423ab 100644 --- a/server/src/api.rs +++ b/server/src/api.rs @@ -4,12 +4,13 @@ use std::collections::HashMap; use std::net::SocketAddr; use std::path::{self, PathBuf}; use std::sync::Arc; -use std::time::SystemTime; +use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::Context; use axum::body; use axum::extract::{Query, State}; use chrono::Duration; +use shell_escape::escape; use ssh_cert_dist_common::*; use axum::{http::StatusCode, response::IntoResponse, Json, Router}; @@ -298,12 +299,36 @@ struct CertInfo { impl From<&Certificate> for CertInfo { fn from(cert: &Certificate) -> Self { 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 expiry = cert.valid_before_time().checked_add(validity).unwrap(); + let expiry_date = expiry.duration_since(UNIX_EPOCH).unwrap(); 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}")).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(",")); + } else { + "" + }; + let opts = cert + .critical_options() + .iter() + .map(|(opt, val)| { + if val.is_empty() { + opt.clone() + } else { + format!("{opt}={val}") + } + }) + .map(|arg| format!("-O {}", escape(arg.into()))) + .collect::>() + .join(" "); + let opts = opts.trim(); + let renew_command = format!( + "ssh-keygen -s ./ca_key {host_key} -I {} -n {} -z {} -V {:#x}:{:#x} {opts} {}.pub", + escape(cert.key_id().into()), + escape(cert.valid_principals().join(",").into()), + cert.serial() + 1, + cert.valid_after(), + expiry_date.as_secs(), + escape(cert.key_id().into()) + ); CertInfo { principals: cert.valid_principals().to_vec(), ca: cert.signature_key().clone().into(), @@ -312,7 +337,7 @@ impl From<&Certificate> for CertInfo { identity_hash: cert.public_key().fingerprint(ssh_key::HashAlg::Sha256), key_id: cert.key_id().to_string(), expiry: cert.valid_before_time(), - renew_command + renew_command, } } } @@ -515,7 +540,7 @@ mod tests { ) }; let res = put_cert_update(PutCert, State(state.clone()), CertificateBody(cert_first)).await; - assert!(res.is_ok()); + assert!(dbg!(res).is_ok()); let res = put_cert_update(PutCert, State(state.clone()), CertificateBody(cert_newer)).await; assert!(res.is_ok()); let res = put_cert_update(