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}