Skip to main content

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}