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    /// Resolved secrets (never serialized to disk).
22    pub secrets: Arc<crate::config::ResolvedSecrets>,
23
24    /// Primary database connection pool (read-write).
25    pub db: sqlx::AnyPool,
26
27    /// Read-only database connection pool.
28    ///
29    /// For SQLite WAL mode, this is a `?mode=ro` pool that never acquires
30    /// the write lock, enabling concurrent reads during writes.  For
31    /// PostgreSQL/MariaDB, this is a clone of `db` (MVCC handles
32    /// concurrency natively).
33    pub db_ro: sqlx::AnyPool,
34
35    /// Database backend discriminant (drives `BEGIN IMMEDIATE` for SQLite).
36    pub db_kind: crate::db::DbKind,
37
38    /// All CAs keyed by their `id`, in config declaration order.
39    pub cas: Arc<IndexMap<String, Arc<CaState>>>,
40
41    /// The CA designated as the default for unlabeled EST requests.
42    pub default_ca_id: Arc<String>,
43
44    /// OTP store (present when `[otp]` is enabled).
45    pub otp_store: Option<Arc<kipuka_otp::OtpStore>>,
46
47    /// HSM context (present when `[hsm]` is configured).
48    pub hsm: Option<Arc<kipuka_hsm::HsmContext>>,
49
50    /// Shared audit state (overflow flag, alarm counter).
51    pub audit: Arc<AuditState>,
52
53    /// Authentication failure tracker / lockout enforcer (FIA_AFL.1).
54    ///
55    /// Always present; a `max_failures = 0` policy makes every operation a
56    /// no-op so the control can be disabled without a `None` branch at call
57    /// sites.
58    pub failure_tracker: Arc<crate::auth::failure_tracker::FailureTracker>,
59
60    /// HA manager for multi-CA failover (present when HA is configured).
61    pub ha_manager: Option<Arc<crate::ha::HaManager>>,
62
63    /// Server-side GSSAPI credential for SPNEGO authentication.
64    ///
65    /// `None` when GSSAPI is not configured.  When present, the auth
66    /// layer uses it to validate `Authorization: Negotiate` tokens.
67    pub gss_cred: Option<Arc<dyn std::any::Any + Send + Sync>>,
68
69    /// Whether GSSAPI requires cryptographic ticket verification.
70    ///
71    /// When `true` (the default), structural-only parsing of Kerberos
72    /// tokens is rejected — libgssapi integration is required.  When
73    /// `false`, the server accepts structural parsing and returns a
74    /// `krb5-sname:` prefixed identity (the service name from the ticket,
75    /// not a verified client identity).
76    pub gssapi_require_crypto: bool,
77
78    /// Dogtag PKI client pool (present when `[dogtag]` is configured).
79    ///
80    /// Routes enrollment, CMC, and key generation requests to Dogtag CA/KRA
81    /// backends when configured as an alternative to direct signing.
82    pub dogtag: Option<Arc<kipuka_dogtag::DogtagPool>>,
83
84    /// STAR certificate manager (present when `[star]` is enabled).
85    ///
86    /// Manages active STAR orders and their renewal state (RFC 8739).
87    pub star_manager: Option<Arc<crate::star::StarManager>>,
88
89    /// Timestamp when the server process started.
90    ///
91    /// Used for uptime reporting in health endpoints and session
92    /// expiry calculations.
93    pub startup_time: Instant,
94}
95
96impl AppState {
97    /// Return the default CA state.
98    ///
99    /// # Panics
100    ///
101    /// Panics if `default_ca_id` is not present in `cas`.  This indicates
102    /// a bug in the startup code — `Config::validate()` ensures the
103    /// default CA exists.
104    pub fn default_ca(&self) -> &Arc<CaState> {
105        self.cas
106            .get(self.default_ca_id.as_str())
107            .expect("default CA always present in cas")
108    }
109
110    /// Look up a CA by its identifier.  Returns `None` for unknown IDs.
111    pub fn get_ca(&self, ca_id: &str) -> Option<&Arc<CaState>> {
112        self.cas.get(ca_id)
113    }
114
115    /// Returns the DER-encoded certificate of the default CA.
116    ///
117    /// Used by the OCSP client (RFC 6960) to build CertID structures for
118    /// revocation checking of client certificates (RHELBU-3536 R21).
119    /// Returns `None` if no default CA is configured or the cert is empty.
120    pub fn default_ca_cert_der(&self) -> Option<Vec<u8>> {
121        let ca = self.default_ca();
122        if ca.cert_der.is_empty() {
123            None
124        } else {
125            Some(ca.cert_der.clone())
126        }
127    }
128
129    /// Required durable admission event before any certificate/key mutation.
130    pub async fn admit_enrollment(
131        &self,
132        identity: &str,
133        operation: &str,
134    ) -> Result<(), crate::error::KipukaError> {
135        crate::audit::record_checked(
136            &self.db,
137            &self.audit,
138            crate::audit::AuditEvent::new(crate::audit::AuditEventType::EnrollRequest)
139                .with_operator(identity)
140                .with_detail(operation),
141        )
142        .await
143    }
144
145    /// Record an audit event, logging (but not propagating) any DB error.
146    ///
147    /// Convenience wrapper that bundles the DB pool and audit state so
148    /// call sites only need to pass the event type and detail.
149    pub async fn record_audit_event(&self, event_type: &str, detail: &str) {
150        crate::audit::record(
151            &self.db,
152            &self.audit,
153            crate::audit::AuditEvent::new(Self::audit_type_for(event_type)).with_detail(detail),
154        )
155        .await;
156    }
157
158    /// Record an audit event that carries a responsible actor identity.
159    ///
160    /// Populates the `actor` column (via [`AuditEvent::with_operator`]) so the
161    /// FAU_SAR.1 review endpoint can filter by actor.  Used where the
162    /// responsible principal is known — enrollment-authorization denials
163    /// (FDP_ACF.1) and audit-trail review — so those events are attributable
164    /// rather than leaving `actor` NULL and the filter inert.
165    ///
166    /// [`AuditEvent::with_operator`]: crate::audit::AuditEvent::with_operator
167    pub async fn record_audit_event_with_actor(&self, event_type: &str, actor: &str, detail: &str) {
168        crate::audit::record(
169            &self.db,
170            &self.audit,
171            crate::audit::AuditEvent::new(Self::audit_type_for(event_type))
172                .with_operator(actor)
173                .with_detail(detail),
174        )
175        .await;
176    }
177
178    /// Map a string event type to its [`crate::audit::AuditEventType`].
179    ///
180    /// Unrecognised types default to
181    /// [`AdminAction`](crate::audit::AuditEventType::AdminAction) so an event is
182    /// never silently dropped.
183    fn audit_type_for(event_type: &str) -> crate::audit::AuditEventType {
184        match event_type {
185            "cacerts" => crate::audit::AuditEventType::EnrollRequest,
186            "cert_issued" | "simpleenroll_success" | "simpleenroll_deferred" => {
187                crate::audit::AuditEventType::CertIssue
188            }
189            "simplereenroll_success" => crate::audit::AuditEventType::CertReenroll,
190            "fullcmc_success" => crate::audit::AuditEventType::CertIssue,
191            "serverkeygen_success" => crate::audit::AuditEventType::CertIssue,
192            // Enrollment-authorization denials (FDP_ACF.1) across every
193            // transport map to the enroll.reject taxonomy so they are
194            // filterable as a class and not lumped under the admin.action
195            // default.  They are not SecurityViolation: a denial is an access
196            // decision, not an alarm condition, and must not trip FAU_ARP.1.
197            "simpleenroll_denied"
198            | "simplereenroll_denied"
199            | "serverkeygen_denied"
200            | "cms_simpleenroll_denied"
201            | "cms_simplereenroll_denied"
202            | "cms_serverkeygen_denied" => crate::audit::AuditEventType::EnrollReject,
203            "otp_generated" => crate::audit::AuditEventType::OtpCreate,
204            "otp_revoked" => crate::audit::AuditEventType::OtpRevoke,
205            "otp_auth_failure" => crate::audit::AuditEventType::AuthFailure,
206            // FIA_AFL.1 threshold reached — a security-relevant event that also
207            // trips the NIAP alarm counter (FAU_ARP.1) via SecurityViolation.
208            "auth_lockout" => crate::audit::AuditEventType::SecurityViolation,
209            "cert_revoked" => crate::audit::AuditEventType::CertRevoke,
210            "star_order_created" | "star_renewal_success" => {
211                crate::audit::AuditEventType::CertIssue
212            }
213            "star_order_cancelled" | "admin_audit_review" => {
214                crate::audit::AuditEventType::AdminAction
215            }
216            // EST-coaps transport (RFC 9148) — mirror the HTTP taxonomy so the
217            // FAU_SAR.1 review endpoint can filter CoAP events by class rather
218            // than lumping them under the admin.action default.  Denials map to
219            // enroll.reject (an access decision, not a SecurityViolation alarm),
220            // matching the HTTP `*_denied` events above.
221            "coap_cacerts" | "coap_csrattrs" => crate::audit::AuditEventType::EnrollRequest,
222            "coap_simpleenroll" => crate::audit::AuditEventType::CertIssue,
223            "coap_simplereenroll" => crate::audit::AuditEventType::CertReenroll,
224            "coap_simpleenroll_denied" | "coap_simplereenroll_denied" => {
225                crate::audit::AuditEventType::EnrollReject
226            }
227            _ => crate::audit::AuditEventType::AdminAction,
228        }
229    }
230}
231
232/// Per-CA key material and issuance policy.
233///
234/// One `CaState` is created for each `[[ca]]` config entry at startup.
235/// The signing key and certificate chain are loaded once and shared
236/// across all concurrent handler tasks via `Arc<CaState>`.
237pub struct CaState {
238    /// Unique identifier (matches `CaConfig.id`).
239    pub id: String,
240
241    /// Key type string from config, e.g., `"ec:P-256"` or `"rsa:2048"`.
242    pub key_type: String,
243
244    /// DER-encoded CA certificate.
245    pub cert_der: Vec<u8>,
246
247    /// Full certificate chain (CA cert + intermediates) as DER blobs.
248    ///
249    /// Used for the `/cacerts` EST endpoint (RFC 7030 §4.1).
250    pub cert_chain: Vec<Vec<u8>>,
251
252    /// Hash algorithm string, e.g., `"sha256"`.
253    pub hash_algorithm: String,
254
255    /// Default validity period for issued certificates.
256    pub validity_days: u32,
257
258    /// Optional CRL distribution point URL.
259    pub crl_url: Option<String>,
260
261    /// Optional OCSP responder URL.
262    pub ocsp_url: Option<String>,
263
264    /// In-memory CRL cache: DER bytes + expiry instant.
265    ///
266    /// Populated lazily on the first CRL request; invalidated after
267    /// revocation events.
268    pub crl_cache: parking_lot::Mutex<Option<(Vec<u8>, std::time::Instant)>>,
269
270    /// CA/B Forum compliance enforcement.
271    pub cab_forum_compliant: bool,
272}
273
274/// Builder for constructing `AppState` during server startup.
275///
276/// Each setter returns `&mut Self` for chaining.  Call [`build`](`AppStateBuilder::build`)
277/// to produce the final `AppState`.
278pub struct AppStateBuilder {
279    config: Option<Arc<Config>>,
280    secrets: Option<Arc<crate::config::ResolvedSecrets>>,
281    db: Option<sqlx::AnyPool>,
282    db_ro: Option<sqlx::AnyPool>,
283    db_kind: Option<crate::db::DbKind>,
284    cas: Option<Arc<IndexMap<String, Arc<CaState>>>>,
285    default_ca_id: Option<Arc<String>>,
286    otp_store: Option<Arc<kipuka_otp::OtpStore>>,
287    hsm: Option<Arc<kipuka_hsm::HsmContext>>,
288    audit: Option<Arc<AuditState>>,
289    failure_tracker: Option<Arc<crate::auth::failure_tracker::FailureTracker>>,
290    ha_manager: Option<Arc<crate::ha::HaManager>>,
291    gss_cred: Option<Arc<dyn std::any::Any + Send + Sync>>,
292    gssapi_require_crypto: bool,
293    dogtag: Option<Arc<kipuka_dogtag::DogtagPool>>,
294    star_manager: Option<Arc<crate::star::StarManager>>,
295}
296
297impl AppStateBuilder {
298    /// Create a new empty builder.
299    pub fn new() -> Self {
300        Self {
301            config: None,
302            secrets: None,
303            db: None,
304            db_ro: None,
305            db_kind: None,
306            cas: None,
307            default_ca_id: None,
308            otp_store: None,
309            hsm: None,
310            audit: None,
311            failure_tracker: None,
312            ha_manager: None,
313            gss_cred: None,
314            gssapi_require_crypto: true,
315            dogtag: None,
316            star_manager: None,
317        }
318    }
319
320    pub fn config(mut self, config: Arc<Config>) -> Self {
321        self.config = Some(config);
322        self
323    }
324
325    pub fn secrets(mut self, secrets: Arc<crate::config::ResolvedSecrets>) -> Self {
326        self.secrets = Some(secrets);
327        self
328    }
329
330    pub fn db(mut self, pool: sqlx::AnyPool) -> Self {
331        self.db = Some(pool);
332        self
333    }
334
335    pub fn db_ro(mut self, pool: sqlx::AnyPool) -> Self {
336        self.db_ro = Some(pool);
337        self
338    }
339
340    pub fn db_kind(mut self, kind: crate::db::DbKind) -> Self {
341        self.db_kind = Some(kind);
342        self
343    }
344
345    pub fn cas(mut self, cas: IndexMap<String, Arc<CaState>>) -> Self {
346        self.cas = Some(Arc::new(cas));
347        self
348    }
349
350    pub fn default_ca_id(mut self, id: String) -> Self {
351        self.default_ca_id = Some(Arc::new(id));
352        self
353    }
354
355    pub fn otp_store(mut self, store: Arc<kipuka_otp::OtpStore>) -> Self {
356        self.otp_store = Some(store);
357        self
358    }
359
360    pub fn hsm(mut self, ctx: Arc<kipuka_hsm::HsmContext>) -> Self {
361        self.hsm = Some(ctx);
362        self
363    }
364
365    pub fn audit(mut self, state: Arc<AuditState>) -> Self {
366        self.audit = Some(state);
367        self
368    }
369
370    pub fn failure_tracker(
371        mut self,
372        tracker: Arc<crate::auth::failure_tracker::FailureTracker>,
373    ) -> Self {
374        self.failure_tracker = Some(tracker);
375        self
376    }
377
378    pub fn ha_manager(mut self, manager: Arc<crate::ha::HaManager>) -> Self {
379        self.ha_manager = Some(manager);
380        self
381    }
382
383    pub fn gss_cred(mut self, cred: Arc<dyn std::any::Any + Send + Sync>) -> Self {
384        self.gss_cred = Some(cred);
385        self
386    }
387
388    pub fn gssapi_require_crypto(mut self, require: bool) -> Self {
389        self.gssapi_require_crypto = require;
390        self
391    }
392
393    pub fn dogtag(mut self, pool: Arc<kipuka_dogtag::DogtagPool>) -> Self {
394        self.dogtag = Some(pool);
395        self
396    }
397
398    pub fn star_manager(mut self, manager: Arc<crate::star::StarManager>) -> Self {
399        self.star_manager = Some(manager);
400        self
401    }
402
403    /// Build the final `AppState`.
404    ///
405    /// # Panics
406    ///
407    /// Panics if required fields (`config`, `db`, `db_kind`, `cas`,
408    /// `default_ca_id`, `audit`) are not set.
409    pub fn build(self) -> AppState {
410        let db = self.db.expect("db is required");
411        let db_ro = self.db_ro.unwrap_or_else(|| db.clone());
412        let config = self.config.expect("config is required");
413
414        // Derive the failure tracker from `[auth_lockout]` when not supplied.
415        let failure_tracker = self.failure_tracker.unwrap_or_else(|| {
416            Arc::new(crate::auth::failure_tracker::FailureTracker::new(
417                config.auth_lockout.to_policy(),
418            ))
419        });
420
421        AppState {
422            config,
423            secrets: self.secrets.expect("secrets is required"),
424            db,
425            db_ro,
426            db_kind: self.db_kind.expect("db_kind is required"),
427            cas: self.cas.expect("cas is required"),
428            default_ca_id: self.default_ca_id.expect("default_ca_id is required"),
429            otp_store: self.otp_store,
430            hsm: self.hsm,
431            audit: self.audit.expect("audit is required"),
432            failure_tracker,
433            ha_manager: self.ha_manager,
434            gss_cred: self.gss_cred,
435            gssapi_require_crypto: self.gssapi_require_crypto,
436            dogtag: self.dogtag,
437            star_manager: self.star_manager,
438            startup_time: Instant::now(),
439        }
440    }
441}
442
443impl Default for AppStateBuilder {
444    fn default() -> Self {
445        Self::new()
446    }
447}