Skip to main content

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 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];
16/// A "space character" according to:
17///
18/// <https://html.spec.whatwg.org/multipage/#space-character>
19static HTML_SPACE_CHARACTERS: StaticCharVec =
20    &['\u{0020}', '\u{0009}', '\u{000a}', '\u{000c}', '\u{000d}'];
21
22#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
23/// The algorithm used. The order is specified <https://w3c.github.io/webappsec-subresource-integrity>
24pub enum Algorithm {
25    Sha256 = 1,
26    Sha384 = 2,
27    Sha512 = 3,
28}
29
30impl TryFrom<&str> for Algorithm {
31    /// Any error means that it is unsuported.
32    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    // TODO : Current version of spec does not define any option.
49    // Can be refactored into appropriate datastructure when future
50    // spec has more details.
51    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    // Step 2.1. Let expression-and-options be the result of splitting item on U+003F (?).
69    let mut expression_and_option = token.split('?');
70
71    // Step 2.2. Let algorithm-expression be expression-and-options[0].
72    let algorithm_expression = expression_and_option.next()?;
73
74    // Step 2.4. Let algorithm-and-value be the result of splitting algorithm-expression on U+002D (-).
75    let mut algorithm_and_value = algorithm_expression.split('-');
76
77    // Step 2.5. Let algorithm be algorithm-and-value[0].
78    let algorithm = algorithm_and_value.next()?;
79
80    // Step 2.6. If algorithm is not a valid SRI hash algorithm token, then continue.
81    let algorithm = algorithm.try_into().ok()?;
82
83    // Step 2.3. Let base64-value be the empty string.
84    // Step 2.7. If algorithm-and-value[1] exists, set base64-value to algorithm-and-value[1].
85    let digest = algorithm_and_value
86        .next()
87        // check if digest follows the base64 grammar defined by CSP spec
88        .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
94/// <https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata>
95pub fn parsed_metadata<'a>(integrity_metadata: &'a str) -> Vec<SriEntry<'a>> {
96    // https://w3c.github.io/webappsec-csp/#grammardef-base64-value
97
98    // Step 1. Let result be the empty set.
99    let mut result = vec![];
100
101    // Step 2. For each item returned by splitting metadata on spaces:
102    let tokens = split_html_space_chars(integrity_metadata);
103    for token in tokens {
104        // Step 2.8. Let metadata be the ordered map «["alg" → algorithm, "val" → base64-value]».
105        // Step 2.9. Append metadata to result.
106        let Some(sri) = parse_token(token) else {
107            continue;
108        };
109        result.push(sri);
110    }
111
112    result
113}
114
115/// <https://w3c.github.io/webappsec-subresource-integrity/#get-the-strongest-metadata>
116pub 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
142/// <https://w3c.github.io/webappsec-subresource-integrity/#apply-algorithm-to-response>
143fn 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
155/// <https://w3c.github.io/webappsec-subresource-integrity/#is-response-eligible>
156fn is_eligible_for_integrity_validation(response: &Response) -> bool {
157    matches!(
158        response.response_type,
159        ResponseType::Basic | ResponseType::Default | ResponseType::Cors
160    )
161}
162
163/// <https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist>
164pub fn is_response_integrity_valid(integrity_metadata: &str, response: &Response) -> bool {
165    let parsed_metadata_list: Vec<SriEntry> = parsed_metadata(integrity_metadata);
166
167    // Step 2 & 4
168    if parsed_metadata_list.is_empty() {
169        return true;
170    }
171
172    // Step 3
173    if !is_eligible_for_integrity_validation(response) {
174        return false;
175    }
176
177    // Step 5
178    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}