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(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}