ic_http_certification/tree/
certification.rs

1use crate::{
2    request_hash, response_hash, DefaultCelBuilder, DefaultFullCelExpression,
3    DefaultResponseOnlyCelExpression, HttpCertificationError, HttpCertificationResult, HttpRequest,
4    HttpResponse, CERTIFICATE_EXPRESSION_HEADER_NAME,
5};
6use ic_certification::Hash;
7use ic_representation_independent_hash::hash;
8use std::borrow::Cow;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11enum HttpCertificationType {
12    Skip {
13        cel_expr_hash: Hash,
14    },
15    ResponseOnly {
16        cel_expr_hash: Hash,
17        response_hash: Hash,
18    },
19    Full {
20        cel_expr_hash: Hash,
21        request_hash: Hash,
22        response_hash: Hash,
23    },
24}
25
26/// A certified [HttpRequest] and [HttpResponse] pair.
27///
28/// It supports three types of certification via associated functions:
29///
30/// - [skip()](HttpCertification::skip()) excludes both an [HttpRequest] and the
31/// corresponding [HttpResponse] from certification.
32///
33/// - [response_only()](HttpCertification::response_only()) includes an
34/// [HttpResponse] but excludes the corresponding [HttpRequest]
35/// from certification.
36///
37/// - [full()](HttpCertification::full()) includes both an [HttpResponse] and
38/// the corresponding [HttpRequest] in certification.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct HttpCertification(HttpCertificationType);
41
42impl HttpCertification {
43    /// Creates a certification that excludes both the [HttpRequest] and
44    /// the corresponding [HttpResponse].
45    pub fn skip() -> HttpCertification {
46        let cel_expr = DefaultCelBuilder::skip_certification().to_string();
47        let cel_expr_hash = hash(cel_expr.as_bytes());
48
49        Self(HttpCertificationType::Skip { cel_expr_hash })
50    }
51
52    /// Creates a certification that includes an [HttpResponse], but excludes the
53    /// corresponding [HttpRequest].
54    pub fn response_only(
55        cel_expr_def: &DefaultResponseOnlyCelExpression,
56        response: &HttpResponse,
57        response_body_hash: Option<Hash>,
58    ) -> HttpCertificationResult<HttpCertification> {
59        let cel_expr = cel_expr_def.to_string();
60        Self::validate_response(response, &cel_expr)?;
61
62        let cel_expr_hash = hash(cel_expr.as_bytes());
63        let response_hash = response_hash(response, &cel_expr_def.response, response_body_hash);
64
65        Ok(Self(HttpCertificationType::ResponseOnly {
66            cel_expr_hash,
67            response_hash,
68        }))
69    }
70
71    /// Creates a certification that includes both an [HttpResponse] and the corresponding
72    /// [HttpRequest].
73    pub fn full(
74        cel_expr_def: &DefaultFullCelExpression,
75        request: &HttpRequest,
76        response: &HttpResponse,
77        response_body_hash: Option<Hash>,
78    ) -> HttpCertificationResult<HttpCertification> {
79        let cel_expr = cel_expr_def.to_string();
80        Self::validate_response(response, &cel_expr)?;
81
82        let cel_expr_hash = hash(cel_expr.as_bytes());
83        let request_hash = request_hash(request, &cel_expr_def.request)?;
84        let response_hash = response_hash(response, &cel_expr_def.response, response_body_hash);
85
86        Ok(Self(HttpCertificationType::Full {
87            cel_expr_hash,
88            request_hash,
89            response_hash,
90        }))
91    }
92
93    pub(crate) fn to_tree_path(self) -> Vec<Vec<u8>> {
94        match self.0 {
95            HttpCertificationType::Skip { cel_expr_hash } => vec![cel_expr_hash.to_vec()],
96            HttpCertificationType::ResponseOnly {
97                cel_expr_hash,
98                response_hash,
99            } => vec![
100                cel_expr_hash.to_vec(),
101                "".as_bytes().to_vec(),
102                response_hash.to_vec(),
103            ],
104            HttpCertificationType::Full {
105                cel_expr_hash,
106                request_hash,
107                response_hash,
108            } => vec![
109                cel_expr_hash.to_vec(),
110                request_hash.to_vec(),
111                response_hash.to_vec(),
112            ],
113        }
114    }
115
116    fn validate_response(response: &HttpResponse, cel_expr: &str) -> HttpCertificationResult {
117        let mut found_header = false;
118
119        for (header_name, header_value) in response.headers() {
120            if header_name.to_lowercase() == CERTIFICATE_EXPRESSION_HEADER_NAME.to_lowercase() {
121                match header_value == cel_expr {
122                    true => {
123                        if found_header {
124                            return Err(
125                                HttpCertificationError::MultipleCertificateExpressionHeaders {
126                                    expected: cel_expr.to_string(),
127                                },
128                            );
129                        }
130
131                        found_header = true;
132                    }
133                    false => {
134                        return Err(
135                            HttpCertificationError::CertificateExpressionHeaderMismatch {
136                                expected: cel_expr.to_string(),
137                                actual: header_value.clone(),
138                            },
139                        )
140                    }
141                };
142            }
143        }
144
145        if found_header {
146            Ok(())
147        } else {
148            Err(HttpCertificationError::CertificateExpressionHeaderMissing {
149                expected: cel_expr.to_string(),
150            })
151        }
152    }
153}
154
155impl<'a> From<HttpCertification> for Cow<'a, HttpCertification> {
156    fn from(cert: HttpCertification) -> Cow<'a, HttpCertification> {
157        Cow::Owned(cert)
158    }
159}
160
161impl<'a> From<&'a HttpCertification> for Cow<'a, HttpCertification> {
162    fn from(cert: &'a HttpCertification) -> Cow<'a, HttpCertification> {
163        Cow::Borrowed(cert)
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use crate::{DefaultResponseCertification, StatusCode};
171    use rstest::*;
172
173    #[rstest]
174    fn no_certification() {
175        let cel_expr = DefaultCelBuilder::skip_certification().to_string();
176        let expected_cel_expr_hash = hash(cel_expr.as_bytes());
177
178        let result = HttpCertification::skip();
179
180        assert!(matches!(
181            result.0,
182            HttpCertificationType::Skip { cel_expr_hash } if cel_expr_hash == expected_cel_expr_hash
183        ));
184        assert_eq!(result.to_tree_path(), vec![expected_cel_expr_hash.to_vec()]);
185    }
186
187    #[rstest]
188    fn response_only_certification() {
189        let cel_expr = DefaultCelBuilder::response_only_certification()
190            .with_response_certification(DefaultResponseCertification::certified_response_headers(
191                vec!["ETag", "Cache-Control"],
192            ))
193            .build();
194        let expected_cel_expr_hash = hash(cel_expr.to_string().as_bytes());
195
196        let response = &HttpResponse::builder()
197            .with_status_code(StatusCode::OK)
198            .with_headers(vec![(
199                CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
200                cel_expr.to_string(),
201            )])
202            .build();
203        let expected_response_hash = response_hash(response, &cel_expr.response, None);
204
205        let result = HttpCertification::response_only(&cel_expr, response, None).unwrap();
206
207        assert!(matches!(
208            result.0,
209            HttpCertificationType::ResponseOnly {
210                cel_expr_hash,
211                response_hash
212            } if cel_expr_hash == expected_cel_expr_hash &&
213                response_hash == expected_response_hash
214        ));
215        assert_eq!(
216            result.to_tree_path(),
217            vec![
218                expected_cel_expr_hash.to_vec(),
219                "".as_bytes().to_vec(),
220                expected_response_hash.to_vec()
221            ]
222        );
223    }
224
225    #[rstest]
226    fn response_only_certification_without_expression_header() {
227        let cel_expr = DefaultCelBuilder::response_only_certification()
228            .with_response_certification(DefaultResponseCertification::certified_response_headers(
229                vec!["ETag", "Cache-Control"],
230            ))
231            .build();
232
233        let response = &HttpResponse::builder()
234            .with_status_code(StatusCode::OK)
235            .build();
236
237        let result = HttpCertification::response_only(&cel_expr, response, None).unwrap_err();
238
239        assert!(matches!(
240            result,
241            HttpCertificationError::CertificateExpressionHeaderMissing { expected } if expected == cel_expr.to_string()
242        ));
243    }
244
245    #[rstest]
246    fn response_only_certification_with_wrong_expression_header() {
247        let cel_expr = DefaultCelBuilder::response_only_certification()
248            .with_response_certification(DefaultResponseCertification::certified_response_headers(
249                vec!["ETag", "Cache-Control"],
250            ))
251            .build();
252        let wrong_cel_expr = DefaultCelBuilder::full_certification().build();
253
254        let response = &HttpResponse::builder()
255            .with_status_code(StatusCode::OK)
256            .with_headers(vec![(
257                CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
258                wrong_cel_expr.to_string(),
259            )])
260            .build();
261
262        let result = HttpCertification::response_only(&cel_expr, response, None).unwrap_err();
263
264        assert!(matches!(
265            result,
266            HttpCertificationError::CertificateExpressionHeaderMismatch { expected, actual }
267                if expected == cel_expr.to_string()
268                && actual == wrong_cel_expr.to_string()
269        ));
270    }
271
272    #[rstest]
273    fn response_only_certification_with_multiple_expression_headers() {
274        let cel_expr = DefaultCelBuilder::response_only_certification()
275            .with_response_certification(DefaultResponseCertification::certified_response_headers(
276                vec!["ETag", "Cache-Control"],
277            ))
278            .build();
279        let response = &HttpResponse::builder()
280            .with_status_code(StatusCode::OK)
281            .with_headers(vec![
282                (
283                    CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
284                    cel_expr.to_string(),
285                ),
286                (
287                    CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
288                    cel_expr.to_string(),
289                ),
290            ])
291            .build();
292
293        let result = HttpCertification::response_only(&cel_expr, response, None).unwrap_err();
294
295        assert!(matches!(
296            result,
297            HttpCertificationError::MultipleCertificateExpressionHeaders { expected } if expected == cel_expr.to_string()
298        ));
299    }
300
301    #[rstest]
302    fn full_certification() {
303        let cel_expr = DefaultCelBuilder::full_certification()
304            .with_request_headers(vec!["If-Match"])
305            .with_request_query_parameters(vec!["foo", "bar", "baz"])
306            .with_response_certification(DefaultResponseCertification::certified_response_headers(
307                vec!["ETag", "Cache-Control"],
308            ))
309            .build();
310        let expected_cel_expr_hash = hash(cel_expr.to_string().as_bytes());
311
312        let request = &HttpRequest::get("/index.html").build();
313        let expected_request_hash = request_hash(request, &cel_expr.request).unwrap();
314
315        let response = &HttpResponse::builder()
316            .with_status_code(StatusCode::OK)
317            .with_headers(vec![(
318                CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
319                cel_expr.to_string(),
320            )])
321            .build();
322        let expected_response_hash = response_hash(response, &cel_expr.response, None);
323
324        let result = HttpCertification::full(&cel_expr, request, response, None).unwrap();
325
326        assert!(matches!(
327            result.0,
328            HttpCertificationType::Full {
329                cel_expr_hash,
330                request_hash,
331                response_hash
332            } if cel_expr_hash == expected_cel_expr_hash &&
333                request_hash == expected_request_hash &&
334                response_hash == expected_response_hash
335        ));
336        assert_eq!(
337            result.to_tree_path(),
338            vec![
339                expected_cel_expr_hash.to_vec(),
340                expected_request_hash.to_vec(),
341                expected_response_hash.to_vec()
342            ]
343        );
344    }
345
346    #[rstest]
347    fn full_certification_without_expression_header() {
348        let cel_expr = DefaultCelBuilder::full_certification()
349            .with_request_headers(vec!["If-Match"])
350            .with_request_query_parameters(vec!["foo", "bar", "baz"])
351            .with_response_certification(DefaultResponseCertification::certified_response_headers(
352                vec!["ETag", "Cache-Control"],
353            ))
354            .build();
355
356        let request = &HttpRequest::get("/index.html").build();
357
358        let response = &HttpResponse::builder()
359            .with_status_code(StatusCode::OK)
360            .build();
361
362        let result = HttpCertification::full(&cel_expr, request, response, None).unwrap_err();
363
364        assert!(matches!(
365            result,
366            HttpCertificationError::CertificateExpressionHeaderMissing { expected } if expected == cel_expr.to_string()
367        ));
368    }
369
370    #[rstest]
371    fn full_certification_with_wrong_expression_header() {
372        let cel_expr = DefaultCelBuilder::full_certification()
373            .with_request_headers(vec!["If-Match"])
374            .with_request_query_parameters(vec!["foo", "bar", "baz"])
375            .with_response_certification(DefaultResponseCertification::certified_response_headers(
376                vec!["ETag", "Cache-Control"],
377            ))
378            .build();
379        let wrong_cel_expr = DefaultCelBuilder::response_only_certification().build();
380
381        let request = &HttpRequest::get("/index.html").build();
382
383        let response = &HttpResponse::builder()
384            .with_status_code(StatusCode::OK)
385            .with_headers(vec![(
386                CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
387                wrong_cel_expr.to_string(),
388            )])
389            .build();
390
391        let result = HttpCertification::full(&cel_expr, request, response, None).unwrap_err();
392
393        assert!(matches!(
394            result,
395            HttpCertificationError::CertificateExpressionHeaderMismatch { expected, actual }
396                if expected == cel_expr.to_string()
397                && actual == wrong_cel_expr.to_string()
398        ));
399    }
400
401    #[rstest]
402    fn full_certification_with_multiple_expression_headers() {
403        let cel_expr = DefaultCelBuilder::full_certification()
404            .with_request_headers(vec!["If-Match"])
405            .with_request_query_parameters(vec!["foo", "bar", "baz"])
406            .with_response_certification(DefaultResponseCertification::certified_response_headers(
407                vec!["ETag", "Cache-Control"],
408            ))
409            .build();
410
411        let request = &HttpRequest::get("/index.html").build();
412
413        let response = &HttpResponse::builder()
414            .with_status_code(StatusCode::OK)
415            .with_headers(vec![
416                (
417                    CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
418                    cel_expr.to_string(),
419                ),
420                (
421                    CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
422                    cel_expr.to_string(),
423                ),
424            ])
425            .build();
426        let result = HttpCertification::full(&cel_expr, request, response, None).unwrap_err();
427
428        assert!(matches!(
429            result,
430            HttpCertificationError::MultipleCertificateExpressionHeaders { expected } if expected == cel_expr.to_string()
431        ));
432    }
433}