1use super::Hash;
2use crate::{cel::DefaultResponseCertificationType, DefaultResponseCertification, HttpResponse};
3use ic_representation_independent_hash::{hash, representation_independent_hash, Value};
4
5pub const CERTIFICATE_HEADER_NAME: &str = "IC-Certificate";
7
8pub const CERTIFICATE_EXPRESSION_HEADER_NAME: &str = "IC-CertificateExpression";
10
11pub const RESPONSE_STATUS_PSEUDO_HEADER_NAME: &str = ":ic-cert-status";
14
15#[derive(Debug)]
17pub struct ResponseHeaders {
18 pub headers: Vec<(String, String)>,
20 pub certificate: Option<String>,
22}
23
24pub fn filter_response_headers(
27 response: &HttpResponse,
28 response_certification: &DefaultResponseCertification<'_>,
29) -> ResponseHeaders {
30 let headers_filter: Box<dyn Fn(_) -> _> = match response_certification.get_type() {
31 DefaultResponseCertificationType::CertifiedResponseHeaders(headers_to_include) => {
32 Box::new(move |header_name: &String| {
33 headers_to_include.iter().any(|header_to_include| {
34 header_to_include.eq_ignore_ascii_case(&header_name.to_string())
35 })
36 })
37 }
38 DefaultResponseCertificationType::ResponseHeaderExclusions(headers_to_exclude) => {
39 Box::new(move |header_name: &String| {
40 !headers_to_exclude.iter().any(|header_to_exclude| {
41 header_to_exclude.eq_ignore_ascii_case(&header_name.to_string())
42 })
43 })
44 }
45 };
46
47 let mut response_headers = ResponseHeaders {
48 headers: vec![],
49 certificate: None,
50 };
51
52 response_headers.headers = response
53 .headers()
54 .iter()
55 .filter_map(|(header_name, header_value)| {
56 let is_certificate_header = header_name
57 .to_string()
58 .eq_ignore_ascii_case(CERTIFICATE_HEADER_NAME);
59 if is_certificate_header {
60 response_headers.certificate = Some(header_value.into());
61 return None;
62 }
63
64 let is_certificate_expression_header = header_name
65 .to_string()
66 .eq_ignore_ascii_case(CERTIFICATE_EXPRESSION_HEADER_NAME);
67 if is_certificate_expression_header {
68 return Some((
69 header_name.to_string().to_ascii_lowercase(),
70 String::from(header_value),
71 ));
72 }
73
74 if headers_filter(header_name) {
75 return Some((
76 header_name.to_string().to_ascii_lowercase(),
77 String::from(header_value),
78 ));
79 }
80
81 None
82 })
83 .collect();
84
85 response_headers
86}
87
88pub fn response_headers_hash(status_code: &u64, response_headers: &ResponseHeaders) -> Hash {
92 let mut headers_to_verify: Vec<(String, Value)> = response_headers
93 .headers
94 .iter()
95 .map(|(header_name, header_value)| {
96 (
97 header_name.to_string(),
98 Value::String(String::from(header_value)),
99 )
100 })
101 .collect();
102
103 headers_to_verify.push((
104 RESPONSE_STATUS_PSEUDO_HEADER_NAME.into(),
105 Value::Number(*status_code),
106 ));
107
108 representation_independent_hash(&headers_to_verify)
109}
110
111pub fn response_hash_from_headers(
144 certified_headers: &[(String, Value)],
145 status_code: u16,
146 body_hash: &Hash,
147) -> Hash {
148 debug_assert!(
149 !certified_headers
150 .iter()
151 .any(|(k, _)| k == RESPONSE_STATUS_PSEUDO_HEADER_NAME),
152 "certified_headers must not contain the status pseudo-header; it is added internally"
153 );
154 let mut headers = Vec::with_capacity(certified_headers.len() + 1);
155 headers.extend_from_slice(certified_headers);
156 headers.push((
157 RESPONSE_STATUS_PSEUDO_HEADER_NAME.into(),
158 Value::Number(status_code.into()),
159 ));
160 let header_hash = representation_independent_hash(&headers);
161 hash(
162 [header_hash.as_ref(), body_hash.as_ref()]
163 .concat()
164 .as_slice(),
165 )
166}
167
168pub fn response_hash(
175 response: &HttpResponse,
176 response_certification: &DefaultResponseCertification,
177 response_body_hash: Option<Hash>,
178) -> Hash {
179 let response_body_hash = response_body_hash.unwrap_or_else(|| hash(response.body()));
180
181 let filtered_headers = filter_response_headers(response, response_certification);
182 let concatenated_hashes = [
183 response_headers_hash(&response.status_code().as_u16().into(), &filtered_headers),
184 response_body_hash,
185 ]
186 .concat();
187
188 hash(concatenated_hashes.as_slice())
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 const HELLO_WORLD_BODY: &[u8] = &[72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33];
196 const CERTIFICATE: &str = "certificate=:SGVsbG8gQ2VydGlmaWNhdGUh:,tree=:SGVsbG8gVHJlZSE=:";
197 const HEADER_EXCLUSIONS_CEL_EXPRESSION: &str = r#"
198 default_certification (
199 ValidationArgs {
200 certification: Certification {
201 no_request_certification: Empty {},
202 response_certification: ResponseCertification {
203 response_header_exclusions: ResponseHeaderList {
204 headers: ["Content-Security-Policy"]
205 }
206 }
207 }
208 }
209 )
210 "#;
211 const CERTIFIED_HEADERS_CEL_EXPRESSION: &str = r#"
212 default_certification (
213 ValidationArgs {
214 certification: Certification {
215 no_request_certification: Empty {},
216 response_certification: ResponseCertification {
217 certified_response_headers: ResponseHeaderList {
218 headers: ["Accept-Encoding", "Cache-Control"]
219 }
220 }
221 }
222 }
223 )
224 "#;
225
226 #[test]
227 fn response_with_certified_headers_without_excluded_headers() {
228 let response_certification =
229 DefaultResponseCertification::certified_response_headers(vec!["Accept-Encoding"]);
230 let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
231 let response_headers = filter_response_headers(&response, &response_certification);
232
233 assert_eq!(
234 response_headers.headers,
235 vec![
236 (
237 CERTIFICATE_EXPRESSION_HEADER_NAME.to_lowercase(),
238 remove_whitespace(CERTIFIED_HEADERS_CEL_EXPRESSION),
239 ),
240 ("accept-encoding".into(), "gzip".into()),
241 ]
242 );
243 }
244
245 #[test]
246 fn response_with_certified_headers() {
247 let response_certification =
248 DefaultResponseCertification::certified_response_headers(vec![
249 "Accept-Encoding",
250 "Cache-Control",
251 ]);
252 let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
253 let response_headers = filter_response_headers(&response, &response_certification);
254
255 assert_eq!(
256 response_headers.headers,
257 vec![
258 (
259 CERTIFICATE_EXPRESSION_HEADER_NAME.to_lowercase(),
260 remove_whitespace(CERTIFIED_HEADERS_CEL_EXPRESSION),
261 ),
262 ("accept-encoding".into(), "gzip".into()),
263 ("cache-control".into(), "no-cache".into()),
264 ("cache-control".into(), "no-store".into()),
265 ]
266 );
267 }
268
269 #[test]
270 fn response_hash_with_certified_headers() {
271 let response_certification =
272 DefaultResponseCertification::certified_response_headers(vec![
273 "Accept-Encoding",
274 "Cache-Control",
275 ]);
276 let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
277 let expected_hash =
278 hex::decode("3393250e3cedc30408dcb7e8963898c3d7549b8a0b76496b82fdfeae99c2ac78")
279 .unwrap();
280
281 let result = response_hash(&response, &response_certification, None);
282
283 assert_eq!(result, expected_hash.as_slice());
284 }
285
286 #[test]
287 fn response_hash_with_certified_headers_without_excluded_headers() {
288 let response_certification =
289 DefaultResponseCertification::certified_response_headers(vec!["Accept-Encoding"]);
290 let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
291 let response_without_excluded_headers = HttpResponse::ok(
292 HELLO_WORLD_BODY,
293 vec![
294 (
295 CERTIFICATE_EXPRESSION_HEADER_NAME.into(),
296 remove_whitespace(CERTIFIED_HEADERS_CEL_EXPRESSION),
297 ),
298 ("Accept-Encoding".into(), "gzip".into()),
299 ],
300 )
301 .build();
302
303 let result = response_hash(&response, &response_certification, None);
304 let result_without_excluded_headers = response_hash(
305 &response_without_excluded_headers,
306 &response_certification,
307 None,
308 );
309
310 assert_eq!(result, result_without_excluded_headers);
311 }
312
313 #[test]
314 fn response_hash_with_header_exclusions() {
315 let response_certification =
316 DefaultResponseCertification::response_header_exclusions(vec![
317 "Accept-Encoding",
318 "Cache-Control",
319 ]);
320 let response = create_response(HEADER_EXCLUSIONS_CEL_EXPRESSION);
321 let expected_hash =
322 hex::decode("a2ffb50ef8971650c2fb46c0a2788b7d5ac5a027d635175e8e06b419ce6c4cda")
323 .unwrap();
324
325 let result = response_hash(&response, &response_certification, None);
326
327 assert_eq!(result, expected_hash.as_slice());
328 }
329
330 #[test]
331 fn response_hash_with_header_exclusions_without_excluded_headers() {
332 let response_certification =
333 DefaultResponseCertification::response_header_exclusions(vec![
334 "Content-Security-Policy",
335 ]);
336 let response = create_response(HEADER_EXCLUSIONS_CEL_EXPRESSION);
337 let response_without_excluded_headers = HttpResponse::ok(
338 HELLO_WORLD_BODY,
339 vec![
340 (
341 CERTIFICATE_EXPRESSION_HEADER_NAME.into(),
342 remove_whitespace(HEADER_EXCLUSIONS_CEL_EXPRESSION),
343 ),
344 ("Accept-Encoding".into(), "gzip".into()),
345 ("Cache-Control".into(), "no-cache".into()),
346 ("Cache-Control".into(), "no-store".into()),
347 ],
348 )
349 .build();
350
351 let result = response_hash(&response, &response_certification, None);
352 let result_without_excluded_headers = response_hash(
353 &response_without_excluded_headers,
354 &response_certification,
355 None,
356 );
357
358 assert_eq!(result, result_without_excluded_headers);
359 }
360
361 #[test]
362 fn response_headers_hash_with_certified_headers() {
363 let response_certification =
364 DefaultResponseCertification::certified_response_headers(vec![
365 "Accept-Encoding",
366 "Cache-Control",
367 ]);
368 let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
369 let expected_hash =
370 hex::decode("eac859a99d5bd7b71f46dbacecff4aaa0a7a7131802c136a77a76c8e018af5f7")
371 .unwrap();
372
373 let filtered_headers = filter_response_headers(&response, &response_certification);
374 let result =
375 response_headers_hash(&response.status_code().as_u16().into(), &filtered_headers);
376
377 assert_eq!(result, expected_hash.as_slice());
378 }
379
380 #[test]
381 fn response_headers_hash_with_certified_headers_without_excluded_headers() {
382 let response_certification =
383 DefaultResponseCertification::certified_response_headers(vec!["Accept-Encoding"]);
384 let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
385 let response_without_excluded_headers = HttpResponse::ok(
386 HELLO_WORLD_BODY,
387 vec![
388 (CERTIFICATE_HEADER_NAME.into(), CERTIFICATE.into()),
389 (
390 CERTIFICATE_EXPRESSION_HEADER_NAME.into(),
391 remove_whitespace(CERTIFIED_HEADERS_CEL_EXPRESSION),
392 ),
393 ("Accept-Encoding".into(), "gzip".into()),
394 ],
395 )
396 .build();
397
398 let filtered_headers = filter_response_headers(&response, &response_certification);
399 let result =
400 response_headers_hash(&response.status_code().as_u16().into(), &filtered_headers);
401 let filtered_headers_without_excluded_headers =
402 filter_response_headers(&response_without_excluded_headers, &response_certification);
403 let result_without_excluded_headers = response_headers_hash(
404 &response_without_excluded_headers
405 .status_code()
406 .as_u16()
407 .into(),
408 &filtered_headers_without_excluded_headers,
409 );
410
411 assert_eq!(result, result_without_excluded_headers);
412 }
413
414 #[test]
415 fn response_headers_hash_with_header_exclusions() {
416 let response_certification =
417 DefaultResponseCertification::response_header_exclusions(vec![
418 "Accept-Encoding",
419 "Cache-Control",
420 ]);
421 let response = create_response(HEADER_EXCLUSIONS_CEL_EXPRESSION);
422 let expected_hash =
423 hex::decode("d618f70bf2578d5a672374ffbaade3910e858384f42d01ac2863946ab596bcac")
424 .unwrap();
425
426 let filtered_headers = filter_response_headers(&response, &response_certification);
427 let result =
428 response_headers_hash(&response.status_code().as_u16().into(), &filtered_headers);
429
430 assert_eq!(result, expected_hash.as_slice());
431 }
432
433 #[test]
434 fn response_headers_hash_with_header_exclusions_without_excluded_headers() {
435 let response_certification =
436 DefaultResponseCertification::response_header_exclusions(vec![
437 "Content-Security-Policy",
438 ]);
439 let response = create_response(HEADER_EXCLUSIONS_CEL_EXPRESSION);
440 let response_without_excluded_headers = HttpResponse::ok(
441 HELLO_WORLD_BODY,
442 vec![
443 (CERTIFICATE_HEADER_NAME.into(), CERTIFICATE.into()),
444 (
445 CERTIFICATE_EXPRESSION_HEADER_NAME.into(),
446 remove_whitespace(HEADER_EXCLUSIONS_CEL_EXPRESSION),
447 ),
448 ("Accept-Encoding".into(), "gzip".into()),
449 ("Cache-Control".into(), "no-cache".into()),
450 ("Cache-Control".into(), "no-store".into()),
451 ],
452 )
453 .build();
454
455 let filtered_headers = filter_response_headers(&response, &response_certification);
456 let result =
457 response_headers_hash(&response.status_code().as_u16().into(), &filtered_headers);
458
459 let response_headers_without_excluded_headers =
460 filter_response_headers(&response_without_excluded_headers, &response_certification);
461 let result_without_excluded_headers = response_headers_hash(
462 &response_without_excluded_headers
463 .status_code()
464 .as_u16()
465 .into(),
466 &response_headers_without_excluded_headers,
467 );
468
469 assert_eq!(result, result_without_excluded_headers);
470 }
471
472 #[test]
473 fn response_hash_with_body_hash_override() {
474 let response_certification =
475 DefaultResponseCertification::certified_response_headers(vec![
476 "Accept-Encoding",
477 "Cache-Control",
478 ]);
479 let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
480 let response_body_hash: Hash =
481 hex::decode("5462fc394013080effc31d578ec3fff8b44cdf24738b38a77ce4afacbc93a7f5")
482 .unwrap()
483 .try_into()
484 .unwrap();
485 let expected_hash =
486 hex::decode("1afc744a377cb8785d1078f53f9bbc9160d86b7a05f490e42c89366326eaef20")
487 .unwrap();
488
489 let result = response_hash(&response, &response_certification, Some(response_body_hash));
490
491 assert_eq!(result, expected_hash.as_slice());
492 }
493
494 #[test]
495 fn response_hash_from_headers_matches_response_hash() {
496 let response_certification =
497 DefaultResponseCertification::certified_response_headers(vec![
498 "Accept-Encoding",
499 "Cache-Control",
500 ]);
501 let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
502 let expected = response_hash(&response, &response_certification, None);
503
504 let filtered = filter_response_headers(&response, &response_certification);
505 let header_pairs: Vec<(String, Value)> = filtered
506 .headers
507 .iter()
508 .map(|(k, v)| (k.clone(), Value::String(v.clone())))
509 .collect();
510 let body_hash = hash(response.body());
511 let result =
512 response_hash_from_headers(&header_pairs, response.status_code().as_u16(), &body_hash);
513
514 assert_eq!(result, expected);
515 }
516
517 fn create_response(cel_expression: &str) -> HttpResponse {
518 HttpResponse::ok(
519 HELLO_WORLD_BODY,
520 vec![
521 (CERTIFICATE_HEADER_NAME.into(), CERTIFICATE.into()),
522 (
523 CERTIFICATE_EXPRESSION_HEADER_NAME.into(),
524 remove_whitespace(cel_expression),
525 ),
526 ("Accept-Encoding".into(), "gzip".into()),
527 ("Cache-Control".into(), "no-cache".into()),
528 ("Cache-Control".into(), "no-store".into()),
529 (
530 "Content-Security-Policy".into(),
531 "default-src 'self'".into(),
532 ),
533 ],
534 )
535 .build()
536 }
537
538 fn remove_whitespace(s: &str) -> String {
543 s.chars().filter(|c| !c.is_whitespace()).collect()
544 }
545}