diff options
Diffstat (limited to 'components/net/hsts.rs')
-rw-r--r-- | components/net/hsts.rs | 134 |
1 files changed, 112 insertions, 22 deletions
diff --git a/components/net/hsts.rs b/components/net/hsts.rs index d74794ce60a..be955980d2b 100644 --- a/components/net/hsts.rs +++ b/components/net/hsts.rs @@ -4,26 +4,37 @@ use std::collections::HashMap; use std::net::{Ipv4Addr, Ipv6Addr}; +use std::num::NonZeroU64; +use std::sync::LazyLock; use std::time::Duration; -use base::cross_process_instant::CrossProcessInstant; use embedder_traits::resources::{self, Resource}; use headers::{HeaderMapExt, StrictTransportSecurity}; use http::HeaderMap; -use log::{error, info}; +use log::{debug, error, info}; use malloc_size_of_derive::MallocSizeOf; use net_traits::IncludeSubdomains; use net_traits::pub_domains::reg_suffix; use serde::{Deserialize, Serialize}; use servo_config::pref; use servo_url::{Host, ServoUrl}; +use time::UtcDateTime; #[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] pub struct HstsEntry { pub host: String, pub include_subdomains: bool, - pub max_age: Option<Duration>, - pub timestamp: Option<CrossProcessInstant>, + // Nonzero to allow for memory optimization + pub expires_at: Option<NonZeroU64>, +} + +// Zero and negative times are all expired +fn unix_timestamp_to_nonzerou64(timestamp: i64) -> NonZeroU64 { + if timestamp <= 0 { + NonZeroU64::new(1).unwrap() + } else { + NonZeroU64::new(timestamp.try_into().unwrap()).unwrap() + } } impl HstsEntry { @@ -32,43 +43,59 @@ impl HstsEntry { subdomains: IncludeSubdomains, max_age: Option<Duration>, ) -> Option<HstsEntry> { + let expires_at = max_age.map(|duration| { + unix_timestamp_to_nonzerou64((UtcDateTime::now() + duration).unix_timestamp()) + }); if host.parse::<Ipv4Addr>().is_ok() || host.parse::<Ipv6Addr>().is_ok() { None } else { Some(HstsEntry { host, include_subdomains: (subdomains == IncludeSubdomains::Included), - max_age, - timestamp: Some(CrossProcessInstant::now()), + expires_at, }) } } pub fn is_expired(&self) -> bool { - match (self.max_age, self.timestamp) { - (Some(max_age), Some(timestamp)) => CrossProcessInstant::now() - timestamp >= max_age, - + match self.expires_at { + Some(timestamp) => { + unix_timestamp_to_nonzerou64(UtcDateTime::now().unix_timestamp()) >= timestamp + }, _ => false, } } fn matches_domain(&self, host: &str) -> bool { - !self.is_expired() && self.host == host + self.host == host } fn matches_subdomain(&self, host: &str) -> bool { - !self.is_expired() && host.ends_with(&format!(".{}", self.host)) + host.ends_with(&format!(".{}", self.host)) } } #[derive(Clone, Debug, Default, Deserialize, MallocSizeOf, Serialize)] pub struct HstsList { + // Map from base domains to a list of entries that are subdomains of base domain pub entries_map: HashMap<String, Vec<HstsEntry>>, } -impl HstsList { +/// Represents the portion of the HSTS list that comes from the preload list +/// it is split out to allow sharing between the private and public http state +/// as well as potentially swpaping out the underlying type to something immutable +/// and more efficient like FSTs or DAFSA/DAWGs. +#[derive(Clone, Debug, Default, Deserialize, MallocSizeOf, Serialize)] +pub struct HstsPreloadList { + pub entries_map: HashMap<String, Vec<HstsEntry>>, +} + +pub static PRELOAD_LIST_ENTRIES: LazyLock<HstsPreloadList> = + LazyLock::new(HstsPreloadList::from_servo_preload); + +impl HstsPreloadList { /// Create an `HstsList` from the bytes of a JSON preload file. - pub fn from_preload(preload_content: &str) -> Option<HstsList> { + pub fn from_preload(preload_content: &str) -> Option<HstsPreloadList> { #[derive(Deserialize)] struct HstsEntries { entries: Vec<HstsEntry>, @@ -77,7 +104,7 @@ impl HstsList { let hsts_entries: Option<HstsEntries> = serde_json::from_str(preload_content).ok(); hsts_entries.map(|hsts_entries| { - let mut hsts_list: HstsList = HstsList::default(); + let mut hsts_list: HstsPreloadList = HstsPreloadList::default(); for hsts_entry in hsts_entries.entries { hsts_list.push(hsts_entry); @@ -87,17 +114,21 @@ impl HstsList { }) } - pub fn from_servo_preload() -> HstsList { + pub fn from_servo_preload() -> HstsPreloadList { + debug!("Intializing HSTS Preload list"); let list = resources::read_string(Resource::HstsPreloadList); - HstsList::from_preload(&list).unwrap_or_else(|| { + HstsPreloadList::from_preload(&list).unwrap_or_else(|| { error!("HSTS preload file is invalid. Setting HSTS list to default values"); - HstsList::default() + HstsPreloadList { + entries_map: Default::default(), + } }) } pub fn is_host_secure(&self, host: &str) -> bool { let base_domain = reg_suffix(host); self.entries_map.get(base_domain).is_some_and(|entries| { + // No need to check for expiration in the preload list entries.iter().any(|e| { if e.include_subdomains { e.matches_subdomain(host) || e.matches_domain(host) @@ -108,16 +139,74 @@ impl HstsList { }) } - fn has_domain(&self, host: &str, base_domain: &str) -> bool { + pub fn has_domain(&self, host: &str, base_domain: &str) -> bool { self.entries_map .get(base_domain) .is_some_and(|entries| entries.iter().any(|e| e.matches_domain(host))) } - fn has_subdomain(&self, host: &str, base_domain: &str) -> bool { + pub fn has_subdomain(&self, host: &str, base_domain: &str) -> bool { + self.entries_map.get(base_domain).is_some_and(|entries| { + entries + .iter() + .any(|e| e.include_subdomains && e.matches_subdomain(host)) + }) + } + + pub fn push(&mut self, entry: HstsEntry) { + let host = entry.host.clone(); + let base_domain = reg_suffix(&host); + let have_domain = self.has_domain(&entry.host, base_domain); + let have_subdomain = self.has_subdomain(&entry.host, base_domain); + + let entries = self.entries_map.entry(base_domain.to_owned()).or_default(); + if !have_domain && !have_subdomain { + entries.push(entry); + } else if !have_subdomain { + for e in entries { + if e.matches_domain(&entry.host) { + e.include_subdomains = entry.include_subdomains; + // TODO(sebsebmc): We could shrink the the HSTS preload memory use further by using a type + // that doesn't store an expiry since all preload entries should be "forever" + e.expires_at = entry.expires_at; + } + } + } + } +} + +impl HstsList { + pub fn is_host_secure(&self, host: &str) -> bool { + debug!("HSTS: is {host} secure?"); + if PRELOAD_LIST_ENTRIES.is_host_secure(host) { + info!("{host} is in the preload list"); + return true; + } + + let base_domain = reg_suffix(host); + self.entries_map.get(base_domain).is_some_and(|entries| { + entries.iter().filter(|e| !e.is_expired()).any(|e| { + if e.include_subdomains { + e.matches_subdomain(host) || e.matches_domain(host) + } else { + e.matches_domain(host) + } + }) + }) + } + + fn has_domain(&self, host: &str, base_domain: &str) -> bool { self.entries_map .get(base_domain) - .is_some_and(|entries| entries.iter().any(|e| e.matches_subdomain(host))) + .is_some_and(|entries| entries.iter().any(|e| e.matches_domain(host))) + } + + fn has_subdomain(&self, host: &str, base_domain: &str) -> bool { + self.entries_map.get(base_domain).is_some_and(|entries| { + entries + .iter() + .any(|e| e.include_subdomains && e.matches_subdomain(host)) + }) } pub fn push(&mut self, entry: HstsEntry) { @@ -130,13 +219,14 @@ impl HstsList { if !have_domain && !have_subdomain { entries.push(entry); } else if !have_subdomain { - for e in entries { + for e in entries.iter_mut() { if e.matches_domain(&entry.host) { e.include_subdomains = entry.include_subdomains; - e.max_age = entry.max_age; + e.expires_at = entry.expires_at; } } } + entries.retain(|e| !e.is_expired()); } /// Step 2.9 of <https://fetch.spec.whatwg.org/#concept-main-fetch>. |