Skip to main content

cloud_sdk_reqwest/blocking/
config.rs

1use core::fmt;
2
3use reqwest::blocking::Client;
4#[cfg(feature = "blocking-rustls-fips")]
5use reqwest::blocking::ClientBuilder;
6use reqwest::redirect::Policy;
7use reqwest::tls::Version;
8#[cfg(feature = "blocking-rustls-fips")]
9use rustls::client::WebPkiServerVerifier;
10#[cfg(feature = "blocking-rustls-fips")]
11use rustls::crypto::CryptoProvider;
12#[cfg(feature = "blocking-rustls-fips")]
13use rustls::pki_types::CertificateRevocationListDer;
14#[cfg(feature = "blocking-rustls-fips")]
15use rustls::{ClientConfig, RootCertStore};
16#[cfg(feature = "blocking-rustls-fips")]
17use std::sync::Arc;
18#[cfg(feature = "blocking-rustls-fips")]
19use std::vec::Vec;
20
21use crate::shared::{BearerToken, BuildError, HttpsEndpoint, RequestTimeouts, UserAgent};
22
23use super::BlockingClient;
24
25/// Deployment-managed trust anchors and complete CRLs for FIPS TLS.
26#[cfg(feature = "blocking-rustls-fips")]
27pub struct FipsTlsPolicy {
28    roots: Arc<RootCertStore>,
29    crls: Vec<CertificateRevocationListDer<'static>>,
30}
31
32#[cfg(feature = "blocking-rustls-fips")]
33impl FipsTlsPolicy {
34    /// Creates a policy that checks the complete certificate chain, rejects
35    /// unknown revocation status, and rejects expired CRLs.
36    pub fn new(
37        roots: RootCertStore,
38        crls: Vec<CertificateRevocationListDer<'static>>,
39    ) -> Result<Self, BuildError> {
40        if roots.is_empty() {
41            return Err(BuildError::FipsTrustRootsRequired);
42        }
43        if crls.is_empty() {
44            return Err(BuildError::FipsCertificateRevocationListsRequired);
45        }
46        Ok(Self {
47            roots: Arc::new(roots),
48            crls,
49        })
50    }
51}
52
53#[cfg(feature = "blocking-rustls-fips")]
54impl fmt::Debug for FipsTlsPolicy {
55    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
56        formatter
57            .debug_struct("FipsTlsPolicy")
58            .field("trust_anchors", &self.roots.len())
59            .field("crls", &self.crls.len())
60            .finish_non_exhaustive()
61    }
62}
63
64/// Builder requiring endpoint, bearer token, user agent, and all timeout
65/// dimensions before a client can be constructed.
66pub struct BlockingClientBuilder {
67    endpoint: HttpsEndpoint,
68    token: BearerToken,
69    user_agent: UserAgent,
70    timeouts: RequestTimeouts,
71    #[cfg(feature = "blocking-rustls-fips")]
72    fips_tls_policy: Option<FipsTlsPolicy>,
73}
74
75impl BlockingClientBuilder {
76    /// Creates a complete blocking-client configuration.
77    #[must_use]
78    pub const fn new(
79        endpoint: HttpsEndpoint,
80        token: BearerToken,
81        user_agent: UserAgent,
82        timeouts: RequestTimeouts,
83    ) -> Self {
84        Self {
85            endpoint,
86            token,
87            user_agent,
88            timeouts,
89            #[cfg(feature = "blocking-rustls-fips")]
90            fips_tls_policy: None,
91        }
92    }
93
94    /// Supplies mandatory deployment-managed roots and CRLs for FIPS TLS.
95    #[cfg(feature = "blocking-rustls-fips")]
96    #[must_use]
97    pub fn with_fips_tls_policy(mut self, policy: FipsTlsPolicy) -> Self {
98        self.fips_tls_policy = Some(policy);
99        self
100    }
101
102    /// Builds a hardened HTTPS-only client.
103    pub fn build(self) -> Result<BlockingClient, BuildError> {
104        self.build_inner(true)
105    }
106
107    fn build_inner(self, https_only: bool) -> Result<BlockingClient, BuildError> {
108        let client = configured_client(&self, https_only)?;
109        Ok(BlockingClient::new(client, self.endpoint, self.token))
110    }
111
112    #[cfg(test)]
113    pub(super) fn build_for_loopback(self) -> Result<BlockingClient, BuildError> {
114        self.build_inner(false)
115    }
116}
117
118impl fmt::Debug for BlockingClientBuilder {
119    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
120        let mut debug = formatter.debug_struct("BlockingClientBuilder");
121        debug
122            .field("endpoint", &"[redacted]")
123            .field("token", &"[redacted]")
124            .field("user_agent", &self.user_agent)
125            .field("timeouts", &self.timeouts);
126        #[cfg(feature = "blocking-rustls-fips")]
127        debug.field("fips_tls_policy", &self.fips_tls_policy);
128        debug.finish()
129    }
130}
131
132fn configured_client(
133    configuration: &BlockingClientBuilder,
134    https_only: bool,
135) -> Result<Client, BuildError> {
136    configured_tls_builder(configuration)?
137        .https_only(https_only)
138        .http1_only()
139        .no_hickory_dns()
140        .min_tls_version(Version::TLS_1_2)
141        .redirect(Policy::none())
142        .retry(reqwest::retry::never())
143        .referer(false)
144        .no_proxy()
145        .no_gzip()
146        .no_brotli()
147        .no_zstd()
148        .no_deflate()
149        .timeout(configuration.timeouts.total())
150        .connect_timeout(configuration.timeouts.connect())
151        .connection_verbose(false)
152        .user_agent(configuration.user_agent.value.clone())
153        .build()
154        .map_err(|_| BuildError::ClientBuildFailed)
155}
156
157#[cfg(not(feature = "blocking-rustls-fips"))]
158fn configured_tls_builder(
159    _configuration: &BlockingClientBuilder,
160) -> Result<reqwest::blocking::ClientBuilder, BuildError> {
161    Ok(Client::builder().tls_backend_rustls())
162}
163
164#[cfg(feature = "blocking-rustls-fips")]
165fn configured_tls_builder(
166    configuration: &BlockingClientBuilder,
167) -> Result<ClientBuilder, BuildError> {
168    let policy = configuration
169        .fips_tls_policy
170        .as_ref()
171        .ok_or(BuildError::FipsTlsPolicyRequired)?;
172    let config = fips_client_config(policy)?;
173    Ok(Client::builder().tls_backend_preconfigured(config))
174}
175
176#[cfg(feature = "blocking-rustls-fips")]
177fn fips_client_config(policy: &FipsTlsPolicy) -> Result<ClientConfig, BuildError> {
178    let provider = Arc::new(rustls::crypto::default_fips_provider());
179    validate_fips_provider(provider.as_ref())?;
180    let config = client_config_with_provider(provider, policy)?;
181    validate_fips_config(&config)?;
182    Ok(config)
183}
184
185#[cfg(feature = "blocking-rustls-fips")]
186fn client_config_with_provider(
187    provider: Arc<CryptoProvider>,
188    policy: &FipsTlsPolicy,
189) -> Result<ClientConfig, BuildError> {
190    let verifier = WebPkiServerVerifier::builder_with_provider(
191        Arc::clone(&policy.roots),
192        Arc::clone(&provider),
193    )
194    .with_crls(policy.crls.iter().cloned())
195    .enforce_revocation_expiration()
196    .build()
197    .map_err(|_| BuildError::FipsRevocationVerifierFailed)?;
198    Ok(ClientConfig::builder_with_provider(provider)
199        .with_safe_default_protocol_versions()
200        .map_err(|_| BuildError::FipsProtocolConfigurationFailed)?
201        .dangerous()
202        .with_custom_certificate_verifier(verifier)
203        .with_no_client_auth())
204}
205
206#[cfg(feature = "blocking-rustls-fips")]
207fn validate_fips_provider(provider: &CryptoProvider) -> Result<(), BuildError> {
208    if provider.fips() {
209        Ok(())
210    } else {
211        Err(BuildError::FipsProviderRejected)
212    }
213}
214
215#[cfg(feature = "blocking-rustls-fips")]
216fn validate_fips_config(config: &ClientConfig) -> Result<(), BuildError> {
217    if config.fips() {
218        Ok(())
219    } else {
220        Err(BuildError::FipsClientConfigurationRejected)
221    }
222}
223
224#[cfg(all(test, feature = "blocking-rustls-fips"))]
225pub(super) fn test_fips_configuration(policy: &FipsTlsPolicy) -> Result<bool, BuildError> {
226    fips_client_config(policy).map(|config| config.fips())
227}
228
229#[cfg(all(test, feature = "blocking-rustls-fips"))]
230fn non_fips_provider() -> CryptoProvider {
231    #[derive(Debug)]
232    struct NonFipsRandom;
233
234    impl rustls::crypto::SecureRandom for NonFipsRandom {
235        fn fill(&self, _buffer: &mut [u8]) -> Result<(), rustls::crypto::GetRandomFailed> {
236            Err(rustls::crypto::GetRandomFailed)
237        }
238    }
239
240    static NON_FIPS_RANDOM: NonFipsRandom = NonFipsRandom;
241    let mut provider = rustls::crypto::default_fips_provider();
242    provider.secure_random = &NON_FIPS_RANDOM;
243    provider
244}
245
246#[cfg(all(test, feature = "blocking-rustls-fips"))]
247pub(super) fn test_non_fips_rejection(policy: &FipsTlsPolicy) -> Result<bool, BuildError> {
248    let provider = non_fips_provider();
249    let provider_rejected =
250        validate_fips_provider(&provider) == Err(BuildError::FipsProviderRejected);
251    let config = client_config_with_provider(Arc::new(provider), policy)?;
252    let config_rejected =
253        validate_fips_config(&config) == Err(BuildError::FipsClientConfigurationRejected);
254    Ok(provider_rejected && config_rejected)
255}
256
257#[cfg(all(test, feature = "blocking-rustls-fips"))]
258pub(super) fn test_non_fips_global_independence(policy: &FipsTlsPolicy) -> bool {
259    let provider = non_fips_provider();
260    if provider.fips() || provider.install_default().is_err() {
261        return false;
262    }
263    let global_is_non_fips = CryptoProvider::get_default().is_some_and(|value| !value.fips());
264    global_is_non_fips && test_fips_configuration(policy) == Ok(true)
265}