From 5b759a2b52172f17822d1c847e590e1bd26ee243 Mon Sep 17 00:00:00 2001 From: Arun Kulshreshtha Date: Tue, 11 May 2021 18:24:26 -0700 Subject: [PATCH] http-client: pass certs to libcurl as in-memory blobs Summary: Instead of passing a client certificate path to libcurl, load the certificate into memory and pass it to libcurl as a blob using `CURLOPT_SSLCERT_BLOB`. This allows us to convert the certificate format in-memory from PEM to PKCS#12, the latter of which is supported by the TLS engines on all platform (and notably SChannel on Windows, which does not support PEM certificate). Reviewed By: quark-zju Differential Revision: D27637069 fbshipit-source-id: f7f8eaafcd1498fabf2ee91c172e896a97ceba7e --- eden/scm/lib/http-client/Cargo.toml | 1 + eden/scm/lib/http-client/src/request.rs | 60 ++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/eden/scm/lib/http-client/Cargo.toml b/eden/scm/lib/http-client/Cargo.toml index 08378accc0..4302c00bfb 100644 --- a/eden/scm/lib/http-client/Cargo.toml +++ b/eden/scm/lib/http-client/Cargo.toml @@ -17,6 +17,7 @@ env_logger = "0.7" futures = { version = "0.3.13", features = ["async-await", "compat"] } http = "0.2" once_cell = "1.4" +openssl = "0.10" parking_lot = "0.10.2" paste = "1.0" pin-project = "0.4" diff --git a/eden/scm/lib/http-client/src/request.rs b/eden/scm/lib/http-client/src/request.rs index 386ea5d5b3..3f829bb11e 100644 --- a/eden/scm/lib/http-client/src/request.rs +++ b/eden/scm/lib/http-client/src/request.rs @@ -5,8 +5,11 @@ * GNU General Public License version 2. */ +use std::borrow::Cow; use std::convert::{TryFrom, TryInto}; use std::fmt; +use std::fs::File; +use std::io::prelude::*; use std::path::{Path, PathBuf}; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering::AcqRel; @@ -21,6 +24,10 @@ use parking_lot::RwLock; use serde::Serialize; use url::Url; +use openssl::pkcs12::Pkcs12; +use openssl::pkey::PKey; +use openssl::x509::X509; + use crate::{ errors::HttpClientError, event_listeners::RequestCreationEventListeners, @@ -513,12 +520,15 @@ impl Request { easy.ssl_verify_host(self.verify_tls_host)?; easy.ssl_verify_peer(self.verify_tls_cert)?; - // Set up client credentials for mTLS. - if let Some(cert) = self.cert { - easy.ssl_cert(cert)?; - } - if let Some(key) = self.key { - easy.ssl_key(key)?; + // SChannel on Windows does not support PEM-encoded certificates, and + // instead requires certificates to be passed in as a PKCS#12 archive. + // Since all of the other TLS engines (OpenSSL on Linux and Secure + // Transport on MacOS) support PKCS#12, let's just always convert the + // certificates and pass them to libcurl as an in-memory blob. + if let Some(cert) = &self.cert { + let blob = pem_to_pkcs12(cert, self.key)?; + easy.ssl_cert_type("P12")?; + easy.ssl_cert_blob(&blob)?; } // Windows enables ssl revocation checking by default, which doesn't work inside the @@ -593,6 +603,44 @@ impl TryFrom> for Easy2> { } } +fn read_file(path: impl AsRef) -> Result, anyhow::Error> { + let mut f = File::open(path)?; + let mut buf = Vec::new(); + f.read_to_end(&mut buf)?; + Ok(buf) +} + +/// Convert a PEM-formatted X.509 certificate chain and private key into a +/// PKCS#12 archive, which can then be directly passed to libcurl using +/// `CURLOPT_SSLCERT_BLOB`. This is useful because not all TLS engines (notably +/// SChannel (WinSSL) on Windows) support loading PEM files, but all major TLS +/// engines support PKCS#12. Returns a DER-encoded binary representation of +/// the combined certificate chain and private key. +fn pem_to_pkcs12( + cert: impl AsRef, + key: Option>, +) -> Result, anyhow::Error> { + // It's common for the certificate and private key to be concatenated + // together in the same PEM file. If a key path isn't specified, assume + // this is the case and use the certificate PEM for the key as well. + let cert_bytes = read_file(cert)?; + let key_bytes = match key { + Some(key) => Cow::Owned(read_file(key)?), + None => Cow::Borrowed(&cert_bytes), + }; + + let cert = X509::from_pem(&cert_bytes)?; + let key = PKey::private_key_from_pem(&key_bytes)?; + + // PKCS#12 archives are encrypted, so we need to specify a password when + // creating one. Here we just use an empty password since it seems like most + // TLS engines will attempt to decrypt using the empty string if no password + // is specified. + let pkcs12 = Pkcs12::builder().build("", "", &key, &cert)?; + + Ok(pkcs12.to_der()?) +} + #[cfg(test)] mod tests { use super::*;