mirror of
https://github.com/servo/servo.git
synced 2025-08-03 04:30:10 +01:00
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:
parent
ab0f48f8e8
commit
bce7622cde
31 changed files with 575 additions and 4399 deletions
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue