Skip to main content

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}