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