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}