server/user: Add sign cert operation
dustin/sshca/pipeline/head There was a failure building this commit
Details
dustin/sshca/pipeline/head There was a failure building this commit
Details
The *POST /user/sign* operation issues SSH user certificates for the public keys provided. The request must include a valid OpenID Connect Identity token in the `Authorization` request header, which will be used to populate the valid principals in the signed certificate. User certificates are typically issued for a very short duration (one hour by default). This precludes the need for revoking certificates that are no longer trusted; users must reauthenticate frequently and obtain a new certificate.master
parent
b945d0f142
commit
be40c05b56
|
@ -65,11 +65,35 @@ impl Default for HostCaConfig {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserCaConfig {
|
||||
/// Path to the User CA private key file
|
||||
#[serde(default = "default_user_ca_key")]
|
||||
pub private_key_file: PathBuf,
|
||||
pub private_key_passphrase_file: Option<PathBuf>,
|
||||
|
||||
/// Duration of issued user certificates
|
||||
#[serde(default = "default_user_cert_duration")]
|
||||
pub cert_duration: u64,
|
||||
}
|
||||
|
||||
impl Default for UserCaConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
private_key_file: default_user_ca_key(),
|
||||
private_key_passphrase_file: None,
|
||||
cert_duration: default_user_cert_duration(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// CA configuration
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct CaConfig {
|
||||
/// Host CA configuration
|
||||
pub host: HostCaConfig,
|
||||
/// User CA configuration
|
||||
pub user: UserCaConfig,
|
||||
}
|
||||
|
||||
/// OpenID Connect configuration
|
||||
|
@ -139,6 +163,14 @@ fn default_host_cert_duration() -> u64 {
|
|||
86400 * 30
|
||||
}
|
||||
|
||||
fn default_user_ca_key() -> PathBuf {
|
||||
default_config_path("user-ca.key")
|
||||
}
|
||||
|
||||
fn default_user_cert_duration() -> u64 {
|
||||
3600
|
||||
}
|
||||
|
||||
/// Load configuration from a TOML file
|
||||
///
|
||||
/// If `path` is provided, the configuration will be loaded from the
|
||||
|
|
|
@ -42,6 +42,7 @@ pub fn make_app(config: Configuration) -> Router {
|
|||
.route("/", get(|| async { "UP" }))
|
||||
.route("/host/sign", post(host::sign_host_cert))
|
||||
.route("/user/oidc-config", get(user::get_oidc_config))
|
||||
.route("/user/sign", post(user::sign_user_cert))
|
||||
.with_state(ctx)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
//! User CA operations
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use axum::async_trait;
|
||||
use axum::extract::multipart::Multipart;
|
||||
use axum::extract::FromRequestParts;
|
||||
use axum::extract::State;
|
||||
use axum::headers::authorization::Bearer;
|
||||
|
@ -17,9 +19,12 @@ use openidconnect::IssuerUrl;
|
|||
use openidconnect::Nonce;
|
||||
use openidconnect::{ClientId, ClientSecret};
|
||||
use serde::Serialize;
|
||||
use tracing::{debug, error, trace, warn};
|
||||
use ssh_key::Algorithm;
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
|
||||
use super::error::SignKeyError;
|
||||
use super::{AuthError, Context};
|
||||
use crate::ca;
|
||||
|
||||
/// Response type for GET /user/openid-config
|
||||
///
|
||||
|
@ -116,6 +121,86 @@ pub(super) async fn get_oidc_config(
|
|||
Json(res)
|
||||
}
|
||||
|
||||
/// User SSH key signing request payload
|
||||
#[derive(Default)]
|
||||
struct SignKeyRequest {
|
||||
/// Public keys to sign
|
||||
pubkey: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Handler for user certificate signing requests
|
||||
///
|
||||
/// An SSH user certificate will be signed for each submitted public
|
||||
/// key. The valid principals on the certificates will be taken from
|
||||
/// the OpenID Connect Identity Token in the Authorization header, via
|
||||
/// the `sub`, `perferred_username`, and `email` claims (if present).
|
||||
pub(super) async fn sign_user_cert(
|
||||
Claims(claims): Claims,
|
||||
State(ctx): State<super::State>,
|
||||
mut form: Multipart,
|
||||
) -> Result<String, SignKeyError> {
|
||||
let username = claims.subject().as_str();
|
||||
let mut body = SignKeyRequest::default();
|
||||
|
||||
while let Some(field) = form.next_field().await? {
|
||||
match field.name() {
|
||||
Some("pubkey") => {
|
||||
body.pubkey = field.bytes().await?.into();
|
||||
}
|
||||
Some(n) => {
|
||||
warn!("Client request included unsupported field {:?}", n);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
if body.pubkey.is_empty() {
|
||||
return Err(SignKeyError::NoKey);
|
||||
}
|
||||
let mut alias = vec![];
|
||||
if let Some(username) = claims.preferred_username() {
|
||||
alias.push(username.as_str());
|
||||
}
|
||||
if let Some(email) = claims.email() {
|
||||
if claims.email_verified() == Some(true) {
|
||||
alias.push(email.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
let config = &ctx.config;
|
||||
let duration = Duration::from_secs(config.ca.user.cert_duration);
|
||||
let privkey = ca::load_private_key(
|
||||
&config.ca.user.private_key_file,
|
||||
config.ca.user.private_key_passphrase_file.as_ref(),
|
||||
)
|
||||
.await
|
||||
.map_err(SignKeyError::LoadPrivateKey)?;
|
||||
|
||||
let pubkey = ca::parse_public_key(&body.pubkey)
|
||||
.map_err(SignKeyError::ParsePublicKey)?;
|
||||
match pubkey.algorithm() {
|
||||
Algorithm::Ecdsa { .. } => (),
|
||||
Algorithm::Ed25519 => (),
|
||||
Algorithm::Rsa { .. } => (),
|
||||
_ => {
|
||||
return Err(SignKeyError::UnsupportedAlgorithm(
|
||||
pubkey.algorithm().as_str().into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
debug!(
|
||||
"Signing {} key for {}",
|
||||
pubkey.algorithm().as_str(),
|
||||
username
|
||||
);
|
||||
let cert = ca::sign_user_cert(username, &pubkey, duration, &privkey, &alias)?;
|
||||
info!(
|
||||
"Signed {} key for {}",
|
||||
pubkey.algorithm().as_str(),
|
||||
username
|
||||
);
|
||||
Ok(cert.to_openssh().map_err(ca::CertError::from)?)
|
||||
}
|
||||
|
||||
/// Get OIDC provider metadata (possibly from cache)
|
||||
///
|
||||
/// This function will return metadata for the configured OIDC identity
|
||||
|
|
Loading…
Reference in New Issue