From 68cef0019642a47b0102f08abe0e67f72cee80ef Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Sun, 28 Jul 2024 11:02:36 -0500 Subject: [PATCH] feat: add optional `CryptoProvider` to the client `Config` It adds a new field to the client `Config, expecting the `CryptoProvider` from the user. It uses aws-lc-rs or ring providers by default if any of these features are enabled. It's based on the suggestion comment at #135, reference: https://github.com/bitcoindevkit/rust-electrum-client/pull/135#issuecomment-2244180324 --- src/client.rs | 26 ++++++++++++++ src/config.rs | 58 +++++++++++++++++++++++++++++++ src/raw_client.rs | 88 +++++++++++++++++++++++++++++++++++++++++++---- src/types.rs | 5 +++ 4 files changed, 170 insertions(+), 7 deletions(-) diff --git a/src/client.rs b/src/client.rs index 81cbd38..98bf7c3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -110,13 +110,39 @@ impl ClientType { pub fn from_config(url: &str, config: &Config) -> Result { if url.starts_with("ssl://") { let url = url.replacen("ssl://", "", 1); + #[cfg(all( + any( + feature = "default", + feature = "use-rustls", + feature = "use-rustls-ring" + ), + not(feature = "use-openssl") + ))] let client = match config.socks5() { Some(socks5) => RawClient::new_proxy_ssl( url.as_str(), config.validate_domain(), socks5, config.timeout(), + config.crypto_provider(), )?, + None => RawClient::new_ssl( + url.as_str(), + config.validate_domain(), + config.timeout(), + config.crypto_provider(), + )?, + }; + + #[cfg(feature = "openssl")] + let client = match config.socks5() { + Some(socks5) => RawClient::new_proxy_ssl( + url.as_str(), + config.validate_domain(), + socks5, + config.timeout(), + )?, + None => { RawClient::new_ssl(url.as_str(), config.validate_domain(), config.timeout())? } diff --git a/src/config.rs b/src/config.rs index 47da1f9..0ab962d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,15 @@ use std::time::Duration; +#[cfg(all( + any( + feature = "default", + feature = "use-rustls", + feature = "use-rustls-ring" + ), + not(feature = "use-openssl") +))] +use rustls::crypto::CryptoProvider; + /// Configuration for an electrum client /// /// Refer to [`Client::from_config`] and [`ClientType::from_config`]. @@ -12,6 +22,16 @@ pub struct Config { socks5: Option, /// timeout in seconds, default None (depends on TcpStream default) timeout: Option, + /// An optional [`CryptoProvider`] for users that don't want either default `aws-lc-rs` or `ring` providers + #[cfg(all( + any( + feature = "default", + feature = "use-rustls", + feature = "use-rustls-ring" + ), + not(feature = "use-openssl") + ))] + crypto_provider: Option, /// number of retry if any error, default 1 retry: u8, /// when ssl, validate the domain, default true @@ -60,6 +80,20 @@ impl ConfigBuilder { self } + /// Sets the custom [`CryptoProvider`]. + #[cfg(all( + any( + feature = "default", + feature = "use-rustls", + feature = "use-rustls-ring" + ), + not(feature = "use-openssl") + ))] + pub fn crypto_provider(mut self, crypto_provider: Option) -> Self { + self.config.crypto_provider = crypto_provider; + self + } + /// Sets the retry attempts number pub fn retry(mut self, retry: u8) -> Self { self.config.retry = retry; @@ -135,6 +169,21 @@ impl Config { pub fn builder() -> ConfigBuilder { ConfigBuilder::new() } + + /// Get the configuration for `crypto_provider` + /// + /// Set this with [`ConfigBuilder::crypto_provider`] + #[cfg(all( + any( + feature = "default", + feature = "use-rustls", + feature = "use-rustls-ring" + ), + not(feature = "use-openssl") + ))] + pub fn crypto_provider(&self) -> Option<&CryptoProvider> { + self.crypto_provider.as_ref() + } } impl Default for Config { @@ -144,6 +193,15 @@ impl Default for Config { timeout: None, retry: 1, validate_domain: true, + #[cfg(all( + any( + feature = "default", + feature = "use-rustls", + feature = "use-rustls-ring" + ), + not(feature = "use-openssl") + ))] + crypto_provider: None, } } } diff --git a/src/raw_client.rs b/src/raw_client.rs index 1b83c73..814e085 100644 --- a/src/raw_client.rs +++ b/src/raw_client.rs @@ -31,6 +31,7 @@ use openssl::ssl::{SslConnector, SslMethod, SslStream, SslVerifyMode}; not(feature = "use-openssl") ))] use rustls::{ + crypto::CryptoProvider, pki_types::ServerName, pki_types::{Der, TrustAnchor}, ClientConfig, ClientConnection, RootCertStore, StreamOwned, @@ -368,7 +369,13 @@ impl RawClient { socket_addrs: A, validate_domain: bool, timeout: Option, + crypto_provider: Option<&CryptoProvider>, ) -> Result { + #[cfg(feature = "use-rustls")] + use rustls::crypto::aws_lc_rs::default_provider; + #[cfg(feature = "use-rustls-ring")] + use rustls::crypto::ring::default_provider; + debug!( "new_ssl socket_addrs.domain():{:?} validate_domain:{} timeout:{:?}", socket_addrs.domain(), @@ -378,16 +385,28 @@ impl RawClient { if validate_domain { socket_addrs.domain().ok_or(Error::MissingDomain)?; } + + let crypto_provider = match crypto_provider { + Some(provider) => provider.to_owned(), + + // TODO: (@leonardo) It should use the proper default of each provider. + #[cfg(feature = "use-rustls")] + None => default_provider(), + + #[cfg(feature = "use-rustls-ring")] + None => default_provider(), + }; + match timeout { Some(timeout) => { let stream = connect_with_total_timeout(socket_addrs.clone(), timeout)?; stream.set_read_timeout(Some(timeout))?; stream.set_write_timeout(Some(timeout))?; - Self::new_ssl_from_stream(socket_addrs, validate_domain, stream) + Self::new_ssl_from_stream(socket_addrs, validate_domain, stream, crypto_provider) } None => { let stream = TcpStream::connect(socket_addrs.clone())?; - Self::new_ssl_from_stream(socket_addrs, validate_domain, stream) + Self::new_ssl_from_stream(socket_addrs, validate_domain, stream, crypto_provider) } } } @@ -397,10 +416,13 @@ impl RawClient { socket_addr: A, validate_domain: bool, tcp_stream: TcpStream, + crypto_provider: CryptoProvider, ) -> Result { use std::convert::TryFrom; - let builder = ClientConfig::builder(); + let builder = ClientConfig::builder_with_provider(crypto_provider.into()) + .with_safe_default_protocol_versions() + .map_err(|e| Error::CouldNotBuildWithSafeDefaultVersion(e))?; let config = if validate_domain { socket_addr.domain().ok_or(Error::MissingDomain)?; @@ -467,14 +489,66 @@ impl RawClient { Ok(stream.into()) } - #[cfg(any( - feature = "use-openssl", - feature = "use-rustls", - feature = "use-rustls-ring" + #[cfg(all( + any( + feature = "default", + feature = "use-rustls", + feature = "use-rustls-ring" + ), + not(feature = "use-openssl") ))] /// Creates a new TLS client that connects to `target_addr` using `proxy_addr` as a socks proxy /// server. The DNS resolution of `target_addr`, if required, is done through the proxy. This /// allows to specify, for instance, `.onion` addresses. + pub fn new_proxy_ssl( + target_addr: T, + validate_domain: bool, + proxy: &crate::Socks5Config, + timeout: Option, + crypto_provider: Option<&CryptoProvider>, + ) -> Result, Error> { + #[cfg(feature = "use-rustls")] + use rustls::crypto::aws_lc_rs::default_provider; + #[cfg(feature = "use-rustls-ring")] + use rustls::crypto::ring::default_provider; + + let target = target_addr.to_target_addr()?; + + let mut stream = match proxy.credentials.as_ref() { + Some(cred) => Socks5Stream::connect_with_password( + &proxy.addr, + target_addr, + &cred.username, + &cred.password, + timeout, + )?, + None => Socks5Stream::connect(&proxy.addr, target.clone(), timeout)?, + }; + stream.get_mut().set_read_timeout(timeout)?; + stream.get_mut().set_write_timeout(timeout)?; + + let crypto_provider = match crypto_provider { + Some(provider) => provider.to_owned(), + + #[cfg(feature = "use-rustls")] + None => default_provider(), + + #[cfg(feature = "use-rustls-ring")] + None => default_provider(), + }; + + RawClient::new_ssl_from_stream( + target, + validate_domain, + stream.into_inner(), + crypto_provider, + ) + } + + #[cfg(feature = "use-openssl")] + /// Creates a new TLS client that connects to `target_addr` using `proxy_addr` as a socks proxy + /// server. The DNS resolution of `target_addr`, if required, is done through the proxy. This + /// allows to specify, for instance, `.onion` addresses. pub fn new_proxy_ssl( target_addr: T, validate_domain: bool, diff --git a/src/types.rs b/src/types.rs index 6ecd692..382fca5 100644 --- a/src/types.rs +++ b/src/types.rs @@ -318,6 +318,9 @@ pub enum Error { #[cfg(any(feature = "use-rustls", feature = "use-rustls-ring"))] /// Could not create a rustls client connection CouldNotCreateConnection(rustls::Error), + #[cfg(any(feature = "use-rustls", feature = "use-rustls-ring"))] + /// Could not create the `ClientConfig` with safe default protocol version + CouldNotBuildWithSafeDefaultVersion(rustls::Error), #[cfg(feature = "use-openssl")] /// Invalid OpenSSL method used @@ -365,6 +368,8 @@ impl Display for Error { Error::MissingDomain => f.write_str("Missing domain while it was explicitly asked to validate it"), Error::CouldntLockReader => f.write_str("Couldn't take a lock on the reader mutex. This means that there's already another reader thread is running"), Error::Mpsc => f.write_str("Broken IPC communication channel: the other thread probably has exited"), + #[cfg(any(feature = "use-rustls", feature = "use-rustls-ring"))] + Error::CouldNotBuildWithSafeDefaultVersion(_) => f.write_str("Couldn't build the `ClientConfig` with safe default version"), } } }