Merge branch 'master' of git.shimun.net:shimun/ssh-cert-dist

This commit is contained in:
shimun 2022-12-07 20:45:11 +01:00
commit cd82dd9deb
Signed by: shimun
GPG Key ID: E0420647856EA39E
7 changed files with 108 additions and 71 deletions

2
Cargo.lock generated
View File

@ -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]]

View File

@ -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 }

View File

@ -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;
};
} }

View File

@ -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
View 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;
};
};
}

View File

@ -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();

View File

@ -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 {