net/
subresource_integrity.rs1use std::iter::Filter;
6use std::str::Split;
7use std::sync::LazyLock;
8
9use base64::Engine;
10use generic_array::ArrayLength;
11use net_traits::response::{Response, ResponseBody, ResponseType};
12use parking_lot::MutexGuard;
13use regex::Regex;
14use sha2::{Digest, Sha256, Sha384, Sha512};
15
16const SUPPORTED_ALGORITHM: &[&str] = &["sha256", "sha384", "sha512"];
17pub type StaticCharVec = &'static [char];
18pub static HTML_SPACE_CHARACTERS: StaticCharVec =
22 &['\u{0020}', '\u{0009}', '\u{000a}', '\u{000c}', '\u{000d}'];
23#[derive(Clone)]
24pub struct SriEntry {
25 pub alg: String,
26 pub val: String,
27 pub opt: Option<String>,
31}
32
33impl SriEntry {
34 pub fn new(alg: &str, val: &str, opt: Option<String>) -> SriEntry {
35 SriEntry {
36 alg: alg.to_owned(),
37 val: val.to_owned(),
38 opt,
39 }
40 }
41}
42
43pub fn parsed_metadata(integrity_metadata: &str) -> Vec<SriEntry> {
45 static BASE64_GRAMMAR: LazyLock<Regex> =
47 LazyLock::new(|| Regex::new(r"^[A-Za-z0-9+/_-]+={0,2}$").unwrap());
48
49 let mut result = vec![];
51
52 let tokens = split_html_space_chars(integrity_metadata);
54 for token in tokens {
55 let expression_and_option: Vec<&str> = token.split('?').collect();
57
58 let algorithm_expression = expression_and_option[0];
60
61 let algorithm_and_value: Vec<&str> = algorithm_expression.split('-').collect();
63
64 let algorithm = algorithm_and_value[0];
66
67 if !SUPPORTED_ALGORITHM.contains(&algorithm) {
69 continue;
70 }
71
72 let Some(digest) = algorithm_and_value
75 .get(1)
76 .filter(|value| BASE64_GRAMMAR.is_match(value))
78 else {
79 continue;
80 };
81
82 let opt = expression_and_option.get(1).map(|opt| (*opt).to_owned());
83
84 result.push(SriEntry::new(algorithm, digest, opt));
87 }
88
89 result
90}
91
92pub fn get_prioritized_hash_function(
94 hash_func_left: &str,
95 hash_func_right: &str,
96) -> Option<String> {
97 let left_priority = SUPPORTED_ALGORITHM
98 .iter()
99 .position(|s| *s == hash_func_left)
100 .unwrap();
101 let right_priority = SUPPORTED_ALGORITHM
102 .iter()
103 .position(|s| *s == hash_func_right)
104 .unwrap();
105
106 if left_priority == right_priority {
107 return None;
108 }
109 if left_priority > right_priority {
110 Some(hash_func_left.to_owned())
111 } else {
112 Some(hash_func_right.to_owned())
113 }
114}
115
116pub fn get_strongest_metadata(integrity_metadata_list: Vec<SriEntry>) -> Vec<SriEntry> {
118 let mut result: Vec<SriEntry> = vec![integrity_metadata_list[0].clone()];
119 let mut current_algorithm = result[0].alg.clone();
120
121 for integrity_metadata in &integrity_metadata_list[1..] {
122 let prioritized_hash =
123 get_prioritized_hash_function(&integrity_metadata.alg, ¤t_algorithm);
124 if prioritized_hash.is_none() {
125 result.push(integrity_metadata.clone());
126 } else if let Some(algorithm) = prioritized_hash {
127 if algorithm != current_algorithm {
128 result = vec![integrity_metadata.clone()];
129 current_algorithm = algorithm;
130 }
131 }
132 }
133
134 result
135}
136
137fn apply_algorithm_to_response<S: ArrayLength<u8>, D: Digest<OutputSize = S>>(
139 body: MutexGuard<ResponseBody>,
140 mut hasher: D,
141) -> String {
142 if let ResponseBody::Done(ref vec) = *body {
143 hasher.update(vec);
144 let response_digest = hasher.finalize(); base64::engine::general_purpose::STANDARD.encode(&response_digest)
146 } else {
147 unreachable!("Tried to calculate digest of incomplete response body")
148 }
149}
150
151fn is_eligible_for_integrity_validation(response: &Response) -> bool {
153 matches!(
154 response.response_type,
155 ResponseType::Basic | ResponseType::Default | ResponseType::Cors
156 )
157}
158
159pub fn is_response_integrity_valid(integrity_metadata: &str, response: &Response) -> bool {
161 let parsed_metadata_list: Vec<SriEntry> = parsed_metadata(integrity_metadata);
162
163 if parsed_metadata_list.is_empty() {
165 return true;
166 }
167
168 if !is_eligible_for_integrity_validation(response) {
170 return false;
171 }
172
173 let metadata: Vec<SriEntry> = get_strongest_metadata(parsed_metadata_list);
175 for item in metadata {
176 let body = response.body.lock();
177 let algorithm = item.alg;
178 let digest = item.val;
179
180 let hashed = match &*algorithm {
181 "sha256" => apply_algorithm_to_response(body, Sha256::new()),
182 "sha384" => apply_algorithm_to_response(body, Sha384::new()),
183 "sha512" => apply_algorithm_to_response(body, Sha512::new()),
184 _ => continue,
185 };
186
187 if hashed == digest {
188 return true;
189 }
190 }
191
192 false
193}
194
195pub fn split_html_space_chars(s: &str) -> Filter<Split<'_, StaticCharVec>, fn(&&str) -> bool> {
196 fn not_empty(&split: &&str) -> bool {
197 !split.is_empty()
198 }
199 s.split(HTML_SPACE_CHARACTERS)
200 .filter(not_empty as fn(&&str) -> bool)
201}