use core::fmt;
use reqwest::blocking::Client;
#[cfg(any(
feature = "blocking-rustls-fips",
feature = "blocking-rustls-webpki-roots"
))]
use reqwest::blocking::ClientBuilder;
use reqwest::redirect::Policy;
use reqwest::tls::Version;
#[cfg(feature = "blocking-rustls-fips")]
use rustls::client::WebPkiServerVerifier;
#[cfg(feature = "blocking-rustls-fips")]
use rustls::crypto::CryptoProvider;
#[cfg(feature = "blocking-rustls-fips")]
use rustls::pki_types::CertificateRevocationListDer;
#[cfg(any(
feature = "blocking-rustls-fips",
feature = "blocking-rustls-webpki-roots"
))]
use rustls::{ClientConfig, RootCertStore};
#[cfg(any(
feature = "blocking-rustls-fips",
feature = "blocking-rustls-webpki-roots"
))]
use std::sync::Arc;
#[cfg(feature = "blocking-rustls-fips")]
use std::vec::Vec;
use crate::shared::{BearerToken, BuildError, HttpsEndpoint, RequestTimeouts, UserAgent};
use super::BlockingClient;
#[cfg(feature = "blocking-rustls-fips")]
pub struct FipsTlsPolicy {
roots: Arc<RootCertStore>,
crls: Vec<CertificateRevocationListDer<'static>>,
}
#[cfg(feature = "blocking-rustls-fips")]
impl FipsTlsPolicy {
pub fn new(
roots: RootCertStore,
crls: Vec<CertificateRevocationListDer<'static>>,
) -> Result<Self, BuildError> {
if roots.is_empty() {
return Err(BuildError::FipsTrustRootsRequired);
}
if crls.is_empty() {
return Err(BuildError::FipsCertificateRevocationListsRequired);
}
Ok(Self {
roots: Arc::new(roots),
crls,
})
}
}
#[cfg(feature = "blocking-rustls-fips")]
impl fmt::Debug for FipsTlsPolicy {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("FipsTlsPolicy")
.field("trust_anchors", &self.roots.len())
.field("crls", &self.crls.len())
.finish_non_exhaustive()
}
}
pub struct BlockingClientBuilder {
endpoint: HttpsEndpoint,
token: BearerToken,
user_agent: UserAgent,
timeouts: RequestTimeouts,
#[cfg(feature = "blocking-rustls-fips")]
fips_tls_policy: Option<FipsTlsPolicy>,
}
impl BlockingClientBuilder {
#[must_use]
pub const fn new(
endpoint: HttpsEndpoint,
token: BearerToken,
user_agent: UserAgent,
timeouts: RequestTimeouts,
) -> Self {
Self {
endpoint,
token,
user_agent,
timeouts,
#[cfg(feature = "blocking-rustls-fips")]
fips_tls_policy: None,
}
}
#[cfg(feature = "blocking-rustls-fips")]
#[must_use]
pub fn with_fips_tls_policy(mut self, policy: FipsTlsPolicy) -> Self {
self.fips_tls_policy = Some(policy);
self
}
pub fn build(self) -> Result<BlockingClient, BuildError> {
self.build_inner(true)
}
fn build_inner(self, https_only: bool) -> Result<BlockingClient, BuildError> {
let client = configured_client(&self, https_only)?;
Ok(BlockingClient::new(client, self.endpoint, self.token))
}
#[cfg(test)]
pub(super) fn build_for_loopback(self) -> Result<BlockingClient, BuildError> {
self.build_inner(false)
}
}
impl fmt::Debug for BlockingClientBuilder {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut debug = formatter.debug_struct("BlockingClientBuilder");
debug
.field("endpoint", &"[redacted]")
.field("token", &"[redacted]")
.field("user_agent", &self.user_agent)
.field("timeouts", &self.timeouts);
#[cfg(feature = "blocking-rustls-fips")]
debug.field("fips_tls_policy", &self.fips_tls_policy);
debug.finish()
}
}
fn configured_client(
configuration: &BlockingClientBuilder,
https_only: bool,
) -> Result<Client, BuildError> {
configured_tls_builder(configuration)?
.https_only(https_only)
.http1_only()
.no_hickory_dns()
.min_tls_version(Version::TLS_1_2)
.redirect(Policy::none())
.retry(reqwest::retry::never())
.referer(false)
.no_proxy()
.no_gzip()
.no_brotli()
.no_zstd()
.no_deflate()
.timeout(configuration.timeouts.total())
.connect_timeout(configuration.timeouts.connect())
.connection_verbose(false)
.user_agent(configuration.user_agent.value.clone())
.build()
.map_err(|_| BuildError::ClientBuildFailed)
}
#[cfg(all(
not(feature = "blocking-rustls-fips"),
not(feature = "blocking-rustls-webpki-roots")
))]
fn configured_tls_builder(
_configuration: &BlockingClientBuilder,
) -> Result<reqwest::blocking::ClientBuilder, BuildError> {
Ok(Client::builder().tls_backend_rustls())
}
#[cfg(all(
not(feature = "blocking-rustls-fips"),
feature = "blocking-rustls-webpki-roots"
))]
fn configured_tls_builder(
_configuration: &BlockingClientBuilder,
) -> Result<ClientBuilder, BuildError> {
let config = webpki_roots_client_config()?;
Ok(Client::builder().tls_backend_preconfigured(config))
}
#[cfg(all(
not(feature = "blocking-rustls-fips"),
feature = "blocking-rustls-webpki-roots"
))]
fn webpki_roots_client_config() -> Result<ClientConfig, BuildError> {
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
let mut roots = RootCertStore::empty();
roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
Ok(ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.map_err(|_| BuildError::WebPkiRootsProtocolConfigurationFailed)?
.with_root_certificates(roots)
.with_no_client_auth())
}
#[cfg(all(
test,
not(feature = "blocking-rustls-fips"),
feature = "blocking-rustls-webpki-roots"
))]
pub(super) fn test_webpki_roots_configuration() -> Result<(usize, bool), BuildError> {
let config = webpki_roots_client_config()?;
Ok((webpki_roots::TLS_SERVER_ROOTS.len(), config.fips()))
}
#[cfg(feature = "blocking-rustls-fips")]
fn configured_tls_builder(
configuration: &BlockingClientBuilder,
) -> Result<ClientBuilder, BuildError> {
let policy = configuration
.fips_tls_policy
.as_ref()
.ok_or(BuildError::FipsTlsPolicyRequired)?;
let config = fips_client_config(policy)?;
Ok(Client::builder().tls_backend_preconfigured(config))
}
#[cfg(feature = "blocking-rustls-fips")]
fn fips_client_config(policy: &FipsTlsPolicy) -> Result<ClientConfig, BuildError> {
let provider = Arc::new(rustls::crypto::default_fips_provider());
validate_fips_provider(provider.as_ref())?;
let config = client_config_with_provider(provider, policy)?;
validate_fips_config(&config)?;
Ok(config)
}
#[cfg(feature = "blocking-rustls-fips")]
fn client_config_with_provider(
provider: Arc<CryptoProvider>,
policy: &FipsTlsPolicy,
) -> Result<ClientConfig, BuildError> {
let verifier = WebPkiServerVerifier::builder_with_provider(
Arc::clone(&policy.roots),
Arc::clone(&provider),
)
.with_crls(policy.crls.iter().cloned())
.enforce_revocation_expiration()
.build()
.map_err(|_| BuildError::FipsRevocationVerifierFailed)?;
Ok(ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.map_err(|_| BuildError::FipsProtocolConfigurationFailed)?
.dangerous()
.with_custom_certificate_verifier(verifier)
.with_no_client_auth())
}
#[cfg(feature = "blocking-rustls-fips")]
fn validate_fips_provider(provider: &CryptoProvider) -> Result<(), BuildError> {
if provider.fips() {
Ok(())
} else {
Err(BuildError::FipsProviderRejected)
}
}
#[cfg(feature = "blocking-rustls-fips")]
fn validate_fips_config(config: &ClientConfig) -> Result<(), BuildError> {
if config.fips() {
Ok(())
} else {
Err(BuildError::FipsClientConfigurationRejected)
}
}
#[cfg(all(test, feature = "blocking-rustls-fips"))]
pub(super) fn test_fips_configuration(policy: &FipsTlsPolicy) -> Result<bool, BuildError> {
fips_client_config(policy).map(|config| config.fips())
}
#[cfg(all(test, feature = "blocking-rustls-fips"))]
fn non_fips_provider() -> CryptoProvider {
#[derive(Debug)]
struct NonFipsRandom;
impl rustls::crypto::SecureRandom for NonFipsRandom {
fn fill(&self, _buffer: &mut [u8]) -> Result<(), rustls::crypto::GetRandomFailed> {
Err(rustls::crypto::GetRandomFailed)
}
}
static NON_FIPS_RANDOM: NonFipsRandom = NonFipsRandom;
let mut provider = rustls::crypto::default_fips_provider();
provider.secure_random = &NON_FIPS_RANDOM;
provider
}
#[cfg(all(test, feature = "blocking-rustls-fips"))]
pub(super) fn test_non_fips_rejection(policy: &FipsTlsPolicy) -> Result<bool, BuildError> {
let provider = non_fips_provider();
let provider_rejected =
validate_fips_provider(&provider) == Err(BuildError::FipsProviderRejected);
let config = client_config_with_provider(Arc::new(provider), policy)?;
let config_rejected =
validate_fips_config(&config) == Err(BuildError::FipsClientConfigurationRejected);
Ok(provider_rejected && config_rejected)
}
#[cfg(all(test, feature = "blocking-rustls-fips"))]
pub(super) fn test_non_fips_global_independence(policy: &FipsTlsPolicy) -> bool {
let provider = non_fips_provider();
if provider.fips() || provider.install_default().is_err() {
return false;
}
let global_is_non_fips = CryptoProvider::get_default().is_some_and(|value| !value.fips());
global_is_non_fips && test_fips_configuration(policy) == Ok(true)
}