Skip to main content

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    /// Creates a response-only certification from pre-computed hashes.
94    ///
95    /// Unlike [`response_only`](HttpCertification::response_only), this constructor does not
96    /// require an [`HttpResponse`] and performs no validation. Use it when the CEL expression
97    /// hash and response hash have already been computed externally (e.g. via
98    /// [`response_hash_from_headers`](crate::response_hash_from_headers)).
99    pub fn response_only_prehashed(cel_expr_hash: Hash, response_hash: Hash) -> Self {
100        Self(HttpCertificationType::ResponseOnly {
101            cel_expr_hash,
102            response_hash,
103        })
104    }
105
106    pub(crate) fn to_tree_path(self) -> Vec<Vec<u8>> {
107        match self.0 {
108            HttpCertificationType::Skip { cel_expr_hash } => vec![cel_expr_hash.to_vec()],
109            HttpCertificationType::ResponseOnly {
110                cel_expr_hash,
111                response_hash,
112            } => vec![
113                cel_expr_hash.to_vec(),
114                "".as_bytes().to_vec(),
115                response_hash.to_vec(),
116            ],
117            HttpCertificationType::Full {
118                cel_expr_hash,
119                request_hash,
120                response_hash,
121            } => vec![
122                cel_expr_hash.to_vec(),
123                request_hash.to_vec(),
124                response_hash.to_vec(),
125            ],
126        }
127    }
128
129    fn validate_response(response: &HttpResponse, cel_expr: &str) -> HttpCertificationResult {
130        let mut found_header = false;
131
132        for (header_name, header_value) in response.headers() {
133            if header_name.to_lowercase() == CERTIFICATE_EXPRESSION_HEADER_NAME.to_lowercase() {
134                match header_value == cel_expr {
135                    true => {
136                        if found_header {
137                            return Err(
138                                HttpCertificationError::MultipleCertificateExpressionHeaders {
139                                    expected: cel_expr.to_string(),
140                                },
141                            );
142                        }
143
144                        found_header = true;
145                    }
146                    false => {
147                        return Err(
148                            HttpCertificationError::CertificateExpressionHeaderMismatch {
149                                expected: cel_expr.to_string(),
150                                actual: header_value.clone(),
151                            },
152                        )
153                    }
154                };
155            }
156        }
157
158        if found_header {
159            Ok(())
160        } else {
161            Err(HttpCertificationError::CertificateExpressionHeaderMissing {
162                expected: cel_expr.to_string(),
163            })
164        }
165    }
166}
167
168impl<'a> From<HttpCertification> for Cow<'a, HttpCertification> {
169    fn from(cert: HttpCertification) -> Cow<'a, HttpCertification> {
170        Cow::Owned(cert)
171    }
172}
173
174impl<'a> From<&'a HttpCertification> for Cow<'a, HttpCertification> {
175    fn from(cert: &'a HttpCertification) -> Cow<'a, HttpCertification> {
176        Cow::Borrowed(cert)
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::{DefaultResponseCertification, StatusCode};
184    use rstest::*;
185
186    #[rstest]
187    fn no_certification() {
188        let cel_expr = DefaultCelBuilder::skip_certification().to_string();
189        let expected_cel_expr_hash = hash(cel_expr.as_bytes());
190
191        let result = HttpCertification::skip();
192
193        assert!(matches!(
194            result.0,
195            HttpCertificationType::Skip { cel_expr_hash } if cel_expr_hash == expected_cel_expr_hash
196        ));
197        assert_eq!(result.to_tree_path(), vec![expected_cel_expr_hash.to_vec()]);
198    }
199
200    #[rstest]
201    fn response_only_certification() {
202        let cel_expr = DefaultCelBuilder::response_only_certification()
203            .with_response_certification(DefaultResponseCertification::certified_response_headers(
204                vec!["ETag", "Cache-Control"],
205            ))
206            .build();
207        let expected_cel_expr_hash = hash(cel_expr.to_string().as_bytes());
208
209        let response = &HttpResponse::builder()
210            .with_status_code(StatusCode::OK)
211            .with_headers(vec![(
212                CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
213                cel_expr.to_string(),
214            )])
215            .build();
216        let expected_response_hash = response_hash(response, &cel_expr.response, None);
217
218        let result = HttpCertification::response_only(&cel_expr, response, None).unwrap();
219
220        assert!(matches!(
221            result.0,
222            HttpCertificationType::ResponseOnly {
223                cel_expr_hash,
224                response_hash
225            } if cel_expr_hash == expected_cel_expr_hash &&
226                response_hash == expected_response_hash
227        ));
228        assert_eq!(
229            result.to_tree_path(),
230            vec![
231                expected_cel_expr_hash.to_vec(),
232                "".as_bytes().to_vec(),
233                expected_response_hash.to_vec()
234            ]
235        );
236    }
237
238    #[rstest]
239    fn response_only_certification_without_expression_header() {
240        let cel_expr = DefaultCelBuilder::response_only_certification()
241            .with_response_certification(DefaultResponseCertification::certified_response_headers(
242                vec!["ETag", "Cache-Control"],
243            ))
244            .build();
245
246        let response = &HttpResponse::builder()
247            .with_status_code(StatusCode::OK)
248            .build();
249
250        let result = HttpCertification::response_only(&cel_expr, response, None).unwrap_err();
251
252        assert!(matches!(
253            result,
254            HttpCertificationError::CertificateExpressionHeaderMissing { expected } if expected == cel_expr.to_string()
255        ));
256    }
257
258    #[rstest]
259    fn response_only_certification_with_wrong_expression_header() {
260        let cel_expr = DefaultCelBuilder::response_only_certification()
261            .with_response_certification(DefaultResponseCertification::certified_response_headers(
262                vec!["ETag", "Cache-Control"],
263            ))
264            .build();
265        let wrong_cel_expr = DefaultCelBuilder::full_certification().build();
266
267        let response = &HttpResponse::builder()
268            .with_status_code(StatusCode::OK)
269            .with_headers(vec![(
270                CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
271                wrong_cel_expr.to_string(),
272            )])
273            .build();
274
275        let result = HttpCertification::response_only(&cel_expr, response, None).unwrap_err();
276
277        assert!(matches!(
278            result,
279            HttpCertificationError::CertificateExpressionHeaderMismatch { expected, actual }
280                if expected == cel_expr.to_string()
281                && actual == wrong_cel_expr.to_string()
282        ));
283    }
284
285    #[rstest]
286    fn response_only_certification_with_multiple_expression_headers() {
287        let cel_expr = DefaultCelBuilder::response_only_certification()
288            .with_response_certification(DefaultResponseCertification::certified_response_headers(
289                vec!["ETag", "Cache-Control"],
290            ))
291            .build();
292        let response = &HttpResponse::builder()
293            .with_status_code(StatusCode::OK)
294            .with_headers(vec![
295                (
296                    CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
297                    cel_expr.to_string(),
298                ),
299                (
300                    CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
301                    cel_expr.to_string(),
302                ),
303            ])
304            .build();
305
306        let result = HttpCertification::response_only(&cel_expr, response, None).unwrap_err();
307
308        assert!(matches!(
309            result,
310            HttpCertificationError::MultipleCertificateExpressionHeaders { expected } if expected == cel_expr.to_string()
311        ));
312    }
313
314    #[rstest]
315    fn response_only_prehashed_matches_response_only() {
316        let cel_expr = DefaultCelBuilder::response_only_certification()
317            .with_response_certification(DefaultResponseCertification::certified_response_headers(
318                vec!["ETag", "Cache-Control"],
319            ))
320            .build();
321
322        let response = &HttpResponse::builder()
323            .with_status_code(StatusCode::OK)
324            .with_headers(vec![(
325                CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
326                cel_expr.to_string(),
327            )])
328            .build();
329
330        let from_response = HttpCertification::response_only(&cel_expr, response, None).unwrap();
331
332        let cel_expr_hash = hash(cel_expr.to_string().as_bytes());
333        let resp_hash = response_hash(response, &cel_expr.response, None);
334        let from_prehashed = HttpCertification::response_only_prehashed(cel_expr_hash, resp_hash);
335
336        assert_eq!(from_prehashed, from_response);
337        assert_eq!(from_prehashed.to_tree_path(), from_response.to_tree_path());
338    }
339
340    #[rstest]
341    fn full_certification() {
342        let cel_expr = DefaultCelBuilder::full_certification()
343            .with_request_headers(vec!["If-Match"])
344            .with_request_query_parameters(vec!["foo", "bar", "baz"])
345            .with_response_certification(DefaultResponseCertification::certified_response_headers(
346                vec!["ETag", "Cache-Control"],
347            ))
348            .build();
349        let expected_cel_expr_hash = hash(cel_expr.to_string().as_bytes());
350
351        let request = &HttpRequest::get("/index.html").build();
352        let expected_request_hash = request_hash(request, &cel_expr.request).unwrap();
353
354        let response = &HttpResponse::builder()
355            .with_status_code(StatusCode::OK)
356            .with_headers(vec![(
357                CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
358                cel_expr.to_string(),
359            )])
360            .build();
361        let expected_response_hash = response_hash(response, &cel_expr.response, None);
362
363        let result = HttpCertification::full(&cel_expr, request, response, None).unwrap();
364
365        assert!(matches!(
366            result.0,
367            HttpCertificationType::Full {
368                cel_expr_hash,
369                request_hash,
370                response_hash
371            } if cel_expr_hash == expected_cel_expr_hash &&
372                request_hash == expected_request_hash &&
373                response_hash == expected_response_hash
374        ));
375        assert_eq!(
376            result.to_tree_path(),
377            vec![
378                expected_cel_expr_hash.to_vec(),
379                expected_request_hash.to_vec(),
380                expected_response_hash.to_vec()
381            ]
382        );
383    }
384
385    #[rstest]
386    fn full_certification_without_expression_header() {
387        let cel_expr = DefaultCelBuilder::full_certification()
388            .with_request_headers(vec!["If-Match"])
389            .with_request_query_parameters(vec!["foo", "bar", "baz"])
390            .with_response_certification(DefaultResponseCertification::certified_response_headers(
391                vec!["ETag", "Cache-Control"],
392            ))
393            .build();
394
395        let request = &HttpRequest::get("/index.html").build();
396
397        let response = &HttpResponse::builder()
398            .with_status_code(StatusCode::OK)
399            .build();
400
401        let result = HttpCertification::full(&cel_expr, request, response, None).unwrap_err();
402
403        assert!(matches!(
404            result,
405            HttpCertificationError::CertificateExpressionHeaderMissing { expected } if expected == cel_expr.to_string()
406        ));
407    }
408
409    #[rstest]
410    fn full_certification_with_wrong_expression_header() {
411        let cel_expr = DefaultCelBuilder::full_certification()
412            .with_request_headers(vec!["If-Match"])
413            .with_request_query_parameters(vec!["foo", "bar", "baz"])
414            .with_response_certification(DefaultResponseCertification::certified_response_headers(
415                vec!["ETag", "Cache-Control"],
416            ))
417            .build();
418        let wrong_cel_expr = DefaultCelBuilder::response_only_certification().build();
419
420        let request = &HttpRequest::get("/index.html").build();
421
422        let response = &HttpResponse::builder()
423            .with_status_code(StatusCode::OK)
424            .with_headers(vec![(
425                CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
426                wrong_cel_expr.to_string(),
427            )])
428            .build();
429
430        let result = HttpCertification::full(&cel_expr, request, response, None).unwrap_err();
431
432        assert!(matches!(
433            result,
434            HttpCertificationError::CertificateExpressionHeaderMismatch { expected, actual }
435                if expected == cel_expr.to_string()
436                && actual == wrong_cel_expr.to_string()
437        ));
438    }
439
440    #[rstest]
441    fn full_certification_with_multiple_expression_headers() {
442        let cel_expr = DefaultCelBuilder::full_certification()
443            .with_request_headers(vec!["If-Match"])
444            .with_request_query_parameters(vec!["foo", "bar", "baz"])
445            .with_response_certification(DefaultResponseCertification::certified_response_headers(
446                vec!["ETag", "Cache-Control"],
447            ))
448            .build();
449
450        let request = &HttpRequest::get("/index.html").build();
451
452        let response = &HttpResponse::builder()
453            .with_status_code(StatusCode::OK)
454            .with_headers(vec![
455                (
456                    CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
457                    cel_expr.to_string(),
458                ),
459                (
460                    CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
461                    cel_expr.to_string(),
462                ),
463            ])
464            .build();
465        let result = HttpCertification::full(&cel_expr, request, response, None).unwrap_err();
466
467        assert!(matches!(
468            result,
469            HttpCertificationError::MultipleCertificateExpressionHeaders { expected } if expected == cel_expr.to_string()
470        ));
471    }
472}