1use std::sync::Arc;
22
23use axum::Router;
24use axum::body::Bytes;
25use axum::extract::{Path, State};
26use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
27use axum::response::{IntoResponse, Response};
28use axum::routing::{get, post};
29
30use crate::auth::EstAuth;
31use crate::error::KipukaError;
32use crate::routes::LabelExtractor;
33use crate::routes::est::{content_types, decode_est_base64, encode_est_base64};
34use crate::star::{StarCertificate, StarError};
35use crate::state::AppState;
36
37pub fn star_router() -> Router<Arc<AppState>> {
43 Router::new()
44 .route("/", post(post_star_order))
45 .route(
46 "/{order_id}",
47 get(get_star_certificate).delete(delete_star_order),
48 )
49 .route("/{order_id}/history", get(get_star_history))
50}
51
52pub async fn post_star_order(
85 auth: EstAuth,
86 label: LabelExtractor,
87 State(state): State<Arc<AppState>>,
88 headers: HeaderMap,
89 body: Bytes,
90) -> Result<Response, KipukaError> {
91 let star_config = state
93 .config
94 .star
95 .as_ref()
96 .filter(|c| c.enabled)
97 .ok_or(KipukaError::NotFound)?;
98
99 let star_manager = state
101 .star_manager
102 .as_ref()
103 .ok_or(KipukaError::ServiceUnavailable(
104 "STAR manager not available".into(),
105 ))?;
106
107 let ca_id = label.ca_id();
108 let identity = &auth.0.identity;
109
110 tracing::info!(
111 ca_id = %ca_id,
112 label = %label.label,
113 identity = %identity,
114 method = ?auth.0.method,
115 "STAR order request"
116 );
117
118 let renewal_interval_secs: u64 = headers
120 .get("star-renewal-interval")
121 .and_then(|v| v.to_str().ok())
122 .and_then(|v| v.parse().ok())
123 .unwrap_or(star_config.default_renewal_interval_secs);
124
125 let lifetime_days: u32 = headers
126 .get("star-lifetime")
127 .and_then(|v| v.to_str().ok())
128 .and_then(|v| v.parse().ok())
129 .unwrap_or(star_config.max_lifetime_days);
130
131 let renewal_interval_secs = renewal_interval_secs
133 .max(star_config.min_renewal_interval_secs)
134 .min(star_config.max_renewal_interval_secs);
135 let lifetime_days = lifetime_days.min(star_config.max_lifetime_days);
136
137 let csr_der = decode_est_base64(&body)
139 .map_err(|e| KipukaError::BadRequest(format!("CSR decoding failed: {e}")))?;
140
141 if csr_der.len() < 60 {
142 return Err(KipukaError::BadRequest(
143 "CSR is too short to be valid".into(),
144 ));
145 }
146
147 let order = star_manager
149 .create_order(
150 identity.clone(),
151 String::new(), "default".to_owned(),
153 renewal_interval_secs,
154 lifetime_days,
155 ca_id.to_owned(),
156 csr_der.clone(),
157 Some(identity.clone()),
158 )
159 .map_err(star_error_to_kipuka)?;
160
161 let order_id = order.id.clone();
162
163 let cert_der: Vec<u8> = Vec::new(); if cert_der.is_empty() {
174 return Err(KipukaError::Ca(
175 "STAR certificate issuance not yet implemented".into(),
176 ));
177 }
178
179 let first_cert = StarCertificate {
181 certificate_der: cert_der.clone(),
182 serial_number: String::new(), not_before: chrono::Utc::now(),
184 not_after: chrono::Utc::now() + chrono::Duration::seconds(renewal_interval_secs as i64),
185 renewal_number: 0,
186 star_order_id: order_id.clone(),
187 };
188 star_manager
189 .store_renewed_certificate(&order_id, first_cert)
190 .map_err(star_error_to_kipuka)?;
191
192 sqlx::query(
194 "INSERT INTO star_orders \
195 (id, subject_dn, key_type, profile, renewal_interval_secs, \
196 lifetime_end, max_renewals, status, requestor_dn, ca_id, csr_der) \
197 VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?)",
198 )
199 .bind(&order_id)
200 .bind(&order.subject_dn)
201 .bind(&order.key_type)
202 .bind(&order.profile)
203 .bind(renewal_interval_secs as i64)
204 .bind(order.lifetime_end.to_rfc3339())
205 .bind(order.max_renewals as i64)
206 .bind(identity)
207 .bind(ca_id)
208 .bind(&csr_der)
209 .execute(&state.db)
210 .await?;
211
212 state
213 .record_audit_event(
214 "star_order_created",
215 &format!("order_id={order_id}, ca_id={ca_id}, identity={identity}"),
216 )
217 .await;
218
219 let pkcs7_der = cert_der; let response_body = encode_est_base64(&pkcs7_der);
222
223 let mut resp = (StatusCode::CREATED, response_body).into_response();
224 resp.headers_mut().insert(
225 header::CONTENT_TYPE,
226 HeaderValue::from_static(content_types::PKCS7_CERTS),
227 );
228 resp.headers_mut().insert(
229 header::HeaderName::from_static("content-transfer-encoding"),
230 HeaderValue::from_static(content_types::TRANSFER_ENCODING_BASE64),
231 );
232 resp.headers_mut().insert(
233 header::HeaderName::from_static("star-order-id"),
234 HeaderValue::from_str(&order_id).unwrap_or_else(|_| HeaderValue::from_static("unknown")),
235 );
236
237 Ok(resp)
238}
239
240pub async fn get_star_certificate(
255 Path(order_id): Path<String>,
256 State(state): State<Arc<AppState>>,
257) -> Result<Response, KipukaError> {
258 let _star_config = state
260 .config
261 .star
262 .as_ref()
263 .filter(|c| c.enabled)
264 .ok_or(KipukaError::NotFound)?;
265
266 let star_manager = state
267 .star_manager
268 .as_ref()
269 .ok_or(KipukaError::ServiceUnavailable(
270 "STAR manager not available".into(),
271 ))?;
272
273 match star_manager.get_current_certificate(&order_id) {
275 Ok(cert) => {
276 let response_body = encode_est_base64(&cert.certificate_der);
277
278 let mut resp = (StatusCode::OK, response_body).into_response();
279 resp.headers_mut().insert(
280 header::CONTENT_TYPE,
281 HeaderValue::from_static(content_types::PKCS7_CERTS),
282 );
283 resp.headers_mut().insert(
284 header::HeaderName::from_static("content-transfer-encoding"),
285 HeaderValue::from_static(content_types::TRANSFER_ENCODING_BASE64),
286 );
287 Ok(resp)
288 }
289 Err(StarError::OrderCancelled(_) | StarError::OrderExpired(_)) => {
290 Ok(StatusCode::GONE.into_response())
292 }
293 Err(StarError::OrderNotFound(_)) => Err(KipukaError::NotFound),
294 Err(e) => Err(KipukaError::Internal(e.to_string())),
295 }
296}
297
298pub async fn delete_star_order(
315 auth: EstAuth,
316 Path(order_id): Path<String>,
317 State(state): State<Arc<AppState>>,
318) -> Result<Response, KipukaError> {
319 let _star_config = state
321 .config
322 .star
323 .as_ref()
324 .filter(|c| c.enabled)
325 .ok_or(KipukaError::NotFound)?;
326
327 let star_manager = state
328 .star_manager
329 .as_ref()
330 .ok_or(KipukaError::ServiceUnavailable(
331 "STAR manager not available".into(),
332 ))?;
333
334 let identity = &auth.0.identity;
335
336 tracing::info!(
337 order_id = %order_id,
338 identity = %identity,
339 "STAR order cancellation request"
340 );
341
342 star_manager
344 .cancel_order(&order_id)
345 .map_err(star_error_to_kipuka)?;
346
347 sqlx::query("UPDATE star_orders SET status = 'cancelled', cancelled_at = ? WHERE id = ?")
349 .bind(chrono::Utc::now().to_rfc3339())
350 .bind(&order_id)
351 .execute(&state.db)
352 .await?;
353
354 state
355 .record_audit_event(
356 "star_order_cancelled",
357 &format!("order_id={order_id}, identity={identity}"),
358 )
359 .await;
360
361 Ok(StatusCode::NO_CONTENT.into_response())
362}
363
364pub async fn get_star_history(
387 Path(order_id): Path<String>,
388 State(state): State<Arc<AppState>>,
389) -> Result<Response, KipukaError> {
390 let _star_config = state
392 .config
393 .star
394 .as_ref()
395 .filter(|c| c.enabled)
396 .ok_or(KipukaError::NotFound)?;
397
398 let star_manager = state.star_manager.as_ref();
400 let in_memory = star_manager.and_then(|m| m.get_order(&order_id)).is_some();
401
402 if !in_memory {
403 let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM star_orders WHERE id = ?")
405 .bind(&order_id)
406 .fetch_one(&state.db)
407 .await
408 .unwrap_or((0,));
409
410 if count.0 == 0 {
411 return Err(KipukaError::NotFound);
412 }
413 }
414
415 let rows: Vec<StarCertRow> = sqlx::query_as(
417 "SELECT serial_number, not_before, not_after, renewal_number \
418 FROM star_certificates WHERE star_order_id = ? ORDER BY renewal_number ASC",
419 )
420 .bind(&order_id)
421 .fetch_all(&state.db)
422 .await?;
423
424 let entries: Vec<serde_json::Value> = rows
425 .iter()
426 .map(|r| {
427 serde_json::json!({
428 "serial": r.serial_number,
429 "not_before": r.not_before,
430 "not_after": r.not_after,
431 "renewal_number": r.renewal_number,
432 })
433 })
434 .collect();
435
436 let json_body = serde_json::to_string(&entries)
437 .map_err(|e| KipukaError::Internal(format!("JSON serialization failed: {e}")))?;
438
439 let mut resp = (StatusCode::OK, json_body).into_response();
440 resp.headers_mut().insert(
441 header::CONTENT_TYPE,
442 HeaderValue::from_static("application/json"),
443 );
444
445 Ok(resp)
446}
447
448#[derive(sqlx::FromRow)]
450struct StarCertRow {
451 serial_number: String,
452 not_before: String,
453 not_after: String,
454 renewal_number: i64,
455}
456
457fn star_error_to_kipuka(e: StarError) -> KipukaError {
459 match e {
460 StarError::OrderNotFound(_) => KipukaError::NotFound,
461 StarError::OrderCancelled(id) => {
462 KipukaError::BadRequest(format!("STAR order {id} is cancelled"))
463 }
464 StarError::OrderExpired(id) => {
465 KipukaError::BadRequest(format!("STAR order {id} has expired"))
466 }
467 StarError::MaxRenewalsReached { order_id, max } => KipukaError::BadRequest(format!(
468 "STAR order {order_id} reached maximum renewals ({max})"
469 )),
470 StarError::MaxOrdersReached { limit } => {
471 KipukaError::ServiceUnavailable(format!("maximum active STAR orders reached ({limit})"))
472 }
473 StarError::InvalidInterval {
474 requested,
475 min,
476 max,
477 } => KipukaError::BadRequest(format!(
478 "renewal interval {requested}s outside allowed range {min}s–{max}s"
479 )),
480 StarError::IssuanceError(msg) => KipukaError::Ca(msg),
481 StarError::DatabaseError(msg) => KipukaError::Db(msg),
482 }
483}