kipuka/routes/admin/certs.rs
1//! Certificate management endpoints for the admin API.
2//!
3//! Provides listing, detail retrieval, and revocation of certificates
4//! issued by the Kipuka EST server.
5
6use std::sync::Arc;
7
8use axum::Json;
9use axum::extract::{Path, Query, State};
10use axum::http::StatusCode;
11use axum::response::{IntoResponse, Response};
12use serde::{Deserialize, Serialize};
13
14use super::AdminAuth;
15use crate::state::AppState;
16
17/// Query parameters for certificate listing.
18#[derive(Deserialize)]
19pub struct ListCertsQuery {
20 /// Filter by CA identifier.
21 pub ca_id: Option<String>,
22
23 /// Filter by certificate status.
24 pub status: Option<String>,
25
26 /// Maximum number of results to return.
27 #[serde(default = "default_limit")]
28 pub limit: u32,
29
30 /// Offset for pagination.
31 #[serde(default)]
32 pub offset: u32,
33}
34
35fn default_limit() -> u32 {
36 50
37}
38
39/// Certificate summary for listing.
40#[derive(Serialize)]
41pub struct CertSummary {
42 /// Certificate serial number (hex-encoded).
43 pub serial: String,
44
45 /// Subject DN of the certificate.
46 pub subject: String,
47
48 /// Which CA issued this certificate.
49 pub ca_id: String,
50
51 /// When the certificate was issued (RFC 3339).
52 pub issued_at: String,
53
54 /// When the certificate expires (RFC 3339).
55 pub expires_at: String,
56
57 /// Certificate status: "valid", "revoked", or "expired".
58 pub status: String,
59}
60
61/// Detailed certificate information.
62#[derive(Serialize)]
63pub struct CertDetail {
64 #[serde(flatten)]
65 pub summary: CertSummary,
66
67 /// Subject Alternative Names.
68 pub sans: Vec<String>,
69
70 /// Key algorithm (e.g., "EC P-256", "RSA 2048").
71 pub key_algorithm: String,
72
73 /// Signature algorithm (e.g., "SHA256withECDSA").
74 pub signature_algorithm: String,
75
76 /// How the client authenticated for enrollment.
77 pub auth_method: String,
78
79 /// Revocation reason (if revoked), per RFC 5280 §5.3.1.
80 pub revocation_reason: Option<String>,
81
82 /// When the certificate was revoked (RFC 3339), if applicable.
83 pub revoked_at: Option<String>,
84}
85
86/// Request body for certificate revocation.
87#[derive(Deserialize)]
88pub struct RevokeCertRequest {
89 /// Revocation reason code (RFC 5280 §5.3.1).
90 ///
91 /// Common values:
92 /// - 0: unspecified
93 /// - 1: keyCompromise
94 /// - 3: affiliationChanged
95 /// - 4: superseded
96 /// - 5: cessationOfOperation
97 #[serde(default)]
98 pub reason: u32,
99}
100
101/// `GET /admin/certs` — List issued certificates.
102///
103/// Returns a paginated list of certificates issued by this server.
104/// Supports filtering by CA and status.
105pub async fn list_certs(
106 _admin: AdminAuth,
107 Query(query): Query<ListCertsQuery>,
108 State(state): State<Arc<AppState>>,
109) -> Response {
110 let _ = &state;
111
112 tracing::debug!(
113 ca_id = ?query.ca_id,
114 status = ?query.status,
115 limit = query.limit,
116 offset = query.offset,
117 "listing certificates"
118 );
119
120 // TODO: Query the certificate database with filters.
121 //
122 // let certs = kipuka_est::db::certs::list(
123 // &state.db,
124 // query.ca_id.as_deref(),
125 // query.status.as_deref(),
126 // query.limit,
127 // query.offset,
128 // ).await?;
129
130 let certs: Vec<CertSummary> = Vec::new(); // Placeholder
131
132 (StatusCode::OK, Json(certs)).into_response()
133}
134
135/// `GET /admin/certs/{serial}` — Certificate details.
136///
137/// Returns detailed information about a specific certificate,
138/// identified by its hex-encoded serial number.
139pub async fn get_cert(
140 _admin: AdminAuth,
141 Path(serial): Path<String>,
142 State(state): State<Arc<AppState>>,
143) -> Response {
144 let _ = &state;
145
146 tracing::debug!(serial = %serial, "retrieving certificate details");
147
148 // TODO: Look up the certificate by serial number.
149 //
150 // let cert = match kipuka_est::db::certs::get_by_serial(&state.db, &serial).await? {
151 // Some(c) => c,
152 // None => return (StatusCode::NOT_FOUND, "certificate not found").into_response(),
153 // };
154
155 (StatusCode::NOT_FOUND, "certificate not found").into_response()
156}
157
158/// `POST /admin/certs/{serial}/revoke` — Revoke a certificate.
159///
160/// Marks the certificate as revoked with the given reason code.
161/// The CRL is regenerated to include the revoked certificate.
162///
163/// # Request
164///
165/// ```json
166/// { "reason": 4 }
167/// ```
168///
169/// # Reason Codes (RFC 5280 §5.3.1)
170///
171/// | Code | Meaning |
172/// |------|----------------------|
173/// | 0 | unspecified |
174/// | 1 | keyCompromise |
175/// | 2 | cACompromise |
176/// | 3 | affiliationChanged |
177/// | 4 | superseded |
178/// | 5 | cessationOfOperation |
179/// | 6 | certificateHold |
180/// | 9 | privilegeWithdrawn |
181/// | 10 | aACompromise |
182pub async fn revoke_cert(
183 _admin: AdminAuth,
184 Path(serial): Path<String>,
185 State(state): State<Arc<AppState>>,
186 Json(req): Json<RevokeCertRequest>,
187) -> Response {
188 tracing::info!(
189 serial = %serial,
190 reason = req.reason,
191 "revoking certificate"
192 );
193
194 // Validate reason code per RFC 5280 §5.3.1.
195 let valid_reasons = [0, 1, 2, 3, 4, 5, 6, 9, 10];
196 if !valid_reasons.contains(&req.reason) {
197 return (
198 StatusCode::BAD_REQUEST,
199 Json(serde_json::json!({
200 "error": "invalid_reason",
201 "detail": format!("reason code {} is not a valid CRL reason", req.reason)
202 })),
203 )
204 .into_response();
205 }
206
207 // TODO: Revoke the certificate in the database and regenerate the CRL.
208 //
209 // kipuka_est::db::certs::revoke(&state.db, &serial, req.reason).await?;
210 //
211 // Invalidate the CRL cache for the issuing CA:
212 // state.invalidate_crl_cache(ca_id);
213
214 state
215 .record_audit_event(
216 "cert_revoked",
217 &format!("serial={serial}, reason={}", req.reason),
218 )
219 .await;
220
221 (
222 StatusCode::OK,
223 Json(serde_json::json!({
224 "serial": serial,
225 "status": "revoked",
226 "reason": req.reason,
227 })),
228 )
229 .into_response()
230}