Skip to main content

kipuka/star/
mod.rs

1//! STAR (Short-Term Automatic Renewal) certificate management (RFC 8739).
2//!
3//! STAR issues short-lived certificates that auto-renew without client
4//! interaction.  The server pre-generates renewal certificates before the
5//! current one expires, so clients just fetch the latest.
6//!
7//! This is the server-side answer to the CA/B Forum 47-day certificate
8//! validity mandate taking effect March 2029.
9
10pub mod renewal;
11
12use std::time::Duration;
13
14use chrono::{DateTime, Utc};
15use dashmap::DashMap;
16use serde::{Deserialize, Serialize};
17use thiserror::Error;
18use tracing::{debug, info, warn};
19use uuid::Uuid;
20
21/// Errors from STAR order and certificate operations.
22///
23/// Maps to the problem document types defined in RFC 8739 §3.4.
24#[derive(Debug, Error)]
25pub enum StarError {
26    /// The requested STAR order does not exist.
27    #[error("STAR order not found: {0}")]
28    OrderNotFound(String),
29
30    /// The STAR order has been cancelled by the subscriber or IdO.
31    ///
32    /// RFC 8739 §3.1.1: a cancelled order MUST NOT issue further certificates.
33    #[error("STAR order cancelled: {0}")]
34    OrderCancelled(String),
35
36    /// The STAR order has exceeded its lifetime window.
37    #[error("STAR order expired: {0}")]
38    OrderExpired(String),
39
40    /// The maximum number of renewals for this order has been reached.
41    ///
42    /// RFC 8739 §3.1: `auto-renewal-end-date` determines when renewals stop.
43    #[error("STAR order {order_id} reached maximum renewals ({max})")]
44    MaxRenewalsReached { order_id: String, max: u32 },
45
46    /// The server has reached its maximum number of active STAR orders.
47    ///
48    /// This is a resource-exhaustion guard configured via `[star].max_active_orders`.
49    #[error("maximum active STAR orders reached ({limit})")]
50    MaxOrdersReached { limit: usize },
51
52    /// The requested renewal interval is outside the configured bounds.
53    ///
54    /// RFC 8739 §3.1: the server advertises acceptable interval ranges.
55    #[error("invalid renewal interval: {requested}s (allowed {min}s–{max}s)")]
56    InvalidInterval { requested: u64, min: u64, max: u64 },
57
58    /// Certificate issuance failed during renewal.
59    #[error("issuance error: {0}")]
60    IssuanceError(String),
61
62    /// Database or storage operation failed.
63    #[error("database error: {0}")]
64    DatabaseError(String),
65}
66
67/// A certificate issued as part of a STAR renewal cycle.
68///
69/// Each renewal produces a new `StarCertificate` that replaces the previous
70/// one.  Clients fetch the latest via the STAR certificate URL
71/// (RFC 8739 §3.3).
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct StarCertificate {
74    /// DER-encoded X.509 certificate (current renewal).
75    pub certificate_der: Vec<u8>,
76    /// Serial number of this certificate (hex string).
77    pub serial_number: String,
78    /// Validity start (Not Before).
79    pub not_before: DateTime<Utc>,
80    /// Validity end (Not After).
81    pub not_after: DateTime<Utc>,
82    /// Which renewal produced this certificate (0 = initial).
83    pub renewal_number: u32,
84    /// Parent STAR order identifier.
85    pub star_order_id: String,
86}
87
88/// Lifecycle status of a STAR order.
89///
90/// RFC 8739 §3.1.1 defines the order state machine.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
92pub enum StarOrderStatus {
93    /// The order is actively renewing certificates.
94    Active,
95    /// The subscriber or IdO cancelled the order; no further renewals.
96    Cancelled,
97    /// All scheduled renewals have been issued (`max_renewals` reached).
98    Completed,
99    /// The order's `lifetime_end` has passed.
100    Expired,
101}
102
103impl StarOrderStatus {
104    /// Return a lowercase string representation for logging and API responses.
105    pub fn as_str(&self) -> &'static str {
106        match self {
107            Self::Active => "active",
108            Self::Cancelled => "cancelled",
109            Self::Completed => "completed",
110            Self::Expired => "expired",
111        }
112    }
113}
114
115/// A STAR order representing a recurring certificate renewal agreement.
116///
117/// RFC 8739 §3.1: the order captures the renewal interval, total lifetime,
118/// subject identity, and the CA responsible for issuance.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct StarOrder {
121    /// Unique order identifier (UUID v4).
122    pub id: String,
123    /// Subject distinguished name for issued certificates.
124    pub subject_dn: String,
125    /// Key algorithm, e.g., `"ec:P-256"` or `"rsa:2048"`.
126    pub key_type: String,
127    /// Enrollment profile name (maps to `EnrollmentProfile`).
128    pub profile: String,
129    /// How often to renew the certificate.
130    ///
131    /// RFC 8739 §3.1: `auto-renewal-lifetime` in the order resource.
132    #[serde(with = "humantime_serde")]
133    pub renewal_interval: Duration,
134    /// When the STAR order expires and no more renewals are issued.
135    ///
136    /// RFC 8739 §3.1: `auto-renewal-end-date`.
137    pub lifetime_end: DateTime<Utc>,
138    /// Maximum number of certificates this order will produce.
139    pub max_renewals: u32,
140    /// How many renewals have been issued so far.
141    pub current_renewals: u32,
142    /// Current lifecycle status.
143    pub status: StarOrderStatus,
144    /// DN of the entity that requested this order (from TLS client cert).
145    pub requestor_dn: Option<String>,
146    /// CA identifier that signs renewal certificates.
147    pub ca_id: String,
148    /// DER-encoded PKCS#10 CSR used for each renewal.
149    pub csr_der: Vec<u8>,
150    /// Most recently issued certificate (if any).
151    pub current_certificate: Option<StarCertificate>,
152    /// When the order was created.
153    pub created_at: DateTime<Utc>,
154    /// When the order was cancelled (only set for `Cancelled` status).
155    pub cancelled_at: Option<DateTime<Utc>>,
156}
157
158/// Manages STAR orders and certificate renewal state.
159///
160/// `StarManager` is the server-side implementation of the STAR protocol
161/// (RFC 8739).  It tracks active orders in a concurrent map and provides
162/// methods for the renewal loop to query which orders need new certificates.
163pub struct StarManager {
164    /// Active STAR orders keyed by order ID.
165    orders: DashMap<String, StarOrder>,
166    /// STAR subsystem configuration.
167    config: crate::config::StarConfig,
168}
169
170impl StarManager {
171    /// Create a new `StarManager` with the given configuration.
172    pub fn new(config: crate::config::StarConfig) -> Self {
173        info!(
174            min_interval = config.min_renewal_interval_secs,
175            max_interval = config.max_renewal_interval_secs,
176            max_orders = config.max_active_orders,
177            pre_renewal = config.pre_renewal_factor,
178            "STAR manager initialised"
179        );
180        Self {
181            orders: DashMap::new(),
182            config,
183        }
184    }
185
186    /// Create a new STAR order.
187    ///
188    /// Validates the renewal interval against configured bounds, checks the
189    /// active-order limit, and computes the total number of renewals from
190    /// the requested lifetime.
191    ///
192    /// RFC 8739 §3.1: the server MUST validate `auto-renewal-lifetime` and
193    /// `auto-renewal-end-date` against its policy before accepting the order.
194    #[allow(clippy::too_many_arguments)]
195    pub fn create_order(
196        &self,
197        subject_dn: String,
198        key_type: String,
199        profile: String,
200        renewal_interval_secs: u64,
201        lifetime_days: u32,
202        ca_id: String,
203        csr_der: Vec<u8>,
204        requestor_dn: Option<String>,
205    ) -> Result<StarOrder, StarError> {
206        // Validate renewal interval against configured bounds.
207        if renewal_interval_secs < self.config.min_renewal_interval_secs
208            || renewal_interval_secs > self.config.max_renewal_interval_secs
209        {
210            warn!(
211                requested = renewal_interval_secs,
212                min = self.config.min_renewal_interval_secs,
213                max = self.config.max_renewal_interval_secs,
214                "STAR renewal interval out of bounds"
215            );
216            return Err(StarError::InvalidInterval {
217                requested: renewal_interval_secs,
218                min: self.config.min_renewal_interval_secs,
219                max: self.config.max_renewal_interval_secs,
220            });
221        }
222
223        // Check resource-exhaustion limit.
224        let active_count = self.active_order_count();
225        if active_count >= self.config.max_active_orders {
226            warn!(
227                active = active_count,
228                limit = self.config.max_active_orders,
229                "STAR order limit reached"
230            );
231            return Err(StarError::MaxOrdersReached {
232                limit: self.config.max_active_orders,
233            });
234        }
235
236        let now = Utc::now();
237        let lifetime_end = now + chrono::Duration::days(i64::from(lifetime_days));
238        let total_lifetime_secs = (lifetime_end - now).num_seconds().max(0) as u64;
239        let max_renewals = (total_lifetime_secs / renewal_interval_secs) as u32;
240
241        let id = Uuid::new_v4().to_string();
242        let order = StarOrder {
243            id: id.clone(),
244            subject_dn: subject_dn.clone(),
245            key_type,
246            profile,
247            renewal_interval: Duration::from_secs(renewal_interval_secs),
248            lifetime_end,
249            max_renewals,
250            current_renewals: 0,
251            status: StarOrderStatus::Active,
252            requestor_dn,
253            ca_id,
254            csr_der,
255            current_certificate: None,
256            created_at: now,
257            cancelled_at: None,
258        };
259
260        info!(
261            order_id = %id,
262            subject = %subject_dn,
263            interval_secs = renewal_interval_secs,
264            max_renewals = max_renewals,
265            lifetime_end = %lifetime_end,
266            "STAR order created"
267        );
268
269        self.orders.insert(id, order.clone());
270        Ok(order)
271    }
272
273    /// Retrieve the current certificate for a STAR order.
274    ///
275    /// RFC 8739 §3.3: clients GET the STAR certificate URL to obtain
276    /// the latest renewal.
277    pub fn get_current_certificate(&self, star_id: &str) -> Result<StarCertificate, StarError> {
278        let order = self
279            .orders
280            .get(star_id)
281            .ok_or_else(|| StarError::OrderNotFound(star_id.to_owned()))?;
282
283        match order.status {
284            StarOrderStatus::Cancelled => {
285                return Err(StarError::OrderCancelled(star_id.to_owned()));
286            }
287            StarOrderStatus::Expired => {
288                return Err(StarError::OrderExpired(star_id.to_owned()));
289            }
290            StarOrderStatus::Active | StarOrderStatus::Completed => {}
291        }
292
293        order
294            .current_certificate
295            .clone()
296            .ok_or_else(|| StarError::OrderNotFound(star_id.to_owned()))
297    }
298
299    /// Store a newly renewed certificate in the order.
300    ///
301    /// Increments the renewal counter and transitions the order to
302    /// `Completed` if `max_renewals` has been reached.
303    pub fn store_renewed_certificate(
304        &self,
305        star_id: &str,
306        cert: StarCertificate,
307    ) -> Result<(), StarError> {
308        let mut order = self
309            .orders
310            .get_mut(star_id)
311            .ok_or_else(|| StarError::OrderNotFound(star_id.to_owned()))?;
312
313        if order.status == StarOrderStatus::Cancelled {
314            return Err(StarError::OrderCancelled(star_id.to_owned()));
315        }
316        if order.status == StarOrderStatus::Expired {
317            return Err(StarError::OrderExpired(star_id.to_owned()));
318        }
319
320        order.current_renewals += 1;
321        let renewal_num = order.current_renewals;
322
323        debug!(
324            order_id = %star_id,
325            renewal = renewal_num,
326            serial = %cert.serial_number,
327            not_after = %cert.not_after,
328            "stored renewed STAR certificate"
329        );
330
331        order.current_certificate = Some(cert);
332
333        if order.current_renewals >= order.max_renewals {
334            info!(
335                order_id = %star_id,
336                renewals = order.current_renewals,
337                "STAR order completed (max renewals reached)"
338            );
339            order.status = StarOrderStatus::Completed;
340        }
341
342        Ok(())
343    }
344
345    /// Cancel a STAR order.
346    ///
347    /// RFC 8739 §3.1.2: the subscriber or IdO may cancel an active order.
348    /// After cancellation, no further certificates are issued.
349    pub fn cancel_order(&self, star_id: &str) -> Result<(), StarError> {
350        let mut order = self
351            .orders
352            .get_mut(star_id)
353            .ok_or_else(|| StarError::OrderNotFound(star_id.to_owned()))?;
354
355        if order.status != StarOrderStatus::Active {
356            warn!(
357                order_id = %star_id,
358                status = order.status.as_str(),
359                "cannot cancel non-active STAR order"
360            );
361            // Idempotent: cancelling an already-cancelled order is fine.
362            if order.status == StarOrderStatus::Cancelled {
363                return Ok(());
364            }
365        }
366
367        info!(order_id = %star_id, "STAR order cancelled");
368        order.status = StarOrderStatus::Cancelled;
369        order.cancelled_at = Some(Utc::now());
370        Ok(())
371    }
372
373    /// Remove orders whose `lifetime_end` has passed.
374    ///
375    /// Should be called periodically (e.g., from a background task) to
376    /// reclaim memory.  Orders past their lifetime are marked `Expired`
377    /// first, then removed entirely.
378    ///
379    /// Returns the number of orders that were cleaned up.
380    pub fn cleanup_expired(&self) -> usize {
381        let now = Utc::now();
382        let mut expired_ids = Vec::new();
383
384        for entry in self.orders.iter() {
385            if entry.lifetime_end <= now {
386                expired_ids.push(entry.id.clone());
387            }
388        }
389
390        for id in &expired_ids {
391            // Mark expired before removal so any concurrent reader sees the
392            // terminal state rather than a vanished order.
393            if let Some(mut order) = self.orders.get_mut(id)
394                && order.status == StarOrderStatus::Active
395            {
396                order.status = StarOrderStatus::Expired;
397            }
398            self.orders.remove(id);
399        }
400
401        let count = expired_ids.len();
402        if count > 0 {
403            info!(count, "cleaned up expired STAR orders");
404        }
405        count
406    }
407
408    /// Count of currently active (not cancelled/completed/expired) orders.
409    pub fn active_order_count(&self) -> usize {
410        self.orders
411            .iter()
412            .filter(|e| e.status == StarOrderStatus::Active)
413            .count()
414    }
415
416    /// Return order IDs that need a renewal certificate issued.
417    ///
418    /// An order needs renewal when:
419    /// 1. Its status is `Active`.
420    /// 2. It has not exhausted `max_renewals`.
421    /// 3. The current certificate's expiry minus the pre-renewal window
422    ///    is in the past (or no certificate has been issued yet).
423    ///
424    /// The pre-renewal window is `renewal_interval * pre_renewal_factor`.
425    /// For example, with a 24-hour interval and factor 0.5, renewal
426    /// triggers when 12 hours remain on the current certificate.
427    pub fn orders_needing_renewal(&self) -> Vec<String> {
428        let now = Utc::now();
429        let factor = self.config.pre_renewal_factor;
430        let mut needs_renewal = Vec::new();
431
432        for entry in self.orders.iter() {
433            let order = entry.value();
434
435            if order.status != StarOrderStatus::Active {
436                continue;
437            }
438            if order.current_renewals >= order.max_renewals {
439                continue;
440            }
441
442            let should_renew = match &order.current_certificate {
443                None => {
444                    // No certificate issued yet — renew immediately.
445                    true
446                }
447                Some(cert) => {
448                    // Renew when the remaining validity drops below the
449                    // pre-renewal threshold.
450                    let interval_secs = order.renewal_interval.as_secs() as f64;
451                    let pre_renewal_secs = (interval_secs * factor) as i64;
452                    let renewal_deadline =
453                        cert.not_after - chrono::Duration::seconds(pre_renewal_secs);
454                    now >= renewal_deadline
455                }
456            };
457
458            if should_renew {
459                needs_renewal.push(order.id.clone());
460            }
461        }
462
463        debug!(
464            count = needs_renewal.len(),
465            total_active = self.active_order_count(),
466            "scanned orders needing renewal"
467        );
468
469        needs_renewal
470    }
471
472    /// Retrieve a clone of a STAR order by ID.
473    ///
474    /// Returns `None` if the order does not exist.
475    pub fn get_order(&self, star_id: &str) -> Option<StarOrder> {
476        self.orders.get(star_id).map(|entry| entry.clone())
477    }
478}
479
480/// Serde helper for `std::time::Duration` via `humantime`.
481///
482/// Serializes durations as human-readable strings (e.g., "24h", "7d")
483/// and deserializes them back.  Used for the `renewal_interval` field
484/// in `StarOrder`.
485mod humantime_serde {
486    use serde::{self, Deserialize, Deserializer, Serializer};
487    use std::time::Duration;
488
489    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
490    where
491        S: Serializer,
492    {
493        serializer.serialize_u64(duration.as_secs())
494    }
495
496    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
497    where
498        D: Deserializer<'de>,
499    {
500        let secs = u64::deserialize(deserializer)?;
501        Ok(Duration::from_secs(secs))
502    }
503}