Skip to main content

canton_core/
config.rs

1//! Shared client connection configuration (the Option-B connection kernel).
2//!
3//! Endpoint, authentication, TLS, and retry live here so every gRPC client in
4//! the SDK (`canton-ledger`, `canton-admin`) builds its channel the same way.
5//! Authentication is decoupled from any concrete provider via the
6//! [`TokenSource`] trait — `canton-auth`'s token provider implements it, which
7//! keeps `canton-core` free of a `canton-auth` dependency (that would be a
8//! cycle) while letting [`Config`] carry any token source.
9
10use std::fmt;
11use std::future::Future;
12use std::pin::Pin;
13use std::sync::Arc;
14use std::time::Duration;
15
16use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint, Identity};
17
18use crate::retry::RetryConfig;
19use crate::{Error, Result};
20
21/// A source of bearer tokens for request authentication.
22///
23/// Implemented by `canton_auth::TokenProvider` (OIDC client-credentials with
24/// caching + refresh). Object-safe by design: [`Auth::Dynamic`] stores it as an
25/// `Arc<dyn TokenSource>`.
26pub trait TokenSource: Send + Sync + fmt::Debug {
27    /// Resolve the current bearer token (fetching/refreshing as needed), or
28    /// `None` for an unauthenticated call.
29    fn fetch_bearer(&self) -> Pin<Box<dyn Future<Output = Result<Option<String>>> + Send + '_>>;
30}
31
32/// How the client authenticates each request.
33///
34/// `#[non_exhaustive]` so new auth modes can be added without a breaking change;
35/// construct via [`Config::with_token`] / [`Config::with_oidc`] (or match with a
36/// wildcard arm).
37#[derive(Clone)]
38#[non_exhaustive]
39pub enum Auth {
40    /// No authentication (unauthenticated endpoints, or shared-secret off).
41    None,
42    /// A fixed bearer token supplied by the caller.
43    Static(String),
44    /// A dynamic token source (e.g. OIDC client-credentials with auto-refresh).
45    Dynamic(Arc<dyn TokenSource>),
46}
47
48impl Auth {
49    /// Resolve the current bearer token, if any.
50    ///
51    /// # Errors
52    /// Propagates any error from the underlying [`TokenSource`].
53    pub async fn bearer(&self) -> Result<Option<String>> {
54        match self {
55            Auth::None => Ok(None),
56            Auth::Static(token) => Ok(Some(token.clone())),
57            Auth::Dynamic(source) => source.fetch_bearer().await,
58        }
59    }
60}
61
62impl fmt::Debug for Auth {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        match self {
65            Auth::None => f.write_str("None"),
66            Auth::Static(_) => f.write_str("Static(<redacted>)"),
67            Auth::Dynamic(source) => write!(f, "Dynamic({source:?})"),
68        }
69    }
70}
71
72/// TLS settings for the gRPC channel.
73///
74/// An empty `TlsConfig` (from [`TlsConfig::new`]) enables server-side TLS using
75/// the platform's native root certificates. Add a custom CA for private/self-
76/// signed servers, a domain-name override for SNI/verification, and a client
77/// identity for mutual TLS. `#[non_exhaustive]`.
78#[derive(Clone, Debug, Default)]
79#[non_exhaustive]
80pub struct TlsConfig {
81    /// Custom CA certificate chain (PEM). When set, replaces the native roots
82    /// (use for self-signed / private CAs).
83    pub ca_certificate_pem: Option<Vec<u8>>,
84    /// Domain name to verify the server certificate against (SNI). Defaults to
85    /// the endpoint host.
86    pub domain_name: Option<String>,
87    /// Client identity `(certificate_pem, private_key_pem)` for mutual TLS.
88    pub client_identity_pem: Option<(Vec<u8>, Vec<u8>)>,
89}
90
91impl TlsConfig {
92    /// Server-side TLS using the platform's native root certificates.
93    #[must_use]
94    pub fn new() -> Self {
95        Self::default()
96    }
97
98    /// Trust this PEM CA certificate (chain) instead of the native roots.
99    #[must_use]
100    pub fn with_ca_certificate(mut self, ca_pem: impl Into<Vec<u8>>) -> Self {
101        self.ca_certificate_pem = Some(ca_pem.into());
102        self
103    }
104
105    /// Override the domain name the server certificate is verified against.
106    #[must_use]
107    pub fn with_domain_name(mut self, domain: impl Into<String>) -> Self {
108        self.domain_name = Some(domain.into());
109        self
110    }
111
112    /// Present a client identity (mutual TLS): `(certificate_pem, key_pem)`.
113    #[must_use]
114    pub fn with_client_identity(
115        mut self,
116        certificate_pem: impl Into<Vec<u8>>,
117        private_key_pem: impl Into<Vec<u8>>,
118    ) -> Self {
119        self.client_identity_pem = Some((certificate_pem.into(), private_key_pem.into()));
120        self
121    }
122}
123
124/// Build a `ClientTlsConfig` from a [`TlsConfig`] (or native roots when TLS is
125/// implicit for an `https` endpoint).
126fn build_tls(tls: Option<&TlsConfig>) -> ClientTlsConfig {
127    let mut config = ClientTlsConfig::new();
128    match tls.and_then(|t| t.ca_certificate_pem.as_ref()) {
129        Some(ca) => config = config.ca_certificate(Certificate::from_pem(ca.clone())),
130        None => config = config.with_native_roots(),
131    }
132    if let Some(domain) = tls.and_then(|t| t.domain_name.as_ref()) {
133        config = config.domain_name(domain.clone());
134    }
135    if let Some((cert, key)) = tls.and_then(|t| t.client_identity_pem.as_ref()) {
136        config = config.identity(Identity::from_pem(cert.clone(), key.clone()));
137    }
138    config
139}
140
141/// Configuration for an SDK gRPC client (shared by `canton-ledger` and
142/// `canton-admin`).
143#[derive(Clone, Debug)]
144pub struct Config {
145    endpoint: String,
146    auth: Auth,
147    retry: Option<RetryConfig>,
148    tls: Option<TlsConfig>,
149    timeout: Option<Duration>,
150}
151
152impl Config {
153    /// Create a configuration targeting `endpoint`, with no authentication and
154    /// no retrying.
155    pub fn new(endpoint: impl Into<String>) -> Self {
156        Self {
157            endpoint: endpoint.into(),
158            auth: Auth::None,
159            retry: None,
160            tls: None,
161            timeout: None,
162        }
163    }
164
165    /// Authenticate with a fixed bearer token.
166    #[must_use]
167    pub fn with_token(mut self, token: impl Into<String>) -> Self {
168        self.auth = Auth::Static(token.into());
169        self
170    }
171
172    /// Authenticate with a dynamic token source (e.g. an OIDC provider with
173    /// client-credentials and auto-refresh).
174    #[must_use]
175    pub fn with_oidc<T: TokenSource + 'static>(mut self, provider: T) -> Self {
176        self.auth = Auth::Dynamic(Arc::new(provider));
177        self
178    }
179
180    /// Enable retrying of unary calls on retriable errors, per `retry`.
181    #[must_use]
182    pub fn with_retry(mut self, retry: RetryConfig) -> Self {
183        self.retry = Some(retry);
184        self
185    }
186
187    /// Use TLS for the gRPC channel (server-side and, if configured, mutual).
188    #[must_use]
189    pub fn with_tls(mut self, tls: TlsConfig) -> Self {
190        self.tls = Some(tls);
191        self
192    }
193
194    /// Set the per-request timeout on the gRPC channel (default 30s). Long
195    /// `submitAndWait` calls under load may need a higher bound.
196    #[must_use]
197    pub fn with_timeout(mut self, timeout: Duration) -> Self {
198        self.timeout = Some(timeout);
199        self
200    }
201
202    /// The gRPC endpoint of the target service, e.g. `http://localhost:3901`.
203    #[must_use]
204    pub fn endpoint(&self) -> &str {
205        &self.endpoint
206    }
207
208    /// The configured authentication mode.
209    #[must_use]
210    pub fn auth(&self) -> &Auth {
211        &self.auth
212    }
213
214    /// The configured retry policy, if any.
215    #[must_use]
216    pub fn retry(&self) -> Option<&RetryConfig> {
217        self.retry.as_ref()
218    }
219
220    /// Build a lazily-connected gRPC [`Channel`] for this configuration.
221    ///
222    /// Returns immediately; the TCP/TLS handshake happens on the first RPC. TLS
223    /// is applied when [`Config::with_tls`] was set or the endpoint is `https`.
224    ///
225    /// # Errors
226    /// Returns [`Error::InvalidRequest`] if the endpoint URI or the TLS
227    /// configuration is invalid.
228    pub fn connect_channel(&self) -> Result<Channel> {
229        let mut endpoint = Endpoint::from_shared(self.endpoint.clone())
230            .map_err(|e| {
231                Error::InvalidRequest(format!("invalid endpoint uri {:?}: {e}", self.endpoint))
232            })?
233            .timeout(self.timeout.unwrap_or(Duration::from_secs(30)))
234            .connect_timeout(Duration::from_secs(10))
235            .http2_keep_alive_interval(Duration::from_secs(30))
236            .keep_alive_timeout(Duration::from_secs(20))
237            .keep_alive_while_idle(true)
238            .tcp_keepalive(Some(Duration::from_secs(60)))
239            .tcp_nodelay(true);
240
241        if self.tls.is_some() || self.endpoint.starts_with("https") {
242            endpoint = endpoint
243                .tls_config(build_tls(self.tls.as_ref()))
244                .map_err(|e| Error::InvalidRequest(format!("invalid TLS config: {e}")))?;
245        }
246
247        Ok(endpoint.connect_lazy())
248    }
249}