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 config: crate::config::StarConfig,
168}
169
170impl StarManager {
171 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 #[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 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 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 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 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 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 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 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 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 pub fn active_order_count(&self) -> usize {
410 self.orders
411 .iter()
412 .filter(|e| e.status == StarOrderStatus::Active)
413 .count()
414 }
415
416 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 true
446 }
447 Some(cert) => {
448 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 pub fn get_order(&self, star_id: &str) -> Option<StarOrder> {
476 self.orders.get(star_id).map(|entry| entry.clone())
477 }
478}
479
480mod 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}