Merge branch 'master' of git.shimun.net:shimun/ssh-cert-dist
This commit is contained in:
commit
cd82dd9deb
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1496,6 +1496,7 @@ dependencies = [
|
|||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"rsa",
|
"rsa",
|
||||||
"sec1",
|
"sec1",
|
||||||
|
"serde",
|
||||||
"sha2 0.10.6",
|
"sha2 0.10.6",
|
||||||
"signature",
|
"signature",
|
||||||
"ssh-encoding",
|
"ssh-encoding",
|
||||||
@ -1696,6 +1697,7 @@ dependencies = [
|
|||||||
"tower",
|
"tower",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -7,8 +7,9 @@ edition = "2021"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [ "client", "reload" ]
|
default = [ "client", "reload", "info" ]
|
||||||
reload = []
|
reload = []
|
||||||
|
info = [ "axum/json", "ssh-key/serde" ]
|
||||||
client = [ "dep:url", "dep:reqwest" ]
|
client = [ "dep:url", "dep:reqwest" ]
|
||||||
|
|
||||||
|
|
||||||
@ -24,7 +25,7 @@ ssh-key = { version = "0.5.1", features = ["ed25519", "p256", "p384", "rsa", "si
|
|||||||
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", features = ["util"] }
|
tower = { version = "0.4.13", features = ["util"] }
|
||||||
tower-http = { version = "0.3.4", features = ["map-request-body"] }
|
tower-http = { version = "0.3.4", features = ["map-request-body", "trace"] }
|
||||||
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", optional = true }
|
url = { version = "2.3.1", optional = true }
|
||||||
|
@ -1,38 +1,10 @@
|
|||||||
{ config, pkgs, lib, ... }: with lib; let
|
{ config, pkgs, lib, ... }: with lib; let
|
||||||
cfg = config.services.ssh-cert-dist;
|
cfg = config.services.ssh-cert-dist;
|
||||||
directoryModule = { name, ... }: {
|
|
||||||
options = {
|
|
||||||
name = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = last (splitString "/" name);
|
|
||||||
};
|
|
||||||
fetch = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = true;
|
|
||||||
};
|
|
||||||
upload = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.services.ssh-cert-dist = {
|
config.imports = [
|
||||||
enable = mkEnableOption "ssh-cert-dist";
|
./options.nix
|
||||||
endpoint = mkOption {
|
];
|
||||||
type = types.str;
|
|
||||||
description = "API endpoint url";
|
|
||||||
};
|
|
||||||
package = mkOption {
|
|
||||||
type = types.package;
|
|
||||||
default = pkgs.ssh-cert-dist;
|
|
||||||
};
|
|
||||||
directories = mkOption {
|
|
||||||
type = with types; attrsOf (submodule directoryModule);
|
|
||||||
default = { };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
config.systemd.user.services = mkIf cfg.enable (mapAttrs'
|
config.systemd.user.services = mkIf cfg.enable (mapAttrs'
|
||||||
(path: options: {
|
(path: options: {
|
||||||
inherit (options) name; value = {
|
inherit (options) name; value = {
|
||||||
@ -41,7 +13,7 @@ in
|
|||||||
Environment = "RUST_LOG=debug";
|
Environment = "RUST_LOG=debug";
|
||||||
ExecStart = toString (pkgs.writeShellApplication {
|
ExecStart = toString (pkgs.writeShellApplication {
|
||||||
name = "ssh-cert-dist-${options.name}";
|
name = "ssh-cert-dist-${options.name}";
|
||||||
runtimeInputs = [ cfg.package ];
|
runtimeInputs = [ pkgs.ssh-cert-dist ];
|
||||||
text = ''
|
text = ''
|
||||||
${optionalString options.fetch ''
|
${optionalString options.fetch ''
|
||||||
ssh-cert-dist client fetch --cert-dir '${path}' --api-endpoint '${cfg.endpoint}'
|
ssh-cert-dist client fetch --cert-dir '${path}' --api-endpoint '${cfg.endpoint}'
|
||||||
@ -56,20 +28,4 @@ in
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
cfg.directories);
|
cfg.directories);
|
||||||
|
|
||||||
options.programs.ssh-cert-dist = {
|
|
||||||
enable = mkEnableOption "ssh-cert-dist";
|
|
||||||
package = mkOption {
|
|
||||||
type = types.package;
|
|
||||||
default = pkgs.ssh-cert-dist;
|
|
||||||
};
|
|
||||||
endpoint = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
description = "API endpoint url";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
config.home = let cfg = config.programs.ssh-cert-dist; in mkIf cfg.enable {
|
|
||||||
packages = [ cfg.package ];
|
|
||||||
sessionVariables.SSH_CD_API = cfg.endpoint;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
{ config, pkgs, lib, ... }: with lib; let
|
{ config, pkgs, lib, ... }: with lib; let
|
||||||
cfg = config.services.ssh-cert-dist;
|
cfg = config.services.ssh-cert-dist;
|
||||||
ca = if isPath cfg.ca then cfg.ca else pkgs.writeText "ssh-ca" cfg.ca;
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.services.ssh-cert-dist = {
|
options.services.ssh-cert-dist = {
|
||||||
@ -18,7 +17,7 @@ in
|
|||||||
default = pkgs.ssh-cert-dist;
|
default = pkgs.ssh-cert-dist;
|
||||||
};
|
};
|
||||||
ca = mkOption {
|
ca = mkOption {
|
||||||
type = with types; either str path;
|
type = types.path;
|
||||||
};
|
};
|
||||||
dataDir = mkOption {
|
dataDir = mkOption {
|
||||||
type = types.path;
|
type = types.path;
|
||||||
@ -47,9 +46,9 @@ in
|
|||||||
environment = {
|
environment = {
|
||||||
SSH_CD_SOCKET_ADDRESS = "${cfg.host}:${toString cfg.port}";
|
SSH_CD_SOCKET_ADDRESS = "${cfg.host}:${toString cfg.port}";
|
||||||
SSH_CD_CERT_DIR = cfg.dataDir;
|
SSH_CD_CERT_DIR = cfg.dataDir;
|
||||||
SSH_CD_VALIDATE_EXPIRY = true;
|
SSH_CD_VALIDATE_EXPIRY = "true";
|
||||||
SSH_CD_VALIDATE_SERIAL = false;
|
SSH_CD_VALIDATE_SERIAL = "false";
|
||||||
SSH_CD_CA = ca;
|
SSH_CD_CA = cfg.ca;
|
||||||
RUST_LOG = "debug";
|
RUST_LOG = "debug";
|
||||||
};
|
};
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
|
42
modules/options.nix
Normal file
42
modules/options.nix
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{ config, lib, pkgs, ... }: with lib; let
|
||||||
|
directoryModule = { name, ... }: {
|
||||||
|
options = {
|
||||||
|
name = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = last (splitString "/" name);
|
||||||
|
};
|
||||||
|
fetch = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
};
|
||||||
|
upload = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
endpointOption = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "API endpoint url";
|
||||||
|
default = "https://pki.shimun.net";
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
services.ssh-cert-dist = {
|
||||||
|
enable = mkEnableOption "ssh-cert-dist";
|
||||||
|
endpoint = endpointOption;
|
||||||
|
directories = mkOption {
|
||||||
|
type = with types; attrsOf (submodule directoryModule);
|
||||||
|
default = { };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
programs.ssh-cert-dist = {
|
||||||
|
enable = mkEnableOption "ssh-cert-dist client";
|
||||||
|
endpoint = endpointOption;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
62
src/api.rs
62
src/api.rs
@ -11,8 +11,8 @@ use crate::env_key;
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use axum::body;
|
use axum::body;
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
use axum::routing::post;
|
|
||||||
use axum::{http::StatusCode, response::IntoResponse, Router};
|
use axum::{http::StatusCode, response::IntoResponse, Json, Router};
|
||||||
use axum_extra::routing::{
|
use axum_extra::routing::{
|
||||||
RouterExt, // for `Router::typed_*`
|
RouterExt, // for `Router::typed_*`
|
||||||
TypedPath,
|
TypedPath,
|
||||||
@ -22,8 +22,8 @@ use serde::Deserialize;
|
|||||||
use ssh_key::{Certificate, PublicKey};
|
use ssh_key::{Certificate, PublicKey};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tower::ServiceBuilder;
|
use tower::ServiceBuilder;
|
||||||
use tower_http::ServiceBuilderExt;
|
use tower_http::{trace::TraceLayer, ServiceBuilderExt};
|
||||||
use tracing::{debug, instrument, trace};
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
use self::extract::CertificateBody;
|
use self::extract::CertificateBody;
|
||||||
|
|
||||||
@ -133,9 +133,11 @@ pub async fn run(
|
|||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.typed_get(get_certs_identifier)
|
.typed_get(get_certs_identifier)
|
||||||
.typed_put(put_cert_update)
|
.typed_put(put_cert_update)
|
||||||
.route("/certs/:identifier", post(post_certs_identifier))
|
.typed_get(get_cert_info)
|
||||||
|
.typed_post(post_certs_identifier)
|
||||||
.fallback(fallback_404)
|
.fallback(fallback_404)
|
||||||
.layer(ServiceBuilder::new().map_request_body(body::boxed))
|
.layer(ServiceBuilder::new().map_request_body(body::boxed))
|
||||||
|
.layer(TraceLayer::new_for_http())
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
// run our app with hyper
|
// run our app with hyper
|
||||||
@ -193,7 +195,6 @@ pub struct GetCert {
|
|||||||
/// return Unauthorized with an challenge
|
/// return Unauthorized with an challenge
|
||||||
/// upon which the client will ssh-keysign
|
/// upon which the client will ssh-keysign
|
||||||
/// the challenge an issue an post request
|
/// the challenge an issue an post request
|
||||||
#[instrument(skip_all, ret)]
|
|
||||||
async fn get_certs_identifier(
|
async fn get_certs_identifier(
|
||||||
GetCert { identifier }: GetCert,
|
GetCert { identifier }: GetCert,
|
||||||
State(ApiState { certs, .. }): State<ApiState>,
|
State(ApiState { certs, .. }): State<ApiState>,
|
||||||
@ -205,9 +206,41 @@ async fn get_certs_identifier(
|
|||||||
Ok(cert.to_openssh().context("to openssh")?)
|
Ok(cert.to_openssh().context("to openssh")?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(TypedPath, Deserialize)]
|
||||||
|
#[typed_path("/certs/:identifier/info")]
|
||||||
|
pub struct GetCertInfo {
|
||||||
|
pub identifier: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "info")]
|
||||||
|
async fn get_cert_info(
|
||||||
|
GetCertInfo { identifier }: GetCertInfo,
|
||||||
|
State(ApiState { certs, .. }): State<ApiState>,
|
||||||
|
) -> ApiResult<Json<Certificate>> {
|
||||||
|
let certs = certs.lock().await;
|
||||||
|
let cert = certs
|
||||||
|
.get(&identifier)
|
||||||
|
.ok_or(ApiError::CertificateNotFound)?;
|
||||||
|
Ok(Json(cert.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "info"))]
|
||||||
|
async fn get_cert_info(
|
||||||
|
GetCertInfo { identifier: _ }: GetCertInfo,
|
||||||
|
State(ApiState { certs: _, .. }): State<ApiState>,
|
||||||
|
) -> ApiResult<()> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TypedPath, Deserialize)]
|
||||||
|
#[typed_path("/certs/:identifier")]
|
||||||
|
pub struct PostCertInfo {
|
||||||
|
pub identifier: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// POST with signed challenge
|
/// POST with signed challenge
|
||||||
#[instrument(skip_all, ret)]
|
|
||||||
async fn post_certs_identifier(
|
async fn post_certs_identifier(
|
||||||
|
PostCertInfo { identifier: _ }: PostCertInfo,
|
||||||
State(ApiState { .. }): State<ApiState>,
|
State(ApiState { .. }): State<ApiState>,
|
||||||
Path(_identifier): Path<String>,
|
Path(_identifier): Path<String>,
|
||||||
) -> ApiResult<String> {
|
) -> ApiResult<String> {
|
||||||
@ -219,7 +252,6 @@ async fn post_certs_identifier(
|
|||||||
pub struct PutCert;
|
pub struct PutCert;
|
||||||
|
|
||||||
/// Upload an cert with an higher serial than the previous
|
/// Upload an cert with an higher serial than the previous
|
||||||
#[instrument(skip_all, ret)]
|
|
||||||
async fn put_cert_update(
|
async fn put_cert_update(
|
||||||
_: PutCert,
|
_: PutCert,
|
||||||
State(ApiState {
|
State(ApiState {
|
||||||
@ -235,9 +267,17 @@ async fn put_cert_update(
|
|||||||
}): State<ApiState>,
|
}): State<ApiState>,
|
||||||
CertificateBody(cert): CertificateBody,
|
CertificateBody(cert): CertificateBody,
|
||||||
) -> ApiResult<String> {
|
) -> ApiResult<String> {
|
||||||
cert.validate(&[ca.fingerprint(Default::default())])
|
let cert = {
|
||||||
.map_err(|_| ApiError::CertificateInvalid)?;
|
let ca = ca.clone();
|
||||||
let _string_repr = cert.to_openssh();
|
tokio::task::spawn_blocking(move || -> ApiResult<Certificate> {
|
||||||
|
let cert = cert;
|
||||||
|
cert.validate(&[ca.fingerprint(Default::default())])
|
||||||
|
.map_err(|_| ApiError::CertificateInvalid)?;
|
||||||
|
Ok(cert)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("signature verification")??
|
||||||
|
};
|
||||||
let prev = load_cert_by_id(&cert_dir, &ca, cert.key_id()).await?;
|
let prev = load_cert_by_id(&cert_dir, &ca, cert.key_id()).await?;
|
||||||
let mut prev_serial = 0;
|
let mut prev_serial = 0;
|
||||||
let serial = cert.serial();
|
let serial = cert.serial();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use anyhow::{bail};
|
use anyhow::bail;
|
||||||
use axum_extra::routing::TypedPath;
|
use axum_extra::routing::TypedPath;
|
||||||
use clap::{Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
use reqwest::{Client, StatusCode};
|
use reqwest::{Client, StatusCode};
|
||||||
@ -13,10 +13,7 @@ use url::Url;
|
|||||||
use crate::api::PutCert;
|
use crate::api::PutCert;
|
||||||
use crate::certs::load_cert;
|
use crate::certs::load_cert;
|
||||||
use crate::env_key;
|
use crate::env_key;
|
||||||
use crate::{
|
use crate::{api::GetCert, certs::read_dir};
|
||||||
api::GetCert,
|
|
||||||
certs::{read_dir},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct ClientArgs {
|
pub struct ClientArgs {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user