Skip to content

Commit

Permalink
Add cross-platform support for SSL_CERT_FILE (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
pimterry authored Oct 25, 2021
1 parent 616f2d7 commit 52c77e2
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 41 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ ring = "0.16.5"
untrusted = "0.7.0"
rustls = "0.20"
x509-parser = "0.9.2"
serial_test = "0.5.1"

[target.'cfg(windows)'.dependencies]
schannel = "0.1.15"
Expand Down
15 changes: 7 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ platform's native certificate store when operating as a TLS client.

This is supported on Windows, macOS and Linux:

- On all platforms, the `SSL_CERT_FILE` environment variable is checked first.
If that's set, certificates are loaded from the path specified by that variable,
or an error is returned if certificates cannot be loaded from the given path.
If it's not set, then the platform-specific certificate source is used.
- On Windows, certificates are loaded from the system certificate store.
The [`schannel`](https://github.com/steffengy/schannel-rs) crate is used to access
the Windows certificate store APIs.
Expand Down Expand Up @@ -47,18 +51,13 @@ If you'd like to help out, please see [CONTRIBUTING.md](CONTRIBUTING.md).
This library exposes a single function with this signature:

```rust
pub fn load_native_certs() -> Result<rustls::RootCertStore, (Option<rustls::RootCertStore>, std::io::Error)>
pub fn load_native_certs() -> Result<Vec<Certificate>, std::io::Error>
```

On success, this returns a `rustls::RootCertStore` loaded with a
snapshop of the root certificates found on this platform. This
On success, this returns a `Vec<Certificate>` loaded with a
snapshot of the root certificates found on this platform. This
function fails in a platform-specific way, expressed in a `std::io::Error`.

When an error is returned, optionally a `rustls::RootCertStore` is also
returned containing the certificates which *could* be loaded. This means
callers can opt-in to "best-effort" behaviour even in the presence of invalid
certificates.

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.
Expand Down
38 changes: 36 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ mod macos;
#[cfg(target_os = "macos")]
use macos as platform;

use std::io::Error;
use std::io::{Error, ErrorKind};
use std::io::BufReader;
use std::fs::File;
use std::path::{Path,PathBuf};
use std::env;

/// Loads root certificates found in the platform's native certificate
/// store, executing callbacks on the provided builder.
Expand All @@ -36,7 +40,37 @@ use std::io::Error;
/// and parsing a ~300KB disk file. It's therefore prudent to call
/// this sparingly.
pub fn load_native_certs() -> Result<Vec<Certificate>, Error> {
platform::load_native_certs()
load_certs_from_env()
.unwrap_or_else(platform::load_native_certs)
}

pub struct Certificate(pub Vec<u8>);

const ENV_CERT_FILE: &str = "SSL_CERT_FILE";

/// Returns None if SSL_CERT_FILE is not defined in the current environment.
///
/// If it is defined, it is always used, so it must be a path to a real
/// file from which certificates can be loaded successfully.
fn load_certs_from_env() -> Option<Result<Vec<Certificate>, Error>> {
let cert_var_path = PathBuf::from(
env::var_os(ENV_CERT_FILE)?
);

Some(load_pem_certs(&cert_var_path))
}

fn load_pem_certs(path: &Path) -> Result<Vec<Certificate>, Error> {
let f = File::open(&path)?;
let mut f = BufReader::new(f);

match rustls_pemfile::certs(&mut f) {
Ok(contents) => {
Ok(contents.into_iter().map(Certificate).collect())
}
Err(_) => Err(Error::new(
ErrorKind::InvalidData,
format!("Could not load PEM file {:?}", path),
)),
}
}
36 changes: 5 additions & 31 deletions src/unix.rs
Original file line number Diff line number Diff line change
@@ -1,39 +1,13 @@
use crate::Certificate;
use crate::load_pem_certs;

use std::io::{Error, ErrorKind};
use std::io::BufReader;
use std::fs::File;
use std::path::Path;

fn load_file(certs: &mut Vec<Certificate>, path: &Path) -> Result<(), Error> {
let f = File::open(&path)?;
let mut f = BufReader::new(f);
match rustls_pemfile::certs(&mut f) {
Ok(contents) => {
certs.extend(contents.into_iter().map(Certificate));
Ok(())
}
Err(_) => Err(Error::new(
ErrorKind::InvalidData,
format!("Could not load PEM file {:?}", path),
)),
}
}
use std::io::Error;

pub fn load_native_certs() -> Result<Vec<Certificate>, Error> {
let likely_locations = openssl_probe::probe();
let mut first_error = None;
let mut certs = Vec::new();

if let Some(file) = likely_locations.cert_file {
if let Err(err) = load_file(&mut certs, &file) {
first_error = first_error.or(Some(err));
}
}

if let Some(err) = first_error {
Err(err)
} else {
Ok(certs)
match likely_locations.cert_file {
Some(cert_file) => load_pem_certs(&cert_file),
None => Ok(Vec::new())
}
}
21 changes: 21 additions & 0 deletions tests/badssl-com-chain.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDeTCCAmGgAwIBAgIJAMnA8BB8xT6wMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp
c2NvMQ8wDQYDVQQKDAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTAeFw0y
MTEwMTEyMDAzNTRaFw0yMzEwMTEyMDAzNTRaMGIxCzAJBgNVBAYTAlVTMRMwEQYD
VQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ8wDQYDVQQK
DAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBAMIE7PiM7gTCs9hQ1XBYzJMY61yoaEmwIrX5lZ6xKyx2
PmzAS2BMTOqytMAPgLaw+XLJhgL5XEFdEyt/ccRLvOmULlA3pmccYYz2QULFRtMW
hyefdOsKnRFSJiFzbIRMeVXk0WvoBj1IFVKtsyjbqv9u/2CVSndrOfEk0TG23U3A
xPxTuW1CrbV8/q71FdIzSOciccfCFHpsKOo3St/qbLVytH5aohbcabFXRNsKEqve
ww9HdFxBIuGa+RuT5q0iBikusbpJHAwnnqP7i/dAcgCskgjZjFeEU4EFy+b+a1SY
QCeFxxC7c3DvaRhBB0VVfPlkPz0sw6l865MaTIbRyoUCAwEAAaMyMDAwCQYDVR0T
BAIwADAjBgNVHREEHDAaggwqLmJhZHNzbC5jb22CCmJhZHNzbC5jb20wDQYJKoZI
hvcNAQELBQADggEBAC4DensZ5tCTeCNJbHABYPwwqLUFOMITKOOgF3t8EqOan0CH
ST1NNi4jPslWrVhQ4Y3UbAhRBdqXl5N/NFfMzDosPpOjFgtifh8Z2s3w8vdlEZzf
A4mYTC8APgdpWyNgMsp8cdXQF7QOfdnqOfdnY+pfc8a8joObR7HEaeVxhJs+XL4E
CLByw5FR+svkYgCbQGWIgrM1cRpmXemt6Gf/XgFNP2PdubxqDEcnWlTMk8FCBVb1
nVDSiPjYShwnWsOOshshCRCAiIBPCKPX0QwKDComQlRrgMIvddaSzFFTKPoNZjC+
CUspSNnL7V9IIHvqKlRSmu+zIpm2VJCp1xLulk8=
-----END CERTIFICATE-----
32 changes: 32 additions & 0 deletions tests/smoketests.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
use std::convert::TryInto;
use std::sync::Arc;

use std::panic;

use std::io::{Read, Write};
use std::net::TcpStream;
use std::env;
use std::path::PathBuf;

// #[serial] is used on all these tests to run them sequentially. If they're run in parallel,
// the global env var configuration in the env var test interferes with the others.
use serial_test::serial;

fn check_site(domain: &str) {
let mut roots = rustls::RootCertStore::empty();
Expand Down Expand Up @@ -37,31 +45,55 @@ fn check_site(domain: &str) {
}

#[test]
#[serial]
fn google() {
check_site("google.com");
}

#[test]
#[serial]
fn amazon() {
check_site("amazon.com");
}

#[test]
#[serial]
fn facebook() {
check_site("facebook.com");
}

#[test]
#[serial]
fn netflix() {
check_site("netflix.com");
}

#[test]
#[serial]
fn ebay() {
check_site("ebay.com");
}

#[test]
#[serial]
fn apple() {
check_site("apple.com");
}

#[test]
#[serial]
fn badssl_with_env() {
let result = panic::catch_unwind(|| {
check_site("self-signed.badssl.com")
});
// Self-signed certs should never be trusted by default:
assert!(result.is_err());

// But they should be trusted if SSL_CERT_FILE is set:
env::set_var("SSL_CERT_FILE",
// The CA cert, downloaded directly from the site itself:
PathBuf::from("./tests/badssl-com-chain.pem")
);
check_site("self-signed.badssl.com");
env::remove_var("SSL_CERT_FILE");
}

0 comments on commit 52c77e2

Please sign in to comment.