net/
subresource_integrity.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use 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];
18/// A "space character" according to:
19///
20/// <https://html.spec.whatwg.org/multipage/#space-character>
21pub 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    // TODO : Current version of spec does not define any option.
28    // Can be refactored into appropriate datastructure when future
29    // spec has more details.
30    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
43/// <https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata>
44pub fn parsed_metadata(integrity_metadata: &str) -> Vec<SriEntry> {
45    // https://w3c.github.io/webappsec-csp/#grammardef-base64-value
46    static BASE64_GRAMMAR: LazyLock<Regex> =
47        LazyLock::new(|| Regex::new(r"^[A-Za-z0-9+/_-]+={0,2}$").unwrap());
48
49    // Step 1. Let result be the empty set.
50    let mut result = vec![];
51
52    // Step 2. For each item returned by splitting metadata on spaces:
53    let tokens = split_html_space_chars(integrity_metadata);
54    for token in tokens {
55        // Step 2.1. Let expression-and-options be the result of splitting item on U+003F (?).
56        let expression_and_option: Vec<&str> = token.split('?').collect();
57
58        // Step 2.2. Let algorithm-expression be expression-and-options[0].
59        let algorithm_expression = expression_and_option[0];
60
61        // Step 2.4. Let algorithm-and-value be the result of splitting algorithm-expression on U+002D (-).
62        let algorithm_and_value: Vec<&str> = algorithm_expression.split('-').collect();
63
64        // Step 2.5. Let algorithm be algorithm-and-value[0].
65        let algorithm = algorithm_and_value[0];
66
67        // Step 2.6. If algorithm is not a valid SRI hash algorithm token, then continue.
68        if !SUPPORTED_ALGORITHM.contains(&algorithm) {
69            continue;
70        }
71
72        // Step 2.3. Let base64-value be the empty string.
73        // Step 2.7. If algorithm-and-value[1] exists, set base64-value to algorithm-and-value[1].
74        let Some(digest) = algorithm_and_value
75            .get(1)
76            // check if digest follows the base64 grammar defined by CSP spec
77            .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        // Step 2.8. Let metadata be the ordered map «["alg" → algorithm, "val" → base64-value]».
85        // Step 2.9. Append metadata to result.
86        result.push(SriEntry::new(algorithm, digest, opt));
87    }
88
89    result
90}
91
92/// <https://w3c.github.io/webappsec-subresource-integrity/#getprioritizedhashfunction>
93pub 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
116/// <https://w3c.github.io/webappsec-subresource-integrity/#get-the-strongest-metadata>
117pub 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, &current_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
137/// <https://w3c.github.io/webappsec-subresource-integrity/#apply-algorithm-to-response>
138fn 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(); // Now hash
145        base64::engine::general_purpose::STANDARD.encode(&response_digest)
146    } else {
147        unreachable!("Tried to calculate digest of incomplete response body")
148    }
149}
150
151/// <https://w3c.github.io/webappsec-subresource-integrity/#is-response-eligible>
152fn is_eligible_for_integrity_validation(response: &Response) -> bool {
153    matches!(
154        response.response_type,
155        ResponseType::Basic | ResponseType::Default | ResponseType::Cors
156    )
157}
158
159/// <https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist>
160pub fn is_response_integrity_valid(integrity_metadata: &str, response: &Response) -> bool {
161    let parsed_metadata_list: Vec<SriEntry> = parsed_metadata(integrity_metadata);
162
163    // Step 2 & 4
164    if parsed_metadata_list.is_empty() {
165        return true;
166    }
167
168    // Step 3
169    if !is_eligible_for_integrity_validation(response) {
170        return false;
171    }
172
173    // Step 5
174    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}