Skip to main content

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}