Skip to main content

kipuka_est/
fullcmc.rs

1//! Full CMC (Certificate Management over CMS) per RFC 7030 §4.3.
2//!
3//! The `/fullcmc` operation provides complete CMC protocol support for complex
4//! enrollment scenarios. Requires id-kp-cmcRA EKU validation for RA certificates.
5
6use crate::{EstError, EstResult};
7use base64::Engine;
8use serde::{Deserialize, Serialize};
9
10/// id-kp-cmcRA OID (1.3.6.1.5.5.7.3.28) per RFC 6402 §3.2.
11///
12/// Registration Authority certificates used for CMC must include this EKU.
13pub const ID_KP_CMC_RA: &str = "1.3.6.1.5.5.7.3.28";
14
15/// Full CMC request (RFC 7030 §4.3.1).
16///
17/// Contains a CMC `PKIData` message wrapped in a PKCS#7 SignedData structure.
18/// The SignedData MUST be signed by an RA certificate with id-kp-cmcRA EKU.
19///
20/// CMC supports advanced features:
21/// - Batch enrollment (multiple CSRs in one request)
22/// - Attribute certification
23/// - Key archival
24/// - Revocation requests
25/// - ML-DSA and ML-KEM enrollment
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct FullCmcRequest {
28    /// CMC request as PKCS#7 SignedData in DER encoding.
29    #[serde(with = "serde_bytes")]
30    cmc_der: Vec<u8>,
31}
32
33impl FullCmcRequest {
34    /// Creates a new Full CMC request from DER-encoded PKCS#7.
35    pub fn new(cmc_der: Vec<u8>) -> Self {
36        Self { cmc_der }
37    }
38
39    /// Returns the raw DER-encoded CMC data.
40    pub fn cmc_der(&self) -> &[u8] {
41        &self.cmc_der
42    }
43
44    /// Consumes self and returns the DER-encoded CMC data.
45    pub fn into_cmc_der(self) -> Vec<u8> {
46        self.cmc_der
47    }
48
49    /// Encodes the request as base64 for HTTP transport.
50    pub fn to_base64(&self) -> String {
51        base64::engine::general_purpose::STANDARD.encode(&self.cmc_der)
52    }
53
54    /// Decodes a base64-encoded Full CMC request.
55    pub fn from_base64(base64_data: &str) -> EstResult<Self> {
56        let cmc_der = base64::engine::general_purpose::STANDARD
57            .decode(base64_data)
58            .map_err(|e| EstError::InvalidBase64(e.to_string()))?;
59
60        Ok(Self::new(cmc_der))
61    }
62
63    /// Validates the CMC structure.
64    ///
65    /// This performs basic DER validation. Full CMC validation (signature
66    /// verification, RA EKU check, PKIData parsing) is delegated to the CA module.
67    pub fn validate(&self) -> EstResult<()> {
68        if self.cmc_der.is_empty() {
69            return Err(EstError::InvalidCmc("Empty CMC request".to_string()));
70        }
71
72        // Basic DER sanity: must start with SEQUENCE tag (0x30)
73        if self.cmc_der[0] != 0x30 {
74            return Err(EstError::InvalidCmc(
75                "Invalid DER: expected SEQUENCE tag".to_string(),
76            ));
77        }
78
79        // Minimum viable CMC is ~500 bytes (SignedData + PKIData overhead)
80        if self.cmc_der.len() < 300 {
81            return Err(EstError::InvalidCmc(format!(
82                "CMC too small: {} bytes",
83                self.cmc_der.len()
84            )));
85        }
86
87        Ok(())
88    }
89
90    /// Validates RA certificate EKU (stub).
91    ///
92    /// The RA certificate used to sign the CMC request MUST contain the
93    /// id-kp-cmcRA (1.3.6.1.5.5.7.3.28) extended key usage.
94    ///
95    /// This is a placeholder - actual validation requires parsing the SignedData
96    /// and verifying the signer certificate's EKU. Delegated to CA module.
97    ///
98    /// # Arguments
99    ///
100    /// * `ra_cert_der` - DER-encoded RA certificate from SignedData
101    ///
102    /// # Errors
103    ///
104    /// Returns `EstError::InvalidEku` if the certificate lacks id-kp-cmcRA.
105    pub fn validate_ra_eku(&self, _ra_cert_der: &[u8]) -> EstResult<()> {
106        // Stub: Full implementation requires X.509 EKU parsing
107        // For now, assume valid and delegate to CA module
108        Ok(())
109    }
110
111    /// Checks if the CMC request contains ML-DSA or ML-KEM enrollment requests.
112    ///
113    /// Searches for post-quantum algorithm OID prefixes in the DER structure.
114    pub fn contains_pqc(&self) -> bool {
115        let ml_dsa_prefix = b"\x06\x0b\x60\x86\x48\x01\x65\x03\x04\x03"; // ML-DSA
116        let ml_kem_prefix = b"\x06\x0b\x60\x86\x48\x01\x65\x03\x04\x04"; // ML-KEM
117
118        self.cmc_der
119            .windows(ml_dsa_prefix.len())
120            .any(|w| w == ml_dsa_prefix || w == ml_kem_prefix)
121    }
122}
123
124/// Full CMC response (RFC 7030 §4.3.2).
125///
126/// Contains a CMC `PKIResponse` message wrapped in a PKCS#7 SignedData structure.
127/// The response includes:
128/// - Status information for each request
129/// - Issued certificates (on success)
130/// - Error details (on failure)
131/// - Transaction IDs for pending requests
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133pub struct FullCmcResponse {
134    /// CMC response as PKCS#7 SignedData in DER encoding.
135    #[serde(with = "serde_bytes")]
136    cmc_der: Vec<u8>,
137}
138
139impl FullCmcResponse {
140    /// Creates a new Full CMC response from DER-encoded PKCS#7.
141    pub fn new(cmc_der: Vec<u8>) -> Self {
142        Self { cmc_der }
143    }
144
145    /// Returns the raw DER-encoded CMC data.
146    pub fn cmc_der(&self) -> &[u8] {
147        &self.cmc_der
148    }
149
150    /// Consumes self and returns the DER-encoded CMC data.
151    pub fn into_cmc_der(self) -> Vec<u8> {
152        self.cmc_der
153    }
154
155    /// Encodes the response as base64 for HTTP transport.
156    pub fn to_base64(&self) -> String {
157        base64::engine::general_purpose::STANDARD.encode(&self.cmc_der)
158    }
159
160    /// Decodes a base64-encoded Full CMC response.
161    pub fn from_base64(base64_data: &str) -> EstResult<Self> {
162        let cmc_der = base64::engine::general_purpose::STANDARD
163            .decode(base64_data)
164            .map_err(|e| EstError::InvalidBase64(e.to_string()))?;
165
166        Ok(Self::new(cmc_der))
167    }
168
169    /// Validates the CMC structure.
170    pub fn validate(&self) -> EstResult<()> {
171        if self.cmc_der.is_empty() {
172            return Err(EstError::InvalidCmc("Empty CMC response".to_string()));
173        }
174
175        if self.cmc_der[0] != 0x30 {
176            return Err(EstError::InvalidCmc(
177                "Invalid DER: expected SEQUENCE tag".to_string(),
178            ));
179        }
180
181        if self.cmc_der.len() < 300 {
182            return Err(EstError::InvalidCmc(format!(
183                "CMC too small: {} bytes",
184                self.cmc_der.len()
185            )));
186        }
187
188        Ok(())
189    }
190}
191
192/// Helper module for serde byte serialization.
193mod serde_bytes {
194    use serde::{Deserialize, Deserializer, Serializer};
195
196    pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
197    where
198        S: Serializer,
199    {
200        serializer.serialize_bytes(bytes)
201    }
202
203    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
204    where
205        D: Deserializer<'de>,
206    {
207        Vec::<u8>::deserialize(deserializer)
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_fullcmc_request_roundtrip() {
217        let mut der = vec![0x30, 0x82, 0x02, 0x00]; // SEQUENCE, length 512
218        der.extend(vec![0x00; 508]);
219
220        let request = FullCmcRequest::new(der.clone());
221        assert_eq!(request.cmc_der(), &der);
222
223        let base64 = request.to_base64();
224        let decoded = FullCmcRequest::from_base64(&base64).unwrap();
225        assert_eq!(decoded.cmc_der(), &der);
226    }
227
228    #[test]
229    fn test_fullcmc_response_roundtrip() {
230        let mut der = vec![0x30, 0x82, 0x02, 0x00];
231        der.extend(vec![0x00; 508]);
232
233        let response = FullCmcResponse::new(der.clone());
234        assert_eq!(response.cmc_der(), &der);
235
236        let base64 = response.to_base64();
237        let decoded = FullCmcResponse::from_base64(&base64).unwrap();
238        assert_eq!(decoded.cmc_der(), &der);
239    }
240
241    #[test]
242    fn test_validate_cmc_request() {
243        let mut der = vec![0x30, 0x82, 0x02, 0x00];
244        der.extend(vec![0x00; 508]);
245
246        let request = FullCmcRequest::new(der);
247        assert!(request.validate().is_ok());
248    }
249
250    #[test]
251    fn test_validate_empty() {
252        let request = FullCmcRequest::new(vec![]);
253        assert!(matches!(request.validate(), Err(EstError::InvalidCmc(_))));
254    }
255
256    #[test]
257    fn test_validate_too_small() {
258        let request = FullCmcRequest::new(vec![0x30, 0x00]);
259        assert!(matches!(request.validate(), Err(EstError::InvalidCmc(_))));
260    }
261
262    #[test]
263    fn test_id_kp_cmc_ra_oid() {
264        assert_eq!(ID_KP_CMC_RA, "1.3.6.1.5.5.7.3.28");
265    }
266
267    #[test]
268    fn test_contains_pqc() {
269        // Mock CMC with ML-DSA OID
270        let mut der = vec![0x30, 0x82, 0x02, 0x00];
271        der.extend_from_slice(b"\x06\x0b\x60\x86\x48\x01\x65\x03\x04\x03\x11"); // ML-DSA-44
272        der.extend(vec![0x00; 496]);
273
274        let request = FullCmcRequest::new(der);
275        assert!(request.contains_pqc());
276    }
277
278    #[test]
279    fn test_validate_ra_eku() {
280        let mut der = vec![0x30, 0x82, 0x02, 0x00];
281        der.extend(vec![0x00; 508]);
282
283        let request = FullCmcRequest::new(der);
284        // Mock RA cert (stub validation always passes)
285        let mock_ra_cert = vec![0x30, 0x82, 0x01, 0x00];
286        assert!(request.validate_ra_eku(&mock_ra_cert).is_ok());
287    }
288}