feat(renew-cmd): added
This commit is contained in:
parent
591858ef05
commit
cbb99138a9
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -1663,8 +1663,10 @@ dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum-extra",
|
||||
"chrono",
|
||||
"hex",
|
||||
"serde",
|
||||
"shell-escape",
|
||||
"ssh-key",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
@ -1686,7 +1688,6 @@ dependencies = [
|
||||
"jwt-compact",
|
||||
"rand",
|
||||
"serde",
|
||||
"shell-escape",
|
||||
"ssh-cert-dist-common",
|
||||
"ssh-key",
|
||||
"tempfile",
|
||||
|
@ -1,9 +1,10 @@
|
||||
use anyhow::bail;
|
||||
use anyhow::{bail, Context};
|
||||
use axum_extra::routing::TypedPath;
|
||||
use clap::{Parser, Subcommand};
|
||||
use reqwest::{Client, StatusCode};
|
||||
use ssh_key::Certificate;
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tokio::fs;
|
||||
use tokio::io::{stdin, AsyncBufReadExt, BufReader};
|
||||
@ -45,6 +46,19 @@ pub struct UploadArgs {
|
||||
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)]
|
||||
pub struct ClientCommand {
|
||||
#[clap(subcommand)]
|
||||
@ -55,12 +69,14 @@ pub struct ClientCommand {
|
||||
pub enum ClientCommands {
|
||||
Fetch(FetchArgs),
|
||||
Upload(UploadArgs),
|
||||
RenewCommand(RenewCommandArgs),
|
||||
}
|
||||
|
||||
pub async fn run(ClientCommand { cmd }: ClientCommand) -> anyhow::Result<()> {
|
||||
match cmd {
|
||||
ClientCommands::Fetch(args) => fetch(args).await,
|
||||
ClientCommands::Upload(args) => upload(args).await,
|
||||
ClientCommands::RenewCommand(args) => renew(args).await,
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,6 +186,40 @@ async fn fetch(
|
||||
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))]
|
||||
async fn fetch_cert(
|
||||
client: Client,
|
||||
|
@ -11,6 +11,7 @@ anyhow = "1.0.66"
|
||||
async-trait = "0.1.59"
|
||||
axum = { version = "0.6.1" }
|
||||
axum-extra = { version = "0.4.1", features = ["typed-routing"] }
|
||||
chrono = "0.4.26"
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
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"] }
|
||||
tracing = { version = "0.1.37", features = ["release_max_level_debug"] }
|
||||
tracing-subscriber = "0.3.16"
|
||||
shell-escape = "0.1.5"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.3.0"
|
||||
|
@ -1,6 +1,8 @@
|
||||
mod certs;
|
||||
mod renew;
|
||||
mod routes;
|
||||
mod util;
|
||||
|
||||
pub use certs::*;
|
||||
pub use renew::*;
|
||||
pub use routes::*;
|
||||
|
49
common/src/renew.rs
Normal file
49
common/src/renew.rs
Normal 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
|
||||
}
|
@ -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-subscriber = "0.3.16"
|
||||
ssh-cert-dist-common = { path = "../common" }
|
||||
shell-escape = "0.1.5"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.3.0"
|
||||
|
@ -15,14 +15,13 @@ use axum::extract::rejection::QueryRejection;
|
||||
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};
|
||||
use axum_extra::routing::RouterExt;
|
||||
use clap::{Args, Parser};
|
||||
use jwt_compact::alg::{Hs256, Hs256Key};
|
||||
use jwt_compact::{AlgorithmExt};
|
||||
use jwt_compact::AlgorithmExt;
|
||||
use rand::{thread_rng, Rng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ssh_key::{Certificate, Fingerprint, PublicKey};
|
||||
@ -31,7 +30,7 @@ use tower::ServiceBuilder;
|
||||
use tower_http::{trace::TraceLayer, ServiceBuilderExt};
|
||||
use tracing::{debug, info, trace};
|
||||
|
||||
use self::extract::{CertificateBody, SignatureBody, JWTAuthenticated, JWTString};
|
||||
use self::extract::{CertificateBody, JWTAuthenticated, JWTString, SignatureBody};
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct ApiArgs {
|
||||
@ -309,40 +308,6 @@ 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 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 {
|
||||
principals: cert.valid_principals().to_vec(),
|
||||
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),
|
||||
key_id: cert.key_id().to_string(),
|
||||
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
|
||||
async fn post_certs_identifier(
|
||||
PostCertInfo { identifier }: PostCertInfo,
|
||||
|
Loading…
x
Reference in New Issue
Block a user