Compare commits
No commits in common. "master" and "split_components" have entirely different histories.
master
...
split_comp
@ -1,5 +0,0 @@
|
|||||||
pipeline:
|
|
||||||
test:
|
|
||||||
image: rust
|
|
||||||
commands:
|
|
||||||
- cargo test
|
|
691
Cargo.lock
generated
691
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -17,14 +17,13 @@ clap = { version = "4.0.29", features = ["env", "derive"] }
|
|||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
reqwest = { version = "0.11.13" }
|
reqwest = { version = "0.11.13" }
|
||||||
serde = { version = "1.0.148", features = ["derive"] }
|
serde = { version = "1.0.148", features = ["derive"] }
|
||||||
ssh-key = { version = "0.6.0-rc.2", features = ["ed25519", "p256", "p384", "rsa", "serde"] }
|
ssh-key = { version = "0.6.0-pre.0", features = ["ed25519", "p256", "p384", "rsa", "serde"] }
|
||||||
thiserror = "1.0.37"
|
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"
|
||||||
url = { version = "2.3.1" }
|
url = { version = "2.3.1" }
|
||||||
ssh-cert-dist-common = { path = "../common" }
|
ssh-cert-dist-common = { path = "../common" }
|
||||||
clap_complete_command = "0.5.1"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.3.0"
|
tempfile = "3.3.0"
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
use anyhow::{bail, Context};
|
use anyhow::bail;
|
||||||
use axum_extra::routing::TypedPath;
|
use axum_extra::routing::TypedPath;
|
||||||
use clap::{CommandFactory, Parser, Subcommand, ValueHint};
|
use clap::{Parser, Subcommand};
|
||||||
use clap_complete_command::Shell;
|
|
||||||
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};
|
||||||
@ -18,7 +16,7 @@ use ssh_cert_dist_common::*;
|
|||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct ClientArgs {
|
pub struct ClientArgs {
|
||||||
/// Url for the API endpoint
|
/// Url for the API endpoint
|
||||||
#[clap(short = 'a', long = "api-endpoint",value_hint = ValueHint::Url, env = env_key!("API"))]
|
#[clap(short = 'a', long = "api-endpoint", env = env_key!("API"))]
|
||||||
api: Url,
|
api: Url,
|
||||||
/// Require interaction before writing certificates
|
/// Require interaction before writing certificates
|
||||||
#[clap(short = 'i', long = "interactive", env = env_key!("INTERACTIVE"))]
|
#[clap(short = 'i', long = "interactive", env = env_key!("INTERACTIVE"))]
|
||||||
@ -31,7 +29,7 @@ pub struct FetchArgs {
|
|||||||
args: ClientArgs,
|
args: ClientArgs,
|
||||||
#[clap(short = 'k', long = "key-update", env = env_key!("KEY_UPDATE"))]
|
#[clap(short = 'k', long = "key-update", env = env_key!("KEY_UPDATE"))]
|
||||||
prohibit_key_update: bool,
|
prohibit_key_update: bool,
|
||||||
#[clap(short = 'c', long = "cert-dir",value_hint = ValueHint::DirPath, env = env_key!("CERT_DIR"))]
|
#[clap(short = 'c', long = "cert-dir", env = env_key!("CERT_DIR"))]
|
||||||
cert_dir: PathBuf,
|
cert_dir: PathBuf,
|
||||||
/// minimum time in days between now and expiry to consider checking
|
/// minimum time in days between now and expiry to consider checking
|
||||||
#[clap(short = 'd', long = "days", default_value = "60", env = env_key!("MIN_DELTA_DAYS"))]
|
#[clap(short = 'd', long = "days", default_value = "60", env = env_key!("MIN_DELTA_DAYS"))]
|
||||||
@ -43,25 +41,11 @@ pub struct UploadArgs {
|
|||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
args: ClientArgs,
|
args: ClientArgs,
|
||||||
/// Certificates to be uploaded
|
/// Certificates to be uploaded
|
||||||
#[clap(value_hint = ValueHint::FilePath, env = env_key!("FILES"))]
|
#[clap(env = env_key!("FILES"))]
|
||||||
files: Vec<PathBuf>,
|
files: Vec<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct RenewCommandArgs {
|
|
||||||
/// Execute the renew command
|
|
||||||
#[clap(short = 'x')]
|
|
||||||
execute: bool,
|
|
||||||
/// Path to the CA private key
|
|
||||||
#[clap(long="ca",value_hint = ValueHint::DirPath, env = env_key!("CA_KEY"))]
|
|
||||||
ca_key: Option<PathBuf>,
|
|
||||||
/// Certificates to generate commands for
|
|
||||||
#[clap(value_hint = ValueHint::FilePath,env = env_key!("FILES"))]
|
|
||||||
files: Vec<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[command(name = "sshcd")]
|
|
||||||
pub struct ClientCommand {
|
pub struct ClientCommand {
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
cmd: ClientCommands,
|
cmd: ClientCommands,
|
||||||
@ -71,23 +55,12 @@ pub struct ClientCommand {
|
|||||||
pub enum ClientCommands {
|
pub enum ClientCommands {
|
||||||
Fetch(FetchArgs),
|
Fetch(FetchArgs),
|
||||||
Upload(UploadArgs),
|
Upload(UploadArgs),
|
||||||
RenewCommand(RenewCommandArgs),
|
|
||||||
#[clap(hide = true)]
|
|
||||||
Completions {
|
|
||||||
#[arg(long = "shell", value_enum)]
|
|
||||||
shell: Shell,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
ClientCommands::Completions { shell } => {
|
|
||||||
shell.generate(&mut ClientCommand::command(), &mut std::io::stdout());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,40 +170,6 @@ 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,
|
||||||
|
@ -11,15 +11,13 @@ 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-rc.2", features = ["ed25519", "p256", "p384", "rsa"] }
|
ssh-key = { version = "0.6.0-pre.0", features = ["ed25519", "p256", "p384", "rsa"] }
|
||||||
thiserror = "1.0.37"
|
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"
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
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::*;
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
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 {}:{} {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(|name| name.trim_end_matches("-cert.pub")).map(Cow::Borrowed)
|
|
||||||
.unwrap_or_else(|| escape(format!("{}.pub", cert.key_id()).into()))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
renew_command
|
|
||||||
}
|
|
18
flake.lock
generated
18
flake.lock
generated
@ -7,11 +7,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1698420672,
|
"lastModified": 1688534083,
|
||||||
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
|
"narHash": "sha256-/bI5vsioXscQTsx+Hk9X5HfweeNZz/6kVKsbdqfwW7g=",
|
||||||
"owner": "nmattia",
|
"owner": "nmattia",
|
||||||
"repo": "naersk",
|
"repo": "naersk",
|
||||||
"rev": "aeb58d5e8faead8980a807c840232697982d47b9",
|
"rev": "abca1fb7a6cfdd355231fc220c3d0302dbb4369a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -22,11 +22,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1705496572,
|
"lastModified": 1688679045,
|
||||||
"narHash": "sha256-rPIe9G5EBLXdBdn9ilGc0nq082lzQd0xGGe092R/5QE=",
|
"narHash": "sha256-t3xGEfYIwhaLTPU8FLtN/pLPytNeDwbLI6a7XFFBlGo=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "842d9d80cfd4560648c785f8a4e6f3b096790e19",
|
"rev": "3c7487575d9445185249a159046cc02ff364bff8",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -61,11 +61,11 @@
|
|||||||
"systems": "systems"
|
"systems": "systems"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1709126324,
|
"lastModified": 1687709756,
|
||||||
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
|
"narHash": "sha256-Y5wKlQSkgEK2weWdOu4J3riRd+kV/VCgHsqLNTTWQ/0=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
|
"rev": "dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
14
flake.nix
14
flake.nix
@ -123,31 +123,17 @@
|
|||||||
];
|
];
|
||||||
nativeBuildInputs = with prev; [
|
nativeBuildInputs = with prev; [
|
||||||
pkg-config
|
pkg-config
|
||||||
installShellFiles
|
|
||||||
];
|
];
|
||||||
installCompletions = cmd: ''
|
|
||||||
mkdir completions
|
|
||||||
for shell in bash zsh fish; do
|
|
||||||
$out/bin/${cmd} completions --shell $shell > completions/${cmd}.$shell
|
|
||||||
installShellCompletion --cmd ${cmd} --$shell completions/${cmd}.$shell
|
|
||||||
done
|
|
||||||
'';
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
"${pname}-server" =
|
"${pname}-server" =
|
||||||
naersk-lib.buildPackage {
|
naersk-lib.buildPackage {
|
||||||
name = "${pname}-server";
|
name = "${pname}-server";
|
||||||
inherit root buildInputs nativeBuildInputs;
|
inherit root buildInputs nativeBuildInputs;
|
||||||
# postInstall = ''
|
|
||||||
# ${installCompletions}
|
|
||||||
# '';
|
|
||||||
};
|
};
|
||||||
"${pname}-client" =
|
"${pname}-client" =
|
||||||
naersk-lib.buildPackage {
|
naersk-lib.buildPackage {
|
||||||
name = "${pname}-client";
|
name = "${pname}-client";
|
||||||
postInstall = ''
|
|
||||||
${installCompletions "sshcd"}
|
|
||||||
'';
|
|
||||||
inherit root buildInputs nativeBuildInputs;
|
inherit root buildInputs nativeBuildInputs;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -11,8 +11,8 @@ in
|
|||||||
Unit.Description = "ssh-cert-dist service for ${path}";
|
Unit.Description = "ssh-cert-dist service for ${path}";
|
||||||
Service = {
|
Service = {
|
||||||
Environment = "RUST_LOG=debug";
|
Environment = "RUST_LOG=debug";
|
||||||
ExecStart = "${pkgs.writeShellApplication {
|
ExecStart = toString (pkgs.writeShellApplication {
|
||||||
name = "sshcd";
|
name = "ssh-cert-dist-${options.name}";
|
||||||
runtimeInputs = [ cfg.package ];
|
runtimeInputs = [ cfg.package ];
|
||||||
text = ''
|
text = ''
|
||||||
${optionalString options.fetch ''
|
${optionalString options.fetch ''
|
||||||
@ -22,24 +22,11 @@ in
|
|||||||
sshcd upload --api-endpoint '${cfg.endpoint}' ${path}/*
|
sshcd upload --api-endpoint '${cfg.endpoint}' ${path}/*
|
||||||
''}
|
''}
|
||||||
'';
|
'';
|
||||||
}}/bin/sshcd";
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
cfg.directories);
|
cfg.directories);
|
||||||
config.systemd.user.timers = mkIf cfg.enable (mapAttrs'
|
|
||||||
(path: options: {
|
|
||||||
inherit (options) name; value = {
|
|
||||||
Unit.Description = "ssh-cert-dist service for ${path}";
|
|
||||||
Timer = {
|
|
||||||
OnCalendar = options.interval;
|
|
||||||
Persistent = true;
|
|
||||||
Unit = "${options.name}.service";
|
|
||||||
};
|
|
||||||
Install.WantedBy = [ "timers.target" ];
|
|
||||||
};
|
|
||||||
})
|
|
||||||
cfg.directories);
|
|
||||||
config.home.sessionVariables = mkIf (cfg.enable && cfg.endpoint != null) {
|
config.home.sessionVariables = mkIf (cfg.enable && cfg.endpoint != null) {
|
||||||
SSH_CD_API = cfg.endpoint;
|
SSH_CD_API = cfg.endpoint;
|
||||||
};
|
};
|
||||||
|
@ -13,11 +13,6 @@
|
|||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
};
|
};
|
||||||
interval = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "daily";
|
|
||||||
description = "https://www.freedesktop.org/software/systemd/man/systemd.time.html";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
endpointOption = mkOption {
|
endpointOption = mkOption {
|
||||||
|
@ -27,7 +27,7 @@ clap = { version = "4.0.29", features = ["env", "derive"] }
|
|||||||
jwt-compact = { version = "0.6.0", features = ["serde_cbor", "std", "clock"], optional = true }
|
jwt-compact = { version = "0.6.0", features = ["serde_cbor", "std", "clock"], optional = true }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
serde = { version = "1.0.148", features = ["derive"] }
|
serde = { version = "1.0.148", features = ["derive"] }
|
||||||
ssh-key = { version = "0.6.0-rc.2", features = ["ed25519", "p256", "p384", "rsa"] }
|
ssh-key = { version = "0.6.0-pre.0", features = ["ed25519", "p256", "p384", "rsa"] }
|
||||||
thiserror = "1.0.37"
|
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"] }
|
||||||
tower = { version = "0.4.13" }
|
tower = { version = "0.4.13" }
|
||||||
@ -35,6 +35,7 @@ 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"
|
||||||
|
@ -6,21 +6,23 @@ use std::fmt::Debug;
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::{self, PathBuf};
|
use std::path::{self, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::SystemTime;
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
|
||||||
use axum::body;
|
use axum::body;
|
||||||
use axum::extract::rejection::QueryRejection;
|
use axum::extract::rejection::QueryRejection;
|
||||||
use axum::extract::{Query, State};
|
use axum::extract::{Query, State};
|
||||||
|
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};
|
||||||
@ -29,10 +31,9 @@ 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, JWTAuthenticated, JWTString, SignatureBody};
|
use self::extract::{CertificateBody, SignatureBody, JWTAuthenticated, JWTString};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "sshcd-server")]
|
|
||||||
pub struct ApiArgs {
|
pub struct ApiArgs {
|
||||||
#[clap(short = 'a', long = "address", env = env_key!("SOCKET_ADDRESS"))]
|
#[clap(short = 'a', long = "address", env = env_key!("SOCKET_ADDRESS"))]
|
||||||
address: SocketAddr,
|
address: SocketAddr,
|
||||||
@ -308,6 +309,40 @@ 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(),
|
||||||
@ -316,7 +351,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(cert, "./ca", None),
|
renew_command,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -352,6 +387,7 @@ 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,
|
||||||
@ -475,8 +511,7 @@ mod tests {
|
|||||||
user_key,
|
user_key,
|
||||||
unix_time(SystemTime::now()),
|
unix_time(SystemTime::now()),
|
||||||
unix_time(SystemTime::now() + validity),
|
unix_time(SystemTime::now() + validity),
|
||||||
)
|
);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.valid_principal("git")
|
.valid_principal("git")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user