Skip to main content

kipuka_coap/
content_format.rs

1//! CoAP content-format IDs for EST media types.
2//!
3//! RFC 9483 §5.4 defines Content-Format option values that map EST HTTP
4//! Content-Type headers to compact integer IDs for CoAP transport.
5//! These IDs are registered in the IANA "CoAP Content-Formats" registry
6//! per RFC 9483 §10.1.
7//!
8//! CoAP uses a single integer in the Content-Format option (option 12)
9//! instead of a textual MIME type string, saving bytes on constrained links.
10
11/// `application/pkcs7-mime; smime-type=server-generated` (RFC 9483 §10.1, TBD280).
12///
13/// Used for server-generated key responses where the server produces the
14/// key pair and returns the private key to the client.
15pub const APPLICATION_PKCS7_MIME_SERVER_GEN_TYPE: u16 = 280;
16
17/// `application/pkcs7-mime; smime-type=certs-only` (RFC 9483 §10.1, TBD281).
18///
19/// Used for CA certificate chain responses (`/cacerts`, `/crts`) and
20/// enrollment certificate responses.
21pub const APPLICATION_PKCS7_MIME_CERTS_ONLY: u16 = 281;
22
23/// `application/pkcs7-mime; smime-type=CMC-Request` (RFC 9483 §10.1, TBD282).
24///
25/// Used for Full CMC enrollment requests per RFC 5272.
26pub const APPLICATION_PKCS7_MIME_CMC_REQUEST: u16 = 282;
27
28/// `application/pkcs7-mime; smime-type=CMC-Response` (RFC 9483 §10.1, TBD283).
29///
30/// Used for Full CMC enrollment responses per RFC 5272.
31pub const APPLICATION_PKCS7_MIME_CMC_RESPONSE: u16 = 283;
32
33/// `application/pkcs7-mime` (RFC 9483 §10.1, TBD284).
34///
35/// Generic PKCS#7 MIME type without smime-type parameter.
36pub const APPLICATION_PKCS7_MIME: u16 = 284;
37
38/// `application/pkcs10` (RFC 9483 §10.1, TBD285).
39///
40/// PKCS#10 certificate signing request, used in `/sen` and `/sren`
41/// request bodies. DER-encoded (not base64-wrapped, unlike HTTP EST).
42pub const APPLICATION_PKCS10: u16 = 285;
43
44/// `application/pkcs8` (RFC 9483 §10.1, TBD286).
45///
46/// PKCS#8 private key, used in `/skg` (server key generation) responses
47/// to deliver the generated private key to the client.
48pub const APPLICATION_PKCS8: u16 = 286;
49
50/// `application/csrattrs` (RFC 9483 §10.1, TBD287).
51///
52/// CSR attributes structure, used in `/att` responses to hint which
53/// attributes the client should include in its CSR.
54pub const APPLICATION_CSRATTRS: u16 = 287;
55
56/// `multipart/core` (RFC 8710).
57///
58/// Used to combine multiple CoAP content items in a single payload,
59/// e.g., certificate + private key in a server key generation response.
60pub const MULTIPART_CORE: u16 = 62;
61
62/// Maps a CoAP Content-Format ID to its HTTP Content-Type equivalent.
63///
64/// Returns `None` for unrecognized format IDs. This mapping is used when
65/// bridging between EST-coaps (CoAP) and EST (HTTP) backends.
66///
67/// # RFC Reference
68///
69/// RFC 9483 §5.4, Table 2: Content-Format ID to media type mapping.
70pub fn to_http_content_type(format_id: u16) -> Option<&'static str> {
71    match format_id {
72        APPLICATION_PKCS7_MIME_SERVER_GEN_TYPE => {
73            Some("application/pkcs7-mime; smime-type=server-generated")
74        }
75        APPLICATION_PKCS7_MIME_CERTS_ONLY => Some("application/pkcs7-mime; smime-type=certs-only"),
76        APPLICATION_PKCS7_MIME_CMC_REQUEST => {
77            Some("application/pkcs7-mime; smime-type=CMC-Request")
78        }
79        APPLICATION_PKCS7_MIME_CMC_RESPONSE => {
80            Some("application/pkcs7-mime; smime-type=CMC-Response")
81        }
82        APPLICATION_PKCS7_MIME => Some("application/pkcs7-mime"),
83        APPLICATION_PKCS10 => Some("application/pkcs10"),
84        APPLICATION_PKCS8 => Some("application/pkcs8"),
85        APPLICATION_CSRATTRS => Some("application/csrattrs"),
86        MULTIPART_CORE => Some("multipart/core"),
87        _ => None,
88    }
89}
90
91/// Maps an HTTP Content-Type string to its CoAP Content-Format ID.
92///
93/// Matches the base media type case-insensitively and recognizes
94/// smime-type parameters for PKCS#7 variants. Returns `None` for
95/// unrecognized MIME types.
96///
97/// # RFC Reference
98///
99/// RFC 9483 §5.4, Table 2: media type to Content-Format ID mapping.
100pub fn from_http_content_type(mime: &str) -> Option<u16> {
101    let lower = mime.to_ascii_lowercase();
102    let lower = lower.trim();
103
104    // Check for smime-type parameter variants first (more specific matches).
105    if lower.starts_with("application/pkcs7-mime") {
106        if lower.contains("server-generated") {
107            return Some(APPLICATION_PKCS7_MIME_SERVER_GEN_TYPE);
108        }
109        if lower.contains("certs-only") {
110            return Some(APPLICATION_PKCS7_MIME_CERTS_ONLY);
111        }
112        if lower.contains("cmc-request") {
113            return Some(APPLICATION_PKCS7_MIME_CMC_REQUEST);
114        }
115        if lower.contains("cmc-response") {
116            return Some(APPLICATION_PKCS7_MIME_CMC_RESPONSE);
117        }
118        return Some(APPLICATION_PKCS7_MIME);
119    }
120
121    // Extract the base media type (before any parameters).
122    let base = lower.split(';').next().unwrap_or("").trim();
123
124    match base {
125        "application/pkcs10" => Some(APPLICATION_PKCS10),
126        "application/pkcs8" => Some(APPLICATION_PKCS8),
127        "application/csrattrs" => Some(APPLICATION_CSRATTRS),
128        "multipart/core" => Some(MULTIPART_CORE),
129        _ => None,
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_content_format_constants() {
139        assert_eq!(APPLICATION_PKCS7_MIME_SERVER_GEN_TYPE, 280);
140        assert_eq!(APPLICATION_PKCS7_MIME_CERTS_ONLY, 281);
141        assert_eq!(APPLICATION_PKCS7_MIME_CMC_REQUEST, 282);
142        assert_eq!(APPLICATION_PKCS7_MIME_CMC_RESPONSE, 283);
143        assert_eq!(APPLICATION_PKCS7_MIME, 284);
144        assert_eq!(APPLICATION_PKCS10, 285);
145        assert_eq!(APPLICATION_PKCS8, 286);
146        assert_eq!(APPLICATION_CSRATTRS, 287);
147        assert_eq!(MULTIPART_CORE, 62);
148    }
149
150    #[test]
151    fn test_to_http_content_type() {
152        assert_eq!(
153            to_http_content_type(APPLICATION_PKCS10),
154            Some("application/pkcs10")
155        );
156        assert_eq!(
157            to_http_content_type(APPLICATION_PKCS8),
158            Some("application/pkcs8")
159        );
160        assert_eq!(
161            to_http_content_type(APPLICATION_PKCS7_MIME_CERTS_ONLY),
162            Some("application/pkcs7-mime; smime-type=certs-only")
163        );
164        assert_eq!(
165            to_http_content_type(APPLICATION_CSRATTRS),
166            Some("application/csrattrs")
167        );
168        assert_eq!(to_http_content_type(MULTIPART_CORE), Some("multipart/core"));
169        assert_eq!(to_http_content_type(9999), None);
170    }
171
172    #[test]
173    fn test_from_http_content_type_exact() {
174        assert_eq!(
175            from_http_content_type("application/pkcs10"),
176            Some(APPLICATION_PKCS10)
177        );
178        assert_eq!(
179            from_http_content_type("application/pkcs8"),
180            Some(APPLICATION_PKCS8)
181        );
182        assert_eq!(
183            from_http_content_type("application/csrattrs"),
184            Some(APPLICATION_CSRATTRS)
185        );
186    }
187
188    #[test]
189    fn test_from_http_content_type_case_insensitive() {
190        assert_eq!(
191            from_http_content_type("Application/PKCS10"),
192            Some(APPLICATION_PKCS10)
193        );
194        assert_eq!(
195            from_http_content_type("APPLICATION/PKCS8"),
196            Some(APPLICATION_PKCS8)
197        );
198    }
199
200    #[test]
201    fn test_from_http_content_type_pkcs7_variants() {
202        assert_eq!(
203            from_http_content_type("application/pkcs7-mime; smime-type=certs-only"),
204            Some(APPLICATION_PKCS7_MIME_CERTS_ONLY)
205        );
206        assert_eq!(
207            from_http_content_type("application/pkcs7-mime; smimeType=CMC-Request"),
208            Some(APPLICATION_PKCS7_MIME_CMC_REQUEST)
209        );
210        assert_eq!(
211            from_http_content_type("application/pkcs7-mime; smimeType=CMC-Response"),
212            Some(APPLICATION_PKCS7_MIME_CMC_RESPONSE)
213        );
214        assert_eq!(
215            from_http_content_type("application/pkcs7-mime; smime-type=server-generated"),
216            Some(APPLICATION_PKCS7_MIME_SERVER_GEN_TYPE)
217        );
218        assert_eq!(
219            from_http_content_type("application/pkcs7-mime"),
220            Some(APPLICATION_PKCS7_MIME)
221        );
222    }
223
224    #[test]
225    fn test_from_http_content_type_unknown() {
226        assert_eq!(from_http_content_type("text/plain"), None);
227        assert_eq!(from_http_content_type("application/json"), None);
228    }
229
230    #[test]
231    fn test_roundtrip_all_formats() {
232        let ids = [
233            APPLICATION_PKCS7_MIME_SERVER_GEN_TYPE,
234            APPLICATION_PKCS7_MIME_CERTS_ONLY,
235            APPLICATION_PKCS7_MIME_CMC_REQUEST,
236            APPLICATION_PKCS7_MIME_CMC_RESPONSE,
237            APPLICATION_PKCS7_MIME,
238            APPLICATION_PKCS10,
239            APPLICATION_PKCS8,
240            APPLICATION_CSRATTRS,
241            MULTIPART_CORE,
242        ];
243
244        for id in ids {
245            let mime = to_http_content_type(id).expect("known format should have MIME type");
246            let back = from_http_content_type(mime).expect("known MIME type should have format ID");
247            assert_eq!(back, id, "roundtrip failed for format ID {id}: {mime}");
248        }
249    }
250}