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 _guard = star_manager.operation_guard().await;
72 let expired_count = star_manager.cleanup_expired();
74
75 let order_ids = star_manager.orders_needing_renewal();
77 if order_ids.is_empty() && expired_count == 0 {
78 debug!("no STAR orders need attention");
79 return;
80 }
81
82 let mut renewed = 0u32;
83 let mut failed = 0u32;
84
85 for id in &order_ids {
87 let order = match star_manager.get_order(id) {
88 Some(o) => o,
89 None => {
90 debug!(order_id = %id, "order disappeared before renewal");
91 continue;
92 }
93 };
94
95 if order.status != StarOrderStatus::Active {
96 debug!(
97 order_id = %id,
98 status = ?order.status,
99 "skipping non-active order"
100 );
101 continue;
102 }
103
104 let ca = match cas.get(&order.ca_id) {
106 Some(ca) => ca,
107 None => {
108 warn!(
109 order_id = %id,
110 ca_id = %order.ca_id,
111 "CA not found for STAR order — skipping"
112 );
113 failed += 1;
114 continue;
115 }
116 };
117
118 let validity_secs = order.renewal_interval.as_secs();
120 let mut profile: EnrollmentProfile = match serde_json::from_str(&order.profile) {
121 Ok(profile) => profile,
122 Err(error) => {
123 warn!(order_id = %id, %error, "STAR order lacks persisted authorization policy");
124 failed += 1;
125 continue;
126 }
127 };
128 profile.max_validity_seconds = Some(validity_secs);
129 profile.max_not_after = Some(order.lifetime_end);
130 if let Err(error) = crate::audit::record_checked(
131 db,
132 audit,
133 AuditEvent::new(AuditEventType::EnrollRequest)
134 .with_ca_id(&order.ca_id)
135 .with_detail(format!("STAR renewal for {id}")),
136 )
137 .await
138 {
139 warn!(%error, "STAR renewal rejected by audit policy");
140 failed += 1;
141 continue;
142 }
143
144 let ca_cfg = match ca_configs.iter().find(|c| c.id == order.ca_id) {
146 Some(cfg) => cfg,
147 None => {
148 warn!(
149 order_id = %id,
150 ca_id = %order.ca_id,
151 "CA config not found for STAR renewal — skipping"
152 );
153 failed += 1;
154 continue;
155 }
156 };
157
158 let resolved_key = match issue::resolve_signing_key_sync(ca_cfg, hsm) {
159 Ok(k) => k,
160 Err(e) => {
161 warn!(
162 order_id = %id,
163 ca_id = %order.ca_id,
164 error = %e,
165 "failed to resolve signing key for STAR renewal — skipping"
166 );
167 failed += 1;
168 continue;
169 }
170 };
171
172 match issue::issue_certificate(
174 &order.csr_der,
175 &profile,
176 &ca.cert_der,
177 resolved_key.as_signing_key(),
178 &ca.hash_algorithm,
179 ca.ocsp_url.as_deref(),
180 ca.crl_url.as_deref(),
181 ) {
182 Ok(result) => {
183 let cert = StarCertificate {
184 serial_number: result.serial_number.clone(),
185 certificate_der: result.certificate_der.clone(),
186 not_before: result.not_before,
187 not_after: result.not_after,
188 renewal_number: order.current_renewals + 1,
189 star_order_id: id.clone(),
190 };
191
192 let issuer_dn = match synta_certificate::Certificate::from_der(&ca.cert_der) {
193 Ok(parsed) => synta_certificate::format_dn(parsed.tbs_certificate.subject.0),
194 Err(error) => {
195 warn!(%error, "cannot parse STAR issuer");
196 failed += 1;
197 continue;
198 }
199 };
200 if let Err(error) =
202 persist_renewal(db, &order, &cert, &issuer_dn, &profile.name, audit).await
203 {
204 error!(order_id = %id, %error, "STAR renewal transaction failed");
205 failed += 1;
206 continue;
207 }
208 if let Err(error) = star_manager.store_renewed_certificate(id, cert.clone()) {
209 error!(order_id = %id, %error, "committed STAR renewal could not be published");
210 failed += 1;
211 continue;
212 }
213
214 info!(
215 order_id = %id,
216 serial = %result.serial_number,
217 renewal = order.current_renewals + 1,
218 validity_secs,
219 "STAR certificate renewed"
220 );
221 renewed += 1;
222 }
223 Err(e) => {
224 warn!(
225 order_id = %id,
226 ca_id = %order.ca_id,
227 error = %e,
228 "STAR certificate issuance failed — will retry next cycle"
229 );
230 failed += 1;
231 }
232 }
233 }
234
235 info!(
236 renewed,
237 failed,
238 expired = expired_count,
239 "STAR renewal cycle complete"
240 );
241}
242
243async fn persist_renewal(
245 db: &sqlx::AnyPool,
246 order: &crate::star::StarOrder,
247 cert: &StarCertificate,
248 issuer_dn: &str,
249 profile_name: &str,
250 audit: &AuditState,
251) -> Result<(), crate::error::KipukaError> {
252 let mut tx = db.begin().await?;
253 let sql = if cert.not_after.timestamp() >= order.lifetime_end.timestamp() {
254 "UPDATE star_orders SET current_renewals = ?, status = 'completed' WHERE id = ? AND current_renewals = ? AND status = 'active'"
255 } else {
256 "UPDATE star_orders SET current_renewals = ?, status = 'active' WHERE id = ? AND current_renewals = ? AND status = 'active'"
257 };
258 let updated = sqlx::query(crate::db::pg_sql(sql))
259 .bind(cert.renewal_number as i64)
260 .bind(&order.id)
261 .bind(order.current_renewals as i64)
262 .execute(&mut *tx)
263 .await?;
264 if updated.rows_affected() != 1 {
265 return Err(sqlx::Error::RowNotFound.into());
266 }
267 sqlx::query(crate::db::pg_sql(
268 "INSERT INTO star_certificates (star_order_id, serial_number, certificate_der, not_before, not_after, renewal_number) VALUES (?, ?, ?, ?, ?, ?)"))
269 .bind(&order.id).bind(&cert.serial_number).bind(&cert.certificate_der)
270 .bind(cert.not_before.to_rfc3339()).bind(cert.not_after.to_rfc3339())
271 .bind(cert.renewal_number as i64).execute(&mut *tx).await?;
272 sqlx::query(crate::db::pg_sql(
273 "INSERT INTO certificates (serial, subject_dn, issuer_dn, not_before, not_after, der_encoded, ca_id, profile, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'active')"))
274 .bind(&cert.serial_number).bind(&order.subject_dn).bind(issuer_dn)
275 .bind(cert.not_before.to_rfc3339()).bind(cert.not_after.to_rfc3339())
276 .bind(&cert.certificate_der).bind(&order.ca_id).bind(profile_name)
277 .execute(&mut *tx).await?;
278 crate::audit::record_issuance_in_transaction(
279 &mut tx,
280 audit,
281 &order.ca_id,
282 format!(
283 "STAR renewal #{} for order {}, serial={}",
284 cert.renewal_number, order.id, cert.serial_number
285 ),
286 )
287 .await?;
288 crate::audit::commit_issuance(tx, audit).await?;
289 Ok(())
290}