feat(renew-cmd): added

This commit is contained in:
shimun 2023-07-09 20:03:20 +02:00
parent 591858ef05
commit cbb99138a9
Signed by: shimun
GPG Key ID: E0420647856EA39E
7 changed files with 109 additions and 42 deletions

3
Cargo.lock generated
View File

@ -1663,8 +1663,10 @@ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
"axum-extra", "axum-extra",
"chrono",
"hex", "hex",
"serde", "serde",
"shell-escape",
"ssh-key", "ssh-key",
"tempfile", "tempfile",
"thiserror", "thiserror",
@ -1686,7 +1688,6 @@ dependencies = [
"jwt-compact", "jwt-compact",
"rand", "rand",
"serde", "serde",
"shell-escape",
"ssh-cert-dist-common", "ssh-cert-dist-common",
"ssh-key", "ssh-key",
"tempfile", "tempfile",

View File

@ -1,9 +1,10 @@
use anyhow::bail; use anyhow::{bail, Context};
use axum_extra::routing::TypedPath; use axum_extra::routing::TypedPath;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use reqwest::{Client, StatusCode}; use reqwest::{Client, StatusCode};
use ssh_key::Certificate; use ssh_key::Certificate;
use std::path::PathBuf; use std::path::PathBuf;
use std::process;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use tokio::fs; use tokio::fs;
use tokio::io::{stdin, AsyncBufReadExt, BufReader}; use tokio::io::{stdin, AsyncBufReadExt, BufReader};
@ -45,6 +46,19 @@ pub struct UploadArgs {
files: Vec<PathBuf>, files: Vec<PathBuf>,
} }
#[derive(Parser)]
pub struct RenewCommandArgs {
/// Execute the renew command
#[clap(short = 'x')]
execute: bool,
/// Path to the CA private key
#[clap(long="ca", env = env_key!("CA_KEY"))]
ca_key: Option<PathBuf>,
/// Certificates to generate commands for
#[clap(env = env_key!("FILES"))]
files: Vec<PathBuf>,
}
#[derive(Parser)] #[derive(Parser)]
pub struct ClientCommand { pub struct ClientCommand {
#[clap(subcommand)] #[clap(subcommand)]
@ -55,12 +69,14 @@ pub struct ClientCommand {
pub enum ClientCommands { pub enum ClientCommands {
Fetch(FetchArgs), Fetch(FetchArgs),
Upload(UploadArgs), Upload(UploadArgs),
RenewCommand(RenewCommandArgs),
} }
pub async fn run(ClientCommand { cmd }: ClientCommand) -> anyhow::Result<()> { pub async fn run(ClientCommand { cmd }: ClientCommand) -> anyhow::Result<()> {
match cmd { match cmd {
ClientCommands::Fetch(args) => fetch(args).await, ClientCommands::Fetch(args) => fetch(args).await,
ClientCommands::Upload(args) => upload(args).await, ClientCommands::Upload(args) => upload(args).await,
ClientCommands::RenewCommand(args) => renew(args).await,
} }
} }
@ -170,6 +186,40 @@ async fn fetch(
Ok(()) Ok(())
} }
async fn renew(
RenewCommandArgs {
files,
ca_key,
execute,
}: RenewCommandArgs,
) -> anyhow::Result<()> {
for file in files.iter() {
let cert = load_cert(&file).await?;
if let Some(cert) = cert {
let command = renew_command(
&cert,
ca_key
.as_deref()
.map(|path| path.to_str())
.flatten()
.unwrap_or("ca"),
file.to_str(),
);
println!("{}", command);
if execute {
process::Command::new("sh")
.arg("-c")
.arg(&command)
.spawn()
.with_context(|| format!("{command}"))?;
}
} else {
bail!("{file:?} doesn't exist");
}
}
Ok(())
}
#[instrument(skip(client, current))] #[instrument(skip(client, current))]
async fn fetch_cert( async fn fetch_cert(
client: Client, client: Client,

View File

@ -11,6 +11,7 @@ anyhow = "1.0.66"
async-trait = "0.1.59" async-trait = "0.1.59"
axum = { version = "0.6.1" } axum = { version = "0.6.1" }
axum-extra = { version = "0.4.1", features = ["typed-routing"] } axum-extra = { version = "0.4.1", features = ["typed-routing"] }
chrono = "0.4.26"
hex = { version = "0.4.3", features = ["serde"] } hex = { version = "0.4.3", features = ["serde"] }
serde = { version = "1.0.148", features = ["derive"] } serde = { version = "1.0.148", features = ["derive"] }
ssh-key = { version = "0.6.0-pre.0", features = ["ed25519", "p256", "p384", "rsa"] } ssh-key = { version = "0.6.0-pre.0", features = ["ed25519", "p256", "p384", "rsa"] }
@ -18,6 +19,7 @@ thiserror = "1.0.37"
tokio = { version = "1.22.0", features = ["io-std", "test-util", "tracing", "macros", "fs"] } tokio = { version = "1.22.0", features = ["io-std", "test-util", "tracing", "macros", "fs"] }
tracing = { version = "0.1.37", features = ["release_max_level_debug"] } tracing = { version = "0.1.37", features = ["release_max_level_debug"] }
tracing-subscriber = "0.3.16" tracing-subscriber = "0.3.16"
shell-escape = "0.1.5"
[dev-dependencies] [dev-dependencies]
tempfile = "3.3.0" tempfile = "3.3.0"

View File

@ -1,6 +1,8 @@
mod certs; mod certs;
mod renew;
mod routes; mod routes;
mod util; mod util;
pub use certs::*; pub use certs::*;
pub use renew::*;
pub use routes::*; pub use routes::*;

49
common/src/renew.rs Normal file
View File

@ -0,0 +1,49 @@
use std::borrow::Cow;
use std::time::UNIX_EPOCH;
use chrono::Duration;
use shell_escape::escape;
use ssh_key::Certificate;
/// Generates an command to renew the given certs
pub fn renew_command(cert: &Certificate, ca_path: &str, file_name: Option<&str>) -> String {
let validity = cert
.valid_before_time()
.duration_since(cert.valid_after_time())
.unwrap_or(Duration::zero().to_std().unwrap());
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 {}", escape(arg.into())))
.collect::<Vec<_>>()
.join(" ");
let opts = opts.trim();
let renew_command = format!(
"ssh-keygen -s {ca_path} {host_key} -I {} -n {} -z {} -V {:#x}:{:#x} {opts} {}",
escape(cert.key_id().into()),
escape(cert.valid_principals().join(",").into()),
cert.serial() + 1,
cert.valid_after(),
expiry_date.as_secs(),
escape(
file_name
.map(Cow::Borrowed)
.unwrap_or_else(|| escape(format!("{}.pub", cert.key_id()).into()))
)
);
renew_command
}

View File

@ -35,7 +35,6 @@ tower-http = { version = "0.3.4", features = ["map-request-body", "trace", "util
tracing = { version = "0.1.37", features = ["release_max_level_debug"] } tracing = { version = "0.1.37", features = ["release_max_level_debug"] }
tracing-subscriber = "0.3.16" tracing-subscriber = "0.3.16"
ssh-cert-dist-common = { path = "../common" } ssh-cert-dist-common = { path = "../common" }
shell-escape = "0.1.5"
[dev-dependencies] [dev-dependencies]
tempfile = "3.3.0" tempfile = "3.3.0"

View File

@ -15,14 +15,13 @@ use axum::extract::rejection::QueryRejection;
use axum::extract::{Query, State}; use axum::extract::{Query, State};
use chrono::Duration; use chrono::Duration;
use shell_escape::escape;
use ssh_cert_dist_common::*; use ssh_cert_dist_common::*;
use axum::{http::StatusCode, response::IntoResponse, Json, Router}; use axum::{http::StatusCode, response::IntoResponse, Json, Router};
use axum_extra::routing::RouterExt; use axum_extra::routing::RouterExt;
use clap::{Args, Parser}; use clap::{Args, Parser};
use jwt_compact::alg::{Hs256, Hs256Key}; use jwt_compact::alg::{Hs256, Hs256Key};
use jwt_compact::{AlgorithmExt}; use jwt_compact::AlgorithmExt;
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ssh_key::{Certificate, Fingerprint, PublicKey}; use ssh_key::{Certificate, Fingerprint, PublicKey};
@ -31,7 +30,7 @@ use tower::ServiceBuilder;
use tower_http::{trace::TraceLayer, ServiceBuilderExt}; use tower_http::{trace::TraceLayer, ServiceBuilderExt};
use tracing::{debug, info, trace}; use tracing::{debug, info, trace};
use self::extract::{CertificateBody, SignatureBody, JWTAuthenticated, JWTString}; use self::extract::{CertificateBody, JWTAuthenticated, JWTString, SignatureBody};
#[derive(Parser)] #[derive(Parser)]
pub struct ApiArgs { pub struct ApiArgs {
@ -309,40 +308,6 @@ struct CertInfo {
impl From<&Certificate> for CertInfo { impl From<&Certificate> for CertInfo {
fn from(cert: &Certificate) -> Self { 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 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 {}", escape(arg.into())))
.collect::<Vec<_>>()
.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 { CertInfo {
principals: cert.valid_principals().to_vec(), principals: cert.valid_principals().to_vec(),
ca: cert.signature_key().clone().into(), ca: cert.signature_key().clone().into(),
@ -351,7 +316,7 @@ impl From<&Certificate> for CertInfo {
identity_hash: cert.public_key().fingerprint(ssh_key::HashAlg::Sha256), identity_hash: cert.public_key().fingerprint(ssh_key::HashAlg::Sha256),
key_id: cert.key_id().to_string(), key_id: cert.key_id().to_string(),
expiry: cert.valid_before_time(), expiry: cert.valid_before_time(),
renew_command, renew_command: renew_command(cert, "./ca", None),
} }
} }
} }
@ -387,7 +352,6 @@ impl From<Query<PostCertsQuery>> for JWTString {
} }
} }
/// POST with signed challenge /// POST with signed challenge
async fn post_certs_identifier( async fn post_certs_identifier(
PostCertInfo { identifier }: PostCertInfo, PostCertInfo { identifier }: PostCertInfo,