Skip to main content

kipuka/auth/
mod.rs

1//! Authentication layer for the Kipuka EST server.
2//!
3//! RFC 7030 §3.2.3 defines several client authentication mechanisms for EST:
4//!
5//! - **mTLS** — client presents a certificate during the TLS handshake.
6//! - **HTTP Basic (OTP)** — username=entity-id, password=one-time password.
7//! - **HTTP Negotiate (GSSAPI)** — Kerberos/SPNEGO authentication.
8//!
9//! Each EST endpoint declares an authentication policy ([`AuthPolicy`]) that
10//! the [`EstAuth`] extractor enforces before the handler runs.  Admin routes
11//! use a separate authentication mechanism (see [`super::routes::admin`]).
12
13pub mod cms_auth;
14pub mod gssapi;
15pub mod mtls;
16pub mod name_match;
17pub mod otp;
18
19use std::sync::Arc;
20
21use axum::extract::{FromRef, FromRequestParts};
22use axum::http::request::Parts;
23use axum::response::{IntoResponse, Response};
24
25use crate::error::KipukaError;
26use crate::state::AppState;
27
28/// How a client authenticated to the EST server.
29///
30/// Stored in [`AuthResult`] so handlers can make authorization decisions
31/// based on the authentication method used (e.g., `/simplereenroll`
32/// requires mTLS, `/fullcmc` requires id-kp-cmcRA EKU).
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum AuthMethod {
35    /// mTLS client certificate presented during TLS handshake.
36    Mtls,
37    /// HTTP Basic authentication with a one-time password.
38    Otp,
39    /// GSSAPI/SPNEGO (Kerberos) via the `Authorization: Negotiate` header.
40    Gssapi,
41    /// CMS SignedData authentication (RFC 8295).
42    Cms,
43    /// No authentication (used for unauthenticated endpoints like `/cacerts`).
44    None,
45}
46
47/// Result of a successful authentication.
48///
49/// Contains the authenticated identity, the method used, and any
50/// attributes extracted from the credential (e.g., certificate subject,
51/// SANs, EKU extensions).
52#[derive(Debug, Clone)]
53pub struct AuthResult {
54    /// The authenticated identity string.
55    ///
56    /// For mTLS: the certificate subject DN or SAN.
57    /// For OTP: the entity-id from HTTP Basic username.
58    /// For GSSAPI: the Kerberos principal name.
59    pub identity: String,
60
61    /// How the client authenticated.
62    pub method: AuthMethod,
63
64    /// DER-encoded client certificate (mTLS only).
65    ///
66    /// Available for POP linking validation in `/simpleenroll` and
67    /// `/simplereenroll` handlers.
68    pub client_cert_der: Option<Vec<u8>>,
69
70    /// Subject DN from the client certificate (mTLS only).
71    pub subject_dn: Option<String>,
72
73    /// Subject Alternative Names from the client certificate (mTLS only).
74    pub subject_alt_names: Vec<String>,
75
76    /// Extended Key Usage OIDs from the client certificate (mTLS only).
77    ///
78    /// Used by `/fullcmc` to verify the signer holds id-kp-cmcRA
79    /// (OID 1.3.6.1.5.5.7.3.28) per RHELBU-3536 R15.
80    pub extended_key_usage: Vec<String>,
81}
82
83impl AuthResult {
84    /// Create an unauthenticated result for endpoints that do not require auth.
85    pub fn anonymous() -> Self {
86        Self {
87            identity: String::new(),
88            method: AuthMethod::None,
89            client_cert_der: None,
90            subject_dn: None,
91            subject_alt_names: Vec::new(),
92            extended_key_usage: Vec::new(),
93        }
94    }
95
96    /// Returns `true` if the client certificate carries the id-kp-cmcRA EKU.
97    ///
98    /// OID: 1.3.6.1.5.5.7.3.28 (RFC 6402 §2.10).
99    pub fn has_cmc_ra_eku(&self) -> bool {
100        const CMC_RA_OID: &str = "1.3.6.1.5.5.7.3.28";
101        self.extended_key_usage.iter().any(|oid| oid == CMC_RA_OID)
102    }
103}
104
105/// Authentication policy for an EST endpoint.
106///
107/// Determines which authentication methods are acceptable and whether
108/// authentication is required at all.
109#[derive(Debug, Clone)]
110pub enum AuthPolicy {
111    /// No authentication required (e.g., `/cacerts`, `/csrattrs`).
112    None,
113    /// At least one of the listed methods must succeed.
114    AnyOf(Vec<AuthMethod>),
115    /// A specific method is required (e.g., mTLS for `/simplereenroll`).
116    Required(AuthMethod),
117}
118
119/// Axum extractor that authenticates EST requests.
120///
121/// Tries each configured authentication method in order:
122/// 1. mTLS client certificate (from TLS session extensions)
123/// 2. HTTP Basic (OTP)
124/// 3. GSSAPI/SPNEGO (`Authorization: Negotiate`)
125///
126/// The extractor succeeds if the endpoint's [`AuthPolicy`] is satisfied.
127/// On failure, returns an appropriate HTTP 401/403 response.
128pub struct EstAuth(pub AuthResult);
129
130impl<S> FromRequestParts<S> for EstAuth
131where
132    S: Send + Sync,
133    Arc<AppState>: FromRef<S>,
134{
135    type Rejection = Response;
136
137    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Response> {
138        let app = Arc::<AppState>::from_ref(state);
139
140        // Try mTLS first — the client certificate is available as a request extension
141        // injected by the TLS accept loop.
142        if let Some(auth) = mtls::try_extract_mtls(parts, &app).await {
143            return Ok(EstAuth(auth));
144        }
145
146        // Try HTTP Basic (OTP) authentication.
147        if let Some(result) = otp::try_extract_otp(parts, &app).await {
148            match result {
149                Ok(auth) => return Ok(EstAuth(auth)),
150                Err(e) => return Err(e),
151            }
152        }
153
154        // Try GSSAPI/SPNEGO authentication.
155        if let Some(result) = gssapi::try_extract_gssapi(parts, &app).await {
156            match result {
157                Ok(auth) => return Ok(EstAuth(auth)),
158                Err(e) => return Err(e),
159            }
160        }
161
162        // No authentication method succeeded.
163        Err(KipukaError::Auth("no valid credentials provided".into()).into_response())
164    }
165}
166
167/// Axum extractor that allows unauthenticated access.
168///
169/// Used on endpoints like `/cacerts` and `/csrattrs` that do not require
170/// authentication per RFC 7030 §4.1 and §4.5.  If credentials are
171/// present they are validated; if absent, an anonymous result is returned.
172pub struct OptionalAuth(pub AuthResult);
173
174impl<S> FromRequestParts<S> for OptionalAuth
175where
176    S: Send + Sync,
177    Arc<AppState>: FromRef<S>,
178{
179    type Rejection = Response;
180
181    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Response> {
182        let app = Arc::<AppState>::from_ref(state);
183
184        // Try mTLS — if a client cert is present, validate it.
185        if let Some(auth) = mtls::try_extract_mtls(parts, &app).await {
186            return Ok(OptionalAuth(auth));
187        }
188
189        // No credentials — that is fine for optional-auth endpoints.
190        Ok(OptionalAuth(AuthResult::anonymous()))
191    }
192}