use std::fmt;
use std::path::PathBuf;
use std::sync::Arc;
use rustls::crypto::CryptoProvider;
use rustls::pki_types::pem::PemObject;
use rustls::pki_types::CertificateDer;
use crate::config::ConfigError;
use crate::response::BackendError;
pub(crate) fn provider() -> Arc<CryptoProvider> {
Arc::new(rustls::crypto::ring::default_provider())
}
#[cfg(any(feature = "http", feature = "ws"))]
pub(crate) fn random_bytes(bytes: &mut [u8]) -> Result<(), ()> {
rustls::crypto::ring::default_provider().secure_random.fill(bytes).map_err(|_| ())
}
#[cfg(feature = "ws")]
pub(crate) fn client_config() -> Result<Arc<rustls::ClientConfig>, String> {
let roots = rustls::RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.to_vec() };
let config = rustls::ClientConfig::builder_with_provider(provider())
.with_safe_default_protocol_versions()
.map_err(|e| e.to_string())?
.with_root_certificates(roots)
.with_no_client_auth();
Ok(Arc::new(config))
}
#[derive(Clone)]
enum PemSource {
Bytes(Vec<u8>),
File(PathBuf),
}
#[derive(Clone, Default)]
pub struct TlsSettings {
#[cfg(feature = "os-certificates")]
os_store: bool,
extra: Vec<PemSource>,
}
impl fmt::Debug for TlsSettings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("TlsSettings");
#[cfg(feature = "os-certificates")]
s.field("os_store", &self.os_store);
s.field("extra_pem_sources", &self.extra.len()).finish()
}
}
impl TlsSettings {
pub fn new() -> Self {
Self::default()
}
pub fn with_root_certificates_pem(mut self, pem: impl AsRef<[u8]>) -> Self {
self.extra.push(PemSource::Bytes(pem.as_ref().to_vec()));
self
}
pub fn with_root_certificates_file(mut self, path: impl Into<PathBuf>) -> Self {
self.extra.push(PemSource::File(path.into()));
self
}
#[cfg(feature = "os-certificates")]
#[cfg_attr(docsrs, doc(cfg(feature = "os-certificates")))]
pub fn with_os_certificates(mut self, on: bool) -> Self {
self.os_store = on;
self
}
pub fn os_certificates(&self) -> bool {
#[cfg(feature = "os-certificates")]
return self.os_store;
#[cfg(not(feature = "os-certificates"))]
false
}
pub fn root_certificate_sources(&self) -> usize {
self.extra.len()
}
pub fn validate(&self) -> Result<(), ConfigError> {
match self.build() {
Some(Err(error)) => Err(ConfigError::Tls(error.to_string())),
_ => Ok(()),
}
}
pub(crate) fn is_default(&self) -> bool {
self.extra.is_empty() && !self.os_certificates()
}
pub(crate) fn build(&self) -> Option<Result<Arc<rustls::ClientConfig>, BackendError>> {
if self.is_default() {
return None;
}
Some(self.config().map(Arc::new))
}
fn config(&self) -> Result<rustls::ClientConfig, BackendError> {
let extra = extra_roots(&self.extra)?;
let builder = rustls::ClientConfig::builder_with_provider(provider())
.with_safe_default_protocol_versions()
.map_err(|e| BackendError::Tls(format!("the TLS configuration could not be built: {e}")))?;
#[cfg(feature = "os-certificates")]
if self.os_store {
let verifier = os_verifier(extra)?;
return Ok(builder.dangerous().with_custom_certificate_verifier(Arc::new(verifier)).with_no_client_auth());
}
let mut roots = rustls::RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.to_vec() };
for (index, cert) in extra.into_iter().enumerate() {
roots.add(cert).map_err(|e| BackendError::InvalidRequest(format!("extra root certificate #{} is not usable as a root: {e}", index + 1)))?;
}
Ok(builder.with_root_certificates(roots).with_no_client_auth())
}
}
#[cfg(feature = "os-certificates")]
fn os_verifier(extra: Vec<CertificateDer<'static>>) -> Result<rustls_platform_verifier::Verifier, BackendError> {
let failed = |e: rustls::Error| BackendError::Tls(format!("the operating system's certificate verifier could not be set up: {e}"));
if extra.is_empty() {
return rustls_platform_verifier::Verifier::new(provider()).map_err(failed);
}
#[cfg(not(target_os = "android"))]
{
rustls_platform_verifier::Verifier::new_with_extra_roots(extra, provider()).map_err(failed)
}
#[cfg(target_os = "android")]
{
Err(BackendError::InvalidRequest("extra root certificates together with the operating system's certificate store are not supported on Android".into()))
}
}
fn extra_roots(sources: &[PemSource]) -> Result<Vec<CertificateDer<'static>>, BackendError> {
let mut roots = Vec::new();
for source in sources {
let (name, read);
let bytes: &[u8] = match source {
PemSource::Bytes(bytes) => {
name = "the root certificate PEM".to_string();
bytes
}
PemSource::File(path) => {
name = format!("the root certificate file `{}`", path.display());
read = std::fs::read(path).map_err(|e| BackendError::InvalidRequest(format!("{name} could not be read: {e}")))?;
&read
}
};
let before = roots.len();
for cert in CertificateDer::pem_slice_iter(bytes) {
roots.push(cert.map_err(|e| BackendError::InvalidRequest(format!("{name} is not valid PEM: {e}")))?);
}
if roots.len() == before {
return Err(BackendError::InvalidRequest(format!("{name} holds no CERTIFICATE block")));
}
}
Ok(roots)
}
#[cfg(test)]
mod tests {
use super::*;
fn ca_pem() -> String {
let key = rcgen::KeyPair::generate().unwrap_or_else(|e| panic!("{e}"));
let mut params = rcgen::CertificateParams::new(Vec::<String>::new()).unwrap_or_else(|e| panic!("{e}"));
params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
params.self_signed(&key).unwrap_or_else(|e| panic!("{e}")).pem()
}
#[test]
fn the_default_builds_nothing_and_has_no_alpn() {
let tls = TlsSettings::new();
assert!(tls.is_default() && tls.build().is_none() && tls.validate().is_ok());
let config = TlsSettings::new().with_root_certificates_pem(ca_pem()).build();
assert!(config.is_some_and(|c| c.is_ok_and(|c| c.alpn_protocols.is_empty())));
}
#[test]
fn pem_sources_add_every_certificate_and_refuse_sources_without_one() {
let two = format!("{}\n{}", ca_pem(), ca_pem());
assert_eq!(extra_roots(&[PemSource::Bytes(two.clone().into_bytes())]).map(|r| r.len()).ok(), Some(2));
assert!(TlsSettings::new().with_root_certificates_pem(&two).validate().is_ok());
let key_only = rcgen::KeyPair::generate().unwrap_or_else(|e| panic!("{e}")).serialize_pem();
let with_key = format!("{key_only}\n{}", ca_pem());
assert_eq!(extra_roots(&[PemSource::Bytes(with_key.into_bytes())]).map(|r| r.len()).ok(), Some(1));
for bad in [key_only.as_bytes(), b"", b"not pem at all"] {
let error = TlsSettings::new().with_root_certificates_pem(bad).validate().err();
assert!(matches!(error, Some(ConfigError::Tls(ref why)) if why.contains("no CERTIFICATE")), "{error:?}");
}
let broken = TlsSettings::new().with_root_certificates_pem("-----BEGIN CERTIFICATE-----\nAAAA\n").build();
assert!(matches!(broken, Some(Err(BackendError::InvalidRequest(_)))), "{broken:?}");
let missing = TlsSettings::new().with_root_certificates_file("this-file-does-not-exist.pem").validate().err();
assert!(matches!(missing, Some(ConfigError::Tls(ref why)) if why.contains("this-file-does-not-exist.pem")), "{missing:?}");
}
#[test]
fn debug_shows_counts_only() {
let tls = TlsSettings::new().with_root_certificates_file("a.pem").with_root_certificates_pem("x");
assert_eq!(tls.root_certificate_sources(), 2);
assert!(!format!("{tls:?}").contains("a.pem"), "{tls:?}");
}
#[test]
#[cfg(feature = "os-certificates")]
fn the_os_store_builds_with_and_without_extra_roots() {
let os = TlsSettings::new().with_os_certificates(true);
assert!(os.os_certificates() && !os.is_default());
assert!(os.build().is_some_and(|c| c.is_ok_and(|c| c.alpn_protocols.is_empty())));
let os = os.with_root_certificates_pem(ca_pem());
assert!(os.validate().is_ok());
assert!(format!("{os:?}").contains("os_store: true"));
}
}