Skip to content

Commit 52c77e2

Browse files
authored
Add cross-platform support for SSL_CERT_FILE (#32)
1 parent 616f2d7 commit 52c77e2

File tree

6 files changed

+102
-41
lines changed

6 files changed

+102
-41
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ ring = "0.16.5"
2020
untrusted = "0.7.0"
2121
rustls = "0.20"
2222
x509-parser = "0.9.2"
23+
serial_test = "0.5.1"
2324

2425
[target.'cfg(windows)'.dependencies]
2526
schannel = "0.1.15"

README.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ platform's native certificate store when operating as a TLS client.
55

66
This is supported on Windows, macOS and Linux:
77

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

4953
```rust
50-
pub fn load_native_certs() -> Result<rustls::RootCertStore, (Option<rustls::RootCertStore>, std::io::Error)>
54+
pub fn load_native_certs() -> Result<Vec<Certificate>, std::io::Error>
5155
```
5256

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

57-
When an error is returned, optionally a `rustls::RootCertStore` is also
58-
returned containing the certificates which *could* be loaded. This means
59-
callers can opt-in to "best-effort" behaviour even in the presence of invalid
60-
certificates.
61-
6261
This function can be expensive: on some platforms it involves loading
6362
and parsing a ~300KB disk file. It's therefore prudent to call
6463
this sparingly.

src/lib.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ mod macos;
2525
#[cfg(target_os = "macos")]
2626
use macos as platform;
2727

28-
use std::io::Error;
28+
use std::io::{Error, ErrorKind};
29+
use std::io::BufReader;
30+
use std::fs::File;
31+
use std::path::{Path,PathBuf};
32+
use std::env;
2933

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

4247
pub struct Certificate(pub Vec<u8>);
48+
49+
const ENV_CERT_FILE: &str = "SSL_CERT_FILE";
50+
51+
/// Returns None if SSL_CERT_FILE is not defined in the current environment.
52+
///
53+
/// If it is defined, it is always used, so it must be a path to a real
54+
/// file from which certificates can be loaded successfully.
55+
fn load_certs_from_env() -> Option<Result<Vec<Certificate>, Error>> {
56+
let cert_var_path = PathBuf::from(
57+
env::var_os(ENV_CERT_FILE)?
58+
);
59+
60+
Some(load_pem_certs(&cert_var_path))
61+
}
62+
63+
fn load_pem_certs(path: &Path) -> Result<Vec<Certificate>, Error> {
64+
let f = File::open(&path)?;
65+
let mut f = BufReader::new(f);
66+
67+
match rustls_pemfile::certs(&mut f) {
68+
Ok(contents) => {
69+
Ok(contents.into_iter().map(Certificate).collect())
70+
}
71+
Err(_) => Err(Error::new(
72+
ErrorKind::InvalidData,
73+
format!("Could not load PEM file {:?}", path),
74+
)),
75+
}
76+
}

src/unix.rs

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,13 @@
11
use crate::Certificate;
2+
use crate::load_pem_certs;
23

3-
use std::io::{Error, ErrorKind};
4-
use std::io::BufReader;
5-
use std::fs::File;
6-
use std::path::Path;
7-
8-
fn load_file(certs: &mut Vec<Certificate>, path: &Path) -> Result<(), Error> {
9-
let f = File::open(&path)?;
10-
let mut f = BufReader::new(f);
11-
match rustls_pemfile::certs(&mut f) {
12-
Ok(contents) => {
13-
certs.extend(contents.into_iter().map(Certificate));
14-
Ok(())
15-
}
16-
Err(_) => Err(Error::new(
17-
ErrorKind::InvalidData,
18-
format!("Could not load PEM file {:?}", path),
19-
)),
20-
}
21-
}
4+
use std::io::Error;
225

236
pub fn load_native_certs() -> Result<Vec<Certificate>, Error> {
247
let likely_locations = openssl_probe::probe();
25-
let mut first_error = None;
26-
let mut certs = Vec::new();
27-
28-
if let Some(file) = likely_locations.cert_file {
29-
if let Err(err) = load_file(&mut certs, &file) {
30-
first_error = first_error.or(Some(err));
31-
}
32-
}
338

34-
if let Some(err) = first_error {
35-
Err(err)
36-
} else {
37-
Ok(certs)
9+
match likely_locations.cert_file {
10+
Some(cert_file) => load_pem_certs(&cert_file),
11+
None => Ok(Vec::new())
3812
}
3913
}

tests/badssl-com-chain.pem

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDeTCCAmGgAwIBAgIJAMnA8BB8xT6wMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNV
3+
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp
4+
c2NvMQ8wDQYDVQQKDAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTAeFw0y
5+
MTEwMTEyMDAzNTRaFw0yMzEwMTEyMDAzNTRaMGIxCzAJBgNVBAYTAlVTMRMwEQYD
6+
VQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ8wDQYDVQQK
7+
DAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTCCASIwDQYJKoZIhvcNAQEB
8+
BQADggEPADCCAQoCggEBAMIE7PiM7gTCs9hQ1XBYzJMY61yoaEmwIrX5lZ6xKyx2
9+
PmzAS2BMTOqytMAPgLaw+XLJhgL5XEFdEyt/ccRLvOmULlA3pmccYYz2QULFRtMW
10+
hyefdOsKnRFSJiFzbIRMeVXk0WvoBj1IFVKtsyjbqv9u/2CVSndrOfEk0TG23U3A
11+
xPxTuW1CrbV8/q71FdIzSOciccfCFHpsKOo3St/qbLVytH5aohbcabFXRNsKEqve
12+
ww9HdFxBIuGa+RuT5q0iBikusbpJHAwnnqP7i/dAcgCskgjZjFeEU4EFy+b+a1SY
13+
QCeFxxC7c3DvaRhBB0VVfPlkPz0sw6l865MaTIbRyoUCAwEAAaMyMDAwCQYDVR0T
14+
BAIwADAjBgNVHREEHDAaggwqLmJhZHNzbC5jb22CCmJhZHNzbC5jb20wDQYJKoZI
15+
hvcNAQELBQADggEBAC4DensZ5tCTeCNJbHABYPwwqLUFOMITKOOgF3t8EqOan0CH
16+
ST1NNi4jPslWrVhQ4Y3UbAhRBdqXl5N/NFfMzDosPpOjFgtifh8Z2s3w8vdlEZzf
17+
A4mYTC8APgdpWyNgMsp8cdXQF7QOfdnqOfdnY+pfc8a8joObR7HEaeVxhJs+XL4E
18+
CLByw5FR+svkYgCbQGWIgrM1cRpmXemt6Gf/XgFNP2PdubxqDEcnWlTMk8FCBVb1
19+
nVDSiPjYShwnWsOOshshCRCAiIBPCKPX0QwKDComQlRrgMIvddaSzFFTKPoNZjC+
20+
CUspSNnL7V9IIHvqKlRSmu+zIpm2VJCp1xLulk8=
21+
-----END CERTIFICATE-----

tests/smoketests.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
use std::convert::TryInto;
22
use std::sync::Arc;
33

4+
use std::panic;
5+
46
use std::io::{Read, Write};
57
use std::net::TcpStream;
8+
use std::env;
9+
use std::path::PathBuf;
10+
11+
// #[serial] is used on all these tests to run them sequentially. If they're run in parallel,
12+
// the global env var configuration in the env var test interferes with the others.
13+
use serial_test::serial;
614

715
fn check_site(domain: &str) {
816
let mut roots = rustls::RootCertStore::empty();
@@ -37,31 +45,55 @@ fn check_site(domain: &str) {
3745
}
3846

3947
#[test]
48+
#[serial]
4049
fn google() {
4150
check_site("google.com");
4251
}
4352

4453
#[test]
54+
#[serial]
4555
fn amazon() {
4656
check_site("amazon.com");
4757
}
4858

4959
#[test]
60+
#[serial]
5061
fn facebook() {
5162
check_site("facebook.com");
5263
}
5364

5465
#[test]
66+
#[serial]
5567
fn netflix() {
5668
check_site("netflix.com");
5769
}
5870

5971
#[test]
72+
#[serial]
6073
fn ebay() {
6174
check_site("ebay.com");
6275
}
6376

6477
#[test]
78+
#[serial]
6579
fn apple() {
6680
check_site("apple.com");
6781
}
82+
83+
#[test]
84+
#[serial]
85+
fn badssl_with_env() {
86+
let result = panic::catch_unwind(|| {
87+
check_site("self-signed.badssl.com")
88+
});
89+
// Self-signed certs should never be trusted by default:
90+
assert!(result.is_err());
91+
92+
// But they should be trusted if SSL_CERT_FILE is set:
93+
env::set_var("SSL_CERT_FILE",
94+
// The CA cert, downloaded directly from the site itself:
95+
PathBuf::from("./tests/badssl-com-chain.pem")
96+
);
97+
check_site("self-signed.badssl.com");
98+
env::remove_var("SSL_CERT_FILE");
99+
}

0 commit comments

Comments
 (0)