mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
183 lines
5.7 KiB
Rust
183 lines
5.7 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
use std::iter::Filter;
|
|
use std::str::Split;
|
|
use std::sync::MutexGuard;
|
|
|
|
use base64::Engine;
|
|
use generic_array::ArrayLength;
|
|
use net_traits::response::{Response, ResponseBody, ResponseType};
|
|
use sha2::{Digest, Sha256, Sha384, Sha512};
|
|
|
|
const SUPPORTED_ALGORITHM: &[&str] = &["sha256", "sha384", "sha512"];
|
|
pub type StaticCharVec = &'static [char];
|
|
/// A "space character" according to:
|
|
///
|
|
/// <https://html.spec.whatwg.org/multipage/#space-character>
|
|
pub static HTML_SPACE_CHARACTERS: StaticCharVec =
|
|
&['\u{0020}', '\u{0009}', '\u{000a}', '\u{000c}', '\u{000d}'];
|
|
#[derive(Clone)]
|
|
pub struct SriEntry {
|
|
pub alg: String,
|
|
pub val: String,
|
|
// TODO : Current version of spec does not define any option.
|
|
// Can be refactored into appropriate datastructure when future
|
|
// spec has more details.
|
|
pub opt: Option<String>,
|
|
}
|
|
|
|
impl SriEntry {
|
|
pub fn new(alg: &str, val: &str, opt: Option<String>) -> SriEntry {
|
|
SriEntry {
|
|
alg: alg.to_owned(),
|
|
val: val.to_owned(),
|
|
opt,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata>
|
|
pub fn parsed_metadata(integrity_metadata: &str) -> Vec<SriEntry> {
|
|
// Step 1
|
|
let mut result = vec![];
|
|
|
|
// Step 3
|
|
let tokens = split_html_space_chars(integrity_metadata);
|
|
for token in tokens {
|
|
let parsed_data: Vec<&str> = token.split('-').collect();
|
|
|
|
if parsed_data.len() > 1 {
|
|
let alg = parsed_data[0];
|
|
|
|
if !SUPPORTED_ALGORITHM.contains(&alg) {
|
|
continue;
|
|
}
|
|
|
|
let data: Vec<&str> = parsed_data[1].split('?').collect();
|
|
let digest = data[0];
|
|
|
|
let opt = if data.len() > 1 {
|
|
Some(data[1].to_owned())
|
|
} else {
|
|
None
|
|
};
|
|
|
|
result.push(SriEntry::new(alg, digest, opt));
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// <https://w3c.github.io/webappsec-subresource-integrity/#getprioritizedhashfunction>
|
|
pub fn get_prioritized_hash_function(
|
|
hash_func_left: &str,
|
|
hash_func_right: &str,
|
|
) -> Option<String> {
|
|
let left_priority = SUPPORTED_ALGORITHM
|
|
.iter()
|
|
.position(|s| *s == hash_func_left)
|
|
.unwrap();
|
|
let right_priority = SUPPORTED_ALGORITHM
|
|
.iter()
|
|
.position(|s| *s == hash_func_right)
|
|
.unwrap();
|
|
|
|
if left_priority == right_priority {
|
|
return None;
|
|
}
|
|
if left_priority > right_priority {
|
|
Some(hash_func_left.to_owned())
|
|
} else {
|
|
Some(hash_func_right.to_owned())
|
|
}
|
|
}
|
|
|
|
/// <https://w3c.github.io/webappsec-subresource-integrity/#get-the-strongest-metadata>
|
|
pub fn get_strongest_metadata(integrity_metadata_list: Vec<SriEntry>) -> Vec<SriEntry> {
|
|
let mut result: Vec<SriEntry> = vec![integrity_metadata_list[0].clone()];
|
|
let mut current_algorithm = result[0].alg.clone();
|
|
|
|
for integrity_metadata in &integrity_metadata_list[1..] {
|
|
let prioritized_hash =
|
|
get_prioritized_hash_function(&integrity_metadata.alg, ¤t_algorithm);
|
|
if prioritized_hash.is_none() {
|
|
result.push(integrity_metadata.clone());
|
|
} else if let Some(algorithm) = prioritized_hash {
|
|
if algorithm != current_algorithm {
|
|
result = vec![integrity_metadata.clone()];
|
|
current_algorithm = algorithm;
|
|
}
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// <https://w3c.github.io/webappsec-subresource-integrity/#apply-algorithm-to-response>
|
|
fn apply_algorithm_to_response<S: ArrayLength<u8>, D: Digest<OutputSize = S>>(
|
|
body: MutexGuard<ResponseBody>,
|
|
mut hasher: D,
|
|
) -> String {
|
|
if let ResponseBody::Done(ref vec) = *body {
|
|
hasher.update(vec);
|
|
let response_digest = hasher.finalize(); //Now hash
|
|
base64::engine::general_purpose::STANDARD.encode(&response_digest)
|
|
} else {
|
|
unreachable!("Tried to calculate digest of incomplete response body")
|
|
}
|
|
}
|
|
|
|
/// <https://w3c.github.io/webappsec-subresource-integrity/#is-response-eligible>
|
|
fn is_eligible_for_integrity_validation(response: &Response) -> bool {
|
|
matches!(
|
|
response.response_type,
|
|
ResponseType::Basic | ResponseType::Default | ResponseType::Cors
|
|
)
|
|
}
|
|
|
|
/// <https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist>
|
|
pub fn is_response_integrity_valid(integrity_metadata: &str, response: &Response) -> bool {
|
|
let parsed_metadata_list: Vec<SriEntry> = parsed_metadata(integrity_metadata);
|
|
|
|
// Step 2 & 4
|
|
if parsed_metadata_list.is_empty() {
|
|
return true;
|
|
}
|
|
|
|
// Step 3
|
|
if !is_eligible_for_integrity_validation(response) {
|
|
return false;
|
|
}
|
|
|
|
// Step 5
|
|
let metadata: Vec<SriEntry> = get_strongest_metadata(parsed_metadata_list);
|
|
for item in metadata {
|
|
let body = response.body.lock().unwrap();
|
|
let algorithm = item.alg;
|
|
let digest = item.val;
|
|
|
|
let hashed = match &*algorithm {
|
|
"sha256" => apply_algorithm_to_response(body, Sha256::new()),
|
|
"sha384" => apply_algorithm_to_response(body, Sha384::new()),
|
|
"sha512" => apply_algorithm_to_response(body, Sha512::new()),
|
|
_ => continue,
|
|
};
|
|
|
|
if hashed == digest {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
pub fn split_html_space_chars(s: &str) -> Filter<Split<'_, StaticCharVec>, fn(&&str) -> bool> {
|
|
fn not_empty(&split: &&str) -> bool {
|
|
!split.is_empty()
|
|
}
|
|
s.split(HTML_SPACE_CHARACTERS)
|
|
.filter(not_empty as fn(&&str) -> bool)
|
|
}
|