1pub 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#[derive(Debug, Error)]
25pub enum StarError {
26 #[error("STAR order not found: {0}")]
28 OrderNotFound(String),
29
30 #[error("STAR order cancelled: {0}")]
34 OrderCancelled(String),
35
36 #[error("STAR order expired: {0}")]
38 OrderExpired(String),
39
40 #[error("STAR order {order_id} reached maximum renewals ({max})")]
44 MaxRenewalsReached { order_id: String, max: u32 },
45
46 #[error("maximum active STAR orders reached ({limit})")]
50 MaxOrdersReached { limit: usize },
51
52 #[error("invalid renewal interval: {requested}s (allowed {min}s–{max}s)")]
56 InvalidInterval { requested: u64, min: u64, max: u64 },
57
58 #[error("issuance error: {0}")]
60 IssuanceError(String),
61
62 #[error("database error: {0}")]
64 DatabaseError(String),
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct StarCertificate {
74 pub certificate_der: Vec<u8>,
76 pub serial_number: String,
78 pub not_before: DateTime<Utc>,
80 pub not_after: DateTime<Utc>,
82 pub renewal_number: u32,
84 pub star_order_id: String,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
92pub enum StarOrderStatus {
93 Active,
95 Cancelled,
97 Completed,
99 Expired,
101}
102
103impl StarOrderStatus {
104 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#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct StarOrder {
121 pub id: String,
123 pub subject_dn: String,
125 pub key_type: String,
127 pub profile: String,
129 #[serde(with = "humantime_serde")]
133 pub renewal_interval: Duration,
134 pub lifetime_end: DateTime<Utc>,
138 pub max_renewals: u32,
140 pub current_renewals: u32,
142 pub status: StarOrderStatus,
144 pub requestor_dn: Option<String>,
146 pub ca_id: String,
148 pub csr_der: Vec<u8>,
150 pub current_certificate: Option<StarCertificate>,
152 pub created_at: DateTime<Utc>,
154 pub cancelled_at: Option<DateTime<Utc>>,
156}
157
158pub struct StarManager {
164 orders: DashMap<String, StarOrder>,
166 operation_lock: tokio::sync::Mutex<()>,
167 config: crate::config::StarConfig,
169}
170
171impl StarManager {
172 pub async fn operation_guard(&self) -> tokio::sync::MutexGuard<'_, ()> {
174 self.operation_lock.lock().await
175 }
176
177 pub async fn restore(&self, db: &sqlx::AnyPool) -> Result<(), StarError> {
179 use sqlx::Row;
180 let rows = sqlx::query("SELECT id, subject_dn, key_type, profile, renewal_interval_secs, lifetime_end, max_renewals, current_renewals, CASE WHEN status = 'active' THEN 'active' WHEN status = 'completed' THEN 'completed' WHEN status = 'cancelled' THEN 'cancelled' ELSE 'expired' END AS status, requestor_dn, ca_id, csr_der, created_at, cancelled_at FROM star_orders").fetch_all(db).await.map_err(|e| StarError::DatabaseError(e.to_string()))?;
181 let parse = |s: String| {
182 DateTime::parse_from_rfc3339(&s)
183 .map(|v| v.with_timezone(&Utc))
184 .map_err(|e| StarError::DatabaseError(e.to_string()))
185 };
186 let mut restored = Vec::new();
187 for row in rows {
188 let get = |key: &str| {
189 row.try_get::<String, _>(key)
190 .map_err(|e| StarError::DatabaseError(e.to_string()))
191 };
192 let num = |key: &str| {
193 row.try_get::<i64, _>(key)
194 .map_err(|e| StarError::DatabaseError(e.to_string()))
195 };
196 let id = get("id")?;
197 let status = match get("status")?.as_str() {
198 "active" => StarOrderStatus::Active,
199 "completed" => StarOrderStatus::Completed,
200 "cancelled" => StarOrderStatus::Cancelled,
201 "expired" => StarOrderStatus::Expired,
202 _ => {
203 return Err(StarError::DatabaseError(
204 "invalid stored order status".into(),
205 ));
206 }
207 };
208 let latest = sqlx::query(crate::db::pg_sql("SELECT serial_number, certificate_der, not_before, not_after, renewal_number FROM star_certificates WHERE star_order_id = ? ORDER BY renewal_number DESC LIMIT 1")).bind(&id).fetch_optional(db).await.map_err(|e| StarError::DatabaseError(e.to_string()))?;
209 let certificate = if let Some(c) = latest {
210 Some(StarCertificate {
211 serial_number: c
212 .try_get("serial_number")
213 .map_err(|e| StarError::DatabaseError(e.to_string()))?,
214 certificate_der: c
215 .try_get("certificate_der")
216 .map_err(|e| StarError::DatabaseError(e.to_string()))?,
217 not_before: parse(
218 c.try_get("not_before")
219 .map_err(|e| StarError::DatabaseError(e.to_string()))?,
220 )?,
221 not_after: parse(
222 c.try_get("not_after")
223 .map_err(|e| StarError::DatabaseError(e.to_string()))?,
224 )?,
225 renewal_number: c
226 .try_get::<i64, _>("renewal_number")
227 .map_err(|e| StarError::DatabaseError(e.to_string()))?
228 as u32,
229 star_order_id: id.clone(),
230 })
231 } else {
232 None
233 };
234 let cancelled: Option<String> = row
235 .try_get("cancelled_at")
236 .map_err(|e| StarError::DatabaseError(e.to_string()))?;
237 restored.push(StarOrder {
238 id,
239 subject_dn: get("subject_dn")?,
240 key_type: get("key_type")?,
241 profile: get("profile")?,
242 renewal_interval: Duration::from_secs(num("renewal_interval_secs")? as u64),
243 lifetime_end: parse(get("lifetime_end")?)?,
244 max_renewals: num("max_renewals")? as u32,
245 current_renewals: num("current_renewals")? as u32,
246 status,
247 requestor_dn: row
248 .try_get("requestor_dn")
249 .map_err(|e| StarError::DatabaseError(e.to_string()))?,
250 ca_id: get("ca_id")?,
251 csr_der: row
252 .try_get("csr_der")
253 .map_err(|e| StarError::DatabaseError(e.to_string()))?,
254 created_at: parse(get("created_at")?)?,
255 cancelled_at: cancelled.map(parse).transpose()?,
256 current_certificate: certificate,
257 });
258 }
259 self.orders.clear();
260 for mut order in restored {
261 if order.status == StarOrderStatus::Completed
262 && order.lifetime_end > Utc::now()
263 && order
264 .current_certificate
265 .as_ref()
266 .is_none_or(|cert| cert.not_after.timestamp() < order.lifetime_end.timestamp())
267 {
268 sqlx::query(crate::db::pg_sql("UPDATE star_orders SET status = 'active' WHERE id = ? AND status = 'completed'"))
269 .bind(&order.id).execute(db).await.map_err(|e| StarError::DatabaseError(e.to_string()))?;
270 order.status = StarOrderStatus::Active;
271 }
272 self.orders.insert(order.id.clone(), order);
273 }
274 self.cleanup_expired();
275 Ok(())
276 }
277
278 pub fn remove_order(&self, id: &str) {
280 self.orders.remove(id);
281 }
282
283 pub fn new(config: crate::config::StarConfig) -> Self {
285 info!(
286 min_interval = config.min_renewal_interval_secs,
287 max_interval = config.max_renewal_interval_secs,
288 max_orders = config.max_active_orders,
289 pre_renewal = config.pre_renewal_factor,
290 "STAR manager initialised"
291 );
292 Self {
293 orders: DashMap::new(),
294 operation_lock: tokio::sync::Mutex::new(()),
295 config,
296 }
297 }
298
299 #[allow(clippy::too_many_arguments)]
308 pub fn create_order(
309 &self,
310 subject_dn: String,
311 key_type: String,
312 profile: String,
313 renewal_interval_secs: u64,
314 lifetime_days: u32,
315 ca_id: String,
316 csr_der: Vec<u8>,
317 requestor_dn: Option<String>,
318 ) -> Result<StarOrder, StarError> {
319 if lifetime_days == 0 || lifetime_days > self.config.max_lifetime_days {
320 return Err(StarError::IssuanceError(
321 "STAR lifetime is outside configured bounds".into(),
322 ));
323 }
324 if renewal_interval_secs < self.config.min_renewal_interval_secs
326 || renewal_interval_secs > self.config.max_renewal_interval_secs
327 {
328 warn!(
329 requested = renewal_interval_secs,
330 min = self.config.min_renewal_interval_secs,
331 max = self.config.max_renewal_interval_secs,
332 "STAR renewal interval out of bounds"
333 );
334 return Err(StarError::InvalidInterval {
335 requested: renewal_interval_secs,
336 min: self.config.min_renewal_interval_secs,
337 max: self.config.max_renewal_interval_secs,
338 });
339 }
340
341 let active_count = self.active_order_count();
343 if active_count >= self.config.max_active_orders {
344 warn!(
345 active = active_count,
346 limit = self.config.max_active_orders,
347 "STAR order limit reached"
348 );
349 return Err(StarError::MaxOrdersReached {
350 limit: self.config.max_active_orders,
351 });
352 }
353
354 let now = Utc::now();
355 let lifetime_end = now + chrono::Duration::days(i64::from(lifetime_days));
356 let total_lifetime_secs = (lifetime_end - now).num_seconds().max(0) as u64;
357 let cadence = ((renewal_interval_secs as f64) * (1.0 - self.config.pre_renewal_factor))
360 .floor()
361 .max(1.0) as u64;
362 let max_renewals = total_lifetime_secs
363 .div_ceil(cadence)
364 .saturating_add(1)
365 .min(u32::MAX as u64) as u32;
366
367 let id = Uuid::new_v4().to_string();
368 let order = StarOrder {
369 id: id.clone(),
370 subject_dn: subject_dn.clone(),
371 key_type,
372 profile,
373 renewal_interval: Duration::from_secs(renewal_interval_secs),
374 lifetime_end,
375 max_renewals,
376 current_renewals: 0,
377 status: StarOrderStatus::Active,
378 requestor_dn,
379 ca_id,
380 csr_der,
381 current_certificate: None,
382 created_at: now,
383 cancelled_at: None,
384 };
385
386 info!(
387 order_id = %id,
388 subject = %subject_dn,
389 interval_secs = renewal_interval_secs,
390 max_renewals = max_renewals,
391 lifetime_end = %lifetime_end,
392 "STAR order created"
393 );
394
395 self.orders.insert(id, order.clone());
396 Ok(order)
397 }
398
399 pub fn get_current_certificate(&self, star_id: &str) -> Result<StarCertificate, StarError> {
404 let order = self
405 .orders
406 .get(star_id)
407 .ok_or_else(|| StarError::OrderNotFound(star_id.to_owned()))?;
408
409 if order.lifetime_end <= Utc::now() {
410 return Err(StarError::OrderExpired(star_id.to_owned()));
411 }
412
413 match order.status {
414 StarOrderStatus::Cancelled => {
415 return Err(StarError::OrderCancelled(star_id.to_owned()));
416 }
417 StarOrderStatus::Expired => {
418 return Err(StarError::OrderExpired(star_id.to_owned()));
419 }
420 StarOrderStatus::Active | StarOrderStatus::Completed => {}
421 }
422
423 order
424 .current_certificate
425 .clone()
426 .ok_or_else(|| StarError::OrderNotFound(star_id.to_owned()))
427 }
428
429 pub fn store_renewed_certificate(
434 &self,
435 star_id: &str,
436 cert: StarCertificate,
437 ) -> Result<(), StarError> {
438 let mut order = self
439 .orders
440 .get_mut(star_id)
441 .ok_or_else(|| StarError::OrderNotFound(star_id.to_owned()))?;
442
443 if order.status == StarOrderStatus::Cancelled {
444 return Err(StarError::OrderCancelled(star_id.to_owned()));
445 }
446 if order.status == StarOrderStatus::Expired {
447 return Err(StarError::OrderExpired(star_id.to_owned()));
448 }
449
450 order.current_renewals += 1;
451 let renewal_num = order.current_renewals;
452
453 debug!(
454 order_id = %star_id,
455 renewal = renewal_num,
456 serial = %cert.serial_number,
457 not_after = %cert.not_after,
458 "stored renewed STAR certificate"
459 );
460
461 let covers_lifetime = cert.not_after.timestamp() >= order.lifetime_end.timestamp();
462 order.current_certificate = Some(cert);
463
464 if covers_lifetime {
465 info!(
466 order_id = %star_id,
467 renewals = order.current_renewals,
468 "STAR order completed (certificate covers lifetime)"
469 );
470 order.status = StarOrderStatus::Completed;
471 }
472
473 Ok(())
474 }
475
476 pub fn cancel_order(&self, star_id: &str) -> Result<(), StarError> {
481 let mut order = self
482 .orders
483 .get_mut(star_id)
484 .ok_or_else(|| StarError::OrderNotFound(star_id.to_owned()))?;
485
486 if order.status != StarOrderStatus::Active {
487 warn!(
488 order_id = %star_id,
489 status = order.status.as_str(),
490 "cannot cancel non-active STAR order"
491 );
492 if order.status == StarOrderStatus::Cancelled {
494 return Ok(());
495 }
496 }
497
498 info!(order_id = %star_id, "STAR order cancelled");
499 order.status = StarOrderStatus::Cancelled;
500 order.cancelled_at = Some(Utc::now());
501 Ok(())
502 }
503
504 pub fn cleanup_expired(&self) -> usize {
512 let now = Utc::now();
513 let mut expired_ids = Vec::new();
514
515 for entry in self.orders.iter() {
516 if entry.lifetime_end <= now {
517 expired_ids.push(entry.id.clone());
518 }
519 }
520
521 for id in &expired_ids {
522 if let Some(mut order) = self.orders.get_mut(id)
525 && order.status == StarOrderStatus::Active
526 {
527 order.status = StarOrderStatus::Expired;
528 }
529 self.orders.remove(id);
530 }
531
532 let count = expired_ids.len();
533 if count > 0 {
534 info!(count, "cleaned up expired STAR orders");
535 }
536 count
537 }
538
539 pub fn active_order_count(&self) -> usize {
541 self.orders
542 .iter()
543 .filter(|e| e.status == StarOrderStatus::Active)
544 .count()
545 }
546
547 pub fn orders_needing_renewal(&self) -> Vec<String> {
559 self.orders_needing_renewal_at(Utc::now())
560 }
561
562 fn orders_needing_renewal_at(&self, now: DateTime<Utc>) -> Vec<String> {
563 let factor = self.config.pre_renewal_factor;
564 let mut needs_renewal = Vec::new();
565
566 for entry in self.orders.iter() {
567 let order = entry.value();
568
569 if order.status != StarOrderStatus::Active || order.lifetime_end <= now {
570 continue;
571 }
572
573 let should_renew = match &order.current_certificate {
574 None => {
575 true
577 }
578 Some(cert) => {
579 let interval_secs = order.renewal_interval.as_secs() as f64;
582 let pre_renewal_secs = (interval_secs * factor) as i64;
583 let renewal_deadline =
584 cert.not_after - chrono::Duration::seconds(pre_renewal_secs);
585 now >= renewal_deadline
586 }
587 };
588
589 if should_renew {
590 needs_renewal.push(order.id.clone());
591 }
592 }
593
594 debug!(
595 count = needs_renewal.len(),
596 total_active = self.active_order_count(),
597 "scanned orders needing renewal"
598 );
599
600 needs_renewal
601 }
602
603 pub fn get_order(&self, star_id: &str) -> Option<StarOrder> {
607 self.orders.get(star_id).map(|entry| entry.clone())
608 }
609}
610
611mod humantime_serde {
617 use serde::{self, Deserialize, Deserializer, Serializer};
618 use std::time::Duration;
619
620 pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
621 where
622 S: Serializer,
623 {
624 serializer.serialize_u64(duration.as_secs())
625 }
626
627 pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
628 where
629 D: Deserializer<'de>,
630 {
631 let secs = u64::deserialize(deserializer)?;
632 Ok(Duration::from_secs(secs))
633 }
634}
635
636#[cfg(test)]
637mod review13_tests {
638 use super::*;
639
640 #[test]
641 fn overlapping_renewals_cover_the_entire_order_lifetime() {
642 for factor in [0.1, 0.5, 0.9] {
643 let manager = StarManager::new(crate::config::StarConfig {
644 pre_renewal_factor: factor,
645 ..Default::default()
646 });
647 let order = manager
648 .create_order(
649 "synthetic".into(),
650 "rsa".into(),
651 "{}".into(),
652 3600,
653 1,
654 "default".into(),
655 vec![],
656 None,
657 )
658 .unwrap();
659 let mut start = order.created_at;
660 let mut issued = 0;
661 loop {
662 let end = (start + chrono::Duration::hours(1)).min(order.lifetime_end);
663 issued += 1;
664 manager
665 .store_renewed_certificate(
666 &order.id,
667 StarCertificate {
668 certificate_der: vec![],
669 serial_number: issued.to_string(),
670 not_before: start,
671 not_after: end,
672 renewal_number: issued,
673 star_order_id: order.id.clone(),
674 },
675 )
676 .unwrap();
677 if end >= order.lifetime_end {
678 assert_eq!(
679 manager.get_order(&order.id).unwrap().status,
680 StarOrderStatus::Completed
681 );
682 break;
683 }
684 start = end - chrono::Duration::seconds((3600.0 * factor) as i64);
685 assert_eq!(
686 manager.orders_needing_renewal_at(start),
687 vec![order.id.clone()]
688 );
689 assert!(issued < 300, "renewal horizon must terminate");
690 }
691 assert!(
692 issued > 24,
693 "overlap requires more than 24 hourly certificates"
694 );
695 assert_eq!(
696 manager
697 .get_current_certificate(&order.id)
698 .unwrap()
699 .not_after,
700 order.lifetime_end
701 );
702 }
703 }
704}