Implement Subresource Integrity

Implemented response validation part of
https://w3c.github.io/webappsec-subresource-integrity/.
Implemented step eighteen of the main fetch. If a request has integrity
metadata, then following steps are performed
*Wait for response body
*If the response does not have a termination reason and response does not
match request’s integrity metadata, set response to a
network error.# Please enter the commit message for your changes. Lines starting
This commit is contained in:
mrnayak 2017-01-08 08:52:18 +05:30
parent 496447a363
commit a3026499f4
19 changed files with 439 additions and 260 deletions

View file

@ -24,6 +24,7 @@ use std::io::Read;
use std::mem;
use std::rc::Rc;
use std::sync::mpsc::{Sender, Receiver};
use subresource_integrity::is_response_integrity_valid;
pub type Target<'a> = &'a mut (FetchTaskTarget + Send);
@ -268,6 +269,7 @@ pub fn main_fetch(request: Rc<Request>,
response
};
let mut response_loaded = false;
{
// Step 14
let network_error_res;
@ -297,50 +299,33 @@ pub fn main_fetch(request: Rc<Request>,
let mut body = internal_response.body.lock().unwrap();
*body = ResponseBody::Empty;
}
// Step 18
// TODO be able to compare response integrity against request integrity metadata
// if !response.is_network_error() {
// // Substep 1
// response.wait_until_done();
// // Substep 2
// if response.termination_reason.is_none() {
// response = Response::network_error();
// internal_response = Response::network_error();
// }
// }
}
// Step 18
let response = if !response.is_network_error() && *request.integrity_metadata.borrow() != "" {
// Substep 1
wait_for_response(&response, target, done_chan);
response_loaded = true;
// Substep 2
let ref integrity_metadata = *request.integrity_metadata.borrow();
if response.termination_reason.is_none() &&
!is_response_integrity_valid(integrity_metadata, &response) {
Response::network_error(NetworkError::Internal("Subresource integrity validation failed".into()))
} else {
response
}
} else {
response
};
// Step 19
if request.synchronous {
// process_response is not supposed to be used
// by sync fetch, but we overload it here for simplicity
target.process_response(&response);
if let Some(ref ch) = *done_chan {
loop {
match ch.1.recv()
.expect("fetch worker should always send Done before terminating") {
Data::Payload(vec) => {
target.process_response_chunk(vec);
}
Data::Done => break,
}
}
} else {
let body = response.body.lock().unwrap();
if let ResponseBody::Done(ref vec) = *body {
// in case there was no channel to wait for, the body was
// obtained synchronously via basic_fetch for data/file/about/etc
// We should still send the body across as a chunk
target.process_response_chunk(vec.clone());
} else {
assert!(*body == ResponseBody::Empty)
}
if !response_loaded {
wait_for_response(&response, target, done_chan);
}
// overloaded similarly to process_response
target.process_response_eof(&response);
return response;
@ -360,13 +345,25 @@ pub fn main_fetch(request: Rc<Request>,
target.process_response(&response);
// Step 22
if !response_loaded {
wait_for_response(&response, target, done_chan);
}
// Step 24
target.process_response_eof(&response);
// TODO remove this line when only asynchronous fetches are used
return response;
}
fn wait_for_response(response: &Response, target: Target, done_chan: &mut DoneChannel) {
if let Some(ref ch) = *done_chan {
loop {
match ch.1.recv()
.expect("fetch worker should always send Done before terminating") {
Data::Payload(vec) => {
target.process_response_chunk(vec);
}
},
Data::Done => break,
}
}
@ -381,12 +378,6 @@ pub fn main_fetch(request: Rc<Request>,
assert!(*body == ResponseBody::Empty)
}
}
// Step 24
target.process_response_eof(&response);
// TODO remove this line when only asynchronous fetches are used
return response;
}
/// [Basic fetch](https://fetch.spec.whatwg.org#basic-fetch)
@ -518,3 +509,4 @@ fn is_null_body_status(status: &Option<StatusCode>) -> bool {
_ => false
}
}

View file

@ -1378,7 +1378,7 @@ fn cors_check(request: Rc<Request>, response: &Response) -> Result<(), ()> {
}
// Step 6
let credentials = request.headers.borrow().get::<AccessControlAllowCredentials>().cloned();
let credentials = response.headers.get::<AccessControlAllowCredentials>().cloned();
// Step 7
if credentials.is_some() {

View file

@ -60,8 +60,8 @@ pub mod image_cache_thread;
pub mod mime_classifier;
pub mod resource_thread;
mod storage_thread;
pub mod subresource_integrity;
mod websocket_loader;
/// An implementation of the [Fetch specification](https://fetch.spec.whatwg.org/)
pub mod fetch {
pub mod cors_cache;

View file

@ -0,0 +1,177 @@
/* 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 http://mozilla.org/MPL/2.0/. */
use net_traits::response::{Response, ResponseBody, ResponseType};
use openssl::crypto::hash::{hash, Type as MessageDigest};
use rustc_serialize::base64::{STANDARD, ToBase64};
use std::iter::Filter;
use std::str::Split;
use std::sync::MutexGuard;
const SUPPORTED_ALGORITHM: &'static [&'static 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: 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));
}
}
return 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.to_owned() == hash_func_left).unwrap();
let right_priority = SUPPORTED_ALGORITHM.iter().position(|s| s.to_owned() == 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,
&*current_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(body: MutexGuard<ResponseBody>,
message_digest: MessageDigest)
-> String {
if let ResponseBody::Done(ref vec) = *body {
let response_digest = hash(message_digest, vec);
response_digest.to_base64(STANDARD)
} 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 {
match response.response_type {
ResponseType::Basic | ResponseType::Default | ResponseType::Cors => true,
_ => false,
}
}
/// 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 message_digest = match &*algorithm {
"sha256" => MessageDigest::SHA256,
"sha384" => MessageDigest::SHA384,
"sha512" => MessageDigest::SHA512,
_ => continue,
};
if apply_algorithm_to_response(body, message_digest) == digest {
return true;
}
}
false
}
pub fn split_html_space_chars<'a>(s: &'a str) ->
Filter<Split<'a, 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)
}

View file

@ -158,6 +158,7 @@ pub struct RequestInit {
pub referrer_policy: Option<ReferrerPolicy>,
pub pipeline_id: Option<PipelineId>,
pub redirect_mode: RedirectMode,
pub integrity_metadata: String,
}
impl Default for RequestInit {
@ -181,6 +182,7 @@ impl Default for RequestInit {
referrer_policy: None,
pipeline_id: None,
redirect_mode: RedirectMode::Follow,
integrity_metadata: "".to_owned(),
}
}
}
@ -291,6 +293,7 @@ impl Request {
req.referrer_policy.set(init.referrer_policy);
req.pipeline_id.set(init.pipeline_id);
req.redirect_mode.set(init.redirect_mode);
*req.integrity_metadata.borrow_mut() = init.integrity_metadata;
req
}

View file

@ -243,16 +243,24 @@ impl HTMLLinkElement {
Some(ref value) => &***value,
None => "",
};
let mut css_parser = CssParser::new(&mq_str);
let media = parse_media_query_list(&mut css_parser);
let im_attribute = element.get_attribute(&ns!(), &local_name!("integrity"));
let integrity_val = im_attribute.r().map(|a| a.value());
let integrity_metadata = match integrity_val {
Some(ref value) => &***value,
None => "",
};
// TODO: #8085 - Don't load external stylesheets if the node's mq
// doesn't match.
let loader = StylesheetLoader::for_element(self.upcast());
loader.load(StylesheetContextSource::LinkElement {
url: url,
media: Some(media),
});
}, integrity_metadata.to_owned());
}
fn handle_favicon_url(&self, rel: &str, href: &str, sizes: &Option<String>) {
@ -328,6 +336,12 @@ impl HTMLLinkElementMethods for HTMLLinkElement {
// https://html.spec.whatwg.org/multipage/#dom-link-media
make_setter!(SetMedia, "media");
// https://html.spec.whatwg.org/multipage/#dom-link-integrity
make_getter!(Integrity, "integrity");
// https://html.spec.whatwg.org/multipage/#dom-link-integrity
make_setter!(SetIntegrity, "integrity");
// https://html.spec.whatwg.org/multipage/#dom-link-hreflang
make_getter!(Hreflang, "hreflang");

View file

@ -40,7 +40,6 @@ use std::ascii::AsciiExt;
use std::cell::Cell;
use std::sync::{Arc, Mutex};
use style::str::{HTML_SPACE_CHARACTERS, StaticStringVec};
#[dom_struct]
pub struct HTMLScriptElement {
htmlelement: HTMLElement,
@ -221,6 +220,7 @@ impl PreInvoke for ScriptContext {}
fn fetch_a_classic_script(script: &HTMLScriptElement,
url: ServoUrl,
cors_setting: Option<CorsSettings>,
integrity_metadata: String,
character_encoding: EncodingRef) {
let doc = document_from_node(script);
@ -245,6 +245,7 @@ fn fetch_a_classic_script(script: &HTMLScriptElement,
pipeline_id: Some(script.global().pipeline_id()),
referrer_url: Some(doc.url()),
referrer_policy: doc.get_referrer_policy(),
integrity_metadata: integrity_metadata,
.. RequestInit::default()
};
@ -365,7 +366,13 @@ impl HTMLScriptElement {
// TODO: Step 15: Nonce.
// TODO: Step 16: Parser state.
// Step 16: Integrity Metadata
let im_attribute = element.get_attribute(&ns!(), &local_name!("integrity"));
let integrity_val = im_attribute.r().map(|a| a.value());
let integrity_metadata = match integrity_val {
Some(ref value) => &***value,
None => "",
};
// TODO: Step 17: environment settings object.
@ -393,7 +400,7 @@ impl HTMLScriptElement {
};
// Step 18.6.
fetch_a_classic_script(self, url, cors_setting, encoding);
fetch_a_classic_script(self, url, cors_setting, integrity_metadata.to_owned(), encoding);
true
},
@ -675,6 +682,11 @@ impl HTMLScriptElementMethods for HTMLScriptElement {
// https://html.spec.whatwg.org/multipage/#dom-script-defer
make_bool_setter!(SetDefer, "defer");
// https://html.spec.whatwg.org/multipage/#dom-script-integrity
make_getter!(Integrity, "integrity");
// https://html.spec.whatwg.org/multipage/#dom-script-integrity
make_setter!(SetIntegrity, "integrity");
// https://html.spec.whatwg.org/multipage/#dom-script-event
make_getter!(Event, "event");
// https://html.spec.whatwg.org/multipage/#dom-script-event

View file

@ -11,6 +11,7 @@ interface HTMLLinkElement : HTMLElement {
attribute DOMString media;
attribute DOMString hreflang;
attribute DOMString type;
attribute DOMString integrity;
// [SameObject, PutForwards=value] readonly attribute DOMTokenList sizes;
// also has obsolete members

View file

@ -12,6 +12,7 @@ interface HTMLScriptElement : HTMLElement {
attribute DOMString? crossOrigin;
[Pure]
attribute DOMString text;
attribute DOMString integrity;
// also has obsolete members
};

View file

@ -193,7 +193,7 @@ impl<'a> StylesheetLoader<'a> {
}
impl<'a> StylesheetLoader<'a> {
pub fn load(&self, source: StylesheetContextSource) {
pub fn load(&self, source: StylesheetContextSource, integrity_metadata: String) {
let url = source.url();
let document = document_from_node(self.elem);
let context = Arc::new(Mutex::new(StylesheetContext {
@ -234,6 +234,7 @@ impl<'a> StylesheetLoader<'a> {
pipeline_id: Some(self.elem.global().pipeline_id()),
referrer_url: Some(document.url()),
referrer_policy: referrer_policy,
integrity_metadata: integrity_metadata,
.. RequestInit::default()
};
@ -243,6 +244,6 @@ impl<'a> StylesheetLoader<'a> {
impl<'a> StyleStylesheetLoader for StylesheetLoader<'a> {
fn request_stylesheet(&self, import: &Arc<RwLock<ImportRule>>) {
self.load(StylesheetContextSource::Import(import.clone()))
self.load(StylesheetContextSource::Import(import.clone()), "".to_owned())
}
}