Skip to main content

kipuka_est/
cacerts.rs

1//! CA Certificates response per RFC 7030 §4.1.
2//!
3//! The `/cacerts` operation returns a PKCS#7 certs-only structure containing
4//! the CA certificate chain. Supports both traditional and ML-DSA CA certificates.
5
6use crate::{EstError, EstResult};
7use base64::Engine;
8use serde::{Deserialize, Serialize};
9
10/// CA certificates response (RFC 7030 §4.1.3).
11///
12/// Contains a PKCS#7 `certs-only` structure with the CA certificate chain.
13/// The chain MAY include:
14/// - Root CA certificate (self-signed)
15/// - Intermediate CA certificates
16/// - ML-DSA signing CA certificates
17/// - Composite (ML-DSA + traditional) CA certificates
18///
19/// The structure is DER-encoded and base64-wrapped for transport.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct CaCertsResponse {
22    /// PKCS#7 certs-only message in DER encoding.
23    #[serde(with = "serde_bytes")]
24    pkcs7_der: Vec<u8>,
25}
26
27impl CaCertsResponse {
28    /// Creates a new CA certificates response from DER-encoded PKCS#7.
29    ///
30    /// # Arguments
31    ///
32    /// * `pkcs7_der` - DER-encoded PKCS#7 certs-only structure
33    ///
34    /// # Returns
35    ///
36    /// A new `CaCertsResponse` instance.
37    pub fn new(pkcs7_der: Vec<u8>) -> Self {
38        Self { pkcs7_der }
39    }
40
41    /// Returns the raw DER-encoded PKCS#7 data.
42    pub fn pkcs7_der(&self) -> &[u8] {
43        &self.pkcs7_der
44    }
45
46    /// Consumes self and returns the DER-encoded PKCS#7 data.
47    pub fn into_pkcs7_der(self) -> Vec<u8> {
48        self.pkcs7_der
49    }
50
51    /// Encodes the response as base64 for HTTP transport.
52    ///
53    /// Uses standard base64 encoding per RFC 7030 §4.1.3.
54    pub fn to_base64(&self) -> String {
55        base64::engine::general_purpose::STANDARD.encode(&self.pkcs7_der)
56    }
57
58    /// Decodes a base64-encoded CA certificates response.
59    ///
60    /// # Arguments
61    ///
62    /// * `base64_data` - Base64-encoded PKCS#7 certs-only structure
63    ///
64    /// # Errors
65    ///
66    /// Returns `EstError::InvalidBase64` if decoding fails.
67    pub fn from_base64(base64_data: &str) -> EstResult<Self> {
68        let pkcs7_der = base64::engine::general_purpose::STANDARD
69            .decode(base64_data)
70            .map_err(|e| EstError::InvalidBase64(e.to_string()))?;
71
72        Ok(Self::new(pkcs7_der))
73    }
74
75    /// Validates the PKCS#7 structure (basic DER sanity check).
76    ///
77    /// This is a lightweight validation that checks for basic DER structure.
78    /// Full cryptographic validation is performed by the CA module.
79    pub fn validate(&self) -> EstResult<()> {
80        // Basic DER sanity: must start with SEQUENCE tag (0x30)
81        if self.pkcs7_der.is_empty() {
82            return Err(EstError::InvalidPkcs7("Empty PKCS#7 structure".to_string()));
83        }
84
85        if self.pkcs7_der[0] != 0x30 {
86            return Err(EstError::InvalidPkcs7(
87                "Invalid DER: expected SEQUENCE tag".to_string(),
88            ));
89        }
90
91        // Minimum viable PKCS#7 certs-only is ~100 bytes
92        if self.pkcs7_der.len() < 50 {
93            return Err(EstError::InvalidPkcs7(format!(
94                "PKCS#7 too small: {} bytes",
95                self.pkcs7_der.len()
96            )));
97        }
98
99        Ok(())
100    }
101}
102
103/// Helper module for serde byte serialization.
104mod serde_bytes {
105    use serde::{Deserialize, Deserializer, Serializer};
106
107    pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
108    where
109        S: Serializer,
110    {
111        serializer.serialize_bytes(bytes)
112    }
113
114    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
115    where
116        D: Deserializer<'de>,
117    {
118        Vec::<u8>::deserialize(deserializer)
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_cacerts_roundtrip() {
128        // Minimal valid DER SEQUENCE (mock PKCS#7)
129        let der = vec![0x30, 0x82, 0x01, 0x00]; // SEQUENCE, length 256 (placeholder)
130        let mut full_der = der.clone();
131        full_der.extend(vec![0x00; 252]); // Pad to 256 bytes
132
133        let response = CaCertsResponse::new(full_der.clone());
134        assert_eq!(response.pkcs7_der(), &full_der);
135
136        let base64 = response.to_base64();
137        let decoded = CaCertsResponse::from_base64(&base64).unwrap();
138        assert_eq!(decoded.pkcs7_der(), &full_der);
139    }
140
141    #[test]
142    fn test_invalid_base64() {
143        let result = CaCertsResponse::from_base64("not-valid-base64!!!");
144        assert!(matches!(result, Err(EstError::InvalidBase64(_))));
145    }
146
147    #[test]
148    fn test_validate_empty() {
149        let response = CaCertsResponse::new(vec![]);
150        assert!(matches!(
151            response.validate(),
152            Err(EstError::InvalidPkcs7(_))
153        ));
154    }
155
156    #[test]
157    fn test_validate_wrong_tag() {
158        let response = CaCertsResponse::new(vec![0x04, 0x00]); // OCTET STRING instead of SEQUENCE
159        assert!(matches!(
160            response.validate(),
161            Err(EstError::InvalidPkcs7(_))
162        ));
163    }
164
165    #[test]
166    fn test_validate_too_small() {
167        let response = CaCertsResponse::new(vec![0x30, 0x00]); // Valid SEQUENCE but too small
168        assert!(matches!(
169            response.validate(),
170            Err(EstError::InvalidPkcs7(_))
171        ));
172    }
173}