Skip to main content

a2a_protocol_client/
tls.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Tom F.
3
4//! TLS connector via rustls.
5//!
6//! When the `tls-rustls` feature is enabled, this module provides HTTPS
7//! support using [`hyper_rustls`] with Mozilla root certificates. No OpenSSL
8//! system dependency is required.
9//!
10//! # Custom CA certificates
11//!
12//! For enterprise/internal PKI, use [`tls_config_with_extra_roots`] to create
13//! a [`rustls::ClientConfig`] with additional trust anchors, then pass it to
14//! the client builder.
15
16use http_body_util::Full;
17use hyper::body::Bytes;
18use hyper_util::client::legacy::connect::HttpConnector;
19use hyper_util::client::legacy::Client;
20use hyper_util::rt::TokioExecutor;
21use rustls::ClientConfig;
22
23/// Type alias for the HTTPS-capable hyper client.
24pub(crate) type HttpsClient = Client<hyper_rustls::HttpsConnector<HttpConnector>, Full<Bytes>>;
25
26/// Builds a default [`ClientConfig`] with Mozilla root certificates.
27///
28/// Uses TLS 1.2+ with ring as the crypto provider.
29#[must_use]
30pub fn default_tls_config() -> ClientConfig {
31    ClientConfig::builder()
32        .with_root_certificates(root_cert_store())
33        .with_no_client_auth()
34}
35
36/// Builds a [`ClientConfig`] with extra CA certificates added to the
37/// Mozilla root store.
38///
39/// Use this for enterprise environments with internal PKI. Returns
40/// the number of certificates that failed to load (if any).
41#[must_use]
42pub fn tls_config_with_extra_roots(
43    certs: Vec<rustls_pki_types::CertificateDer<'static>>,
44) -> ClientConfig {
45    let mut store = root_cert_store();
46    for cert in certs {
47        if let Err(_err) = store.add(cert) {
48            trace_warn!(error = %_err, "failed to add custom CA certificate to root store");
49        }
50    }
51    ClientConfig::builder()
52        .with_root_certificates(store)
53        .with_no_client_auth()
54}
55
56/// Returns a root certificate store populated with Mozilla's trusted roots.
57fn root_cert_store() -> rustls::RootCertStore {
58    let mut store = rustls::RootCertStore::empty();
59    store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
60    store
61}
62
63/// Builds an HTTPS-capable hyper client using the default TLS configuration.
64pub(crate) fn build_https_client() -> HttpsClient {
65    build_https_client_with_config(default_tls_config())
66}
67
68/// Builds an HTTPS-capable hyper client using a custom TLS configuration.
69pub(crate) fn build_https_client_with_config(tls_config: ClientConfig) -> HttpsClient {
70    let https = hyper_rustls::HttpsConnectorBuilder::new()
71        .with_tls_config(tls_config)
72        .https_or_http()
73        .enable_all_versions()
74        .build();
75
76    Client::builder(TokioExecutor::new()).build(https)
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn default_tls_config_creates_valid_config() {
85        // Verify the config builds without panicking and has a crypto
86        // provider (ring). The returned config is opaque, so we just
87        // verify construction succeeds.
88        let _config = default_tls_config();
89    }
90
91    #[test]
92    fn tls_config_with_extra_roots_handles_empty() {
93        let _config = tls_config_with_extra_roots(vec![]);
94    }
95
96    #[test]
97    fn build_https_client_creates_client() {
98        let _client = build_https_client();
99    }
100
101    #[test]
102    fn build_https_client_with_custom_config() {
103        let config = default_tls_config();
104        let _client = build_https_client_with_config(config);
105    }
106}