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