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::MutexGuard;
8
9use base64::Engine;
10use generic_array::ArrayLength;
11use net_traits::response::{Response, ResponseBody, ResponseType};
12use sha2::{Digest, Sha256, Sha384, Sha512};
13
14const SUPPORTED_ALGORITHM: &[&str] = &["sha256", "sha384", "sha512"];
15pub type StaticCharVec = &'static [char];
16/// A "space character" according to:
17///
18/// <https://html.spec.whatwg.org/multipage/#space-character>
19pub static HTML_SPACE_CHARACTERS: StaticCharVec =
20    &['\u{0020}', '\u{0009}', '\u{000a}', '\u{000c}', '\u{000d}'];
21#[derive(Clone)]
22pub struct SriEntry {
23    pub alg: String,
24    pub val: String,
25    // TODO : Current version of spec does not define any option.
26    // Can be refactored into appropriate datastructure when future
27    // spec has more details.
28    pub opt: Option<String>,
29}
30
31impl SriEntry {
32    pub fn new(alg: &str, val: &str, opt: Option<String>) -> SriEntry {
33        SriEntry {
34            alg: alg.to_owned(),
35            val: val.to_owned(),
36            opt,
37        }
38    }
39}
40
41/// <https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata>
42pub fn parsed_metadata(integrity_metadata: &str) -> Vec<SriEntry> {
43    // Step 1
44    let mut result = vec![];
45
46    // Step 3
47    let tokens = split_html_space_chars(integrity_metadata);
48    for token in tokens {
49        let parsed_data: Vec<&str> = token.split('-').collect();
50
51        if parsed_data.len() > 1 {
52            let alg = parsed_data[0];
53
54            if !SUPPORTED_ALGORITHM.contains(&alg) {
55                continue;
56            }
57
58            let data: Vec<&str> = parsed_data[1].split('?').collect();
59            let digest = data[0];
60
61            let opt = if data.len() > 1 {
62                Some(data[1].to_owned())
63            } else {
64                None
65            };
66
67            result.push(SriEntry::new(alg, digest, opt));
68        }
69    }
70
71    result
72}
73
74/// <https://w3c.github.io/webappsec-subresource-integrity/#getprioritizedhashfunction>
75pub fn get_prioritized_hash_function(
76    hash_func_left: &str,
77    hash_func_right: &str,
78) -> Option<String> {
79    let left_priority = SUPPORTED_ALGORITHM
80        .iter()
81        .position(|s| *s == hash_func_left)
82        .unwrap();
83    let right_priority = SUPPORTED_ALGORITHM
84        .iter()
85        .position(|s| *s == hash_func_right)
86        .unwrap();
87
88    if left_priority == right_priority {
89        return None;
90    }
91    if left_priority > right_priority {
92        Some(hash_func_left.to_owned())
93    } else {
94        Some(hash_func_right.to_owned())
95    }
96}
97
98/// <https://w3c.github.io/webappsec-subresource-integrity/#get-the-strongest-metadata>
99pub fn get_strongest_metadata(integrity_metadata_list: Vec<SriEntry>) -> Vec<SriEntry> {
100    let mut result: Vec<SriEntry> = vec![integrity_metadata_list[0].clone()];
101    let mut current_algorithm = result[0].alg.clone();
102
103    for integrity_metadata in &integrity_metadata_list[1..] {
104        let prioritized_hash =
105            get_prioritized_hash_function(&integrity_metadata.alg, &current_algorithm);
106        if prioritized_hash.is_none() {
107            result.push(integrity_metadata.clone());
108        } else if let Some(algorithm) = prioritized_hash {
109            if algorithm != current_algorithm {
110                result = vec![integrity_metadata.clone()];
111                current_algorithm = algorithm;
112            }
113        }
114    }
115
116    result
117}
118
119/// <https://w3c.github.io/webappsec-subresource-integrity/#apply-algorithm-to-response>
120fn apply_algorithm_to_response<S: ArrayLength<u8>, D: Digest<OutputSize = S>>(
121    body: MutexGuard<ResponseBody>,
122    mut hasher: D,
123) -> String {
124    if let ResponseBody::Done(ref vec) = *body {
125        hasher.update(vec);
126        let response_digest = hasher.finalize(); // Now hash
127        base64::engine::general_purpose::STANDARD.encode(&response_digest)
128    } else {
129        unreachable!("Tried to calculate digest of incomplete response body")
130    }
131}
132
133/// <https://w3c.github.io/webappsec-subresource-integrity/#is-response-eligible>
134fn is_eligible_for_integrity_validation(response: &Response) -> bool {
135    matches!(
136        response.response_type,
137        ResponseType::Basic | ResponseType::Default | ResponseType::Cors
138    )
139}
140
141/// <https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist>
142pub fn is_response_integrity_valid(integrity_metadata: &str, response: &Response) -> bool {
143    let parsed_metadata_list: Vec<SriEntry> = parsed_metadata(integrity_metadata);
144
145    // Step 2 & 4
146    if parsed_metadata_list.is_empty() {
147        return true;
148    }
149
150    // Step 3
151    if !is_eligible_for_integrity_validation(response) {
152        return false;
153    }
154
155    // Step 5
156    let metadata: Vec<SriEntry> = get_strongest_metadata(parsed_metadata_list);
157    for item in metadata {
158        let body = response.body.lock().unwrap();
159        let algorithm = item.alg;
160        let digest = item.val;
161
162        let hashed = match &*algorithm {
163            "sha256" => apply_algorithm_to_response(body, Sha256::new()),
164            "sha384" => apply_algorithm_to_response(body, Sha384::new()),
165            "sha512" => apply_algorithm_to_response(body, Sha512::new()),
166            _ => continue,
167        };
168
169        if hashed == digest {
170            return true;
171        }
172    }
173
174    false
175}
176
177pub fn split_html_space_chars(s: &str) -> Filter<Split<'_, StaticCharVec>, fn(&&str) -> bool> {
178    fn not_empty(&split: &&str) -> bool {
179        !split.is_empty()
180    }
181    s.split(HTML_SPACE_CHARACTERS)
182        .filter(not_empty as fn(&&str) -> bool)
183}