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
11const RESPONSE_STATUS_PSEUDO_HEADER_NAME: &str = ":ic-cert-status";
12
13#[derive(Debug)]
15pub struct ResponseHeaders {
16 pub headers: Vec<(String, String)>,
18 pub certificate: Option<String>,
20}
21
22pub fn filter_response_headers(
25 response: &HttpResponse,
26 response_certification: &DefaultResponseCertification<'_>,
27) -> ResponseHeaders {
28 let headers_filter: Box<dyn Fn(_) -> _> = match response_certification.get_type() {
29 DefaultResponseCertificationType::CertifiedResponseHeaders(headers_to_include) => {
30 Box::new(move |header_name: &String| {
31 headers_to_include.iter().any(|header_to_include| {
32 header_to_include.eq_ignore_ascii_case(&header_name.to_string())
33 })
34 })
35 }
36 DefaultResponseCertificationType::ResponseHeaderExclusions(headers_to_exclude) => {
37 Box::new(move |header_name: &String| {
38 !headers_to_exclude.iter().any(|header_to_exclude| {
39 header_to_exclude.eq_ignore_ascii_case(&header_name.to_string())
40 })
41 })
42 }
43 };
44
45 let mut response_headers = ResponseHeaders {
46 headers: vec![],
47 certificate: None,
48 };
49
50 response_headers.headers = response
51 .headers()
52 .iter()
53 .filter_map(|(header_name, header_value)| {
54 let is_certificate_header = header_name
55 .to_string()
56 .eq_ignore_ascii_case(CERTIFICATE_HEADER_NAME);
57 if is_certificate_header {
58 response_headers.certificate = Some(header_value.into());
59 return None;
60 }
61
62 let is_certificate_expression_header = header_name
63 .to_string()
64 .eq_ignore_ascii_case(CERTIFICATE_EXPRESSION_HEADER_NAME);
65 if is_certificate_expression_header {
66 return Some((
67 header_name.to_string().to_ascii_lowercase(),
68 String::from(header_value),
69 ));
70 }
71
72 if headers_filter(header_name) {
73 return Some((
74 header_name.to_string().to_ascii_lowercase(),
75 String::from(header_value),
76 ));
77 }
78
79 None
80 })
81 .collect();
82
83 response_headers
84}
85
86pub fn response_headers_hash(status_code: &u64, response_headers: &ResponseHeaders) -> Hash {
90 let mut headers_to_verify: Vec<(String, Value)> = response_headers
91 .headers
92 .iter()
93 .map(|(header_name, header_value)| {
94 (
95 header_name.to_string(),
96 Value::String(String::from(header_value)),
97 )
98 })
99 .collect();
100
101 headers_to_verify.push((
102 RESPONSE_STATUS_PSEUDO_HEADER_NAME.into(),
103 Value::Number(*status_code),
104 ));
105
106 representation_independent_hash(&headers_to_verify)
107}
108
109pub fn response_hash(
116 response: &HttpResponse,
117 response_certification: &DefaultResponseCertification,
118 response_body_hash: Option<Hash>,
119) -> Hash {
120 let response_body_hash = response_body_hash.unwrap_or_else(|| hash(response.body()));
121
122 let filtered_headers = filter_response_headers(response, response_certification);
123 let concatenated_hashes = [
124 response_headers_hash(&response.status_code().as_u16().into(), &filtered_headers),
125 response_body_hash,
126 ]
127 .concat();
128
129 hash(concatenated_hashes.as_slice())
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 const HELLO_WORLD_BODY: &[u8] = &[72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33];
137 const CERTIFICATE: &str = "certificate=:SGVsbG8gQ2VydGlmaWNhdGUh:,tree=:SGVsbG8gVHJlZSE=:";
138 const HEADER_EXCLUSIONS_CEL_EXPRESSION: &str = r#"
139 default_certification (
140 ValidationArgs {
141 certification: Certification {
142 no_request_certification: Empty {},
143 response_certification: ResponseCertification {
144 response_header_exclusions: ResponseHeaderList {
145 headers: ["Content-Security-Policy"]
146 }
147 }
148 }
149 }
150 )
151 "#;
152 const CERTIFIED_HEADERS_CEL_EXPRESSION: &str = r#"
153 default_certification (
154 ValidationArgs {
155 certification: Certification {
156 no_request_certification: Empty {},
157 response_certification: ResponseCertification {
158 certified_response_headers: ResponseHeaderList {
159 headers: ["Accept-Encoding", "Cache-Control"]
160 }
161 }
162 }
163 }
164 )
165 "#;
166
167 #[test]
168 fn response_with_certified_headers_without_excluded_headers() {
169 let response_certification =
170 DefaultResponseCertification::certified_response_headers(vec!["Accept-Encoding"]);
171 let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
172 let response_headers = filter_response_headers(&response, &response_certification);
173
174 assert_eq!(
175 response_headers.headers,
176 vec![
177 (
178 CERTIFICATE_EXPRESSION_HEADER_NAME.to_lowercase(),
179 remove_whitespace(CERTIFIED_HEADERS_CEL_EXPRESSION),
180 ),
181 ("accept-encoding".into(), "gzip".into()),
182 ]
183 );
184 }
185
186 #[test]
187 fn response_with_certified_headers() {
188 let response_certification =
189 DefaultResponseCertification::certified_response_headers(vec![
190 "Accept-Encoding",
191 "Cache-Control",
192 ]);
193 let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
194 let response_headers = filter_response_headers(&response, &response_certification);
195
196 assert_eq!(
197 response_headers.headers,
198 vec![
199 (
200 CERTIFICATE_EXPRESSION_HEADER_NAME.to_lowercase(),
201 remove_whitespace(CERTIFIED_HEADERS_CEL_EXPRESSION),
202 ),
203 ("accept-encoding".into(), "gzip".into()),
204 ("cache-control".into(), "no-cache".into()),
205 ("cache-control".into(), "no-store".into()),
206 ]
207 );
208 }
209
210 #[test]
211 fn response_hash_with_certified_headers() {
212 let response_certification =
213 DefaultResponseCertification::certified_response_headers(vec![
214 "Accept-Encoding",
215 "Cache-Control",
216 ]);
217 let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
218 let expected_hash =
219 hex::decode("3393250e3cedc30408dcb7e8963898c3d7549b8a0b76496b82fdfeae99c2ac78")
220 .unwrap();
221
222 let result = response_hash(&response, &response_certification, None);
223
224 assert_eq!(result, expected_hash.as_slice());
225 }
226
227 #[test]
228 fn response_hash_with_certified_headers_without_excluded_headers() {
229 let response_certification =
230 DefaultResponseCertification::certified_response_headers(vec!["Accept-Encoding"]);
231 let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
232 let response_without_excluded_headers = HttpResponse::ok(
233 HELLO_WORLD_BODY,
234 vec![
235 (
236 CERTIFICATE_EXPRESSION_HEADER_NAME.into(),
237 remove_whitespace(CERTIFIED_HEADERS_CEL_EXPRESSION),
238 ),
239 ("Accept-Encoding".into(), "gzip".into()),
240 ],
241 )
242 .build();
243
244 let result = response_hash(&response, &response_certification, None);
245 let result_without_excluded_headers = response_hash(
246 &response_without_excluded_headers,
247 &response_certification,
248 None,
249 );
250
251 assert_eq!(result, result_without_excluded_headers);
252 }
253
254 #[test]
255 fn response_hash_with_header_exclusions() {
256 let response_certification =
257 DefaultResponseCertification::response_header_exclusions(vec![
258 "Accept-Encoding",
259 "Cache-Control",
260 ]);
261 let response = create_response(HEADER_EXCLUSIONS_CEL_EXPRESSION);
262 let expected_hash =
263 hex::decode("a2ffb50ef8971650c2fb46c0a2788b7d5ac5a027d635175e8e06b419ce6c4cda")
264 .unwrap();
265
266 let result = response_hash(&response, &response_certification, None);
267
268 assert_eq!(result, expected_hash.as_slice());
269 }
270
271 #[test]
272 fn response_hash_with_header_exclusions_without_excluded_headers() {
273 let response_certification =
274 DefaultResponseCertification::response_header_exclusions(vec![
275 "Content-Security-Policy",
276 ]);
277 let response = create_response(HEADER_EXCLUSIONS_CEL_EXPRESSION);
278 let response_without_excluded_headers = HttpResponse::ok(
279 HELLO_WORLD_BODY,
280 vec![
281 (
282 CERTIFICATE_EXPRESSION_HEADER_NAME.into(),
283 remove_whitespace(HEADER_EXCLUSIONS_CEL_EXPRESSION),
284 ),
285 ("Accept-Encoding".into(), "gzip".into()),
286 ("Cache-Control".into(), "no-cache".into()),
287 ("Cache-Control".into(), "no-store".into()),
288 ],
289 )
290 .build();
291
292 let result = response_hash(&response, &response_certification, None);
293 let result_without_excluded_headers = response_hash(
294 &response_without_excluded_headers,
295 &response_certification,
296 None,
297 );
298
299 assert_eq!(result, result_without_excluded_headers);
300 }
301
302 #[test]
303 fn response_headers_hash_with_certified_headers() {
304 let response_certification =
305 DefaultResponseCertification::certified_response_headers(vec![
306 "Accept-Encoding",
307 "Cache-Control",
308 ]);
309 let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
310 let expected_hash =
311 hex::decode("eac859a99d5bd7b71f46dbacecff4aaa0a7a7131802c136a77a76c8e018af5f7")
312 .unwrap();
313
314 let filtered_headers = filter_response_headers(&response, &response_certification);
315 let result =
316 response_headers_hash(&response.status_code().as_u16().into(), &filtered_headers);
317
318 assert_eq!(result, expected_hash.as_slice());
319 }
320
321 #[test]
322 fn response_headers_hash_with_certified_headers_without_excluded_headers() {
323 let response_certification =
324 DefaultResponseCertification::certified_response_headers(vec!["Accept-Encoding"]);
325 let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
326 let response_without_excluded_headers = HttpResponse::ok(
327 HELLO_WORLD_BODY,
328 vec![
329 (CERTIFICATE_HEADER_NAME.into(), CERTIFICATE.into()),
330 (
331 CERTIFICATE_EXPRESSION_HEADER_NAME.into(),
332 remove_whitespace(CERTIFIED_HEADERS_CEL_EXPRESSION),
333 ),
334 ("Accept-Encoding".into(), "gzip".into()),
335 ],
336 )
337 .build();
338
339 let filtered_headers = filter_response_headers(&response, &response_certification);
340 let result =
341 response_headers_hash(&response.status_code().as_u16().into(), &filtered_headers);
342 let filtered_headers_without_excluded_headers =
343 filter_response_headers(&response_without_excluded_headers, &response_certification);
344 let result_without_excluded_headers = response_headers_hash(
345 &response_without_excluded_headers
346 .status_code()
347 .as_u16()
348 .into(),
349 &filtered_headers_without_excluded_headers,
350 );
351
352 assert_eq!(result, result_without_excluded_headers);
353 }
354
355 #[test]
356 fn response_headers_hash_with_header_exclusions() {
357 let response_certification =
358 DefaultResponseCertification::response_header_exclusions(vec![
359 "Accept-Encoding",
360 "Cache-Control",
361 ]);
362 let response = create_response(HEADER_EXCLUSIONS_CEL_EXPRESSION);
363 let expected_hash =
364 hex::decode("d618f70bf2578d5a672374ffbaade3910e858384f42d01ac2863946ab596bcac")
365 .unwrap();
366
367 let filtered_headers = filter_response_headers(&response, &response_certification);
368 let result =
369 response_headers_hash(&response.status_code().as_u16().into(), &filtered_headers);
370
371 assert_eq!(result, expected_hash.as_slice());
372 }
373
374 #[test]
375 fn response_headers_hash_with_header_exclusions_without_excluded_headers() {
376 let response_certification =
377 DefaultResponseCertification::response_header_exclusions(vec![
378 "Content-Security-Policy",
379 ]);
380 let response = create_response(HEADER_EXCLUSIONS_CEL_EXPRESSION);
381 let response_without_excluded_headers = HttpResponse::ok(
382 HELLO_WORLD_BODY,
383 vec![
384 (CERTIFICATE_HEADER_NAME.into(), CERTIFICATE.into()),
385 (
386 CERTIFICATE_EXPRESSION_HEADER_NAME.into(),
387 remove_whitespace(HEADER_EXCLUSIONS_CEL_EXPRESSION),
388 ),
389 ("Accept-Encoding".into(), "gzip".into()),
390 ("Cache-Control".into(), "no-cache".into()),
391 ("Cache-Control".into(), "no-store".into()),
392 ],
393 )
394 .build();
395
396 let filtered_headers = filter_response_headers(&response, &response_certification);
397 let result =
398 response_headers_hash(&response.status_code().as_u16().into(), &filtered_headers);
399
400 let response_headers_without_excluded_headers =
401 filter_response_headers(&response_without_excluded_headers, &response_certification);
402 let result_without_excluded_headers = response_headers_hash(
403 &response_without_excluded_headers
404 .status_code()
405 .as_u16()
406 .into(),
407 &response_headers_without_excluded_headers,
408 );
409
410 assert_eq!(result, result_without_excluded_headers);
411 }
412
413 #[test]
414 fn response_hash_with_body_hash_override() {
415 let response_certification =
416 DefaultResponseCertification::certified_response_headers(vec![
417 "Accept-Encoding",
418 "Cache-Control",
419 ]);
420 let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
421 let response_body_hash: Hash =
422 hex::decode("5462fc394013080effc31d578ec3fff8b44cdf24738b38a77ce4afacbc93a7f5")
423 .unwrap()
424 .try_into()
425 .unwrap();
426 let expected_hash =
427 hex::decode("1afc744a377cb8785d1078f53f9bbc9160d86b7a05f490e42c89366326eaef20")
428 .unwrap();
429
430 let result = response_hash(&response, &response_certification, Some(response_body_hash));
431
432 assert_eq!(result, expected_hash.as_slice());
433 }
434
435 fn create_response(cel_expression: &str) -> HttpResponse {
436 HttpResponse::ok(
437 HELLO_WORLD_BODY,
438 vec![
439 (CERTIFICATE_HEADER_NAME.into(), CERTIFICATE.into()),
440 (
441 CERTIFICATE_EXPRESSION_HEADER_NAME.into(),
442 remove_whitespace(cel_expression),
443 ),
444 ("Accept-Encoding".into(), "gzip".into()),
445 ("Cache-Control".into(), "no-cache".into()),
446 ("Cache-Control".into(), "no-store".into()),
447 (
448 "Content-Security-Policy".into(),
449 "default-src 'self'".into(),
450 ),
451 ],
452 )
453 .build()
454 }
455
456 fn remove_whitespace(s: &str) -> String {
461 s.chars().filter(|c| !c.is_whitespace()).collect()
462 }
463}