net/
subresource_integrity.rs1use std::iter::Filter;
6use std::str::Split;
7use std::sync::LazyLock;
8
9use aws_lc_rs::digest::{self};
10use base64::Engine;
11use net_traits::response::{Response, ResponseBody, ResponseType};
12use parking_lot::MutexGuard;
13use regex::Regex;
14
15type StaticCharVec = &'static [char];
16static HTML_SPACE_CHARACTERS: StaticCharVec =
20 &['\u{0020}', '\u{0009}', '\u{000a}', '\u{000c}', '\u{000d}'];
21
22#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
23pub enum Algorithm {
25 Sha256 = 1,
26 Sha384 = 2,
27 Sha512 = 3,
28}
29
30impl TryFrom<&str> for Algorithm {
31 type Error = ();
33
34 fn try_from(value: &str) -> Result<Self, Self::Error> {
35 match value {
36 "sha256" => Ok(Algorithm::Sha256),
37 "sha384" => Ok(Algorithm::Sha384),
38 "sha512" => Ok(Algorithm::Sha512),
39 _ => Err(()),
40 }
41 }
42}
43
44#[derive(Clone, Debug)]
45pub struct SriEntry<'a> {
46 pub algorithm: Algorithm,
47 pub val: &'a str,
48 pub opt: Option<String>,
52}
53
54impl<'a> SriEntry<'a> {
55 pub fn new(algorithm: Algorithm, val: &'a str, opt: Option<String>) -> SriEntry<'a> {
56 SriEntry {
57 algorithm,
58 val,
59 opt,
60 }
61 }
62}
63
64static BASE64_GRAMMAR: LazyLock<Regex> =
65 LazyLock::new(|| Regex::new(r"^[A-Za-z0-9+/_-]+={0,2}$").unwrap());
66
67fn parse_token<'a>(token: &'a str) -> Option<SriEntry<'a>> {
68 let mut expression_and_option = token.split('?');
70
71 let algorithm_expression = expression_and_option.next()?;
73
74 let mut algorithm_and_value = algorithm_expression.split('-');
76
77 let algorithm = algorithm_and_value.next()?;
79
80 let algorithm = algorithm.try_into().ok()?;
82
83 let digest = algorithm_and_value
86 .next()
87 .filter(|value| BASE64_GRAMMAR.is_match(value))?;
89
90 let opt = expression_and_option.next().map(|opt| (*opt).to_owned());
91 Some(SriEntry::new(algorithm, digest, opt))
92}
93
94pub fn parsed_metadata<'a>(integrity_metadata: &'a str) -> Vec<SriEntry<'a>> {
96 let mut result = vec![];
100
101 let tokens = split_html_space_chars(integrity_metadata);
103 for token in tokens {
104 let Some(sri) = parse_token(token) else {
107 continue;
108 };
109 result.push(sri);
110 }
111
112 result
113}
114
115pub fn get_strongest_metadata<'a>(integrity_metadata_list: Vec<SriEntry<'a>>) -> Vec<SriEntry<'a>> {
117 let mut result: Vec<SriEntry> = vec![integrity_metadata_list[0].clone()];
118 let mut current_algorithm = result[0].algorithm;
119
120 for integrity_metadata in integrity_metadata_list.into_iter().skip(1) {
121 let prioritized_hash = if integrity_metadata.algorithm < current_algorithm {
122 Some(current_algorithm)
123 } else if integrity_metadata.algorithm > current_algorithm {
124 Some(integrity_metadata.algorithm)
125 } else {
126 None
127 };
128
129 if prioritized_hash.is_none() {
130 result.push(integrity_metadata);
131 } else if let Some(algorithm) = prioritized_hash &&
132 algorithm != current_algorithm
133 {
134 result = vec![integrity_metadata];
135 current_algorithm = algorithm;
136 }
137 }
138
139 result
140}
141
142fn apply_algorithm_to_response(
144 body: MutexGuard<ResponseBody>,
145 algorithm: &'static digest::Algorithm,
146) -> String {
147 if let ResponseBody::Done(ref vec) = *body {
148 let response_digest = digest::digest(algorithm, vec);
149 base64::engine::general_purpose::STANDARD.encode(response_digest)
150 } else {
151 unreachable!("Tried to calculate digest of incomplete response body")
152 }
153}
154
155fn is_eligible_for_integrity_validation(response: &Response) -> bool {
157 matches!(
158 response.response_type,
159 ResponseType::Basic | ResponseType::Default | ResponseType::Cors
160 )
161}
162
163pub fn is_response_integrity_valid(integrity_metadata: &str, response: &Response) -> bool {
165 let parsed_metadata_list: Vec<SriEntry> = parsed_metadata(integrity_metadata);
166
167 if parsed_metadata_list.is_empty() {
169 return true;
170 }
171
172 if !is_eligible_for_integrity_validation(response) {
174 return false;
175 }
176
177 let metadata: Vec<SriEntry> = get_strongest_metadata(parsed_metadata_list);
179 for item in metadata {
180 let body = response.body.lock();
181 let digest = item.val;
182
183 let hashed = match item.algorithm {
184 Algorithm::Sha256 => apply_algorithm_to_response(body, &digest::SHA256),
185 Algorithm::Sha384 => apply_algorithm_to_response(body, &digest::SHA384),
186 Algorithm::Sha512 => apply_algorithm_to_response(body, &digest::SHA512),
187 };
188
189 if hashed == digest {
190 return true;
191 }
192 }
193
194 false
195}
196
197pub fn split_html_space_chars(s: &str) -> Filter<Split<'_, StaticCharVec>, fn(&&str) -> bool> {
198 fn not_empty(&split: &&str) -> bool {
199 !split.is_empty()
200 }
201 s.split(HTML_SPACE_CHARACTERS)
202 .filter(not_empty as fn(&&str) -> bool)
203}