diff --git a/Cargo.lock b/Cargo.lock index 777456de0e..a8b90921fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,6 +92,16 @@ name = "bitflags" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "ca-loader" +version = "0.1.0" +dependencies = [ + "crypt32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", + "security-framework 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "cfg-if" version = "0.1.0" @@ -181,6 +191,7 @@ dependencies = [ name = "download" version = "0.3.0" dependencies = [ + "ca-loader 0.1.0", "curl 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "error-chain 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/src/ca-loader/Cargo.toml b/src/ca-loader/Cargo.toml new file mode 100644 index 0000000000..7e82afb2f7 --- /dev/null +++ b/src/ca-loader/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ca-loader" +version = "0.1.0" +authors = [ "Ivan Nejgebauer " ] + +[dependencies] +libc = "0.2" + +[target."cfg(windows)".dependencies] +winapi = "0.2.8" +crypt32-sys = "0.2" + +[target.'cfg(target_os = "macos")'.dependencies] +security-framework = "0.1.5" diff --git a/src/ca-loader/src/lib.rs b/src/ca-loader/src/lib.rs new file mode 100644 index 0000000000..5cbfb2c735 --- /dev/null +++ b/src/ca-loader/src/lib.rs @@ -0,0 +1,10 @@ +#[macro_use] +mod macros; +mod sys; + +pub use self::sys::CertBundle; + +pub enum CertItem { + File(String), + Blob(Vec) +} diff --git a/src/ca-loader/src/macros.rs b/src/ca-loader/src/macros.rs new file mode 100644 index 0000000000..e7d26490d4 --- /dev/null +++ b/src/ca-loader/src/macros.rs @@ -0,0 +1,38 @@ +// Taken from the libc crate, see for +// authorship and copyright information. + +// A macro for defining #[cfg] if-else statements. +// +// This is similar to the `if/elif` C preprocessor macro by allowing definition +// of a cascade of `#[cfg]` cases, emitting the implementation which matches +// first. +// +// This allows you to conveniently provide a long list #[cfg]'d blocks of code +// without having to rewrite each clause multiple times. +macro_rules! cfg_if { + ($( + if #[cfg($($meta:meta),*)] { $($it:item)* } + ) else * else { + $($it2:item)* + }) => { + __cfg_if_items! { + () ; + $( ( ($($meta),*) ($($it)*) ), )* + ( () ($($it2)*) ), + } + } +} + +macro_rules! __cfg_if_items { + (($($not:meta,)*) ; ) => {}; + (($($not:meta,)*) ; ( ($($m:meta),*) ($($it:item)*) ), $($rest:tt)*) => { + __cfg_if_apply! { cfg(all(not(any($($not),*)), $($m,)*)), $($it)* } + __cfg_if_items! { ($($not,)* $($m,)*) ; $($rest)* } + } +} + +macro_rules! __cfg_if_apply { + ($m:meta, $($it:item)*) => { + $(#[$m] $it)* + } +} diff --git a/src/ca-loader/src/sys/macos.rs b/src/ca-loader/src/sys/macos.rs new file mode 100644 index 0000000000..469a09c205 --- /dev/null +++ b/src/ca-loader/src/sys/macos.rs @@ -0,0 +1,53 @@ +extern crate security_framework as sf; + +use super::super::CertItem; +use self::sf::item::{ItemClass, ItemSearchOptions, Reference, SearchResult}; +use self::sf::keychain::SecKeychain; +use self::sf::os::macos::keychain::SecKeychainExt; +use std::i32; +use std::result::Result; + +pub struct CertBundle { + rv: Vec +} + +pub struct CertIter { + it: Box> +} + +impl IntoIterator for CertBundle { + type Item = CertItem; + type IntoIter = CertIter; + + fn into_iter(self) -> Self::IntoIter { + CertIter { it: Box::new(self.rv.into_iter()) } + } +} + +impl Iterator for CertIter { + type Item = CertItem; + + fn next(&mut self) -> Option { + if let Some(res) = self.it.next() { + if let Some(ref rref) = res.reference { + match rref { + &Reference::Certificate(ref cert) => return Some(CertItem::Blob(cert.to_der())), + _ => () + } + } + return self.next(); + } + None + } +} + +impl CertBundle { + pub fn new() -> Result { + let root_kc = try!(SecKeychain::open("/System/Library/Keychains/SystemRootCertificates.keychain").map_err(|_| ())); + let chains = [ root_kc ]; + let mut opts = ItemSearchOptions::new(); + let opts = opts.keychains(&chains).class(ItemClass::Certificate).load_refs(true).limit(i32::MAX as i64); + let rv = try!(opts.search().map_err(|_| ())); + Ok(CertBundle { rv: rv }) + } +} diff --git a/src/ca-loader/src/sys/mod.rs b/src/ca-loader/src/sys/mod.rs new file mode 100644 index 0000000000..1bf5bdd0e7 --- /dev/null +++ b/src/ca-loader/src/sys/mod.rs @@ -0,0 +1,14 @@ +cfg_if! { + if #[cfg(windows)] { + mod windows; + pub use self::windows::CertBundle; + } else if #[cfg(target_os = "macos")] { + mod macos; + pub use self::macos::CertBundle; + } else if #[cfg(unix)] { + mod unix; + pub use self::unix::CertBundle; + } else { + // Unknown + } +} diff --git a/src/ca-loader/src/sys/unix.rs b/src/ca-loader/src/sys/unix.rs new file mode 100644 index 0000000000..d974bc1e06 --- /dev/null +++ b/src/ca-loader/src/sys/unix.rs @@ -0,0 +1,140 @@ +extern crate libc; + +use std::ffi::CStr; +use std::fs; +use std::mem; +use std::result::Result; +use super::super::CertItem; + +cfg_if! { + if #[cfg(any(target_os = "android", target_os = "solaris"))] { + use std::fs::{read_dir, ReadDir}; + + pub struct CertBundle(&'static str); + + pub struct CertIter(&'static str, Option); + + impl IntoIterator for CertBundle { + type Item = CertItem; + type IntoIter = CertIter; + + fn into_iter(self) -> Self::IntoIter { + if let Ok(dir) = read_dir(self.0) { + CertIter(self.0, Some(dir)) + } else { + CertIter(self.0, None) + } + } + } + + impl Iterator for CertIter { + type Item = CertItem; + + fn next(&mut self) -> Option { + match self.1 { + None => return None, + Some(ref mut dir) => { + match dir.next() { + None => return None, + Some(Err(_)) => return None, + Some(Ok(ref de)) => { + if let Ok(ftyp) = de.file_type() { + if !ftyp.is_dir() { + if let Some(s) = de.file_name().to_str() { + let mut full_name = String::from(self.0); + full_name.push('/'); + full_name.push_str(s); + return Some(CertItem::File(full_name)); + } + } + } + } + } + } + } + self.next() + } + } + + impl CertBundle { + pub fn new() -> Result { + Ok(CertBundle(try!(sys_path()))) + } + } + } else { + use std::option; + + pub struct CertBundle(Option); + + impl IntoIterator for CertBundle { + type Item = CertItem; + type IntoIter = option::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } + } + + impl CertBundle { + pub fn new() -> Result { + Ok(CertBundle(Some(CertItem::File(try!(sys_path()).to_string())))) + } + } + } +} + +pub fn sys_path() -> Result<&'static str, ()> { + // Why use mem::uninitialized()? If we didn't, we'd need a bunch of + // #cfg's for OS variants, since the components of struct utsname are + // fixed-size char arrays (so no generic initializers), and that size + // is different across OSs. Furthermore, uname() doesn't care about + // the contents of struct utsname on input, and will fill it with + // properly NUL-terminated strings on successful return. + unsafe { + let mut uts = mem::uninitialized::(); + + if libc::uname(&mut uts) < 0 { + return Err(()); + } + let sysname = try!(CStr::from_ptr(uts.sysname.as_ptr()).to_str().map_err(|_| ())); + let release = try!(CStr::from_ptr(uts.release.as_ptr()).to_str().map_err(|_| ())); + let path = match sysname { + "FreeBSD" | "OpenBSD" => "/etc/ssl/cert.pem", + "NetBSD" => "/etc/ssl/certs", + "Linux" => linux_distro_guess_ca_path(), + "SunOS" => { + let major = release.split('.').take(1).collect::(); + let major = major.parse::().unwrap_or(5); + let minor = release.split('.').skip(1).take(1).collect::(); + let minor = minor.parse::().unwrap_or(10); + if major < 5 || (major == 5 && minor < 11) { + "/opt/csw/share/cacertificates/mozilla" + } else { + "/etc/certs/CA" + } + } + _ => unimplemented!() + }; + Ok(path) + } +} + +cfg_if! { + if #[cfg(target_os = "android")] { + fn linux_distro_guess_ca_path() -> &'static str { + "/system/etc/security/cacerts" + } + } else { + fn linux_distro_guess_ca_path() -> &'static str { + if let Ok(_debian) = fs::metadata("/etc/debian_version") { + "/etc/ssl/certs/ca-certificates.crt" + } else if let Ok(_rh) = fs::metadata("/etc/redhat-release") { + "/etc/pki/tls/certs/ca-bundle.crt" + } else if let Ok(_suse) = fs::metadata("/etc/SuSE-release") { + "/etc/ssl/ca-bundle.pem" + } else { // fallback + "/etc/pki/tls/cacert.pem" + } + } + } +} diff --git a/src/ca-loader/src/sys/windows.rs b/src/ca-loader/src/sys/windows.rs new file mode 100644 index 0000000000..e779333867 --- /dev/null +++ b/src/ca-loader/src/sys/windows.rs @@ -0,0 +1,79 @@ +extern crate crypt32; +extern crate winapi; + +use super::super::CertItem; +use std::ffi::CString; +use std::ptr; +use std::result::Result; +use std::slice::from_raw_parts; + +pub struct CertBundle { + store: winapi::HCERTSTORE, + ctx_p: winapi::PCCERT_CONTEXT +} + +pub struct CertIter { + bundle: CertBundle +} + +impl IntoIterator for CertBundle { + type Item = CertItem; + type IntoIter = CertIter; + + fn into_iter(self) -> Self::IntoIter { + CertIter { bundle: self } + } +} + +impl Iterator for CertIter { + type Item = CertItem; + + fn next(&mut self) -> Option { + if self.bundle.ctx_p.is_null() { + return None; + } + unsafe { + let ctx = *self.bundle.ctx_p; + let enc_slice = from_raw_parts( + ctx.pbCertEncoded as *const u8, + ctx.cbCertEncoded as usize); + let mut blob = Vec::with_capacity(ctx.cbCertEncoded as usize); + blob.extend_from_slice(enc_slice); + self.bundle.ctx_p = crypt32::CertEnumCertificatesInStore( + self.bundle.store, + self.bundle.ctx_p); + Some(CertItem::Blob(blob)) + } + } +} + +impl CertBundle { + pub fn new() -> Result { + unsafe { + let store = crypt32::CertOpenSystemStoreA( + 0, + CString::new("Root").unwrap().as_ptr() as winapi::LPCSTR); + if store.is_null() { + return Err(()); + } + let ctx_p = crypt32::CertEnumCertificatesInStore( + store, + ptr::null()); + Ok(CertBundle { + store: store, + ctx_p: ctx_p + }) + } + } +} + +impl Drop for CertBundle { + fn drop(&mut self) { + unsafe { + if !self.ctx_p.is_null() { + crypt32::CertFreeCertificateContext(self.ctx_p); + } + crypt32::CertCloseStore(self.store, 0); + } + } +} diff --git a/src/download/Cargo.toml b/src/download/Cargo.toml index 73cab899b3..e7cd5bc784 100644 --- a/src/download/Cargo.toml +++ b/src/download/Cargo.toml @@ -12,7 +12,7 @@ default = ["hyper-backend"] curl-backend = ["curl"] hyper-backend = ["hyper", "native-tls", "openssl-sys"] -rustls-backend = ["hyper", "rustls", "lazy_static"] +rustls-backend = ["hyper", "rustls", "lazy_static", "ca-loader"] [dependencies] error-chain = "0.2.1" @@ -35,3 +35,8 @@ openssl-sys = { version = "0.7.11", optional = true } [dependencies.rustls] git = "https://github.com/ctz/rustls.git" optional = true + +[dependencies.ca-loader] +path = "../ca-loader" +version = "0.1.0" +optional = true diff --git a/src/download/src/lib.rs b/src/download/src/lib.rs index e172a5d1f0..3648400b66 100644 --- a/src/download/src/lib.rs +++ b/src/download/src/lib.rs @@ -7,6 +7,8 @@ extern crate url; #[cfg(feature = "rustls-backend")] #[macro_use] extern crate lazy_static; +#[cfg(feature = "rustls-backend")] +extern crate ca_loader; use url::Url; use std::path::Path; @@ -394,6 +396,7 @@ pub mod rustls { } fn global_config() -> Arc { + use ca_loader::{CertBundle, CertItem}; use std::fs::File; use std::io::BufReader; @@ -403,21 +406,39 @@ pub mod rustls { fn init() -> Arc { let mut config = rustls::ClientConfig::new(); - for cert in find_root_cert_paths() { - let certfile = File::open(cert).unwrap(); // FIXME - let mut reader = BufReader::new(certfile); - config.root_store.add_pem_file(&mut reader).unwrap(); // FIXME - } + let bundle = CertBundle::new().expect("cannot initialize CA cert bundle"); + let mut added = 0; + let mut invalid = 0; + for cert in bundle { + let (c_added, c_invalid) = match cert { + CertItem::Blob(blob) => match config.root_store.add(&blob) { + Ok(_) => (1, 0), + Err(_) => (0, 1) + }, + CertItem::File(name) => { + if let Ok(cf) = File::open(name) { + let mut reader = BufReader::new(cf); + match config.root_store.add_pem_file(&mut reader) { + Ok(pair) => pair, + Err(_) => (0, 1) + } + } else { + (0, 1) + } + } + }; + added += c_added; + invalid += c_invalid; + } + if added == 0 { + panic!("no CA certs added, {} were invalid", invalid); + } Arc::new(config) } CONFIG.clone() } - fn find_root_cert_paths() -> Vec { - panic!("FIXME: load root certs") - } - #[derive(Clone)] struct NativeSslStream(Arc>);