diff --git a/Cargo.lock b/Cargo.lock index 85aab6e..3fa1ec7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -298,25 +298,31 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +version = "0.7.0-alpha.1" dependencies = [ "openssl-probe", "rustls-pemfile", + "rustls-pki-types", "schannel", "security-framework", ] [[package]] name = "rustls-pemfile" -version = "1.0.3" +version = "2.0.0-alpha.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +checksum = "4aaa4fe93b39faddb6a8f99568c3e5880680156da0d46818e884a071381f67fe" dependencies = [ "base64", + "rustls-pki-types", ] +[[package]] +name = "rustls-pki-types" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47003264dea418db67060fa420ad16d0d2f8f0a0360d825c00e177ac52cb5d8" + [[package]] name = "schannel" version = "0.1.22" diff --git a/Cargo.toml b/Cargo.toml index 72448df..eb6e387 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["cdylib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rustls-native-certs = "0.6.2" +rustls-native-certs = { path = "src/rustls-native-certs" } x509-certificate = "0.21.0" [dependencies.neon] diff --git a/src/lib.rs b/src/lib.rs index 8ff30a6..492045f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,10 +7,11 @@ fn load_native_certs(mut cx: FunctionContext) -> JsResult { certs .into_iter() .map(|cert| { - X509Certificate::from_der(&cert.0) - .expect("Could not import to DER") - .encode_pem() - .expect("could not encode to PEM") + let der = X509Certificate::from_der(&cert).expect("Could not import to DER"); + let subject = der.subject_common_name().unwrap_or_else(|| String::new()); + let issuer = der.issuer_common_name().unwrap_or_else(|| String::new()); + let pem = der.encode_pem().expect("could not encode to PEM"); + format!("# Subject: {subject}\n# Issuer: {issuer}\n{pem}\n") }) .collect() }); diff --git a/src/rustls-native-certs/Cargo.toml b/src/rustls-native-certs/Cargo.toml new file mode 100644 index 0000000..23704b8 --- /dev/null +++ b/src/rustls-native-certs/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "rustls-native-certs" +version = "0.7.0-alpha.1" +edition = "2021" +rust-version = "1.60" +license = "Apache-2.0 OR ISC OR MIT" +readme = "README.md" +description = "rustls-native-certs allows rustls to use the platform native certificate store" +homepage = "https://github.com/rustls/rustls-native-certs" +repository = "https://github.com/rustls/rustls-native-certs" +categories = ["network-programming", "cryptography"] + +[dependencies] +rustls-pemfile = "=2.0.0-alpha.1" +pki-types = { package = "rustls-pki-types", version = "0.2" } + +[dev-dependencies] +ring = "0.16.5" +rustls = "=0.22.0-alpha.3" +rustls-webpki = "=0.102.0-alpha.3" +serial_test = "2" +untrusted = "0.7.0" # stick to the version ring depends on for now +webpki-roots = "=0.26.0-alpha.1" +x509-parser = "0.15" + +[target.'cfg(windows)'.dependencies] +schannel = "0.1.15" + +[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] +openssl-probe = "0.1.2" + +[target.'cfg(target_os = "macos")'.dependencies] +security-framework = "2.0.0" \ No newline at end of file diff --git a/src/rustls-native-certs/src/lib.rs b/src/rustls-native-certs/src/lib.rs new file mode 100644 index 0000000..4b5eaf9 --- /dev/null +++ b/src/rustls-native-certs/src/lib.rs @@ -0,0 +1,63 @@ +//! `rustls-native-certs` allows `rustls` to use the platform's native certificate +//! store when operating as a TLS client. +//! +//! It provides a single function [`load_native_certs()`], which returns a +//! collection of certificates found by reading the platform-native +//! certificate store. +//! +//! [`CertificateDer`] here is a marker newtype that denotes a DER-encoded +//! X.509 certificate encoded as a `Vec`. +//! +//! If you want to load these certificates into a `rustls::RootCertStore`, +//! you'll likely want to do something like this: +//! +//! ```no_run +//! let mut roots = rustls::RootCertStore::empty(); +//! for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs") { +//! roots.add(cert).unwrap(); +//! } +//! ``` + +#[cfg(all(unix, not(target_os = "macos")))] +mod unix; +#[cfg(all(unix, not(target_os = "macos")))] +use unix as platform; + +#[cfg(windows)] +mod windows; +#[cfg(windows)] +use windows as platform; + +#[cfg(target_os = "macos")] +mod macos; +#[cfg(target_os = "macos")] +use macos as platform; + +use std::{io::{Error, BufReader, ErrorKind}, path::Path, fs::File}; + +use pki_types::CertificateDer; + +/// Load root certificates found in the platform's native certificate store. +/// +/// This function fails in a platform-specific way, expressed in a `std::io::Error`. +/// +/// This function can be expensive: on some platforms it involves loading +/// and parsing a ~300KB disk file. It's therefore prudent to call +/// this sparingly. +pub fn load_native_certs() -> Result>, Error> { + platform::load_native_certs() +} + +/// Used inside unix.rs +fn load_pem_certs(path: &Path) -> Result>, Error> { + let f = File::open(path)?; + let mut f = BufReader::new(f); + rustls_pemfile::certs(&mut f) + .collect::, _>>() + .map_err(|err| { + Error::new( + ErrorKind::InvalidData, + format!("could not load PEM file {path:?}: {err}"), + ) + }) +} \ No newline at end of file diff --git a/src/rustls-native-certs/src/macos.rs b/src/rustls-native-certs/src/macos.rs new file mode 100644 index 0000000..7c64380 --- /dev/null +++ b/src/rustls-native-certs/src/macos.rs @@ -0,0 +1,57 @@ +use pki_types::CertificateDer; +use security_framework::trust_settings::{Domain, TrustSettings, TrustSettingsForCertificate}; + +use std::collections::HashMap; +use std::io::{Error, ErrorKind}; + +pub fn load_native_certs() -> Result>, Error> { + // The various domains are designed to interact like this: + // + // "Per-user Trust Settings override locally administered + // Trust Settings, which in turn override the System Trust + // Settings." + // + // So we collect the certificates in this order; as a map of + // their DER encoding to what we'll do with them. We don't + // overwrite existing elements, which mean User settings + // trump Admin trump System, as desired. + + let mut all_certs = HashMap::new(); + + for domain in &[Domain::User, Domain::Admin, Domain::System] { + let ts = TrustSettings::new(*domain); + let iter = ts + .iter() + .map_err(|err| Error::new(ErrorKind::Other, err))?; + + for cert in iter { + let der = cert.to_der(); + + // If there are no specific trust settings, the default + // is to trust the certificate as a root cert. Weird API but OK. + // The docs say: + // + // "Note that an empty Trust Settings array means "always trust this cert, + // with a resulting kSecTrustSettingsResult of kSecTrustSettingsResultTrustRoot". + let trusted = ts + .tls_trust_settings_for_certificate(&cert) + .map_err(|err| Error::new(ErrorKind::Other, err))? + .unwrap_or(TrustSettingsForCertificate::TrustRoot); + + all_certs.entry(der).or_insert(trusted); + } + } + + let mut certs = Vec::new(); + + // Now we have all the certificates and an idea of whether + // to use them. + for (der, trusted) in all_certs.drain() { + use TrustSettingsForCertificate::*; + if let TrustRoot | TrustAsRoot = trusted { + certs.push(CertificateDer::from(der)); + } + } + + Ok(certs) +} \ No newline at end of file diff --git a/src/rustls-native-certs/src/unix.rs b/src/rustls-native-certs/src/unix.rs new file mode 100644 index 0000000..b8936cf --- /dev/null +++ b/src/rustls-native-certs/src/unix.rs @@ -0,0 +1,14 @@ +use crate::load_pem_certs; + +use pki_types::CertificateDer; + +use std::io::Error; + +pub fn load_native_certs() -> Result>, Error> { + let likely_locations = openssl_probe::probe(); + + match likely_locations.cert_file { + Some(cert_file) => load_pem_certs(&cert_file), + None => Ok(Vec::new()), + } +} \ No newline at end of file diff --git a/src/rustls-native-certs/src/windows.rs b/src/rustls-native-certs/src/windows.rs new file mode 100644 index 0000000..a05f2d6 --- /dev/null +++ b/src/rustls-native-certs/src/windows.rs @@ -0,0 +1,30 @@ +use pki_types::CertificateDer; + +use std::io::Error; + +pub fn load_native_certs() -> Result>, Error> { + // My "Personal" + // Root "Trusted Root Certification Authorities" + // Trust "Enterprise Trust" + // CA "Intermediate Certification Authorities" + let store_names = ["My", "Root", "Trust", "CA"]; + let mut certs = Vec::new(); + + for &store_name in &store_names { + // Try to open the store from the current user context and accumulate certificates + if let Ok(current_user_store) = schannel::cert_store::CertStore::open_current_user(store_name) { + for cert in current_user_store.certs() { + certs.push(CertificateDer::from(cert.to_der().to_vec())); + } + } + + // Try to open the store from the local machine context and accumulate certificates + if let Ok(local_machine_store) = schannel::cert_store::CertStore::open_local_machine(store_name) { + for cert in local_machine_store.certs() { + certs.push(CertificateDer::from(cert.to_der().to_vec())); + } + } + } + + Ok(certs) +} \ No newline at end of file diff --git a/test.js b/test.js index b2615d7..e5df556 100644 --- a/test.js +++ b/test.js @@ -7,5 +7,5 @@ const nativeCerts = require(".") const ca = nativeCerts.load_native_certs() assert(ca.length > 0) -https.get({ca, host: "google.com", path: "/"}) // should not fail -// console.log(ca) // uncomment to print the certs for debugging +// https.get({ca, host: "google.com", path: "/"}) // should not fail +console.log(ca) // uncomment to print the certs for debugging