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    // Generate the key pair on the server.
121    //
122    // TODO: Implement server-side key generation.
123    //
124    // When an HSM is configured for this CA:
125    //   let (pub_key, priv_key_handle) = kipuka_hsm::generate_key_pair(
126    //       &state.hsm, ca.key_type_for_keygen()
127    //   ).await?;
128    //
129    // When using software key generation:
130    //   let (pub_key_der, priv_key_pkcs8) = kipuka_util::keygen::generate(
131    //       &ca.key_type
132    //   )?;
133    //
134    // Then:
135    // 1. Build a new CSR using the generated public key and the template's
136    //    requested subject/extensions
137    // 2. Sign the certificate with the CA key
138    // 3. Optionally archive the private key via KRA integration
139
140    let cert_pkcs7_der: Vec<u8> = Vec::new(); // Placeholder
141    let private_key_pkcs8: Vec<u8> = Vec::new(); // Placeholder
142
143    if cert_pkcs7_der.is_empty() || private_key_pkcs8.is_empty() {
144        return Err(KipukaError::Ca(
145            "server-side key generation not yet implemented".into(),
146        ));
147    }
148
149    // Build the multipart/mixed response.
150    let response_body = build_multipart_response(&cert_pkcs7_der, &private_key_pkcs8);
151
152    let content_type = format!(
153        "{}; boundary={}",
154        content_types::MULTIPART_MIXED,
155        MULTIPART_BOUNDARY
156    );
157
158    let mut resp = (StatusCode::OK, response_body).into_response();
159    if let Ok(hv) = HeaderValue::from_str(&content_type) {
160        resp.headers_mut().insert(header::CONTENT_TYPE, hv);
161    }
162
163    state
164        .record_audit_event(
165            "serverkeygen_success",
166            &format!("ca_id={ca_id}, identity={identity}"),
167        )
168        .await;
169
170    Ok(resp)
171}
172
173/// Build a `multipart/mixed` response body with the certificate and private key.
174///
175/// RFC 7030 §4.4.2: the response contains two MIME parts:
176/// 1. The certificate chain (PKCS#7 certs-only, base64-encoded)
177/// 2. The private key (PKCS#8 DER, base64-encoded)
178fn build_multipart_response(cert_pkcs7_der: &[u8], private_key_pkcs8: &[u8]) -> String {
179    let cert_b64 = encode_est_base64(cert_pkcs7_der);
180    let key_b64 = encode_est_base64(private_key_pkcs8);
181
182    format!(
183        "\r\n--{boundary}\r\n\
184         Content-Type: {cert_type}\r\n\
185         Content-Transfer-Encoding: base64\r\n\
186         \r\n\
187         {cert_b64}\r\n\
188         --{boundary}\r\n\
189         Content-Type: {key_type}\r\n\
190         Content-Transfer-Encoding: base64\r\n\
191         \r\n\
192         {key_b64}\r\n\
193         --{boundary}--\r\n",
194        boundary = MULTIPART_BOUNDARY,
195        cert_type = content_types::PKCS7_CERTS,
196        key_type = content_types::PKCS8,
197    )
198}