Skip to content

Commit

Permalink
Load system trusted root certs for rustls (#369)
Browse files Browse the repository at this point in the history
Add ability to use trusted root certificates from the OS with rustls automatically. This also might benefit from refactoring our SSL/TLS configuration APIs which always assumed the OS default TLS engine prior. That can be done separately.
  • Loading branch information
sagebind authored Mar 12, 2022
1 parent e995eb4 commit 79338ab
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 27 deletions.
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ jobs:
- ubuntu-latest
- macos-latest
- windows-latest
features:
- "http2,text-decoding,cookies,psl,unstable-interceptors,native-tls,static-curl"
- "http2,text-decoding,cookies,psl,unstable-interceptors,rustls-tls-native-certs"
runs-on: ${{ matrix.os }}
timeout-minutes: 20
env:
Expand All @@ -35,7 +38,7 @@ jobs:
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: "1.46.0"
toolchain: "1.56"
default: true

- name: Update Cargo.lock
Expand All @@ -55,7 +58,7 @@ jobs:
cargo-${{ runner.os }}-
- name: Run tests
run: cargo test --features ${{ env.FEATURES }},spnego,unstable-interceptors
run: cargo test --no-default-features --features ${{ matrix.features }}

- name: Run example program
run: cargo run --release --example simple
Expand Down
28 changes: 24 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,25 @@ features = ["cookies", "json", "nightly"]
status = "actively-developed"

[features]
default = ["http2", "static-curl", "text-decoding"]
default = ["http2", "native-tls", "static-curl", "text-decoding"]
cookies = ["httpdate"]
http2 = ["curl/http2"]
json = ["serde", "serde_json"]
native-tls = ["curl/ssl", "curl-sys/ssl"]
nightly = []
psl = ["httpdate", "parking_lot", "publicsuffix"]
rustls-tls = ["rustls-ffi", "curl/rustls", "curl/static-curl"]
rustls-tls-native-certs = ["rustls-tls", "data-encoding", "rustls-native-certs"]
spnego = ["curl-sys/spnego"]
static-curl = ["curl/static-curl"]
static-ssl = ["curl/static-ssl"]
text-decoding = ["encoding_rs", "mime"]
unstable-rustls-tls = ["curl/rustls"]
unstable-interceptors = []

[dependencies]
async-channel = "1.4.2"
castaway = "0.1.1"
crossbeam-utils = ">=0.7.0, <0.9.0"
curl = "0.4.42"
curl-sys = "0.4.52"
event-listener = "2.3.3"
futures-lite = "1.10.1"
http = "0.2.1"
Expand All @@ -49,6 +49,18 @@ sluice = "0.5.4"
url = "2.1"
waker-fn = "1"

[dependencies.curl]
version = "0.4.43"
default-features = false

[dependencies.curl-sys]
version = "0.4.53"
default-features = false

[dependencies.data-encoding]
version = "2"
optional = true

[dependencies.encoding_rs]
version = "0.8"
optional = true
Expand All @@ -70,6 +82,14 @@ version = "2.0.6"
features = ["std"]
optional = true

[dependencies.rustls-ffi]
version = "0.8"
optional = true

[dependencies.rustls-native-certs]
version = "0.6"
optional = true

[dependencies.serde]
version = "1.0"
optional = true
Expand Down
4 changes: 2 additions & 2 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ pub(crate) mod dns;
pub(crate) mod proxy;
pub(crate) mod redirect;
pub(crate) mod request;
pub(crate) mod ssl;
pub(crate) mod tls;

pub use dial::{Dialer, DialerParseError};
pub use dns::{DnsCache, ResolveMap};
pub use redirect::RedirectPolicy;
pub use ssl::{CaCertificate, ClientCertificate, PrivateKey, SslOption};
pub use tls::{CaCertificate, ClientCertificate, PrivateKey, SslOption};

/// Provides additional methods when building a request for configuring various
/// execution-related options on how the request should be sent.
Expand Down
32 changes: 18 additions & 14 deletions src/config/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,6 @@ macro_rules! define_request_config {
}

impl RequestConfig {
pub(crate) fn client_defaults() -> Self {
Self {
// Always start out with latest compatible HTTP version.
version_negotiation: Some(VersionNegotiation::default()),
// Enable automatic decompression by default for convenience
// (and maintain backwards compatibility).
automatic_decompression: Some(true),
// Erase curl's default auth method of Basic.
authentication: Some(Authentication::default()),
..Default::default()
}
}

/// Merge another request configuration into this one. Unspecified
/// values in this config are replaced with values in the given
/// config.
Expand Down Expand Up @@ -84,7 +71,7 @@ define_request_config! {
max_download_speed: Option<u64>,
ssl_client_certificate: Option<ClientCertificate>,
ssl_ca_certificate: Option<CaCertificate>,
ssl_ciphers: Option<ssl::Ciphers>,
ssl_ciphers: Option<tls::Ciphers>,
ssl_options: Option<SslOption>,
enable_metrics: Option<bool>,

Expand All @@ -94,6 +81,23 @@ define_request_config! {
title_case_headers: Option<bool>,
}

impl RequestConfig {
pub(crate) fn client_defaults() -> Self {
Self {
// Always start out with latest compatible HTTP version.
version_negotiation: Some(VersionNegotiation::default()),
// Enable automatic decompression by default for convenience
// (and maintain backwards compatibility).
automatic_decompression: Some(true),
// Erase curl's default auth method of Basic.
authentication: Some(Authentication::default()),
// Set native root certificates if available.
ssl_ca_certificate: CaCertificate::native(),
..Default::default()
}
}
}

impl SetOpt for RequestConfig {
fn set_opt<H>(&self, easy: &mut Easy2<H>) -> Result<(), curl::Error> {
if let Some(timeout) = self.timeout {
Expand Down
86 changes: 81 additions & 5 deletions src/config/ssl.rs → src/config/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
use super::SetOpt;
use curl::easy::{Easy2, SslOpt};
use once_cell::sync::Lazy;
use std::{
iter::FromIterator,
ops::{BitOr, BitOrAssign},
Expand Down Expand Up @@ -284,12 +285,58 @@ impl SetOpt for PrivateKey {
/// A public CA certificate bundle file.
#[derive(Clone, Debug)]
pub struct CaCertificate {
/// Path to the certificate bundle file. Currently only file paths are
/// supported.
path: PathBuf,
/// The certificate data, either a path or a blob.
data: PathOrBlob,
}

impl CaCertificate {
/// Use one or more PEM-encoded certificates in the given byte buffer.
///
/// The certificate object takes ownership of the byte buffer. If a borrowed
/// type is supplied, such as `&[u8]`, then the bytes will be copied.
///
/// The certificates are not parsed or validated here. If a certificate is
/// malformed or the format is not supported by the underlying SSL/TLS
/// engine, an error will be returned when attempting to send a request
/// using the offending certificate.
pub fn pem<B>(bytes: B) -> Self
where
B: Into<Vec<u8>>,
{
Self {
data: PathOrBlob::Blob(bytes.into()),
}
}

/// Use one or more DER-encoded certificates stored in memory.
///
/// The certificates are not parsed or validated here. If a certificate is
/// malformed or the format is not supported by the underlying SSL/TLS
/// engine, an error will be returned when attempting to send a request
/// using the offending certificate.
#[cfg(feature = "data-encoding")]
#[allow(dead_code)]
pub(crate) fn der_multiple<I, B>(certificates: I) -> Self
where
I: IntoIterator<Item = B>,
B: AsRef<[u8]>,
{
let mut base64_spec = data_encoding::BASE64.specification();
base64_spec.wrap.width = 64;
base64_spec.wrap.separator.push_str("\r\n");
let base64 = base64_spec.encoding().unwrap();

let mut pem = String::new();

for certificate in certificates {
pem.push_str("-----BEGIN CERTIFICATE-----\r\n");
base64.encode_append(certificate.as_ref(), &mut pem);
pem.push_str("-----END CERTIFICATE-----\r\n");
}

Self::pem(pem)
}

/// Get a CA certificate from a path to a certificate bundle file.
///
/// The certificate file is not loaded or validated here. If the file does
Expand All @@ -298,14 +345,43 @@ impl CaCertificate {
/// using the offending certificate.
pub fn file(ca_bundle_path: impl Into<PathBuf>) -> Self {
Self {
path: ca_bundle_path.into(),
data: PathOrBlob::Path(ca_bundle_path.into()),
}
}

/// Get native root certificates trusted by the operating system, if any.
#[allow(unreachable_code)]
pub(crate) fn native() -> Option<Self> {
static NATIVE: Lazy<Option<CaCertificate>> = Lazy::new(|| {
#[cfg(feature = "rustls-native-certs")]
{
match rustls_native_certs::load_native_certs() {
Ok(certs) => {
if !certs.is_empty() {
return Some(CaCertificate::der_multiple(
certs.into_iter().map(|cert| cert.0),
));
}
}
Err(e) => {
log::warn!("failed to load native certificate chain: {}", e);
}
}
}

None
});

NATIVE.clone()
}
}

impl SetOpt for CaCertificate {
fn set_opt<H>(&self, easy: &mut Easy2<H>) -> Result<(), curl::Error> {
easy.cainfo(&self.path)
match &self.data {
PathOrBlob::Path(path) => easy.cainfo(path.as_path()),
PathOrBlob::Blob(bytes) => easy.ssl_cainfo_blob(bytes.as_slice()),
}
}
}

Expand Down

0 comments on commit 79338ab

Please sign in to comment.