deribit_websocket/tls.rs
1//! TLS backend selection and `rustls` crypto-provider installation.
2//!
3//! `deribit-websocket` supports three mutually-exclusive TLS backends,
4//! selected at compile time via Cargo features (see this crate's
5//! `Cargo.toml` or the README):
6//!
7//! - `rustls-aws-lc` (default) — `rustls` with the `aws-lc-rs` crypto
8//! provider, OS-native root store.
9//! - `rustls-ring` — `rustls` with the `ring` crypto provider, OS-native
10//! root store.
11//! - `native-tls` — OS-native TLS stack (SChannel / SecureTransport /
12//! OpenSSL); no `rustls` dependency is pulled in.
13//!
14//! Exactly one of the three must be active. The [`compile_error!`]
15//! gates below reject any other combination — zero or two or three — at
16//! compile time so misconfigured builds fail fast rather than at
17//! runtime during the first WebSocket handshake.
18//!
19//! # Crypto provider
20//!
21//! The two `rustls-*` backends rely on `rustls`'s process-global crypto
22//! provider slot being populated before any TLS handshake starts.
23//! Applications must call [`install_default_crypto_provider`] once at
24//! startup; the helper picks the right provider for the active feature
25//! and is a no-op under `native-tls`.
26
27// ---------------------------------------------------------------------
28// Compile-time mutex — exactly one TLS backend.
29// ---------------------------------------------------------------------
30
31#[cfg(not(any(
32 feature = "rustls-aws-lc",
33 feature = "rustls-ring",
34 feature = "native-tls"
35)))]
36compile_error!(
37 "deribit-websocket: select one of the TLS backends via Cargo features: \
38 `rustls-aws-lc`, `rustls-ring`, or `native-tls`"
39);
40
41#[cfg(any(
42 all(feature = "rustls-aws-lc", feature = "rustls-ring"),
43 all(feature = "rustls-aws-lc", feature = "native-tls"),
44 all(feature = "rustls-ring", feature = "native-tls"),
45))]
46compile_error!(
47 "deribit-websocket: select exactly one TLS backend; the features \
48 `rustls-aws-lc`, `rustls-ring`, and `native-tls` are mutually exclusive"
49);
50
51// ---------------------------------------------------------------------
52// Public API — crypto-provider installation.
53// ---------------------------------------------------------------------
54
55/// Errors raised when installing the `rustls` crypto provider.
56///
57/// Separate from [`crate::error::WebSocketError`] because this is a
58/// one-shot startup concern distinct from runtime protocol errors;
59/// folding it into `WebSocketError` would bloat the main enum for a
60/// call that runs once per process.
61#[derive(Debug, thiserror::Error)]
62pub enum CryptoProviderError {
63 /// A `rustls` crypto provider is already installed in this process.
64 ///
65 /// `rustls` stores its crypto provider in a process-global
66 /// `OnceCell`-style slot; subsequent `install_default` calls are
67 /// rejected. This variant carries no payload because the already-
68 /// installed provider is rarely actionable from the caller, and
69 /// exposing an `Arc<CryptoProvider>` here would leak a `rustls`
70 /// type into callers that are compiled under `native-tls`.
71 #[error("a rustls crypto provider is already installed in this process")]
72 AlreadyInstalled,
73}
74
75/// Install the process-global `rustls` crypto provider matching the
76/// active TLS feature.
77///
78/// - Under `rustls-aws-lc`, installs
79/// `rustls::crypto::aws_lc_rs::default_provider()`.
80/// - Under `rustls-ring`, installs
81/// `rustls::crypto::ring::default_provider()`.
82/// - Under `native-tls`, this is a no-op that returns `Ok(())` — the
83/// OS TLS stack does not require any process-level initialization.
84///
85/// Call this exactly once at application startup, before any call to
86/// [`crate::client::DeribitWebSocketClient::connect`]. Subsequent calls
87/// return [`CryptoProviderError::AlreadyInstalled`] rather than panic, which
88/// lets callers be robust against multiple entry points (tests, libs,
89/// `main`) all trying to initialize the provider.
90///
91/// # Errors
92///
93/// Returns [`CryptoProviderError::AlreadyInstalled`] if a provider is
94/// already installed in this process by this call, a previous call, or
95/// any other library that uses `rustls`.
96///
97/// # Examples
98///
99/// ```no_run
100/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
101/// // Idempotent: subsequent calls return AlreadyInstalled but are
102/// // otherwise safe.
103/// let _ = deribit_websocket::install_default_crypto_provider();
104/// # Ok(())
105/// # }
106/// ```
107// `Result` already carries `#[must_use]`, so an extra attribute on the
108// function itself would be redundant (see `clippy::double_must_use`).
109#[inline(never)]
110// The explicit `return`s below are load-bearing: without them the
111// function body would have multiple `#[cfg]`-gated tail expressions and
112// fail to type-check when more than one TLS feature is active — the
113// very case the `compile_error!` mutex above is meant to reject
114// cleanly. Allowing `needless_return` keeps the diagnostic limited to
115// the mutex message alone.
116#[allow(clippy::needless_return)]
117pub fn install_default_crypto_provider() -> Result<(), CryptoProviderError> {
118 // The compile-time mutex above guarantees exactly one of the three
119 // branches below is live; the priority order (aws-lc > ring >
120 // native-tls) only matters when the mutex has already triggered —
121 // making each branch mutually exclusive via `not(...)` guards keeps
122 // the function body type-correct under any feature combination so
123 // the build output contains only the mutex diagnostic, not
124 // cascading `E0308` noise.
125 #[cfg(feature = "rustls-aws-lc")]
126 {
127 return rustls::crypto::aws_lc_rs::default_provider()
128 .install_default()
129 .map_err(|_| CryptoProviderError::AlreadyInstalled);
130 }
131 #[cfg(all(feature = "rustls-ring", not(feature = "rustls-aws-lc")))]
132 {
133 return rustls::crypto::ring::default_provider()
134 .install_default()
135 .map_err(|_| CryptoProviderError::AlreadyInstalled);
136 }
137 #[cfg(all(
138 feature = "native-tls",
139 not(feature = "rustls-aws-lc"),
140 not(feature = "rustls-ring")
141 ))]
142 {
143 return Ok(());
144 }
145
146 // Fallback for the "no TLS feature selected" case: the
147 // `compile_error!` at the top of this module has already fired, so
148 // this path is unreachable in any valid build. Returning a typed
149 // `Ok(())` keeps `rustc` from piling a secondary `E0308` on top of
150 // the real diagnostic.
151 #[cfg(not(any(
152 feature = "rustls-aws-lc",
153 feature = "rustls-ring",
154 feature = "native-tls"
155 )))]
156 #[allow(unreachable_code)]
157 Ok(())
158}
159
160// ---------------------------------------------------------------------
161// Tests
162// ---------------------------------------------------------------------
163
164#[cfg(test)]
165mod tests {
166 use super::install_default_crypto_provider;
167
168 #[cfg(any(feature = "rustls-aws-lc", feature = "rustls-ring"))]
169 use super::CryptoProviderError;
170
171 /// Under `native-tls` the helper is a no-op and always succeeds.
172 /// Under the `rustls-*` backends the *first* install may race with
173 /// other tests, so we don't assert its result; instead we
174 /// guarantee that a *second* call deterministically returns
175 /// `AlreadyInstalled`.
176 #[test]
177 fn second_install_is_deterministic() {
178 // Prime the slot. Outcome does not matter — another test in the
179 // same process may have installed already.
180 let _ = install_default_crypto_provider();
181
182 #[cfg(any(feature = "rustls-aws-lc", feature = "rustls-ring"))]
183 {
184 match install_default_crypto_provider() {
185 Err(CryptoProviderError::AlreadyInstalled) => {}
186 other => panic!("expected AlreadyInstalled, got {other:?}"),
187 }
188 }
189
190 #[cfg(feature = "native-tls")]
191 {
192 // Under native-tls every call is Ok(()).
193 match install_default_crypto_provider() {
194 Ok(()) => {}
195 other => panic!("expected Ok under native-tls, got {other:?}"),
196 }
197 }
198 }
199}