kipuka/routes/fullcmc.rs
1//! `POST /.well-known/est/fullcmc` — Full CMC Request.
2//!
3//! RFC 7030 §4.3: EST clients submit a Full CMC request (PKCS#7 SignedData
4//! containing a CMC PKIData) for complex enrollment scenarios that require
5//! RA intermediation.
6//!
7//! The signer of the CMC request MUST hold the id-kp-cmcRA EKU
8//! (OID 1.3.6.1.5.5.7.3.28) per RHELBU-3536 R15.
9//!
10//! The server proxies the CMC request to the CA backend and returns
11//! the CMC response.
12
13use std::sync::Arc;
14
15use axum::body::Bytes;
16use axum::extract::State;
17use axum::http::{HeaderValue, StatusCode, header};
18use axum::response::{IntoResponse, Response};
19
20use crate::auth::{AuthMethod, EstAuth};
21use crate::error::KipukaError;
22use crate::routes::LabelExtractor;
23use crate::routes::est::{content_types, decode_est_base64, encode_est_base64};
24use crate::state::AppState;
25
26/// CMC error codes mapped to HTTP status codes (RHELBU-3536 R17).
27///
28/// RFC 5272 §15.2 defines CMC failure codes. These are mapped to
29/// HTTP status codes for the EST response.
30#[derive(Debug, Clone, Copy)]
31pub enum CmcErrorCode {
32 /// badAlg (0) — unrecognized or unsupported algorithm.
33 BadAlgorithm,
34 /// badMessageCheck (1) — integrity check failed.
35 BadMessageCheck,
36 /// badRequest (2) — transaction not permitted or supported.
37 BadRequest,
38 /// badTime (3) — message time field not sufficiently close to system time.
39 BadTime,
40 /// badCertId (4) — no certificate found matching provided criteria.
41 BadCertId,
42 /// badDataFormat (5) — data not formatted as expected.
43 BadDataFormat,
44 /// wrongAuthority (6) — wrong authority specified in request.
45 WrongAuthority,
46 /// incorrectData (7) — included data is incorrect.
47 IncorrectData,
48 /// missingTimeStamp (8) — required timestamp missing.
49 MissingTimestamp,
50 /// badPOP (9) — proof-of-possession failed.
51 BadPop,
52}
53
54impl CmcErrorCode {
55 /// Map a CMC error code to an HTTP status code.
56 pub fn to_http_status(self) -> StatusCode {
57 match self {
58 CmcErrorCode::BadAlgorithm => StatusCode::BAD_REQUEST,
59 CmcErrorCode::BadMessageCheck => StatusCode::BAD_REQUEST,
60 CmcErrorCode::BadRequest => StatusCode::BAD_REQUEST,
61 CmcErrorCode::BadTime => StatusCode::BAD_REQUEST,
62 CmcErrorCode::BadCertId => StatusCode::NOT_FOUND,
63 CmcErrorCode::BadDataFormat => StatusCode::BAD_REQUEST,
64 CmcErrorCode::WrongAuthority => StatusCode::FORBIDDEN,
65 CmcErrorCode::IncorrectData => StatusCode::BAD_REQUEST,
66 CmcErrorCode::MissingTimestamp => StatusCode::BAD_REQUEST,
67 CmcErrorCode::BadPop => StatusCode::FORBIDDEN,
68 }
69 }
70}
71
72/// `POST /.well-known/est/fullcmc`
73///
74/// Accepts a CMC request (PKCS#7 SignedData) and returns a CMC response.
75///
76/// # Authentication
77///
78/// Requires mTLS with a certificate carrying the id-kp-cmcRA EKU
79/// (OID 1.3.6.1.5.5.7.3.28, RHELBU-3536 R15).
80///
81/// # Request
82///
83/// | Header | Value |
84/// |----------------|----------------------------------------------|
85/// | Content-Type | `application/pkcs7-mime; smime-type=CMC-request` |
86/// | Body | Base64-encoded DER PKCS#7 SignedData (CMC PKIData) |
87///
88/// # Response
89///
90/// | Header | Value |
91/// |----------------|----------------------------------------------|
92/// | Status | `200 OK` |
93/// | Content-Type | `application/pkcs7-mime; smime-type=CMC-response` |
94///
95/// # Errors
96///
97/// - `400 Bad Request` — malformed CMC request
98/// - `401 Unauthorized` — authentication failed
99/// - `403 Forbidden` — signer lacks id-kp-cmcRA EKU
100/// - `500 Internal Server Error` — CA backend error
101pub async fn post_fullcmc(
102 auth: EstAuth,
103 label: LabelExtractor,
104 State(state): State<Arc<AppState>>,
105 body: Bytes,
106) -> Result<Response, KipukaError> {
107 let ca_id = label.ca_id();
108 let identity = &auth.0.identity;
109
110 // Check that fullcmc is enabled in the configuration.
111 if !state.config.est.fullcmc {
112 return Err(KipukaError::Est("Full CMC is not enabled".into()));
113 }
114
115 // Full CMC requires mTLS authentication.
116 if auth.0.method != AuthMethod::Mtls {
117 return Err(KipukaError::Auth(
118 "Full CMC requires mTLS client certificate authentication".into(),
119 ));
120 }
121
122 // RHELBU-3536 R15: Validate that the signer certificate carries the
123 // id-kp-cmcRA Extended Key Usage.
124 if !auth.0.has_cmc_ra_eku() {
125 tracing::warn!(
126 identity = %identity,
127 "fullcmc rejected: signer lacks id-kp-cmcRA EKU"
128 );
129 return Err(KipukaError::Auth(
130 "CMC signer certificate must have id-kp-cmcRA EKU (1.3.6.1.5.5.7.3.28)".into(),
131 ));
132 }
133
134 tracing::info!(
135 ca_id = %ca_id,
136 label = %label.label,
137 identity = %identity,
138 "fullcmc request"
139 );
140
141 // Decode the base64-encoded CMC request.
142 let cmc_request_der = decode_est_base64(&body)
143 .map_err(|e| KipukaError::BadRequest(format!("CMC request decoding failed: {e}")))?;
144
145 if cmc_request_der.is_empty() {
146 return Err(KipukaError::BadRequest("empty CMC request".into()));
147 }
148
149 // Validate the CMC request structure.
150 //
151 // TODO: Parse the PKCS#7 SignedData and extract the CMC PKIData.
152 //
153 // 1. Verify the outer SignedData signature
154 // 2. Extract the CMC PKIData from the encapsulated content
155 // 3. Validate the CMC control attributes
156 // 4. Extract the certification requests from the reqSequence
157
158 // Look up the CA backend.
159 let _ca = state.get_ca(ca_id).ok_or(KipukaError::NotFound)?;
160
161 // Proxy the CMC request to the CA backend.
162 //
163 // TODO: Implement CMC request forwarding.
164 // let cmc_response_der = kipuka_est::cmc::process_request(ca, &cmc_request_der).await?;
165 let cmc_response_der: Vec<u8> = Vec::new(); // Placeholder
166
167 if cmc_response_der.is_empty() {
168 return Err(KipukaError::Ca("CMC processing not yet implemented".into()));
169 }
170
171 // Encode the CMC response.
172 let body = encode_est_base64(&cmc_response_der);
173
174 let mut resp = (StatusCode::OK, body).into_response();
175 resp.headers_mut().insert(
176 header::CONTENT_TYPE,
177 HeaderValue::from_static(content_types::CMC_RESPONSE),
178 );
179 resp.headers_mut().insert(
180 header::HeaderName::from_static("content-transfer-encoding"),
181 HeaderValue::from_static(content_types::TRANSFER_ENCODING_BASE64),
182 );
183
184 state
185 .record_audit_event(
186 "fullcmc_success",
187 &format!("ca_id={ca_id}, identity={identity}"),
188 )
189 .await;
190
191 Ok(resp)
192}