diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /security/manager/ssl/cert_storage | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream/115.8.0esr.tar.xz firefox-esr-upstream/115.8.0esr.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'security/manager/ssl/cert_storage')
-rw-r--r-- | security/manager/ssl/cert_storage/Cargo.toml | 24 | ||||
-rw-r--r-- | security/manager/ssl/cert_storage/src/cert_storage.h | 24 | ||||
-rw-r--r-- | security/manager/ssl/cert_storage/src/lib.rs | 1807 |
3 files changed, 1855 insertions, 0 deletions
diff --git a/security/manager/ssl/cert_storage/Cargo.toml b/security/manager/ssl/cert_storage/Cargo.toml new file mode 100644 index 0000000000..e8b4690421 --- /dev/null +++ b/security/manager/ssl/cert_storage/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "cert_storage" +version = "0.0.1" +authors = ["Dana Keeler <dkeeler@mozilla.com>", "Mark Goodwin <mgoodwin@mozilla.com"] +license = "MPL-2.0" + +[dependencies] +base64 = "0.21.0" +byteorder = "1.2.7" +crossbeam-utils = "0.8" +cstr = "0.2" +log = "0.4" +moz_task = { path = "../../../../xpcom/rust/moz_task" } +nserror = { path = "../../../../xpcom/rust/nserror" } +nsstring = { path = "../../../../xpcom/rust/nsstring" } +rkv = { version = "0.18", default-features = false } +rust_cascade = "1.4.0" +sha2 = "0.10.2" +storage_variant = { path = "../../../../storage/variant" } +tempfile = "3" +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } +time = "0.1" +xpcom = { path = "../../../../xpcom/rust/xpcom" } +wr_malloc_size_of = { path = "../../../../gfx/wr/wr_malloc_size_of" } diff --git a/security/manager/ssl/cert_storage/src/cert_storage.h b/security/manager/ssl/cert_storage/src/cert_storage.h new file mode 100644 index 0000000000..e420067b03 --- /dev/null +++ b/security/manager/ssl/cert_storage/src/cert_storage.h @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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/. */ + +#ifndef _cert_storage_h_ +#define _cert_storage_h_ + +#include "nsISupportsUtils.h" // for nsresult, etc. + +// {16e5c837-f877-4e23-9c64-eddf905e30e6} +#define NS_CERT_STORAGE_CID \ + { \ + 0x16e5c837, 0xf877, 0x4e23, { \ + 0x9c, 0x64, 0xed, 0xdf, 0x90, 0x5e, 0x30, 0xe6 \ + } \ + } + +extern "C" { +nsresult cert_storage_constructor(REFNSIID iid, void** result); +}; + +#endif // _cert_storage_h_ diff --git a/security/manager/ssl/cert_storage/src/lib.rs b/security/manager/ssl/cert_storage/src/lib.rs new file mode 100644 index 0000000000..71c966fa4c --- /dev/null +++ b/security/manager/ssl/cert_storage/src/lib.rs @@ -0,0 +1,1807 @@ +/* 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/. */ + +extern crate base64; +extern crate byteorder; +extern crate crossbeam_utils; +#[macro_use] +extern crate cstr; +#[macro_use] +extern crate log; +extern crate moz_task; +extern crate nserror; +extern crate nsstring; +extern crate rkv; +extern crate rust_cascade; +extern crate sha2; +extern crate thin_vec; +extern crate time; +#[macro_use] +extern crate xpcom; +extern crate storage_variant; +extern crate tempfile; + +extern crate wr_malloc_size_of; +use wr_malloc_size_of as malloc_size_of; + +use base64::prelude::*; +use byteorder::{LittleEndian, NetworkEndian, ReadBytesExt, WriteBytesExt}; +use crossbeam_utils::atomic::AtomicCell; +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; +use moz_task::{create_background_task_queue, is_main_thread, Task, TaskRunnable}; +use nserror::{ + nsresult, NS_ERROR_FAILURE, NS_ERROR_NOT_SAME_THREAD, NS_ERROR_NULL_POINTER, + NS_ERROR_UNEXPECTED, NS_OK, +}; +use nsstring::{nsACString, nsCStr, nsCString, nsString}; +use rkv::backend::{BackendEnvironmentBuilder, SafeMode, SafeModeDatabase, SafeModeEnvironment}; +use rkv::{StoreError, StoreOptions, Value}; +use rust_cascade::Cascade; +use sha2::{Digest, Sha256}; +use std::collections::{HashMap, HashSet}; +use std::ffi::CString; +use std::fmt::Display; +use std::fs::{create_dir_all, remove_file, File, OpenOptions}; +use std::io::{BufRead, BufReader, Read, Write}; +use std::mem::size_of; +use std::path::{Path, PathBuf}; +use std::str; +use std::sync::{Arc, RwLock}; +use std::time::{SystemTime, UNIX_EPOCH}; +use storage_variant::VariantType; +use thin_vec::ThinVec; +use xpcom::interfaces::{ + nsICRLiteCoverage, nsICRLiteTimestamp, nsICertInfo, nsICertStorage, nsICertStorageCallback, + nsIFile, nsIHandleReportCallback, nsIIssuerAndSerialRevocationState, nsIMemoryReporter, + nsIMemoryReporterManager, nsIProperties, nsIRevocationState, nsISerialEventTarget, + nsISubjectAndPubKeyRevocationState, nsISupports, +}; +use xpcom::{nsIID, GetterAddrefs, RefPtr, ThreadBoundRefPtr, XpCom}; + +const PREFIX_REV_IS: &str = "is"; +const PREFIX_REV_SPK: &str = "spk"; +const PREFIX_SUBJECT: &str = "subject"; +const PREFIX_CERT: &str = "cert"; +const PREFIX_DATA_TYPE: &str = "datatype"; + +const LAST_CRLITE_UPDATE_KEY: &str = "last_crlite_update"; + +const COVERAGE_SERIALIZATION_VERSION: u8 = 1; +const COVERAGE_V1_ENTRY_BYTES: usize = 48; + +const ENROLLMENT_SERIALIZATION_VERSION: u8 = 1; +const ENROLLMENT_V1_ENTRY_BYTES: usize = 32; + +type Rkv = rkv::Rkv<SafeModeEnvironment>; +type SingleStore = rkv::SingleStore<SafeModeDatabase>; + +macro_rules! make_key { + ( $prefix:expr, $( $part:expr ),+ ) => { + { + let mut key = $prefix.as_bytes().to_owned(); + $( key.extend_from_slice($part); )+ + key + } + } +} + +#[allow(non_camel_case_types, non_snake_case)] + +/// `SecurityStateError` is a type to represent errors in accessing or +/// modifying security state. +#[derive(Debug)] +struct SecurityStateError { + message: String, +} + +impl<T: Display> From<T> for SecurityStateError { + /// Creates a new instance of `SecurityStateError` from something that + /// implements the `Display` trait. + fn from(err: T) -> SecurityStateError { + SecurityStateError { + message: format!("{}", err), + } + } +} + +struct EnvAndStore { + env: Rkv, + store: SingleStore, +} + +impl MallocSizeOf for EnvAndStore { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + self.env + .read() + .and_then(|reader| { + let iter = self.store.iter_start(&reader)?.into_iter(); + Ok(iter + .map(|r| { + r.map(|(k, v)| k.len() + v.serialized_size().unwrap_or(0) as usize) + .unwrap_or(0) + }) + .sum()) + }) + .unwrap_or(0) + } +} + +/// `SecurityState` +struct SecurityState { + profile_path: PathBuf, + env_and_store: Option<EnvAndStore>, + crlite_filter: Option<Cascade>, + /// Maps issuer spki hashes to sets of serial numbers. + crlite_stash: Option<HashMap<Vec<u8>, HashSet<Vec<u8>>>>, + /// Maps an RFC 6962 LogID to a pair of 64 bit unix timestamps + crlite_coverage: Option<HashMap<Vec<u8>, (u64, u64)>>, + /// Set of `SHA256(subject || spki)` values for enrolled issuers + crlite_enrollment: Option<HashSet<Vec<u8>>>, + /// Tracks the number of asynchronous operations which have been dispatched but not completed. + remaining_ops: i32, +} + +impl SecurityState { + pub fn new(profile_path: PathBuf) -> SecurityState { + // Since this gets called on the main thread, we don't actually want to open the DB yet. + // We do this on-demand later, when we're probably on a certificate verification thread. + SecurityState { + profile_path, + env_and_store: None, + crlite_filter: None, + crlite_stash: None, + crlite_coverage: None, + crlite_enrollment: None, + remaining_ops: 0, + } + } + + pub fn db_needs_opening(&self) -> bool { + self.env_and_store.is_none() + } + + pub fn open_db(&mut self) -> Result<(), SecurityStateError> { + if self.env_and_store.is_some() { + return Ok(()); + } + + let store_path = get_store_path(&self.profile_path)?; + + // Open the store in read-write mode to create it (if needed) and migrate data from the old + // store (if any). + // If opening initially fails, try to remove and recreate the database. Consumers will + // repopulate the database as necessary if this happens (see bug 1546361). + let env = make_env(store_path.as_path()).or_else(|_| { + remove_db(store_path.as_path())?; + make_env(store_path.as_path()) + })?; + let store = env.open_single("cert_storage", StoreOptions::create())?; + + // if the profile has a revocations.txt, migrate it and remove the file + let mut revocations_path = self.profile_path.clone(); + revocations_path.push("revocations.txt"); + if revocations_path.exists() { + SecurityState::migrate(&revocations_path, &env, &store)?; + remove_file(revocations_path)?; + } + + // We already returned early if env_and_store was Some, so this should take the None branch. + match self.env_and_store.replace(EnvAndStore { env, store }) { + Some(_) => Err(SecurityStateError::from( + "env and store already initialized? (did we mess up our threading model?)", + )), + None => Ok(()), + }?; + self.load_crlite_filter()?; + Ok(()) + } + + fn migrate( + revocations_path: &PathBuf, + env: &Rkv, + store: &SingleStore, + ) -> Result<(), SecurityStateError> { + let f = File::open(revocations_path)?; + let file = BufReader::new(f); + let value = Value::I64(nsICertStorage::STATE_ENFORCE as i64); + let mut writer = env.write()?; + + // Add the data from revocations.txt + let mut dn: Option<Vec<u8>> = None; + for line in file.lines() { + let l = match line.map_err(|_| SecurityStateError::from("io error reading line data")) { + Ok(data) => data, + Err(e) => return Err(e), + }; + if l.len() == 0 || l.starts_with("#") { + continue; + } + let leading_char = match l.chars().next() { + Some(c) => c, + None => { + return Err(SecurityStateError::from( + "couldn't get char from non-empty str?", + )); + } + }; + // In future, we can maybe log migration failures. For now, ignore decoding and storage + // errors and attempt to continue. + // Check if we have a new DN + if leading_char != '\t' && leading_char != ' ' { + if let Ok(decoded_dn) = BASE64_STANDARD.decode(&l) { + dn = Some(decoded_dn); + } + continue; + } + let l_sans_prefix = match BASE64_STANDARD.decode(&l[1..]) { + Ok(decoded) => decoded, + Err(_) => continue, + }; + if let Some(name) = &dn { + if leading_char == '\t' { + let _ = store.put( + &mut writer, + &make_key!(PREFIX_REV_SPK, name, &l_sans_prefix), + &value, + ); + } else { + let _ = store.put( + &mut writer, + &make_key!(PREFIX_REV_IS, name, &l_sans_prefix), + &value, + ); + } + } + } + + writer.commit()?; + Ok(()) + } + + fn read_entry(&self, key: &[u8]) -> Result<Option<i16>, SecurityStateError> { + let env_and_store = match self.env_and_store.as_ref() { + Some(env_and_store) => env_and_store, + None => return Err(SecurityStateError::from("env and store not initialized?")), + }; + let reader = env_and_store.env.read()?; + match env_and_store.store.get(&reader, key) { + Ok(Some(Value::I64(i))) + if i <= (std::i16::MAX as i64) && i >= (std::i16::MIN as i64) => + { + Ok(Some(i as i16)) + } + Ok(None) => Ok(None), + Ok(_) => Err(SecurityStateError::from( + "Unexpected type when trying to get a Value::I64", + )), + Err(_) => Err(SecurityStateError::from( + "There was a problem getting the value", + )), + } + } + + pub fn get_has_prior_data(&self, data_type: u8) -> Result<bool, SecurityStateError> { + if data_type == nsICertStorage::DATA_TYPE_CRLITE_FILTER_FULL { + return Ok(self.crlite_filter.is_some() + && self.crlite_coverage.is_some() + && self.crlite_enrollment.is_some()); + } + if data_type == nsICertStorage::DATA_TYPE_CRLITE_FILTER_INCREMENTAL { + return Ok(self.crlite_stash.is_some()); + } + + let env_and_store = match self.env_and_store.as_ref() { + Some(env_and_store) => env_and_store, + None => return Err(SecurityStateError::from("env and store not initialized?")), + }; + let reader = env_and_store.env.read()?; + match env_and_store + .store + .get(&reader, &make_key!(PREFIX_DATA_TYPE, &[data_type])) + { + Ok(Some(Value::Bool(true))) => Ok(true), + Ok(None) => Ok(false), + Ok(_) => Err(SecurityStateError::from( + "Unexpected type when trying to get a Value::Bool", + )), + Err(_) => Err(SecurityStateError::from( + "There was a problem getting the value", + )), + } + } + + pub fn set_batch_state( + &mut self, + entries: &[EncodedSecurityState], + typ: u8, + ) -> Result<(), SecurityStateError> { + let env_and_store = match self.env_and_store.as_mut() { + Some(env_and_store) => env_and_store, + None => return Err(SecurityStateError::from("env and store not initialized?")), + }; + let mut writer = env_and_store.env.write()?; + // Make a note that we have prior data of the given type now. + env_and_store.store.put( + &mut writer, + &make_key!(PREFIX_DATA_TYPE, &[typ]), + &Value::Bool(true), + )?; + + for entry in entries { + let key = match entry.key() { + Ok(key) => key, + Err(e) => { + warn!("error base64-decoding key parts - ignoring: {}", e.message); + continue; + } + }; + env_and_store + .store + .put(&mut writer, &key, &Value::I64(entry.state() as i64))?; + } + + writer.commit()?; + Ok(()) + } + + pub fn get_revocation_state( + &self, + issuer: &[u8], + serial: &[u8], + subject: &[u8], + pub_key: &[u8], + ) -> Result<i16, SecurityStateError> { + let mut digest = Sha256::default(); + digest.update(pub_key); + let pub_key_hash = digest.finalize(); + + let subject_pubkey = make_key!(PREFIX_REV_SPK, subject, &pub_key_hash); + let issuer_serial = make_key!(PREFIX_REV_IS, issuer, serial); + + let st: i16 = match self.read_entry(&issuer_serial) { + Ok(Some(value)) => value, + Ok(None) => nsICertStorage::STATE_UNSET, + Err(_) => { + return Err(SecurityStateError::from( + "problem reading revocation state (from issuer / serial)", + )); + } + }; + + if st != nsICertStorage::STATE_UNSET { + return Ok(st); + } + + match self.read_entry(&subject_pubkey) { + Ok(Some(value)) => Ok(value), + Ok(None) => Ok(nsICertStorage::STATE_UNSET), + Err(_) => { + return Err(SecurityStateError::from( + "problem reading revocation state (from subject / pubkey)", + )); + } + } + } + + fn issuer_is_enrolled(&self, subject: &[u8], pub_key: &[u8]) -> bool { + if let Some(crlite_enrollment) = self.crlite_enrollment.as_ref() { + let mut digest = Sha256::default(); + digest.update(subject); + digest.update(pub_key); + let issuer_id = digest.finalize(); + return crlite_enrollment.contains(&issuer_id.to_vec()); + } + return false; + } + + fn filter_covers_some_timestamp(&self, timestamps: &[CRLiteTimestamp]) -> bool { + if let Some(crlite_coverage) = self.crlite_coverage.as_ref() { + for entry in timestamps { + if let Some(&(low, high)) = crlite_coverage.get(entry.log_id.as_ref()) { + if low <= entry.timestamp && entry.timestamp <= high { + return true; + } + } + } + } + return false; + } + + fn note_crlite_update_time(&mut self) -> Result<(), SecurityStateError> { + let seconds_since_epoch = Value::U64( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| SecurityStateError::from("could not get current time"))? + .as_secs(), + ); + let env_and_store = match self.env_and_store.as_mut() { + Some(env_and_store) => env_and_store, + None => return Err(SecurityStateError::from("env and store not initialized?")), + }; + let mut writer = env_and_store.env.write()?; + env_and_store + .store + .put(&mut writer, LAST_CRLITE_UPDATE_KEY, &seconds_since_epoch) + .map_err(|_| SecurityStateError::from("could not store timestamp"))?; + writer.commit()?; + Ok(()) + } + + fn is_crlite_fresh(&self) -> bool { + let now = match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(t) => t.as_secs(), + _ => return false, + }; + let env_and_store = match self.env_and_store.as_ref() { + Some(env_and_store) => env_and_store, + None => return false, + }; + let reader = match env_and_store.env.read() { + Ok(reader) => reader, + _ => return false, + }; + match env_and_store.store.get(&reader, LAST_CRLITE_UPDATE_KEY) { + Ok(Some(Value::U64(last_update))) if last_update < u64::MAX / 2 => { + now < last_update + 60 * 60 * 24 * 10 + } + _ => false, + } + } + + pub fn set_full_crlite_filter( + &mut self, + filter: Vec<u8>, + enrolled_issuers: Vec<nsCString>, + coverage_entries: &[(nsCString, u64, u64)], + ) -> Result<(), SecurityStateError> { + // First drop any existing crlite filter and clear the accumulated stash. + { + let _ = self.crlite_filter.take(); + let _ = self.crlite_stash.take(); + let _ = self.crlite_coverage.take(); + let _ = self.crlite_enrollment.take(); + let mut path = get_store_path(&self.profile_path)?; + path.push("crlite.stash"); + // Truncate the stash file if it exists. + if path.exists() { + File::create(path).map_err(|e| { + SecurityStateError::from(format!("couldn't truncate stash file: {}", e)) + })?; + } + } + // Write the new full filter. + let mut path = get_store_path(&self.profile_path)?; + path.push("crlite.filter"); + { + let mut filter_file = File::create(&path)?; + filter_file.write_all(&filter)?; + } + + // Serialize the coverage metadata as a 1 byte version number followed by any number of 48 + // byte entries. Each entry is a 32 byte (opaque) log id, followed by two 8 byte + // timestamps. Each timestamp is an 8 byte unsigned integer in little endian. + let mut coverage_bytes = + Vec::with_capacity(size_of::<u8>() + coverage_entries.len() * COVERAGE_V1_ENTRY_BYTES); + coverage_bytes.push(COVERAGE_SERIALIZATION_VERSION); + for (b64_log_id, min_t, max_t) in coverage_entries { + let log_id = match BASE64_STANDARD.decode(&b64_log_id) { + Ok(log_id) if log_id.len() == 32 => log_id, + _ => { + warn!("malformed log ID - skipping: {}", b64_log_id); + continue; + } + }; + coverage_bytes.extend_from_slice(&log_id); + coverage_bytes.extend_from_slice(&min_t.to_le_bytes()); + coverage_bytes.extend_from_slice(&max_t.to_le_bytes()); + } + // Write the coverage file for the new filter + let mut path = get_store_path(&self.profile_path)?; + path.push("crlite.coverage"); + { + let mut coverage_file = File::create(&path)?; + coverage_file.write_all(&coverage_bytes)?; + } + + // Serialize the enrollment list as a 1 byte version number followed by: + // Version 1: any number of 32 byte values of the form `SHA256(subject || spki)`. + let mut enrollment_bytes = Vec::with_capacity( + size_of::<u8>() + enrolled_issuers.len() * ENROLLMENT_V1_ENTRY_BYTES, + ); + enrollment_bytes.push(ENROLLMENT_SERIALIZATION_VERSION); + for b64_issuer_id in enrolled_issuers { + let issuer_id = match BASE64_STANDARD.decode(&b64_issuer_id) { + Ok(issuer_id) if issuer_id.len() == 32 => issuer_id, + _ => { + warn!("malformed issuer ID - skipping: {}", b64_issuer_id); + continue; + } + }; + enrollment_bytes.extend_from_slice(&issuer_id); + } + // Write the enrollment file for the new filter + let mut path = get_store_path(&self.profile_path)?; + path.push("crlite.enrollment"); + { + let mut enrollment_file = File::create(&path)?; + enrollment_file.write_all(&enrollment_bytes)?; + } + + self.note_crlite_update_time()?; + self.load_crlite_filter()?; + Ok(()) + } + + fn load_crlite_filter(&mut self) -> Result<(), SecurityStateError> { + if self.crlite_filter.is_some() || self.crlite_coverage.is_some() { + return Err(SecurityStateError::from( + "Both crlite_filter and crlite_coverage should be None here", + )); + } + + let mut path = get_store_path(&self.profile_path)?; + path.push("crlite.filter"); + // Before we've downloaded any filters, this file won't exist. + if !path.exists() { + return Ok(()); + } + let mut filter_file = File::open(path)?; + let mut filter_bytes = Vec::new(); + let _ = filter_file.read_to_end(&mut filter_bytes)?; + let crlite_filter = Cascade::from_bytes(filter_bytes) + .map_err(|_| SecurityStateError::from("invalid CRLite filter"))? + .ok_or(SecurityStateError::from("expecting non-empty filter"))?; + + let mut path = get_store_path(&self.profile_path)?; + path.push("crlite.coverage"); + if !path.exists() { + return Ok(()); + } + + // Deserialize the coverage metadata. + // The format is described in `set_full_crlite_filter`. + let coverage_file = File::open(path)?; + let coverage_file_len = coverage_file.metadata()?.len() as usize; + let mut coverage_reader = BufReader::new(coverage_file); + match coverage_reader.read_u8() { + Ok(COVERAGE_SERIALIZATION_VERSION) => (), + _ => return Err(SecurityStateError::from("unknown CRLite coverage version")), + } + if (coverage_file_len - 1) % COVERAGE_V1_ENTRY_BYTES != 0 { + return Err(SecurityStateError::from("truncated CRLite coverage file")); + } + let coverage_count = (coverage_file_len - 1) / COVERAGE_V1_ENTRY_BYTES; + let mut crlite_coverage: HashMap<Vec<u8>, (u64, u64)> = HashMap::new(); + for _ in 0..coverage_count { + let mut coverage_entry = [0u8; COVERAGE_V1_ENTRY_BYTES]; + match coverage_reader.read_exact(&mut coverage_entry) { + Ok(()) => (), + _ => return Err(SecurityStateError::from("truncated CRLite coverage file")), + }; + let log_id = &coverage_entry[0..32]; + let min_timestamp: u64; + let max_timestamp: u64; + match (&coverage_entry[32..40]).read_u64::<LittleEndian>() { + Ok(value) => min_timestamp = value, + _ => return Err(SecurityStateError::from("truncated CRLite coverage file")), + } + match (&coverage_entry[40..48]).read_u64::<LittleEndian>() { + Ok(value) => max_timestamp = value, + _ => return Err(SecurityStateError::from("truncated CRLite coverage file")), + } + crlite_coverage.insert(log_id.to_vec(), (min_timestamp, max_timestamp)); + } + + let mut path = get_store_path(&self.profile_path)?; + path.push("crlite.enrollment"); + if !path.exists() { + return Ok(()); + } + + // Deserialize the enrollment metadata. + // The format is described in `set_full_crlite_filter`. + let enrollment_file = File::open(path)?; + let enrollment_file_len = enrollment_file.metadata()?.len() as usize; + let mut enrollment_reader = BufReader::new(enrollment_file); + match enrollment_reader.read_u8() { + Ok(ENROLLMENT_SERIALIZATION_VERSION) => (), + _ => { + return Err(SecurityStateError::from( + "unknown CRLite enrollment version", + )) + } + } + if (enrollment_file_len - 1) % ENROLLMENT_V1_ENTRY_BYTES != 0 { + return Err(SecurityStateError::from("truncated CRLite enrollment file")); + } + let enrollment_count = (enrollment_file_len - 1) / ENROLLMENT_V1_ENTRY_BYTES; + let mut crlite_enrollment: HashSet<Vec<u8>> = HashSet::new(); + for _ in 0..enrollment_count { + let mut enrollment_entry = [0u8; ENROLLMENT_V1_ENTRY_BYTES]; + match enrollment_reader.read_exact(&mut enrollment_entry) { + Ok(()) => (), + _ => return Err(SecurityStateError::from("truncated CRLite enrollment file")), + }; + let issuer_id = &enrollment_entry[..]; + crlite_enrollment.insert(issuer_id.to_vec()); + } + + let old_crlite_filter_should_be_none = self.crlite_filter.replace(crlite_filter); + assert!(old_crlite_filter_should_be_none.is_none()); + let old_crlite_coverage_should_be_none = self.crlite_coverage.replace(crlite_coverage); + assert!(old_crlite_coverage_should_be_none.is_none()); + let old_crlite_enrollment_should_be_none = + self.crlite_enrollment.replace(crlite_enrollment); + assert!(old_crlite_enrollment_should_be_none.is_none()); + Ok(()) + } + + pub fn add_crlite_stash(&mut self, stash: Vec<u8>) -> Result<(), SecurityStateError> { + // Append the update to the previously-seen stashes. + let mut path = get_store_path(&self.profile_path)?; + path.push("crlite.stash"); + let mut stash_file = OpenOptions::new().append(true).create(true).open(path)?; + stash_file.write_all(&stash)?; + let crlite_stash = self.crlite_stash.get_or_insert(HashMap::new()); + load_crlite_stash_from_reader_into_map(&mut stash.as_slice(), crlite_stash)?; + self.note_crlite_update_time()?; + Ok(()) + } + + pub fn is_cert_revoked_by_stash( + &self, + issuer_spki: &[u8], + serial: &[u8], + ) -> Result<bool, SecurityStateError> { + let crlite_stash = match self.crlite_stash.as_ref() { + Some(crlite_stash) => crlite_stash, + None => return Ok(false), + }; + let mut digest = Sha256::default(); + digest.update(issuer_spki); + let lookup_key = digest.finalize().to_vec(); + let serials = match crlite_stash.get(&lookup_key) { + Some(serials) => serials, + None => return Ok(false), + }; + Ok(serials.contains(&serial.to_vec())) + } + + pub fn get_crlite_revocation_state( + &self, + issuer: &[u8], + issuer_spki: &[u8], + serial_number: &[u8], + timestamps: &[CRLiteTimestamp], + ) -> i16 { + if !self.is_crlite_fresh() { + return nsICertStorage::STATE_NO_FILTER; + } + if !self.issuer_is_enrolled(issuer, issuer_spki) { + return nsICertStorage::STATE_NOT_ENROLLED; + } + if !self.filter_covers_some_timestamp(timestamps) { + return nsICertStorage::STATE_NOT_COVERED; + } + let mut digest = Sha256::default(); + digest.update(issuer_spki); + let mut lookup_key = digest.finalize().to_vec(); + lookup_key.extend_from_slice(serial_number); + debug!("CRLite lookup key: {:?}", lookup_key); + let result = match &self.crlite_filter { + Some(crlite_filter) => crlite_filter.has(lookup_key), + // This can only happen if the backing file was deleted or if it or our database has + // become corrupted. In any case, we have no information. + None => return nsICertStorage::STATE_NO_FILTER, + }; + match result { + true => nsICertStorage::STATE_ENFORCE, + false => nsICertStorage::STATE_UNSET, + } + } + + // To store certificates, we create a Cert out of each given cert, subject, and trust tuple. We + // hash each certificate with sha-256 to obtain a unique* key for that certificate, and we store + // the Cert in the database. We also look up or create a CertHashList for the given subject and + // add the new certificate's hash if it isn't present in the list. If it wasn't present, we + // write out the updated CertHashList. + // *By the pigeon-hole principle, there exist collisions for sha-256, so this key is not + // actually unique. We rely on the assumption that sha-256 is a cryptographically strong hash. + // If an adversary can find two different certificates with the same sha-256 hash, they can + // probably forge a sha-256-based signature, so assuming the keys we create here are unique is + // not a security issue. + pub fn add_certs( + &mut self, + certs: &[(nsCString, nsCString, i16)], + ) -> Result<(), SecurityStateError> { + let env_and_store = match self.env_and_store.as_mut() { + Some(env_and_store) => env_and_store, + None => return Err(SecurityStateError::from("env and store not initialized?")), + }; + let mut writer = env_and_store.env.write()?; + // Make a note that we have prior cert data now. + env_and_store.store.put( + &mut writer, + &make_key!(PREFIX_DATA_TYPE, &[nsICertStorage::DATA_TYPE_CERTIFICATE]), + &Value::Bool(true), + )?; + + for (cert_der_base64, subject_base64, trust) in certs { + let cert_der = match BASE64_STANDARD.decode(&cert_der_base64) { + Ok(cert_der) => cert_der, + Err(e) => { + warn!("error base64-decoding cert - skipping: {}", e); + continue; + } + }; + let subject = match BASE64_STANDARD.decode(&subject_base64) { + Ok(subject) => subject, + Err(e) => { + warn!("error base64-decoding subject - skipping: {}", e); + continue; + } + }; + let mut digest = Sha256::default(); + digest.update(&cert_der); + let cert_hash = digest.finalize(); + let cert_key = make_key!(PREFIX_CERT, &cert_hash); + let cert = Cert::new(&cert_der, &subject, *trust)?; + env_and_store + .store + .put(&mut writer, &cert_key, &Value::Blob(&cert.to_bytes()?))?; + let subject_key = make_key!(PREFIX_SUBJECT, &subject); + let empty_vec = Vec::new(); + let old_cert_hash_list = match env_and_store.store.get(&writer, &subject_key)? { + Some(Value::Blob(hashes)) => hashes.to_owned(), + Some(_) => empty_vec, + None => empty_vec, + }; + let new_cert_hash_list = CertHashList::add(&old_cert_hash_list, &cert_hash)?; + if new_cert_hash_list.len() != old_cert_hash_list.len() { + env_and_store.store.put( + &mut writer, + &subject_key, + &Value::Blob(&new_cert_hash_list), + )?; + } + } + + writer.commit()?; + Ok(()) + } + + // Given a list of certificate sha-256 hashes, we can look up each Cert entry in the database. + // We use this to find the corresponding subject so we can look up the CertHashList it should + // appear in. If that list contains the given hash, we remove it and update the CertHashList. + // Finally we delete the Cert entry. + pub fn remove_certs_by_hashes( + &mut self, + hashes_base64: &[nsCString], + ) -> Result<(), SecurityStateError> { + let env_and_store = match self.env_and_store.as_mut() { + Some(env_and_store) => env_and_store, + None => return Err(SecurityStateError::from("env and store not initialized?")), + }; + let mut writer = env_and_store.env.write()?; + let reader = env_and_store.env.read()?; + + for hash in hashes_base64 { + let hash = match BASE64_STANDARD.decode(&hash) { + Ok(hash) => hash, + Err(e) => { + warn!("error decoding hash - ignoring: {}", e); + continue; + } + }; + let cert_key = make_key!(PREFIX_CERT, &hash); + if let Some(Value::Blob(cert_bytes)) = env_and_store.store.get(&reader, &cert_key)? { + if let Ok(cert) = Cert::from_bytes(cert_bytes) { + let subject_key = make_key!(PREFIX_SUBJECT, &cert.subject); + let empty_vec = Vec::new(); + // We have to use the writer here to make sure we have an up-to-date view of + // the cert hash list. + let old_cert_hash_list = match env_and_store.store.get(&writer, &subject_key)? { + Some(Value::Blob(hashes)) => hashes.to_owned(), + Some(_) => empty_vec, + None => empty_vec, + }; + let new_cert_hash_list = CertHashList::remove(&old_cert_hash_list, &hash)?; + if new_cert_hash_list.len() != old_cert_hash_list.len() { + env_and_store.store.put( + &mut writer, + &subject_key, + &Value::Blob(&new_cert_hash_list), + )?; + } + } + } + match env_and_store.store.delete(&mut writer, &cert_key) { + Ok(()) => {} + Err(StoreError::KeyValuePairNotFound) => {} + Err(e) => return Err(SecurityStateError::from(e)), + }; + } + writer.commit()?; + Ok(()) + } + + // Given a certificate's subject, we look up the corresponding CertHashList. In theory, each + // hash in that list corresponds to a certificate with the given subject, so we look up each of + // these (assuming the database is consistent and contains them) and add them to the given list. + // If we encounter an inconsistency, we continue looking as best we can. + pub fn find_certs_by_subject( + &self, + subject: &[u8], + certs: &mut ThinVec<ThinVec<u8>>, + ) -> Result<(), SecurityStateError> { + let env_and_store = match self.env_and_store.as_ref() { + Some(env_and_store) => env_and_store, + None => return Err(SecurityStateError::from("env and store not initialized?")), + }; + let reader = env_and_store.env.read()?; + certs.clear(); + let subject_key = make_key!(PREFIX_SUBJECT, subject); + let empty_vec = Vec::new(); + let cert_hash_list_bytes = match env_and_store.store.get(&reader, &subject_key)? { + Some(Value::Blob(hashes)) => hashes, + Some(_) => &empty_vec, + None => &empty_vec, + }; + let cert_hash_list = CertHashList::new(cert_hash_list_bytes)?; + for cert_hash in cert_hash_list.into_iter() { + let cert_key = make_key!(PREFIX_CERT, cert_hash); + // If there's some inconsistency, we don't want to fail the whole operation - just go + // for best effort and find as many certificates as we can. + if let Some(Value::Blob(cert_bytes)) = env_and_store.store.get(&reader, &cert_key)? { + if let Ok(cert) = Cert::from_bytes(cert_bytes) { + let mut thin_vec_cert = ThinVec::with_capacity(cert.der.len()); + thin_vec_cert.extend_from_slice(&cert.der); + certs.push(thin_vec_cert); + } + } + } + Ok(()) + } +} + +impl MallocSizeOf for SecurityState { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.profile_path.size_of(ops) + + self.env_and_store.size_of(ops) + + self + .crlite_filter + .as_ref() + .map_or(0, |crlite_filter| crlite_filter.approximate_size_of()) + + self.crlite_stash.size_of(ops) + + self.crlite_coverage.size_of(ops) + + self.remaining_ops.size_of(ops) + } +} + +const CERT_SERIALIZATION_VERSION_1: u8 = 1; + +// A Cert consists of its DER encoding, its DER-encoded subject, and its trust (currently +// nsICertStorage::TRUST_INHERIT, but in the future nsICertStorage::TRUST_ANCHOR may also be used). +// The length of each encoding must be representable by a u16 (so 65535 bytes is the longest a +// certificate can be). +struct Cert<'a> { + der: &'a [u8], + subject: &'a [u8], + trust: i16, +} + +impl<'a> Cert<'a> { + fn new(der: &'a [u8], subject: &'a [u8], trust: i16) -> Result<Cert<'a>, SecurityStateError> { + if der.len() > u16::max as usize { + return Err(SecurityStateError::from("certificate is too long")); + } + if subject.len() > u16::max as usize { + return Err(SecurityStateError::from("subject is too long")); + } + Ok(Cert { + der, + subject, + trust, + }) + } + + fn from_bytes(encoded: &'a [u8]) -> Result<Cert<'a>, SecurityStateError> { + if encoded.len() < size_of::<u8>() { + return Err(SecurityStateError::from("invalid Cert: no version?")); + } + let (mut version, rest) = encoded.split_at(size_of::<u8>()); + let version = version.read_u8()?; + if version != CERT_SERIALIZATION_VERSION_1 { + return Err(SecurityStateError::from("invalid Cert: unexpected version")); + } + + if rest.len() < size_of::<u16>() { + return Err(SecurityStateError::from("invalid Cert: no der len?")); + } + let (mut der_len, rest) = rest.split_at(size_of::<u16>()); + let der_len = der_len.read_u16::<NetworkEndian>()? as usize; + if rest.len() < der_len { + return Err(SecurityStateError::from("invalid Cert: no der?")); + } + let (der, rest) = rest.split_at(der_len); + + if rest.len() < size_of::<u16>() { + return Err(SecurityStateError::from("invalid Cert: no subject len?")); + } + let (mut subject_len, rest) = rest.split_at(size_of::<u16>()); + let subject_len = subject_len.read_u16::<NetworkEndian>()? as usize; + if rest.len() < subject_len { + return Err(SecurityStateError::from("invalid Cert: no subject?")); + } + let (subject, mut rest) = rest.split_at(subject_len); + + if rest.len() < size_of::<i16>() { + return Err(SecurityStateError::from("invalid Cert: no trust?")); + } + let trust = rest.read_i16::<NetworkEndian>()?; + if rest.len() > 0 { + return Err(SecurityStateError::from("invalid Cert: trailing data?")); + } + + Ok(Cert { + der, + subject, + trust, + }) + } + + fn to_bytes(&self) -> Result<Vec<u8>, SecurityStateError> { + let mut bytes = Vec::with_capacity( + size_of::<u8>() + + size_of::<u16>() + + self.der.len() + + size_of::<u16>() + + self.subject.len() + + size_of::<i16>(), + ); + bytes.write_u8(CERT_SERIALIZATION_VERSION_1)?; + if self.der.len() > u16::max as usize { + return Err(SecurityStateError::from("certificate is too long")); + } + bytes.write_u16::<NetworkEndian>(self.der.len() as u16)?; + bytes.extend_from_slice(&self.der); + if self.subject.len() > u16::max as usize { + return Err(SecurityStateError::from("subject is too long")); + } + bytes.write_u16::<NetworkEndian>(self.subject.len() as u16)?; + bytes.extend_from_slice(&self.subject); + bytes.write_i16::<NetworkEndian>(self.trust)?; + Ok(bytes) + } +} + +// A CertHashList is a list of sha-256 hashes of DER-encoded certificates. +struct CertHashList<'a> { + hashes: Vec<&'a [u8]>, +} + +impl<'a> CertHashList<'a> { + fn new(hashes_bytes: &'a [u8]) -> Result<CertHashList<'a>, SecurityStateError> { + if hashes_bytes.len() % Sha256::output_size() != 0 { + return Err(SecurityStateError::from( + "unexpected length for cert hash list", + )); + } + let mut hashes = Vec::with_capacity(hashes_bytes.len() / Sha256::output_size()); + for hash in hashes_bytes.chunks_exact(Sha256::output_size()) { + hashes.push(hash); + } + Ok(CertHashList { hashes }) + } + + fn add(hashes_bytes: &[u8], new_hash: &[u8]) -> Result<Vec<u8>, SecurityStateError> { + if hashes_bytes.len() % Sha256::output_size() != 0 { + return Err(SecurityStateError::from( + "unexpected length for cert hash list", + )); + } + if new_hash.len() != Sha256::output_size() { + return Err(SecurityStateError::from("unexpected cert hash length")); + } + for hash in hashes_bytes.chunks_exact(Sha256::output_size()) { + if hash == new_hash { + return Ok(hashes_bytes.to_owned()); + } + } + let mut combined = hashes_bytes.to_owned(); + combined.extend_from_slice(new_hash); + Ok(combined) + } + + fn remove(hashes_bytes: &[u8], cert_hash: &[u8]) -> Result<Vec<u8>, SecurityStateError> { + if hashes_bytes.len() % Sha256::output_size() != 0 { + return Err(SecurityStateError::from( + "unexpected length for cert hash list", + )); + } + if cert_hash.len() != Sha256::output_size() { + return Err(SecurityStateError::from("unexpected cert hash length")); + } + let mut result = Vec::with_capacity(hashes_bytes.len()); + for hash in hashes_bytes.chunks_exact(Sha256::output_size()) { + if hash != cert_hash { + result.extend_from_slice(hash); + } + } + Ok(result) + } +} + +impl<'a> IntoIterator for CertHashList<'a> { + type Item = &'a [u8]; + type IntoIter = std::vec::IntoIter<&'a [u8]>; + + fn into_iter(self) -> Self::IntoIter { + self.hashes.into_iter() + } +} + +// Helper struct for get_crlite_revocation_state. +struct CRLiteTimestamp { + log_id: ThinVec<u8>, + timestamp: u64, +} + +// Helper struct for set_batch_state. Takes a prefix, two base64-encoded key +// parts, and a security state value. +struct EncodedSecurityState { + prefix: &'static str, + key_part_1_base64: nsCString, + key_part_2_base64: nsCString, + state: i16, +} + +impl EncodedSecurityState { + fn new( + prefix: &'static str, + key_part_1_base64: nsCString, + key_part_2_base64: nsCString, + state: i16, + ) -> EncodedSecurityState { + EncodedSecurityState { + prefix, + key_part_1_base64, + key_part_2_base64, + state, + } + } + + fn key(&self) -> Result<Vec<u8>, SecurityStateError> { + let key_part_1 = BASE64_STANDARD.decode(&self.key_part_1_base64)?; + let key_part_2 = BASE64_STANDARD.decode(&self.key_part_2_base64)?; + Ok(make_key!(self.prefix, &key_part_1, &key_part_2)) + } + + fn state(&self) -> i16 { + self.state + } +} + +fn get_path_from_directory_service(key: &str) -> Result<PathBuf, nserror::nsresult> { + let directory_service: RefPtr<nsIProperties> = + xpcom::components::Directory::service().map_err(|_| NS_ERROR_FAILURE)?; + let cs_key = CString::new(key).map_err(|_| NS_ERROR_FAILURE)?; + + let mut requested_dir = GetterAddrefs::<nsIFile>::new(); + unsafe { + (*directory_service) + .Get( + (&cs_key).as_ptr(), + &nsIFile::IID as *const nsIID, + requested_dir.void_ptr(), + ) + .to_result() + }?; + + let dir_path = requested_dir.refptr().ok_or(NS_ERROR_FAILURE)?; + let mut path = nsString::new(); + unsafe { (*dir_path).GetPath(&mut *path).to_result() }?; + Ok(PathBuf::from(format!("{}", path))) +} + +fn get_profile_path() -> Result<PathBuf, nserror::nsresult> { + get_path_from_directory_service("ProfD").or_else(|_| get_path_from_directory_service("TmpD")) +} + +fn get_store_path(profile_path: &PathBuf) -> Result<PathBuf, SecurityStateError> { + let mut store_path = profile_path.clone(); + store_path.push("security_state"); + create_dir_all(store_path.as_path())?; + Ok(store_path) +} + +fn make_env(path: &Path) -> Result<Rkv, SecurityStateError> { + let mut builder = Rkv::environment_builder::<SafeMode>(); + builder.set_max_dbs(2); + + // 16MB is a little over twice the size of the current dataset. When we + // eventually switch to the LMDB backend to create the builder above, + // we should set this as the map size, since it cannot currently resize. + // (The SafeMode backend warns when a map size is specified, so we skip it + // for now to avoid console spam.) + + // builder.set_map_size(16777216); + + // Bug 1595004: Migrate databases between backends in the future, + // and handle 32 and 64 bit architectures in case of LMDB. + Rkv::from_builder(path, builder).map_err(SecurityStateError::from) +} + +fn unconditionally_remove_file(path: &Path) -> Result<(), SecurityStateError> { + match remove_file(path) { + Ok(()) => Ok(()), + Err(e) => match e.kind() { + std::io::ErrorKind::NotFound => Ok(()), + _ => Err(SecurityStateError::from(e)), + }, + } +} + +fn remove_db(path: &Path) -> Result<(), SecurityStateError> { + // Remove LMDB-related files. + let db = path.join("data.mdb"); + unconditionally_remove_file(&db)?; + let lock = path.join("lock.mdb"); + unconditionally_remove_file(&lock)?; + + // Remove SafeMode-related files. + let db = path.join("data.safe.bin"); + unconditionally_remove_file(&db)?; + + Ok(()) +} + +// Helper function to read stash information from the given reader and insert the results into the +// given stash map. +fn load_crlite_stash_from_reader_into_map( + reader: &mut dyn Read, + dest: &mut HashMap<Vec<u8>, HashSet<Vec<u8>>>, +) -> Result<(), SecurityStateError> { + // The basic unit of the stash file is an issuer subject public key info + // hash (sha-256) followed by a number of serial numbers corresponding + // to revoked certificates issued by that issuer. More specifically, + // each unit consists of: + // 4 bytes little-endian: the number of serial numbers following the issuer spki hash + // 1 byte: the length of the issuer spki hash + // issuer spki hash length bytes: the issuer spki hash + // as many times as the indicated serial numbers: + // 1 byte: the length of the serial number + // serial number length bytes: the serial number + // The stash file consists of any number of these units concatenated + // together. + loop { + let num_serials = match reader.read_u32::<LittleEndian>() { + Ok(num_serials) => num_serials, + Err(_) => break, // end of input, presumably + }; + let issuer_spki_hash_len = reader.read_u8().map_err(|e| { + SecurityStateError::from(format!("error reading stash issuer_spki_hash_len: {}", e)) + })?; + let mut issuer_spki_hash = vec![0; issuer_spki_hash_len as usize]; + reader.read_exact(&mut issuer_spki_hash).map_err(|e| { + SecurityStateError::from(format!("error reading stash issuer_spki_hash: {}", e)) + })?; + let serials = dest.entry(issuer_spki_hash).or_insert(HashSet::new()); + for _ in 0..num_serials { + let serial_len = reader.read_u8().map_err(|e| { + SecurityStateError::from(format!("error reading stash serial_len: {}", e)) + })?; + let mut serial = vec![0; serial_len as usize]; + reader.read_exact(&mut serial).map_err(|e| { + SecurityStateError::from(format!("error reading stash serial: {}", e)) + })?; + let _ = serials.insert(serial); + } + } + Ok(()) +} + +// This is a helper struct that implements the task that asynchronously reads the CRLite stash on a +// background thread. +struct BackgroundReadStashTask { + profile_path: PathBuf, + security_state: Arc<RwLock<SecurityState>>, +} + +impl BackgroundReadStashTask { + fn new( + profile_path: PathBuf, + security_state: &Arc<RwLock<SecurityState>>, + ) -> BackgroundReadStashTask { + BackgroundReadStashTask { + profile_path, + security_state: Arc::clone(security_state), + } + } +} + +impl Task for BackgroundReadStashTask { + fn run(&self) { + let mut path = match get_store_path(&self.profile_path) { + Ok(path) => path, + Err(e) => { + error!("error getting security_state path: {}", e.message); + return; + } + }; + path.push("crlite.stash"); + // Before we've downloaded any stashes, this file won't exist. + if !path.exists() { + return; + } + let stash_file = match File::open(path) { + Ok(file) => file, + Err(e) => { + error!("error opening stash file: {}", e); + return; + } + }; + let mut stash_reader = BufReader::new(stash_file); + let mut crlite_stash = HashMap::new(); + match load_crlite_stash_from_reader_into_map(&mut stash_reader, &mut crlite_stash) { + Ok(()) => {} + Err(e) => { + error!("error loading crlite stash: {}", e.message); + return; + } + } + let mut ss = match self.security_state.write() { + Ok(ss) => ss, + Err(_) => return, + }; + match ss.crlite_stash.replace(crlite_stash) { + Some(_) => { + error!("replacing existing crlite stash when reading for the first time?"); + return; + } + None => {} + } + } + + fn done(&self) -> Result<(), nsresult> { + Ok(()) + } +} + +fn do_construct_cert_storage( + iid: *const xpcom::nsIID, + result: *mut *mut xpcom::reexports::libc::c_void, +) -> Result<(), nserror::nsresult> { + let path_buf = get_profile_path()?; + let security_state = Arc::new(RwLock::new(SecurityState::new(path_buf.clone()))); + let cert_storage = CertStorage::allocate(InitCertStorage { + security_state: security_state.clone(), + queue: create_background_task_queue(cstr!("cert_storage"))?, + }); + let memory_reporter = MemoryReporter::allocate(InitMemoryReporter { security_state }); + + // Dispatch a task to the background task queue to asynchronously read the CRLite stash file (if + // present) and load it into cert_storage. This task does not hold the + // cert_storage.security_state mutex for the majority of its operation, which allows certificate + // verification threads to query cert_storage without blocking. This is important for + // performance, but it means that certificate verifications that happen before the task has + // completed will not have stash information, and thus may not know of revocations that have + // occurred since the last full CRLite filter was downloaded. As long as the last full filter + // was downloaded no more than 10 days ago, this is no worse than relying on OCSP responses, + // which have a maximum validity of 10 days. + // NB: because the background task queue is serial, this task will complete before other tasks + // later dispatched to the queue run. This means that other tasks that interact with the stash + // will do so with the correct set of preconditions. + let load_crlite_stash_task = Box::new(BackgroundReadStashTask::new( + path_buf, + &cert_storage.security_state, + )); + let runnable = TaskRunnable::new("LoadCrliteStash", load_crlite_stash_task)?; + TaskRunnable::dispatch(runnable, cert_storage.queue.coerce())?; + + if let Some(reporter) = memory_reporter.query_interface::<nsIMemoryReporter>() { + if let Some(reporter_manager) = xpcom::get_service::<nsIMemoryReporterManager>(cstr!( + "@mozilla.org/memory-reporter-manager;1" + )) { + unsafe { reporter_manager.RegisterStrongReporter(&*reporter) }; + } + } + + unsafe { cert_storage.QueryInterface(iid, result).to_result() } +} + +// This is a helper for creating a task that will perform a specific action on a background thread. +struct SecurityStateTask< + T: Default + VariantType, + F: FnOnce(&mut SecurityState) -> Result<T, SecurityStateError>, +> { + callback: AtomicCell<Option<ThreadBoundRefPtr<nsICertStorageCallback>>>, + security_state: Arc<RwLock<SecurityState>>, + result: AtomicCell<(nserror::nsresult, T)>, + task_action: AtomicCell<Option<F>>, +} + +impl<T: Default + VariantType, F: FnOnce(&mut SecurityState) -> Result<T, SecurityStateError>> + SecurityStateTask<T, F> +{ + fn new( + callback: &nsICertStorageCallback, + security_state: &Arc<RwLock<SecurityState>>, + task_action: F, + ) -> Result<SecurityStateTask<T, F>, nsresult> { + let mut ss = security_state.write().or(Err(NS_ERROR_FAILURE))?; + ss.remaining_ops = ss.remaining_ops.wrapping_add(1); + + Ok(SecurityStateTask { + callback: AtomicCell::new(Some(ThreadBoundRefPtr::new(RefPtr::new(callback)))), + security_state: Arc::clone(security_state), + result: AtomicCell::new((NS_ERROR_FAILURE, T::default())), + task_action: AtomicCell::new(Some(task_action)), + }) + } +} + +impl<T: Default + VariantType, F: FnOnce(&mut SecurityState) -> Result<T, SecurityStateError>> Task + for SecurityStateTask<T, F> +{ + fn run(&self) { + let mut ss = match self.security_state.write() { + Ok(ss) => ss, + Err(_) => return, + }; + // this is a no-op if the DB is already open + if ss.open_db().is_err() { + return; + } + if let Some(task_action) = self.task_action.swap(None) { + let rv = task_action(&mut ss) + .and_then(|v| Ok((NS_OK, v))) + .unwrap_or((NS_ERROR_FAILURE, T::default())); + self.result.store(rv); + } + } + + fn done(&self) -> Result<(), nsresult> { + let threadbound = self.callback.swap(None).ok_or(NS_ERROR_FAILURE)?; + let callback = threadbound.get_ref().ok_or(NS_ERROR_FAILURE)?; + let result = self.result.swap((NS_ERROR_FAILURE, T::default())); + let variant = result.1.into_variant(); + let nsrv = unsafe { callback.Done(result.0, &*variant) }; + + let mut ss = self.security_state.write().or(Err(NS_ERROR_FAILURE))?; + ss.remaining_ops = ss.remaining_ops.wrapping_sub(1); + + match nsrv { + NS_OK => Ok(()), + e => Err(e), + } + } +} + +#[no_mangle] +pub extern "C" fn cert_storage_constructor( + iid: *const xpcom::nsIID, + result: *mut *mut xpcom::reexports::libc::c_void, +) -> nserror::nsresult { + if !is_main_thread() { + return NS_ERROR_NOT_SAME_THREAD; + } + match do_construct_cert_storage(iid, result) { + Ok(()) => NS_OK, + Err(e) => e, + } +} + +macro_rules! try_ns { + ($e:expr) => { + match $e { + Ok(value) => value, + Err(_) => return NS_ERROR_FAILURE, + } + }; + ($e:expr, or continue) => { + match $e { + Ok(value) => value, + Err(err) => { + error!("{}", err); + continue; + } + } + }; +} + +// This macro is a way to ensure the DB has been opened while minimizing lock acquisitions in the +// common (read-only) case. First we acquire a read lock and see if we even need to open the DB. If +// not, we can continue with the read lock we already have. Otherwise, we drop the read lock, +// acquire the write lock, open the DB, drop the write lock, and re-acquire the read lock. While it +// is possible for two or more threads to all come to the conclusion that they need to open the DB, +// this isn't ultimately an issue - `open_db` will exit early if another thread has already done the +// work. +macro_rules! get_security_state { + ($self:expr) => {{ + let ss_read_only = try_ns!($self.security_state.read()); + if !ss_read_only.db_needs_opening() { + ss_read_only + } else { + drop(ss_read_only); + { + let mut ss_write = try_ns!($self.security_state.write()); + try_ns!(ss_write.open_db()); + } + try_ns!($self.security_state.read()) + } + }}; +} + +#[xpcom(implement(nsICertStorage), atomic)] +struct CertStorage { + security_state: Arc<RwLock<SecurityState>>, + queue: RefPtr<nsISerialEventTarget>, +} + +/// CertStorage implements the nsICertStorage interface. The actual work is done by the +/// SecurityState. To handle any threading issues, we have an atomic-refcounted read/write lock on +/// the one and only SecurityState. So, only one thread can use SecurityState's &mut self functions +/// at a time, while multiple threads can use &self functions simultaneously (as long as there are +/// no threads using an &mut self function). The Arc is to allow for the creation of background +/// tasks that use the SecurityState on the queue owned by CertStorage. This allows us to not block +/// the main thread. +#[allow(non_snake_case)] +impl CertStorage { + unsafe fn HasPriorData( + &self, + data_type: u8, + callback: *const nsICertStorageCallback, + ) -> nserror::nsresult { + if !is_main_thread() { + return NS_ERROR_NOT_SAME_THREAD; + } + if callback.is_null() { + return NS_ERROR_NULL_POINTER; + } + let task = Box::new(try_ns!(SecurityStateTask::new( + &*callback, + &self.security_state, + move |ss| ss.get_has_prior_data(data_type), + ))); + let runnable = try_ns!(TaskRunnable::new("HasPriorData", task)); + try_ns!(TaskRunnable::dispatch(runnable, self.queue.coerce())); + NS_OK + } + + unsafe fn GetRemainingOperationCount(&self, state: *mut i32) -> nserror::nsresult { + if !is_main_thread() { + return NS_ERROR_NOT_SAME_THREAD; + } + if state.is_null() { + return NS_ERROR_NULL_POINTER; + } + let ss = try_ns!(self.security_state.read()); + *state = ss.remaining_ops; + NS_OK + } + + unsafe fn SetRevocations( + &self, + revocations: *const ThinVec<Option<RefPtr<nsIRevocationState>>>, + callback: *const nsICertStorageCallback, + ) -> nserror::nsresult { + if !is_main_thread() { + return NS_ERROR_NOT_SAME_THREAD; + } + if revocations.is_null() || callback.is_null() { + return NS_ERROR_NULL_POINTER; + } + + let revocations = &*revocations; + let mut entries = Vec::with_capacity(revocations.len()); + + // By continuing when an nsIRevocationState attribute value is invalid, + // we prevent errors relating to individual blocklist entries from + // causing sync to fail. We will accumulate telemetry on these failures + // in bug 1254099. + + for revocation in revocations.iter().flatten() { + let mut state: i16 = 0; + try_ns!(revocation.GetState(&mut state).to_result(), or continue); + + if let Some(revocation) = + (*revocation).query_interface::<nsIIssuerAndSerialRevocationState>() + { + let mut issuer = nsCString::new(); + try_ns!(revocation.GetIssuer(&mut *issuer).to_result(), or continue); + + let mut serial = nsCString::new(); + try_ns!(revocation.GetSerial(&mut *serial).to_result(), or continue); + + entries.push(EncodedSecurityState::new( + PREFIX_REV_IS, + issuer, + serial, + state, + )); + } else if let Some(revocation) = + (*revocation).query_interface::<nsISubjectAndPubKeyRevocationState>() + { + let mut subject = nsCString::new(); + try_ns!(revocation.GetSubject(&mut *subject).to_result(), or continue); + + let mut pub_key_hash = nsCString::new(); + try_ns!(revocation.GetPubKey(&mut *pub_key_hash).to_result(), or continue); + + entries.push(EncodedSecurityState::new( + PREFIX_REV_SPK, + subject, + pub_key_hash, + state, + )); + } + } + + let task = Box::new(try_ns!(SecurityStateTask::new( + &*callback, + &self.security_state, + move |ss| ss.set_batch_state(&entries, nsICertStorage::DATA_TYPE_REVOCATION), + ))); + let runnable = try_ns!(TaskRunnable::new("SetRevocations", task)); + try_ns!(TaskRunnable::dispatch(runnable, self.queue.coerce())); + NS_OK + } + + unsafe fn GetRevocationState( + &self, + issuer: *const ThinVec<u8>, + serial: *const ThinVec<u8>, + subject: *const ThinVec<u8>, + pub_key: *const ThinVec<u8>, + state: *mut i16, + ) -> nserror::nsresult { + // TODO (bug 1541212): We really want to restrict this to non-main-threads only in non-test + // contexts, but we can't do so until bug 1406854 is fixed. + if issuer.is_null() || serial.is_null() || subject.is_null() || pub_key.is_null() { + return NS_ERROR_NULL_POINTER; + } + *state = nsICertStorage::STATE_UNSET; + let ss = get_security_state!(self); + match ss.get_revocation_state(&*issuer, &*serial, &*subject, &*pub_key) { + Ok(st) => { + *state = st; + NS_OK + } + _ => NS_ERROR_FAILURE, + } + } + + unsafe fn SetFullCRLiteFilter( + &self, + filter: *const ThinVec<u8>, + enrolled_issuers: *const ThinVec<nsCString>, + coverage: *const ThinVec<Option<RefPtr<nsICRLiteCoverage>>>, + callback: *const nsICertStorageCallback, + ) -> nserror::nsresult { + if !is_main_thread() { + return NS_ERROR_NOT_SAME_THREAD; + } + if filter.is_null() + || coverage.is_null() + || callback.is_null() + || enrolled_issuers.is_null() + { + return NS_ERROR_NULL_POINTER; + } + + let filter_owned = (*filter).to_vec(); + let enrolled_issuers_owned = (*enrolled_issuers).to_vec(); + + let coverage = &*coverage; + let mut coverage_entries = Vec::with_capacity(coverage.len()); + for entry in coverage.iter().flatten() { + let mut b64_log_id = nsCString::new(); + try_ns!((*entry).GetB64LogID(&mut *b64_log_id).to_result(), or continue); + let mut min_timestamp: u64 = 0; + try_ns!((*entry).GetMinTimestamp(&mut min_timestamp).to_result(), or continue); + let mut max_timestamp: u64 = 0; + try_ns!((*entry).GetMaxTimestamp(&mut max_timestamp).to_result(), or continue); + coverage_entries.push((b64_log_id, min_timestamp, max_timestamp)); + } + + let task = Box::new(try_ns!(SecurityStateTask::new( + &*callback, + &self.security_state, + move |ss| ss.set_full_crlite_filter( + filter_owned, + enrolled_issuers_owned, + &coverage_entries + ), + ))); + let runnable = try_ns!(TaskRunnable::new("SetFullCRLiteFilter", task)); + try_ns!(TaskRunnable::dispatch(runnable, self.queue.coerce())); + NS_OK + } + + unsafe fn AddCRLiteStash( + &self, + stash: *const ThinVec<u8>, + callback: *const nsICertStorageCallback, + ) -> nserror::nsresult { + if !is_main_thread() { + return NS_ERROR_NOT_SAME_THREAD; + } + if stash.is_null() || callback.is_null() { + return NS_ERROR_NULL_POINTER; + } + let stash_owned = (*stash).to_vec(); + let task = Box::new(try_ns!(SecurityStateTask::new( + &*callback, + &self.security_state, + move |ss| ss.add_crlite_stash(stash_owned), + ))); + let runnable = try_ns!(TaskRunnable::new("AddCRLiteStash", task)); + try_ns!(TaskRunnable::dispatch(runnable, self.queue.coerce())); + NS_OK + } + + unsafe fn IsCertRevokedByStash( + &self, + issuer_spki: *const ThinVec<u8>, + serial_number: *const ThinVec<u8>, + is_revoked: *mut bool, + ) -> nserror::nsresult { + if issuer_spki.is_null() || serial_number.is_null() || is_revoked.is_null() { + return NS_ERROR_NULL_POINTER; + } + let ss = get_security_state!(self); + *is_revoked = match ss.is_cert_revoked_by_stash(&*issuer_spki, &*serial_number) { + Ok(is_revoked) => is_revoked, + Err(_) => return NS_ERROR_FAILURE, + }; + NS_OK + } + + unsafe fn GetCRLiteRevocationState( + &self, + issuer: *const ThinVec<u8>, + issuerSPKI: *const ThinVec<u8>, + serialNumber: *const ThinVec<u8>, + timestamps: *const ThinVec<Option<RefPtr<nsICRLiteTimestamp>>>, + state: *mut i16, + ) -> nserror::nsresult { + // TODO (bug 1541212): We really want to restrict this to non-main-threads only, but we + // can't do so until bug 1406854 is fixed. + if issuer.is_null() + || issuerSPKI.is_null() + || serialNumber.is_null() + || state.is_null() + || timestamps.is_null() + { + return NS_ERROR_NULL_POINTER; + } + let timestamps = &*timestamps; + let mut timestamp_entries = Vec::with_capacity(timestamps.len()); + for timestamp_entry in timestamps.iter().flatten() { + let mut log_id = ThinVec::with_capacity(32); + try_ns!(timestamp_entry.GetLogID(&mut log_id).to_result(), or continue); + let mut timestamp: u64 = 0; + try_ns!(timestamp_entry.GetTimestamp(&mut timestamp).to_result(), or continue); + timestamp_entries.push(CRLiteTimestamp { log_id, timestamp }); + } + let ss = get_security_state!(self); + *state = ss.get_crlite_revocation_state( + &*issuer, + &*issuerSPKI, + &*serialNumber, + ×tamp_entries, + ); + NS_OK + } + + unsafe fn AddCerts( + &self, + certs: *const ThinVec<Option<RefPtr<nsICertInfo>>>, + callback: *const nsICertStorageCallback, + ) -> nserror::nsresult { + if !is_main_thread() { + return NS_ERROR_NOT_SAME_THREAD; + } + if certs.is_null() || callback.is_null() { + return NS_ERROR_NULL_POINTER; + } + let certs = &*certs; + let mut cert_entries = Vec::with_capacity(certs.len()); + for cert in certs.iter().flatten() { + let mut der = nsCString::new(); + try_ns!((*cert).GetCert(&mut *der).to_result(), or continue); + let mut subject = nsCString::new(); + try_ns!((*cert).GetSubject(&mut *subject).to_result(), or continue); + let mut trust: i16 = 0; + try_ns!((*cert).GetTrust(&mut trust).to_result(), or continue); + cert_entries.push((der, subject, trust)); + } + let task = Box::new(try_ns!(SecurityStateTask::new( + &*callback, + &self.security_state, + move |ss| ss.add_certs(&cert_entries), + ))); + let runnable = try_ns!(TaskRunnable::new("AddCerts", task)); + try_ns!(TaskRunnable::dispatch(runnable, self.queue.coerce())); + NS_OK + } + + unsafe fn RemoveCertsByHashes( + &self, + hashes: *const ThinVec<nsCString>, + callback: *const nsICertStorageCallback, + ) -> nserror::nsresult { + if !is_main_thread() { + return NS_ERROR_NOT_SAME_THREAD; + } + if hashes.is_null() || callback.is_null() { + return NS_ERROR_NULL_POINTER; + } + let hashes = (*hashes).to_vec(); + let task = Box::new(try_ns!(SecurityStateTask::new( + &*callback, + &self.security_state, + move |ss| ss.remove_certs_by_hashes(&hashes), + ))); + let runnable = try_ns!(TaskRunnable::new("RemoveCertsByHashes", task)); + try_ns!(TaskRunnable::dispatch(runnable, self.queue.coerce())); + NS_OK + } + + unsafe fn FindCertsBySubject( + &self, + subject: *const ThinVec<u8>, + certs: *mut ThinVec<ThinVec<u8>>, + ) -> nserror::nsresult { + // TODO (bug 1541212): We really want to restrict this to non-main-threads only, but we + // can't do so until bug 1406854 is fixed. + if subject.is_null() || certs.is_null() { + return NS_ERROR_NULL_POINTER; + } + let ss = get_security_state!(self); + match ss.find_certs_by_subject(&*subject, &mut *certs) { + Ok(()) => NS_OK, + Err(_) => NS_ERROR_FAILURE, + } + } +} + +extern "C" { + fn cert_storage_malloc_size_of(ptr: *const xpcom::reexports::libc::c_void) -> usize; +} + +#[xpcom(implement(nsIMemoryReporter), atomic)] +struct MemoryReporter { + security_state: Arc<RwLock<SecurityState>>, +} + +#[allow(non_snake_case)] +impl MemoryReporter { + unsafe fn CollectReports( + &self, + callback: *const nsIHandleReportCallback, + data: *const nsISupports, + _anonymize: bool, + ) -> nserror::nsresult { + let ss = try_ns!(self.security_state.read()); + let mut ops = MallocSizeOfOps::new(cert_storage_malloc_size_of, None); + let size = ss.size_of(&mut ops); + let callback = match RefPtr::from_raw(callback) { + Some(ptr) => ptr, + None => return NS_ERROR_UNEXPECTED, + }; + // This does the same as MOZ_COLLECT_REPORT + callback.Callback( + &nsCStr::new() as &nsACString, + &nsCStr::from("explicit/cert-storage/storage") as &nsACString, + nsIMemoryReporter::KIND_HEAP, + nsIMemoryReporter::UNITS_BYTES, + size as i64, + &nsCStr::from("Memory used by certificate storage") as &nsACString, + data, + ); + NS_OK + } +} |