Skip to main content

kipuka/
state.rs

1//! Shared application state threaded through axum handlers via `Arc<AppState>`.
2//!
3//! `AppState` is constructed once at startup and cloned (cheaply, via `Arc`)
4//! into every axum handler.  It holds the parsed config, database pools,
5//! per-CA key material, and optional subsystem state (HSM, OTP, audit).
6
7use std::sync::Arc;
8use std::time::Instant;
9
10use indexmap::IndexMap;
11
12use crate::audit::AuditState;
13use crate::config::Config;
14
15/// Top-level application state cloned into every axum handler.
16#[derive(Clone)]
17pub struct AppState {
18    /// Parsed and validated configuration.
19    pub config: Arc<Config>,
20
21    /// Primary database connection pool (read-write).
22    pub db: sqlx::AnyPool,
23
24    /// Read-only database connection pool.
25    ///
26    /// For SQLite WAL mode, this is a `?mode=ro` pool that never acquires
27    /// the write lock, enabling concurrent reads during writes.  For
28    /// PostgreSQL/MariaDB, this is a clone of `db` (MVCC handles
29    /// concurrency natively).
30    pub db_ro: sqlx::AnyPool,
31
32    /// Database backend discriminant (drives `BEGIN IMMEDIATE` for SQLite).
33    pub db_kind: crate::db::DbKind,
34
35    /// All CAs keyed by their `id`, in config declaration order.
36    pub cas: Arc<IndexMap<String, Arc<CaState>>>,
37
38    /// The CA designated as the default for unlabeled EST requests.
39    pub default_ca_id: Arc<String>,
40
41    /// OTP store (present when `[otp]` is enabled).
42    pub otp_store: Option<Arc<kipuka_otp::OtpStore>>,
43
44    /// HSM context (present when `[hsm]` is configured).
45    pub hsm: Option<Arc<kipuka_hsm::HsmContext>>,
46
47    /// Shared audit state (overflow flag, alarm counter).
48    pub audit: Arc<AuditState>,
49
50    /// HA manager for multi-CA failover (present when HA is configured).
51    pub ha_manager: Option<Arc<crate::ha::HaManager>>,
52
53    /// Server-side GSSAPI credential for SPNEGO authentication.
54    ///
55    /// `None` when GSSAPI is not configured.  When present, the auth
56    /// layer uses it to validate `Authorization: Negotiate` tokens.
57    pub gss_cred: Option<Arc<dyn std::any::Any + Send + Sync>>,
58
59    /// STAR certificate manager (present when `[star]` is enabled).
60    ///
61    /// Manages active STAR orders and their renewal state (RFC 8739).
62    pub star_manager: Option<Arc<crate::star::StarManager>>,
63
64    /// Timestamp when the server process started.
65    ///
66    /// Used for uptime reporting in health endpoints and session
67    /// expiry calculations.
68    pub startup_time: Instant,
69}
70
71impl AppState {
72    /// Return the default CA state.
73    ///
74    /// # Panics
75    ///
76    /// Panics if `default_ca_id` is not present in `cas`.  This indicates
77    /// a bug in the startup code — `Config::validate()` ensures the
78    /// default CA exists.
79    pub fn default_ca(&self) -> &Arc<CaState> {
80        self.cas
81            .get(self.default_ca_id.as_str())
82            .expect("default CA always present in cas")
83    }
84
85    /// Look up a CA by its identifier.  Returns `None` for unknown IDs.
86    pub fn get_ca(&self, ca_id: &str) -> Option<&Arc<CaState>> {
87        self.cas.get(ca_id)
88    }
89
90    /// Returns the DER-encoded certificate of the default CA.
91    ///
92    /// Used by the OCSP client (RFC 6960) to build CertID structures for
93    /// revocation checking of client certificates (RHELBU-3536 R21).
94    /// Returns `None` if no default CA is configured or the cert is empty.
95    pub fn default_ca_cert_der(&self) -> Option<Vec<u8>> {
96        let ca = self.default_ca();
97        if ca.cert_der.is_empty() {
98            None
99        } else {
100            Some(ca.cert_der.clone())
101        }
102    }
103
104    /// Record an audit event, logging (but not propagating) any DB error.
105    ///
106    /// Convenience wrapper that bundles the DB pool and audit state so
107    /// call sites only need to pass the event type and detail.
108    pub async fn record_audit_event(&self, event_type: &str, detail: &str) {
109        // Map the string event type to the enum; default to AdminAction
110        // for unrecognised types so we never silently drop events.
111        let audit_type = match event_type {
112            "cacerts" => crate::audit::AuditEventType::EnrollRequest,
113            "simpleenroll_success" | "simpleenroll_deferred" => {
114                crate::audit::AuditEventType::CertIssue
115            }
116            "simplereenroll_success" => crate::audit::AuditEventType::CertReenroll,
117            "fullcmc_success" => crate::audit::AuditEventType::CertIssue,
118            "serverkeygen_success" => crate::audit::AuditEventType::CertIssue,
119            "otp_generated" => crate::audit::AuditEventType::OtpCreate,
120            "otp_revoked" => crate::audit::AuditEventType::OtpRevoke,
121            "otp_auth_failure" => crate::audit::AuditEventType::AuthFailure,
122            "cert_revoked" => crate::audit::AuditEventType::CertRevoke,
123            "star_order_created" | "star_renewal_success" => {
124                crate::audit::AuditEventType::CertIssue
125            }
126            "star_order_cancelled" => crate::audit::AuditEventType::AdminAction,
127            _ => crate::audit::AuditEventType::AdminAction,
128        };
129
130        crate::audit::record(
131            &self.db,
132            &self.audit,
133            crate::audit::AuditEvent::new(audit_type).with_detail(detail),
134        )
135        .await;
136    }
137}
138
139/// Per-CA key material and issuance policy.
140///
141/// One `CaState` is created for each `[[ca]]` config entry at startup.
142/// The signing key and certificate chain are loaded once and shared
143/// across all concurrent handler tasks via `Arc<CaState>`.
144pub struct CaState {
145    /// Unique identifier (matches `CaConfig.id`).
146    pub id: String,
147
148    /// Key type string from config, e.g., `"ec:P-256"` or `"rsa:2048"`.
149    pub key_type: String,
150
151    /// DER-encoded CA certificate.
152    pub cert_der: Vec<u8>,
153
154    /// Full certificate chain (CA cert + intermediates) as DER blobs.
155    ///
156    /// Used for the `/cacerts` EST endpoint (RFC 7030 §4.1).
157    pub cert_chain: Vec<Vec<u8>>,
158
159    /// Hash algorithm string, e.g., `"sha256"`.
160    pub hash_algorithm: String,
161
162    /// Default validity period for issued certificates.
163    pub validity_days: u32,
164
165    /// Optional CRL distribution point URL.
166    pub crl_url: Option<String>,
167
168    /// Optional OCSP responder URL.
169    pub ocsp_url: Option<String>,
170
171    /// In-memory CRL cache: DER bytes + expiry instant.
172    ///
173    /// Populated lazily on the first CRL request; invalidated after
174    /// revocation events.
175    pub crl_cache: parking_lot::Mutex<Option<(Vec<u8>, std::time::Instant)>>,
176
177    /// CA/B Forum compliance enforcement.
178    pub cab_forum_compliant: bool,
179}
180
181/// Builder for constructing `AppState` during server startup.
182///
183/// Each setter returns `&mut Self` for chaining.  Call [`build`](`AppStateBuilder::build`)
184/// to produce the final `AppState`.
185pub struct AppStateBuilder {
186    config: Option<Arc<Config>>,
187    db: Option<sqlx::AnyPool>,
188    db_ro: Option<sqlx::AnyPool>,
189    db_kind: Option<crate::db::DbKind>,
190    cas: Option<Arc<IndexMap<String, Arc<CaState>>>>,
191    default_ca_id: Option<Arc<String>>,
192    otp_store: Option<Arc<kipuka_otp::OtpStore>>,
193    hsm: Option<Arc<kipuka_hsm::HsmContext>>,
194    audit: Option<Arc<AuditState>>,
195    ha_manager: Option<Arc<crate::ha::HaManager>>,
196    gss_cred: Option<Arc<dyn std::any::Any + Send + Sync>>,
197    star_manager: Option<Arc<crate::star::StarManager>>,
198}
199
200impl AppStateBuilder {
201    /// Create a new empty builder.
202    pub fn new() -> Self {
203        Self {
204            config: None,
205            db: None,
206            db_ro: None,
207            db_kind: None,
208            cas: None,
209            default_ca_id: None,
210            otp_store: None,
211            hsm: None,
212            audit: None,
213            ha_manager: None,
214            gss_cred: None,
215            star_manager: None,
216        }
217    }
218
219    pub fn config(mut self, config: Arc<Config>) -> Self {
220        self.config = Some(config);
221        self
222    }
223
224    pub fn db(mut self, pool: sqlx::AnyPool) -> Self {
225        self.db = Some(pool);
226        self
227    }
228
229    pub fn db_ro(mut self, pool: sqlx::AnyPool) -> Self {
230        self.db_ro = Some(pool);
231        self
232    }
233
234    pub fn db_kind(mut self, kind: crate::db::DbKind) -> Self {
235        self.db_kind = Some(kind);
236        self
237    }
238
239    pub fn cas(mut self, cas: IndexMap<String, Arc<CaState>>) -> Self {
240        self.cas = Some(Arc::new(cas));
241        self
242    }
243
244    pub fn default_ca_id(mut self, id: String) -> Self {
245        self.default_ca_id = Some(Arc::new(id));
246        self
247    }
248
249    pub fn otp_store(mut self, store: Arc<kipuka_otp::OtpStore>) -> Self {
250        self.otp_store = Some(store);
251        self
252    }
253
254    pub fn hsm(mut self, ctx: Arc<kipuka_hsm::HsmContext>) -> Self {
255        self.hsm = Some(ctx);
256        self
257    }
258
259    pub fn audit(mut self, state: Arc<AuditState>) -> Self {
260        self.audit = Some(state);
261        self
262    }
263
264    pub fn ha_manager(mut self, manager: Arc<crate::ha::HaManager>) -> Self {
265        self.ha_manager = Some(manager);
266        self
267    }
268
269    pub fn gss_cred(mut self, cred: Arc<dyn std::any::Any + Send + Sync>) -> Self {
270        self.gss_cred = Some(cred);
271        self
272    }
273
274    pub fn star_manager(mut self, manager: Arc<crate::star::StarManager>) -> Self {
275        self.star_manager = Some(manager);
276        self
277    }
278
279    /// Build the final `AppState`.
280    ///
281    /// # Panics
282    ///
283    /// Panics if required fields (`config`, `db`, `db_kind`, `cas`,
284    /// `default_ca_id`, `audit`) are not set.
285    pub fn build(self) -> AppState {
286        let db = self.db.expect("db is required");
287        let db_ro = self.db_ro.unwrap_or_else(|| db.clone());
288
289        AppState {
290            config: self.config.expect("config is required"),
291            db,
292            db_ro,
293            db_kind: self.db_kind.expect("db_kind is required"),
294            cas: self.cas.expect("cas is required"),
295            default_ca_id: self.default_ca_id.expect("default_ca_id is required"),
296            otp_store: self.otp_store,
297            hsm: self.hsm,
298            audit: self.audit.expect("audit is required"),
299            ha_manager: self.ha_manager,
300            gss_cred: self.gss_cred,
301            star_manager: self.star_manager,
302            startup_time: Instant::now(),
303        }
304    }
305}
306
307impl Default for AppStateBuilder {
308    fn default() -> Self {
309        Self::new()
310    }
311}