Skip to main content

kipuka_est/
content_type.rs

1//! RFC 7030 MIME content types for EST operations.
2//!
3//! Defines the standard Content-Type values used in EST HTTP requests and responses.
4//! Media type registrations follow:
5//! - RFC 5967 (application/pkcs10)
6//! - RFC 5958 (application/pkcs8)
7//! - RFC 2311 / RFC 8551 (application/pkcs7-mime)
8
9/// PKCS#7 certificates-only message (DER-encoded, base64-wrapped).
10///
11/// Used for `/cacerts` responses and enrollment certificate responses.
12/// RFC 7030 §4.1.3, §4.2.3.
13pub const PKCS7_MIME: &str = "application/pkcs7-mime";
14
15/// PKCS#7 with smimeType=certs-only parameter.
16pub const PKCS7_CERTS_ONLY: &str = "application/pkcs7-mime; smimeType=certs-only";
17
18/// PKCS#10 certificate signing request (DER-encoded, base64-wrapped).
19///
20/// Registered by RFC 5967 ("The application/pkcs10 Media Type"). This is the
21/// mandatory Content-Type for CSR submission in EST `/simpleenroll` and
22/// `/simplereenroll` request bodies (RFC 7030 §4.2.1).
23///
24/// The DER-encoded CSR is base64-wrapped for HTTP transport per RFC 7030 §4.
25/// Optional parameters defined by RFC 5967 §2:
26/// - `charset` (not used; DER is binary)
27/// - `smime-type` (see [`APPLICATION_PKCS10_SMIME`])
28pub const PKCS10: &str = "application/pkcs10";
29
30/// PKCS#10 CSR with S/MIME type parameter per RFC 5967 §2.
31///
32/// Used when wrapping a CSR inside an S/MIME message (e.g., for CMC requests).
33/// The `smime-type=certs-only` variant indicates the content is a bare CSR
34/// suitable for S/MIME processing pipelines.
35pub const APPLICATION_PKCS10_SMIME: &str = "application/pkcs10; smime-type=certs-only";
36
37/// PKCS#8 private key (DER-encoded, base64-wrapped).
38///
39/// Used for `/serverkeygen` response part 2 containing the ML-KEM private key.
40/// RFC 7030 §4.4.2, format per RFC 5958 (OneAsymmetricKey / PrivateKeyInfo).
41pub const PKCS8: &str = "application/pkcs8";
42
43/// CSR attributes structure (DER-encoded, base64-wrapped).
44///
45/// Used for `/csrattrs` responses.
46/// RFC 7030 §4.5.2.
47pub const CSRATTRS: &str = "application/csrattrs";
48
49/// CMC request/response (DER-encoded, base64-wrapped).
50///
51/// Used for `/fullcmc` request and response bodies.
52/// RFC 5272, RFC 7030 §4.3.
53pub const CMC_REQUEST: &str = "application/pkcs7-mime; smimeType=CMC-Request";
54pub const CMC_RESPONSE: &str = "application/pkcs7-mime; smimeType=CMC-Response";
55
56/// Multipart MIME container for `/serverkeygen` responses.
57///
58/// Contains two parts:
59/// 1. `application/pkcs7-mime` - Certificate signed by ML-DSA or composite CA
60/// 2. `application/pkcs8` - ML-KEM private key for client
61///
62/// RFC 7030 §4.4.2.
63pub const MULTIPART_MIXED: &str = "multipart/mixed";
64
65/// Default boundary string for multipart/mixed responses.
66pub const DEFAULT_BOUNDARY: &str = "----=_EST_ServerKeyGen_Boundary";
67
68/// Constructs a multipart/mixed Content-Type header with custom boundary.
69pub fn multipart_content_type(boundary: &str) -> String {
70    format!("multipart/mixed; boundary={boundary}")
71}
72
73/// Validates a Content-Type header value against an expected EST media type.
74///
75/// Per RFC 5967 §2 and RFC 7030 §4, Content-Type headers may include optional
76/// parameters (charset, smime-type, boundary). This function matches the base
77/// media type case-insensitively and tolerates optional parameters.
78///
79/// # Examples
80///
81/// ```
82/// # use kipuka_est::content_type::validate_content_type;
83/// assert!(validate_content_type("application/pkcs10", "application/pkcs10"));
84/// assert!(validate_content_type("application/pkcs10; charset=utf-8", "application/pkcs10"));
85/// assert!(validate_content_type("Application/PKCS10", "application/pkcs10"));
86/// assert!(!validate_content_type("text/plain", "application/pkcs10"));
87/// ```
88pub fn validate_content_type(header_value: &str, expected_base: &str) -> bool {
89    let base = header_value.split(';').next().unwrap_or("").trim();
90    base.eq_ignore_ascii_case(expected_base)
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn test_content_types() {
99        assert_eq!(PKCS7_MIME, "application/pkcs7-mime");
100        assert_eq!(PKCS10, "application/pkcs10");
101        assert_eq!(PKCS8, "application/pkcs8");
102        assert_eq!(CSRATTRS, "application/csrattrs");
103        assert_eq!(MULTIPART_MIXED, "multipart/mixed");
104    }
105
106    #[test]
107    fn test_pkcs10_smime_variant() {
108        assert!(APPLICATION_PKCS10_SMIME.starts_with("application/pkcs10"));
109        assert!(APPLICATION_PKCS10_SMIME.contains("smime-type"));
110    }
111
112    #[test]
113    fn test_multipart_content_type() {
114        let ct = multipart_content_type("my-boundary");
115        assert_eq!(ct, "multipart/mixed; boundary=my-boundary");
116    }
117
118    #[test]
119    fn test_cmc_types() {
120        assert!(CMC_REQUEST.contains("CMC-Request"));
121        assert!(CMC_RESPONSE.contains("CMC-Response"));
122    }
123
124    #[test]
125    fn test_validate_content_type_exact() {
126        assert!(validate_content_type("application/pkcs10", PKCS10));
127        assert!(validate_content_type("application/pkcs8", PKCS8));
128    }
129
130    #[test]
131    fn test_validate_content_type_with_params() {
132        assert!(validate_content_type(
133            "application/pkcs10; charset=utf-8",
134            PKCS10
135        ));
136        assert!(validate_content_type(
137            "application/pkcs10; smime-type=certs-only",
138            PKCS10
139        ));
140    }
141
142    #[test]
143    fn test_validate_content_type_case_insensitive() {
144        assert!(validate_content_type("Application/PKCS10", PKCS10));
145        assert!(validate_content_type("APPLICATION/PKCS8", PKCS8));
146    }
147
148    #[test]
149    fn test_validate_content_type_mismatch() {
150        assert!(!validate_content_type("text/plain", PKCS10));
151        assert!(!validate_content_type("application/pkcs7-mime", PKCS10));
152    }
153}