est-ca 0.2.0

RFC 7030 Enrollment over Secure Transport (EST) — client, server, and an internal X.509 CA in pure Rust.
//! RFC 7030 EST server: [`EstServer`] builds an [`axum::Router`] that
//! serves `/cacerts`, `/simpleenroll`, and `/simplereenroll`.
//!
//! # Authentication
//!
//! Bootstrap enrollment is authenticated by an [`AuthBackend`]
//! implementation (typically HTTP Basic, verified in constant time).
//!
//! # Renewal (`/simplereenroll`)
//!
//! The renewal route has **no in-crate auth** — the consumer is
//! expected to put a TLS-terminating layer (or a reverse proxy with
//! mTLS) in front of this router and inject the verified client-cert
//! Common Name into the request via axum's request extensions:
//!
//! ```ignore
//! use axum::middleware;
//! use est_ca::auth::Principal;
//! let router = EstServer::new(issuer, auth).router()
//!     .layer(middleware::from_fn(|mut req: axum::http::Request<_>, next| async move {
//!         if let Some(cn) = extract_peer_cert_cn(&req) {
//!             req.extensions_mut().insert(Principal::new(cn));
//!         }
//!         next.run(req).await
//!     }));
//! ```
//!
//! Request extensions are in-process Rust state that cannot be set by
//! an HTTP client, so a consumer that misconfigures their TLS layer
//! fails closed (requests land without a `Principal`, the handler
//! rejects with `401`).

mod handlers;

use std::sync::Arc;

use axum::routing::{get, post};
use axum::Router;

use crate::auth::AuthBackend;
use crate::ca::Issuer;
use crate::est::WELL_KNOWN_PREFIX;

/// Shared server state passed to every EST handler.
#[derive(Clone)]
pub struct EstServer {
    pub(crate) issuer: Issuer,
    pub(crate) auth: Arc<dyn AuthBackend>,
}

impl EstServer {
    /// Construct a server from an [`Issuer`] and an [`AuthBackend`].
    pub fn new(issuer: Issuer, auth: Arc<dyn AuthBackend>) -> Self {
        Self { issuer, auth }
    }

    /// Build the axum [`Router`] serving the EST endpoints under
    /// [`WELL_KNOWN_PREFIX`].
    pub fn router(self) -> Router {
        Router::new()
            .route(&format!("{WELL_KNOWN_PREFIX}/cacerts"), get(handlers::cacerts))
            .route(&format!("{WELL_KNOWN_PREFIX}/simpleenroll"), post(handlers::simpleenroll))
            .route(
                &format!("{WELL_KNOWN_PREFIX}/simplereenroll"),
                post(handlers::simplereenroll),
            )
            .with_state(self)
    }
}