kipuka/routes/serverkeygen.rs
1//! `POST /.well-known/est/serverkeygen` — Server-Side Key Generation.
2//!
3//! RFC 7030 §4.4: The EST server generates a key pair on behalf of the
4//! client, signs a certificate, and returns both the certificate and
5//! the private key.
6//!
7//! The response is `multipart/mixed` containing two parts:
8//! - Part 1: `application/pkcs7-mime; smime-type=certs-only` (certificate)
9//! - Part 2: `application/pkcs8` (DER-encoded private key)
10//!
11//! RHELBU-3536 R27: Authentication (mTLS or OTP) is required.
12//! Server-side key generation requires HSM or software key generation
13//! capability per configuration.
14
15use std::sync::Arc;
16
17use axum::body::Bytes;
18use axum::extract::State;
19use axum::http::{HeaderValue, StatusCode, header};
20use axum::response::{IntoResponse, Response};
21
22use crate::auth::EstAuth;
23use crate::error::KipukaError;
24use crate::routes::LabelExtractor;
25use crate::routes::est::{content_types, decode_est_base64, encode_est_base64};
26use crate::state::AppState;
27
28/// MIME boundary for the multipart/mixed response.
29///
30/// RFC 7030 §4.4.2: The server returns the certificate and private key
31/// as separate MIME parts in a multipart/mixed response.
32const MULTIPART_BOUNDARY: &str = "estServerKeyGenBoundary";
33
34/// `POST /.well-known/est/serverkeygen`
35///
36/// Accepts a PKCS#10 CSR (with placeholder key or desired attributes) and
37/// returns a multipart response with the issued certificate and the
38/// server-generated private key.
39///
40/// # Authentication
41///
42/// Requires mTLS or OTP authentication (RHELBU-3536 R27).
43///
44/// # Request
45///
46/// | Header | Value |
47/// |----------------|----------------------|
48/// | Content-Type | `application/pkcs10` |
49/// | Body | Base64-encoded DER PKCS#10 CSR |
50///
51/// The CSR may contain a placeholder public key; the server replaces it
52/// with the generated key pair. The CSR's requested subject and extensions
53/// are used as a template for the issued certificate.
54///
55/// # Response
56///
57/// | Header | Value |
58/// |----------------|-------------------------------|
59/// | Status | `200 OK` |
60/// | Content-Type | `multipart/mixed; boundary=...` |
61///
62/// Response body parts:
63///
64/// ```text
65/// --estServerKeyGenBoundary
66/// Content-Type: application/pkcs7-mime; smime-type=certs-only
67/// Content-Transfer-Encoding: base64
68///
69/// <base64 PKCS#7 certificate>
70/// --estServerKeyGenBoundary
71/// Content-Type: application/pkcs8
72/// Content-Transfer-Encoding: base64
73///
74/// <base64 PKCS#8 private key>
75/// --estServerKeyGenBoundary--
76/// ```
77///
78/// # Errors
79///
80/// - `400 Bad Request` — malformed CSR
81/// - `401 Unauthorized` — authentication failed
82/// - `403 Forbidden` — serverkeygen not enabled
83/// - `500 Internal Server Error` — key generation or CA signing failure
84/// - `503 Service Unavailable` — HSM offline
85pub async fn post_serverkeygen(
86 auth: EstAuth,
87 label: LabelExtractor,
88 State(state): State<Arc<AppState>>,
89 body: Bytes,
90) -> Result<Response, KipukaError> {
91 let ca_id = label.ca_id();
92 let identity = &auth.0.identity;
93
94 // Check that serverkeygen is enabled.
95 if !state.config.est.serverkeygen {
96 return Err(KipukaError::Est(
97 "server-side key generation is not enabled".into(),
98 ));
99 }
100
101 tracing::info!(
102 ca_id = %ca_id,
103 label = %label.label,
104 identity = %identity,
105 method = ?auth.0.method,
106 "serverkeygen request"
107 );
108
109 // Decode the base64-encoded CSR template.
110 let csr_der = decode_est_base64(&body)
111 .map_err(|e| KipukaError::BadRequest(format!("CSR template decoding failed: {e}")))?;
112
113 if csr_der.is_empty() {
114 return Err(KipukaError::BadRequest("empty CSR template".into()));
115 }
116
117 // Look up the CA backend.
118 let _ca = state.get_ca(ca_id).ok_or(KipukaError::NotFound)?;
119
120 // ── Dogtag KRA path ─────────────────────────────────────────────────────
121 //
122 // If a Dogtag backend with KRA is configured, generate the key pair on
123 // the KRA, enroll the certificate via the CA, and return both.
124 if let Some(ref dogtag_pool) = state.dogtag {
125 let dogtag_cfg = state
126 .config
127 .dogtag
128 .as_ref()
129 .expect("dogtag config present when pool is set");
130
131 if dogtag_cfg.kra_url.is_none() {
132 return Err(KipukaError::Ca(
133 "server-side key generation requires dogtag.kra_url to be configured".into(),
134 ));
135 }
136
137 let kra_client = kipuka_dogtag::KraClient::new(dogtag_cfg).map_err(|e| {
138 KipukaError::Ca(format!("KRA client initialization failed: {e}"))
139 })?;
140
141 // Determine key type from the CSR template or use defaults.
142 // For now, default to RSA 2048 — a full implementation would
143 // extract the desired key type from the CSR template attributes.
144 let key_type = "RSA";
145 let key_size = 2048u32;
146
147 tracing::info!(
148 ca_id = %ca_id,
149 identity = %identity,
150 key_type = key_type,
151 key_size = key_size,
152 "generating key pair on Dogtag KRA"
153 );
154
155 let keygen_result = kra_client
156 .generate_key(key_type, key_size)
157 .await
158 .map_err(|e| KipukaError::Ca(format!("KRA key generation failed: {e}")))?;
159
160 // Now enroll the certificate via the CA using the generated public key.
161 // Build a CSR from the template + generated public key.
162 // For now, we forward the original CSR template and let Dogtag handle it.
163 let client = dogtag_pool.get_client().map_err(|e| {
164 KipukaError::ServiceUnavailable(format!("Dogtag CA unavailable: {e}"))
165 })?;
166
167 use base64::Engine;
168 let csr_b64 = base64::engine::general_purpose::STANDARD.encode(&csr_der);
169 let csr_pem = format!(
170 "-----BEGIN CERTIFICATE REQUEST-----\n{}\n-----END CERTIFICATE REQUEST-----",
171 csr_b64
172 );
173
174 let enroll_result = client
175 .enroll_certificate(&csr_pem, &dogtag_cfg.profile_id)
176 .await
177 .map_err(|e| KipukaError::Ca(format!("Dogtag enrollment for keygen failed: {e}")))?;
178
179 if enroll_result.status != kipuka_dogtag::EnrollStatus::Complete {
180 return Err(KipukaError::Ca(format!(
181 "Dogtag enrollment not complete for keygen: status={:?}, request_id={}",
182 enroll_result.status, enroll_result.request_id
183 )));
184 }
185
186 let cert_der = enroll_result.certificate_der.ok_or_else(|| {
187 KipukaError::Ca("Dogtag returned complete but no certificate for keygen".into())
188 })?;
189
190 // The private key from KRA — use wrapped key if available, else public key DER.
191 let private_key_der = keygen_result
192 .wrapped_private_key
193 .unwrap_or(keygen_result.public_key_der);
194
195 // Build the multipart/mixed response.
196 let response_body = build_multipart_response(&cert_der, &private_key_der);
197
198 let content_type = format!(
199 "{}; boundary={}",
200 content_types::MULTIPART_MIXED,
201 MULTIPART_BOUNDARY
202 );
203
204 let mut resp = (StatusCode::OK, response_body).into_response();
205 if let Ok(hv) = HeaderValue::from_str(&content_type) {
206 resp.headers_mut().insert(header::CONTENT_TYPE, hv);
207 }
208
209 state
210 .record_audit_event(
211 "serverkeygen_success",
212 &format!(
213 "ca_id={ca_id}, identity={identity}, backend=dogtag, kra_key_id={}",
214 keygen_result.key_id
215 ),
216 )
217 .await;
218
219 return Ok(resp);
220 }
221
222 // ── Software/HSM key generation path (no Dogtag) ────────────────────────
223 //
224 // TODO: Implement software/HSM server-side key generation.
225 //
226 // When an HSM is configured for this CA:
227 // let (pub_key, priv_key_handle) = kipuka_hsm::generate_key_pair(
228 // &state.hsm, ca.key_type_for_keygen()
229 // ).await?;
230 //
231 // When using software key generation:
232 // let (pub_key_der, priv_key_pkcs8) = kipuka_util::keygen::generate(
233 // &ca.key_type
234 // )?;
235 //
236 // Then:
237 // 1. Build a new CSR using the generated public key and the template's
238 // requested subject/extensions
239 // 2. Sign the certificate with the CA key
240 // 3. Optionally archive the private key via KRA integration
241
242 let cert_pkcs7_der: Vec<u8> = Vec::new(); // Placeholder
243 let private_key_pkcs8: Vec<u8> = Vec::new(); // Placeholder
244
245 if cert_pkcs7_der.is_empty() || private_key_pkcs8.is_empty() {
246 return Err(KipukaError::Ca(
247 "server-side key generation not yet implemented (configure [dogtag] with kra_url for KRA-backed keygen)".into(),
248 ));
249 }
250
251 // Build the multipart/mixed response.
252 let response_body = build_multipart_response(&cert_pkcs7_der, &private_key_pkcs8);
253
254 let content_type = format!(
255 "{}; boundary={}",
256 content_types::MULTIPART_MIXED,
257 MULTIPART_BOUNDARY
258 );
259
260 let mut resp = (StatusCode::OK, response_body).into_response();
261 if let Ok(hv) = HeaderValue::from_str(&content_type) {
262 resp.headers_mut().insert(header::CONTENT_TYPE, hv);
263 }
264
265 state
266 .record_audit_event(
267 "serverkeygen_success",
268 &format!("ca_id={ca_id}, identity={identity}"),
269 )
270 .await;
271
272 Ok(resp)
273}
274
275/// Build a `multipart/mixed` response body with the certificate and private key.
276///
277/// RFC 7030 §4.4.2: the response contains two MIME parts:
278/// 1. The certificate chain (PKCS#7 certs-only, base64-encoded)
279/// 2. The private key (PKCS#8 DER, base64-encoded)
280fn build_multipart_response(cert_pkcs7_der: &[u8], private_key_pkcs8: &[u8]) -> String {
281 let cert_b64 = encode_est_base64(cert_pkcs7_der);
282 let key_b64 = encode_est_base64(private_key_pkcs8);
283
284 format!(
285 "\r\n--{boundary}\r\n\
286 Content-Type: {cert_type}\r\n\
287 Content-Transfer-Encoding: base64\r\n\
288 \r\n\
289 {cert_b64}\r\n\
290 --{boundary}\r\n\
291 Content-Type: {key_type}\r\n\
292 Content-Transfer-Encoding: base64\r\n\
293 \r\n\
294 {key_b64}\r\n\
295 --{boundary}--\r\n",
296 boundary = MULTIPART_BOUNDARY,
297 cert_type = content_types::PKCS7_CERTS,
298 key_type = content_types::PKCS8,
299 )
300}