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
This commit is contained in:
Arun Kulshreshtha 2021-05-11 18:24:26 -07:00 committed by Facebook GitHub Bot
parent 356e56bd4f
commit 5b759a2b52
2 changed files with 55 additions and 6 deletions

View File

@ -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"

View File

@ -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<R: Receiver> TryFrom<StreamRequest<R>> for Easy2<Streaming<R>> {
}
}
fn read_file(path: impl AsRef<Path>) -> Result<Vec<u8>, 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<Path>,
key: Option<impl AsRef<Path>>,
) -> Result<Vec<u8>, 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::*;