1use std::sync::Arc;
17use std::time::Duration;
18
19use tracing::{debug, error, info, warn};
20
21use crate::audit::{AuditEvent, AuditEventType, AuditState};
22use crate::ca::issue::{self, EnrollmentProfile};
23use crate::config::CaConfig;
24use crate::star::{StarCertificate, StarManager, StarOrderStatus};
25use crate::state::CaState;
26
27pub async fn spawn_renewal_task(
42 star_manager: Arc<StarManager>,
43 db: sqlx::AnyPool,
44 cas: Arc<indexmap::IndexMap<String, Arc<CaState>>>,
45 ca_configs: Arc<Vec<CaConfig>>,
46 hsm: Option<Arc<kipuka_hsm::HsmContext>>,
47 audit: Arc<AuditState>,
48) -> tokio::task::JoinHandle<()> {
49 tokio::spawn(async move {
50 let mut interval = tokio::time::interval(Duration::from_secs(60));
51
52 loop {
53 interval.tick().await;
54 renewal_cycle(&star_manager, &db, &cas, &ca_configs, hsm.as_ref(), &audit).await;
55 }
56 })
57}
58
59async fn renewal_cycle(
61 star_manager: &StarManager,
62 db: &sqlx::AnyPool,
63 cas: &indexmap::IndexMap<String, Arc<CaState>>,
64 ca_configs: &[CaConfig],
65 hsm: Option<&Arc<kipuka_hsm::HsmContext>>,
66 audit: &AuditState,
67) {
68 let span = tracing::info_span!("star_renewal_cycle");
69 let _enter = span.enter();
70
71 let expired_count = star_manager.cleanup_expired();
73
74 let order_ids = star_manager.orders_needing_renewal();
76 if order_ids.is_empty() && expired_count == 0 {
77 debug!("no STAR orders need attention");
78 return;
79 }
80
81 let mut renewed = 0u32;
82 let mut failed = 0u32;
83
84 for id in &order_ids {
86 let order = match star_manager.get_order(id) {
87 Some(o) => o,
88 None => {
89 debug!(order_id = %id, "order disappeared before renewal");
90 continue;
91 }
92 };
93
94 if order.status != StarOrderStatus::Active {
95 debug!(
96 order_id = %id,
97 status = ?order.status,
98 "skipping non-active order"
99 );
100 continue;
101 }
102
103 let ca = match cas.get(&order.ca_id) {
105 Some(ca) => ca,
106 None => {
107 warn!(
108 order_id = %id,
109 ca_id = %order.ca_id,
110 "CA not found for STAR order — skipping"
111 );
112 failed += 1;
113 continue;
114 }
115 };
116
117 let validity_days = (order.renewal_interval.as_secs() as u32 / 86400).max(1);
119 let profile = EnrollmentProfile {
120 max_validity_days: validity_days,
121 ..EnrollmentProfile::default()
122 };
123
124 let ca_cfg = ca_configs.iter().find(|c| c.id == order.ca_id);
126 let ca_key_pem: Vec<u8>;
127 let key_label_owned: String;
128
129 let signing_key = match ca_cfg {
130 Some(cfg) if cfg.is_hsm_backed() => match hsm {
131 Some(hsm_ctx) => {
132 key_label_owned = match crate::routes::simpleenroll::parse_pkcs11_object_label(
133 cfg.pkcs11_uri.as_deref().unwrap(),
134 ) {
135 Ok(l) => l,
136 Err(e) => {
137 warn!(
138 order_id = %id,
139 error = %e,
140 "invalid pkcs11_uri for STAR renewal — skipping"
141 );
142 failed += 1;
143 continue;
144 }
145 };
146 issue::CaSigningKey::Hsm {
147 context: hsm_ctx,
148 key_label: &key_label_owned,
149 }
150 }
151 None => {
152 warn!(
153 order_id = %id,
154 ca_id = %order.ca_id,
155 "HSM not configured but CA has pkcs11_uri — skipping"
156 );
157 failed += 1;
158 continue;
159 }
160 },
161 Some(cfg) => match std::fs::read(&cfg.key_file) {
162 Ok(pem) => {
163 ca_key_pem = pem;
164 issue::CaSigningKey::Pem(&ca_key_pem)
165 }
166 Err(e) => {
167 warn!(
168 order_id = %id,
169 ca_id = %order.ca_id,
170 error = %e,
171 "failed to read CA key for STAR renewal — skipping"
172 );
173 failed += 1;
174 continue;
175 }
176 },
177 None => {
178 warn!(
179 order_id = %id,
180 ca_id = %order.ca_id,
181 "CA config not found for STAR renewal — skipping"
182 );
183 failed += 1;
184 continue;
185 }
186 };
187
188 match issue::issue_certificate(
190 &order.csr_der,
191 &profile,
192 &ca.cert_der,
193 signing_key,
194 &ca.hash_algorithm,
195 ) {
196 Ok(result) => {
197 let cert = StarCertificate {
198 serial_number: result.serial_number.clone(),
199 certificate_der: result.certificate_der.clone(),
200 not_before: result.not_before,
201 not_after: result.not_after,
202 renewal_number: order.current_renewals + 1,
203 star_order_id: id.clone(),
204 };
205
206 if let Err(e) = star_manager.store_renewed_certificate(id, cert.clone()) {
208 warn!(
209 order_id = %id,
210 error = %e,
211 "failed to store renewed certificate in manager"
212 );
213 failed += 1;
214 continue;
215 }
216
217 if let Err(e) = persist_certificate(db, id, &cert).await {
219 error!(
220 order_id = %id,
221 serial = %cert.serial_number,
222 error = %e,
223 "failed to persist renewed certificate to database"
224 );
225 }
229
230 if let Err(e) = update_renewal_count(db, id, order.current_renewals + 1).await {
232 error!(
233 order_id = %id,
234 error = %e,
235 "failed to update renewal count in database"
236 );
237 }
238
239 crate::audit::record(
241 db,
242 audit,
243 AuditEvent::new(AuditEventType::CertIssue)
244 .with_ca_id(&order.ca_id)
245 .with_detail(format!(
246 "STAR renewal #{} for order {id}, serial={}, validity={validity_days}d",
247 order.current_renewals + 1,
248 result.serial_number,
249 )),
250 )
251 .await;
252
253 info!(
254 order_id = %id,
255 serial = %result.serial_number,
256 renewal = order.current_renewals + 1,
257 validity_days,
258 "STAR certificate renewed"
259 );
260 renewed += 1;
261 }
262 Err(e) => {
263 warn!(
264 order_id = %id,
265 ca_id = %order.ca_id,
266 error = %e,
267 "STAR certificate issuance failed — will retry next cycle"
268 );
269 failed += 1;
270 }
271 }
272 }
273
274 info!(
275 renewed,
276 failed,
277 expired = expired_count,
278 "STAR renewal cycle complete"
279 );
280}
281
282async fn persist_certificate(
284 db: &sqlx::AnyPool,
285 order_id: &str,
286 cert: &StarCertificate,
287) -> Result<(), sqlx::Error> {
288 sqlx::query(
289 "INSERT INTO star_certificates \
290 (star_order_id, serial_number, certificate_der, not_before, not_after, renewal_number) \
291 VALUES (?, ?, ?, ?, ?, ?)",
292 )
293 .bind(order_id)
294 .bind(&cert.serial_number)
295 .bind(&cert.certificate_der)
296 .bind(cert.not_before.to_rfc3339())
297 .bind(cert.not_after.to_rfc3339())
298 .bind(cert.renewal_number as i64)
299 .execute(db)
300 .await?;
301
302 Ok(())
303}
304
305async fn update_renewal_count(
307 db: &sqlx::AnyPool,
308 order_id: &str,
309 count: u32,
310) -> Result<(), sqlx::Error> {
311 sqlx::query("UPDATE star_orders SET current_renewals = ? WHERE id = ?")
312 .bind(count as i64)
313 .bind(order_id)
314 .execute(db)
315 .await?;
316
317 Ok(())
318}