Switch to rustls and webpki-roots (#30025)

This change replaces OpenSSL with rustls and also the manually curated
CA certs file with webpki-roots (effectively the same thing, but as a
crate).

Generally speaking the design of the network stack is the same. Changes:

- Code around certificate overrides needed to be refactored to work with
  rustls so the various thread-safe list of certificates is refactored
  into `CertificateErrorOverrideManager`
- hyper-rustls takes care of setting ALPN protocols for HTTP requests,
  so for WebSockets this is moved to the WebSocket code.
- The safe set of cypher suites is chosen, which seem to correspond to
  the "Modern" configuration from [1]. This can be adjusted later.
- Instead of passing a string of PEM CA certificates around, an enum is
  used that includes parsed Certificates (or the default which reads
  them from webpki-roots).
- Code for starting up an SSL server for testing is cleaned up a little,
  due to the fact that the certificates need to be overriden explicitly
  now. This is due to the fact that the `webpki` crate is more stringent
  with self-signed certificates than SSL (CA certificates cannot used as
  end-entity certificates). [2]

1. https://wiki.mozilla.org/Security/Server_Side_TLS
2. https://github.com/briansmith/webpki/issues/114

Fixes #7888.
Fixes #13749.
Fixes #26835.
Fixes #29291.
This commit is contained in:
Martin Robinson 2023-08-08 16:00:10 +02:00 committed by GitHub
parent ab0f48f8e8
commit bce7622cde
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 575 additions and 4399 deletions

View file

@ -9,80 +9,30 @@ use http::uri::{Authority, Uri as Destination};
use hyper::client::HttpConnector as HyperHttpConnector;
use hyper::rt::Executor;
use hyper::{service::Service, Body, Client};
use hyper_openssl::HttpsConnector;
use openssl::ex_data::Index;
use openssl::ssl::{
Ssl, SslConnector, SslConnectorBuilder, SslContext, SslMethod, SslOptions, SslVerifyMode,
};
use openssl::x509::{self, X509StoreContext};
use std::collections::hash_map::{Entry, HashMap};
use hyper_rustls::HttpsConnector as HyperRustlsHttpsConnector;
use rustls::client::WebPkiVerifier;
use rustls::{Certificate, ClientConfig, OwnedTrustAnchor, RootCertStore, ServerName};
use std::collections::hash_map::HashMap;
use std::convert::TryFrom;
use std::sync::{Arc, Mutex};
pub const BUF_SIZE: usize = 32768;
pub const ALPN_H2_H1: &'static [u8] = b"\x02h2\x08http/1.1";
pub const ALPN_H1: &'static [u8] = b"\x08http/1.1";
// See https://wiki.mozilla.org/Security/Server_Side_TLS for orientation.
const TLS1_2_CIPHERSUITES: &'static str = concat!(
"ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:",
"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:",
"ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:",
"ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA@SECLEVEL=2"
);
const SIGNATURE_ALGORITHMS: &'static str = concat!(
"ed448:ed25519:",
"ECDSA+SHA384:ECDSA+SHA256:",
"RSA-PSS+SHA512:RSA-PSS+SHA384:RSA-PSS+SHA256:",
"RSA+SHA512:RSA+SHA384:RSA+SHA256"
);
#[derive(Clone)]
pub struct ConnectionCerts {
certs: Arc<Mutex<HashMap<String, (Vec<u8>, u32)>>>,
}
impl ConnectionCerts {
pub fn new() -> Self {
Self {
certs: Arc::new(Mutex::new(HashMap::new())),
}
}
fn store(&self, host: String, cert_bytes: Vec<u8>) {
let mut certs = self.certs.lock().unwrap();
let entry = certs.entry(host).or_insert((cert_bytes, 0));
entry.1 += 1;
}
pub(crate) fn remove(&self, host: String) -> Option<Vec<u8>> {
match self.certs.lock().unwrap().entry(host) {
Entry::Vacant(_) => return None,
Entry::Occupied(mut e) => {
e.get_mut().1 -= 1;
if e.get().1 == 0 {
return Some((e.remove_entry().1).0);
}
Some(e.get().0.clone())
},
}
}
}
#[derive(Clone)]
pub struct HttpConnector {
pub struct ServoHttpConnector {
inner: HyperHttpConnector,
}
impl HttpConnector {
fn new() -> HttpConnector {
impl ServoHttpConnector {
fn new() -> ServoHttpConnector {
let mut inner = HyperHttpConnector::new();
inner.enforce_http(false);
inner.set_happy_eyeballs_timeout(None);
HttpConnector { inner }
ServoHttpConnector { inner }
}
}
impl Service<Destination> for HttpConnector {
impl Service<Destination> for ServoHttpConnector {
type Response = <HyperHttpConnector as Service<Destination>>::Response;
type Error = <HyperHttpConnector as Service<Destination>>::Error;
type Future = <HyperHttpConnector as Service<Destination>>::Future;
@ -118,119 +68,85 @@ impl Service<Destination> for HttpConnector {
}
}
pub type Connector = HttpsConnector<HttpConnector>;
pub type TlsConfig = SslConnectorBuilder;
pub type Connector = HyperRustlsHttpsConnector<ServoHttpConnector>;
pub type TlsConfig = ClientConfig;
#[derive(Clone)]
pub struct ExtraCerts(Arc<Mutex<Vec<Vec<u8>>>>);
#[derive(Clone, Debug, Default)]
struct CertificateErrorOverrideManagerInternal {
/// A mapping of certificates and their hosts, which have seen certificate errors.
/// This is used to later create an override in this [CertificateErrorOverrideManager].
certificates_failing_to_verify: HashMap<ServerName, Certificate>,
/// A list of certificates that should be accepted despite encountering verification
/// errors.
overrides: Vec<Certificate>,
}
impl ExtraCerts {
/// This data structure is used to track certificate verification errors and overrides.
/// It tracks:
/// - A list of [Certificate]s with verification errors mapped by their [ServerName]
/// - A list of [Certificate]s for which to ignore verification errors.
#[derive(Clone, Debug, Default)]
pub struct CertificateErrorOverrideManager(Arc<Mutex<CertificateErrorOverrideManagerInternal>>);
impl CertificateErrorOverrideManager {
pub fn new() -> Self {
Self(Arc::new(Mutex::new(vec![])))
Self(Default::default())
}
pub fn add(&self, bytes: Vec<u8>) {
self.0.lock().unwrap().push(bytes);
/// Add a certificate to this manager's list of certificates for which to ignore
/// validation errors.
pub fn add_override(&self, certificate: &Certificate) {
self.0.lock().unwrap().overrides.push(certificate.clone());
}
/// Given the a string representation of a sever host name, remove information about
/// a [Certificate] with verification errors. If a certificate with
/// verification errors was found, return it, otherwise None.
pub(crate) fn remove_certificate_failing_verification(
&self,
host: &str,
) -> Option<Certificate> {
let server_name = match ServerName::try_from(host) {
Ok(name) => name,
Err(error) => {
warn!("Could not convert host string into RustTLS ServerName: {error:?}");
return None;
},
};
self.0
.lock()
.unwrap()
.certificates_failing_to_verify
.remove(&server_name)
}
}
struct Host(String);
lazy_static! {
static ref EXTRA_INDEX: Index<SslContext, ExtraCerts> = SslContext::new_ex_index().unwrap();
static ref CONNECTION_INDEX: Index<SslContext, ConnectionCerts> =
SslContext::new_ex_index().unwrap();
static ref HOST_INDEX: Index<Ssl, Host> = Ssl::new_ex_index().unwrap();
#[derive(Clone, Debug)]
pub enum CACertificates {
Default,
Override(RootCertStore),
}
/// Create a [TlsConfig] to use for managing a HTTP connection. This currently creates
/// a rustls [ClientConfig].
///
/// FIXME: The `ignore_certificate_errors` argument ignores all certificate errors. This
/// is used when running the WPT tests, because rustls currently rejects the WPT certificiate.
/// See https://github.com/servo/servo/issues/30080
pub fn create_tls_config(
certs: &str,
alpn: &[u8],
extra_certs: ExtraCerts,
connection_certs: ConnectionCerts,
ca_certificates: CACertificates,
ignore_certificate_errors: bool,
override_manager: CertificateErrorOverrideManager,
) -> TlsConfig {
// certs include multiple certificates. We could add all of them at once,
// but if any of them were already added, openssl would fail to insert all
// of them.
let mut certs = certs;
let mut cfg = SslConnector::builder(SslMethod::tls()).unwrap();
loop {
let token = "-----END CERTIFICATE-----";
if let Some(index) = certs.find(token) {
let (cert, rest) = certs.split_at(index + token.len());
certs = rest;
let cert = x509::X509::from_pem(cert.as_bytes()).unwrap();
cfg.cert_store_mut()
.add_cert(cert)
.or_else(|e| {
let v: Option<Option<&str>> = e.errors().iter().nth(0).map(|e| e.reason());
if v == Some(Some("cert already in hash table")) {
warn!("Cert already in hash table. Ignoring.");
// Ignore error X509_R_CERT_ALREADY_IN_HASH_TABLE which means the
// certificate is already in the store.
Ok(())
} else {
Err(e)
}
})
.expect("could not set CA file");
} else {
break;
}
}
cfg.set_alpn_protos(alpn)
.expect("could not set alpn protocols");
cfg.set_cipher_list(TLS1_2_CIPHERSUITES)
.expect("could not set TLS 1.2 ciphersuites");
cfg.set_sigalgs_list(SIGNATURE_ALGORITHMS)
.expect("could not set signature algorithms");
cfg.set_options(
SslOptions::NO_SSLV2 |
SslOptions::NO_SSLV3 |
SslOptions::NO_TLSV1 |
SslOptions::NO_TLSV1_1 |
SslOptions::NO_COMPRESSION,
let verifier = CertificateVerificationOverrideVerifier::new(
ca_certificates,
ignore_certificate_errors,
override_manager,
);
cfg.set_ex_data(*EXTRA_INDEX, extra_certs);
cfg.set_ex_data(*CONNECTION_INDEX, connection_certs);
cfg.set_verify_callback(SslVerifyMode::PEER, |verified, x509_store_context| {
if verified {
return true;
}
let ssl_idx = X509StoreContext::ssl_idx().unwrap();
let ssl = x509_store_context.ex_data(ssl_idx).unwrap();
// Obtain the cert bytes for this connection.
let cert = match x509_store_context.current_cert() {
Some(cert) => cert,
None => return false,
};
let pem = match cert.to_pem() {
Ok(pem) => pem,
Err(_) => return false,
};
let ssl_context = ssl.ssl_context();
// Ensure there's an entry stored in the set of known connection certs for this connection.
if let Some(host) = ssl.ex_data(*HOST_INDEX) {
let connection_certs = ssl_context.ex_data(*CONNECTION_INDEX).unwrap();
connection_certs.store((*host).0.clone(), pem.clone());
}
// Fall back to the dynamic set of allowed certs.
let extra_certs = ssl_context.ex_data(*EXTRA_INDEX).unwrap();
for cert in &*extra_certs.0.lock().unwrap() {
if pem == *cert {
return true;
}
}
false
});
cfg
rustls::ClientConfig::builder()
.with_safe_defaults()
.with_custom_certificate_verifier(Arc::new(verifier))
.with_no_client_auth()
}
struct TokioExecutor {}
@ -244,14 +160,95 @@ where
}
}
pub fn create_http_client(tls_config: TlsConfig) -> Client<Connector, Body> {
let mut connector = HttpsConnector::with_connector(HttpConnector::new(), tls_config).unwrap();
connector.set_callback(|configuration, destination| {
if let Some(host) = destination.host() {
configuration.set_ex_data(*HOST_INDEX, Host(host.to_owned()));
struct CertificateVerificationOverrideVerifier {
webpki_verifier: WebPkiVerifier,
ignore_certificate_errors: bool,
override_manager: CertificateErrorOverrideManager,
}
impl CertificateVerificationOverrideVerifier {
fn new(
ca_certficates: CACertificates,
ignore_certificate_errors: bool,
override_manager: CertificateErrorOverrideManager,
) -> Self {
let root_cert_store = match ca_certficates {
CACertificates::Default => {
let mut root_cert_store = rustls::RootCertStore::empty();
root_cert_store.add_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(
|trust_anchor| {
OwnedTrustAnchor::from_subject_spki_name_constraints(
trust_anchor.subject,
trust_anchor.spki,
trust_anchor.name_constraints,
)
},
));
root_cert_store
},
CACertificates::Override(root_cert_store) => root_cert_store,
};
Self {
// See https://github.com/rustls/rustls/blame/v/0.21.6/rustls/src/client/builder.rs#L141
// This is the default verifier for Rustls that we are wrapping.
webpki_verifier: WebPkiVerifier::new(root_cert_store, None),
ignore_certificate_errors,
override_manager,
}
Ok(())
});
}
}
impl rustls::client::ServerCertVerifier for CertificateVerificationOverrideVerifier {
fn verify_server_cert(
&self,
end_entity: &Certificate,
intermediates: &[Certificate],
server_name: &ServerName,
scts: &mut dyn Iterator<Item = &[u8]>,
ocsp_response: &[u8],
now: std::time::SystemTime,
) -> Result<rustls::client::ServerCertVerified, rustls::Error> {
let error = match self.webpki_verifier.verify_server_cert(
end_entity,
intermediates,
server_name,
scts,
ocsp_response,
now,
) {
Ok(result) => return Ok(result),
Err(error) => error,
};
if self.ignore_certificate_errors {
warn!("Ignoring certficate error: {error:?}");
return Ok(rustls::client::ServerCertVerified::assertion());
}
// If there's an override for this certificate, just accept it.
for cert_with_exception in &*self.override_manager.0.lock().unwrap().overrides {
if *end_entity == *cert_with_exception {
return Ok(rustls::client::ServerCertVerified::assertion());
}
}
self.override_manager
.0
.lock()
.unwrap()
.certificates_failing_to_verify
.insert(server_name.clone(), end_entity.clone());
Err(error)
}
}
pub fn create_http_client(tls_config: TlsConfig) -> Client<Connector, Body> {
let connector = hyper_rustls::HttpsConnectorBuilder::new()
.with_tls_config(tls_config)
.https_or_http()
.enable_http1()
.enable_http2()
.wrap_connector(ServoHttpConnector::new());
Client::builder()
.http1_title_case_headers(true)