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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct HttpCertification(HttpCertificationType);
41
42impl HttpCertification {
43 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 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 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 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}