1use std::sync::Arc;
23
24use axum::Router;
25use axum::body::Bytes;
26use axum::extract::State;
27use axum::http::{HeaderValue, StatusCode, header};
28use axum::response::{IntoResponse, Response};
29use axum::routing::post;
30
31use crate::auth::cms_auth;
32use crate::error::KipukaError;
33use crate::routes::LabelExtractor;
34use crate::state::AppState;
35
36const CONTENT_TYPE_PKCS7: &str = "application/pkcs7-mime";
38
39pub fn cms_est_router() -> Router<Arc<AppState>> {
46 Router::new()
47 .route("/simpleenroll", post(post_cms_simpleenroll))
48 .route("/simplereenroll", post(post_cms_simplereenroll))
49 .route("/serverkeygen", post(post_cms_serverkeygen))
50 .route("/fullcmc", post(post_cms_fullcmc))
51}
52
53fn get_cms_est_config(state: &AppState) -> Result<&crate::config::CmsEstConfig, KipukaError> {
58 match state.config.cms_est {
59 Some(ref cfg) if cfg.enabled => Ok(cfg),
60 _ => Err(KipukaError::Est("CMS-EST is not enabled".into())),
61 }
62}
63
64fn build_truststore(state: &AppState) -> Vec<Vec<u8>> {
71 state
72 .cas
73 .values()
74 .flat_map(|ca| ca.cert_chain.iter().cloned())
75 .collect()
76}
77
78pub async fn post_cms_simpleenroll(
104 label: LabelExtractor,
105 State(state): State<Arc<AppState>>,
106 body: Bytes,
107) -> Result<Response, KipukaError> {
108 let cms_config = get_cms_est_config(&state)?;
109 let ca_id = label.ca_id();
110
111 tracing::info!(
112 ca_id = %ca_id,
113 label = %label.label,
114 "CMS simpleenroll request"
115 );
116
117 if body.is_empty() {
119 return Err(KipukaError::BadRequest(
120 "empty CMS SignedData request body".into(),
121 ));
122 }
123
124 let truststore = build_truststore(&state);
126 let cms_result = cms_auth::verify_cms_signed_data(&body, &truststore)?;
127
128 let auth_result = cms_auth::extract_signer_identity(&cms_result)?;
130 let identity = &auth_result.identity;
131
132 tracing::info!(
133 ca_id = %ca_id,
134 identity = %identity,
135 signature_algorithm = %cms_result.signature_algorithm,
136 "CMS signature verified for simpleenroll"
137 );
138
139 let csr_der = &cms_result.payload;
141 if csr_der.len() < 60 {
142 return Err(KipukaError::BadRequest(
143 "extracted CSR is too short to be valid".into(),
144 ));
145 }
146
147 let cert_der: Vec<u8> = Vec::new(); if cert_der.is_empty() {
157 return Err(KipukaError::Ca(
158 "CMS-EST enrollment not yet implemented".into(),
159 ));
160 }
161
162 let response_body = if cms_config.encrypt_responses {
164 let enc_alg = cms_config
165 .allowed_content_encryption
166 .first()
167 .map(|s| s.as_str())
168 .unwrap_or("AES-256-GCM");
169
170 cms_auth::build_cms_enveloped_data(&cert_der, &cms_result.signer_cert_der, enc_alg)?
171 } else {
172 cert_der
173 };
174
175 state
176 .record_audit_event(
177 "cms_simpleenroll_success",
178 &format!("ca_id={ca_id}, identity={identity}"),
179 )
180 .await;
181
182 build_cms_response(StatusCode::OK, &response_body)
183}
184
185pub async fn post_cms_simplereenroll(
193 label: LabelExtractor,
194 State(state): State<Arc<AppState>>,
195 body: Bytes,
196) -> Result<Response, KipukaError> {
197 let cms_config = get_cms_est_config(&state)?;
198 let ca_id = label.ca_id();
199
200 tracing::info!(
201 ca_id = %ca_id,
202 label = %label.label,
203 "CMS simplereenroll request"
204 );
205
206 if body.is_empty() {
207 return Err(KipukaError::BadRequest(
208 "empty CMS SignedData request body".into(),
209 ));
210 }
211
212 let truststore = build_truststore(&state);
213 let cms_result = cms_auth::verify_cms_signed_data(&body, &truststore)?;
214 let auth_result = cms_auth::extract_signer_identity(&cms_result)?;
215 let identity = &auth_result.identity;
216
217 tracing::info!(
218 ca_id = %ca_id,
219 identity = %identity,
220 signature_algorithm = %cms_result.signature_algorithm,
221 "CMS signature verified for simplereenroll"
222 );
223
224 let csr_der = &cms_result.payload;
225 if csr_der.len() < 60 {
226 return Err(KipukaError::BadRequest(
227 "extracted CSR is too short to be valid".into(),
228 ));
229 }
230
231 let cert_der: Vec<u8> = Vec::new(); if cert_der.is_empty() {
250 return Err(KipukaError::Ca(
251 "CMS-EST re-enrollment not yet implemented".into(),
252 ));
253 }
254
255 let response_body = if cms_config.encrypt_responses {
256 let enc_alg = cms_config
257 .allowed_content_encryption
258 .first()
259 .map(|s| s.as_str())
260 .unwrap_or("AES-256-GCM");
261
262 cms_auth::build_cms_enveloped_data(&cert_der, &cms_result.signer_cert_der, enc_alg)?
263 } else {
264 cert_der
265 };
266
267 state
268 .record_audit_event(
269 "cms_simplereenroll_success",
270 &format!("ca_id={ca_id}, identity={identity}"),
271 )
272 .await;
273
274 build_cms_response(StatusCode::OK, &response_body)
275}
276
277pub async fn post_cms_serverkeygen(
285 label: LabelExtractor,
286 State(state): State<Arc<AppState>>,
287 body: Bytes,
288) -> Result<Response, KipukaError> {
289 let cms_config = get_cms_est_config(&state)?;
290 let ca_id = label.ca_id();
291
292 if !state.config.est.serverkeygen {
293 return Err(KipukaError::Est(
294 "server-side key generation is not enabled".into(),
295 ));
296 }
297
298 tracing::info!(
299 ca_id = %ca_id,
300 label = %label.label,
301 "CMS serverkeygen request"
302 );
303
304 if body.is_empty() {
305 return Err(KipukaError::BadRequest(
306 "empty CMS SignedData request body".into(),
307 ));
308 }
309
310 let truststore = build_truststore(&state);
311 let cms_result = cms_auth::verify_cms_signed_data(&body, &truststore)?;
312 let auth_result = cms_auth::extract_signer_identity(&cms_result)?;
313 let identity = &auth_result.identity;
314
315 tracing::info!(
316 ca_id = %ca_id,
317 identity = %identity,
318 signature_algorithm = %cms_result.signature_algorithm,
319 "CMS signature verified for serverkeygen"
320 );
321
322 let _csr_template = &cms_result.payload;
324
325 let combined: Vec<u8> = Vec::new(); if combined.is_empty() {
337 return Err(KipukaError::Ca(
338 "CMS-EST server key generation not yet implemented".into(),
339 ));
340 }
341
342 let enc_alg = cms_config
345 .allowed_content_encryption
346 .first()
347 .map(|s| s.as_str())
348 .unwrap_or("AES-256-GCM");
349
350 let response_body =
351 cms_auth::build_cms_enveloped_data(&combined, &cms_result.signer_cert_der, enc_alg)?;
352
353 state
354 .record_audit_event(
355 "cms_serverkeygen_success",
356 &format!("ca_id={ca_id}, identity={identity}"),
357 )
358 .await;
359
360 build_cms_response(StatusCode::OK, &response_body)
361}
362
363pub async fn post_cms_fullcmc(
371 label: LabelExtractor,
372 State(state): State<Arc<AppState>>,
373 body: Bytes,
374) -> Result<Response, KipukaError> {
375 let cms_config = get_cms_est_config(&state)?;
376 let ca_id = label.ca_id();
377
378 if !state.config.est.fullcmc {
379 return Err(KipukaError::Est("Full CMC is not enabled".into()));
380 }
381
382 tracing::info!(
383 ca_id = %ca_id,
384 label = %label.label,
385 "CMS fullcmc request"
386 );
387
388 if body.is_empty() {
389 return Err(KipukaError::BadRequest(
390 "empty CMS SignedData request body".into(),
391 ));
392 }
393
394 let truststore = build_truststore(&state);
395 let cms_result = cms_auth::verify_cms_signed_data(&body, &truststore)?;
396 let auth_result = cms_auth::extract_signer_identity(&cms_result)?;
397 let identity = &auth_result.identity;
398
399 tracing::info!(
409 ca_id = %ca_id,
410 identity = %identity,
411 signature_algorithm = %cms_result.signature_algorithm,
412 "CMS signature verified for fullcmc"
413 );
414
415 let _cmc_request_der = &cms_result.payload;
417
418 let cmc_response_der: Vec<u8> = Vec::new(); if cmc_response_der.is_empty() {
424 return Err(KipukaError::Ca(
425 "CMS-EST Full CMC not yet implemented".into(),
426 ));
427 }
428
429 let response_body = if cms_config.encrypt_responses {
430 let enc_alg = cms_config
431 .allowed_content_encryption
432 .first()
433 .map(|s| s.as_str())
434 .unwrap_or("AES-256-GCM");
435
436 cms_auth::build_cms_enveloped_data(&cmc_response_der, &cms_result.signer_cert_der, enc_alg)?
437 } else {
438 cmc_response_der
439 };
440
441 state
442 .record_audit_event(
443 "cms_fullcmc_success",
444 &format!("ca_id={ca_id}, identity={identity}"),
445 )
446 .await;
447
448 build_cms_response(StatusCode::OK, &response_body)
449}
450
451fn build_cms_response(status: StatusCode, body: &[u8]) -> Result<Response, KipukaError> {
457 let mut resp = (status, body.to_vec()).into_response();
458 resp.headers_mut().insert(
459 header::CONTENT_TYPE,
460 HeaderValue::from_static(CONTENT_TYPE_PKCS7),
461 );
462 Ok(resp)
463}