diff --git a/Cargo.lock b/Cargo.lock index f1d6c039..d468a08b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1493,7 +1493,7 @@ dependencies = [ "byteorder", "libc", "log", - "rustls 0.23.22", + "rustls 0.23.27", "serde", "serde_json", "webpki-roots", @@ -4676,15 +4676,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.22" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.102.8", + "rustls-webpki 0.103.3", "subtle", "zeroize", ] @@ -4700,9 +4700,12 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] [[package]] name = "rustls-webpki" @@ -4716,9 +4719,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ "ring", "rustls-pki-types", diff --git a/contrib/lianad_config_example.toml b/contrib/lianad_config_example.toml index 35b4b256..ba23b56e 100644 --- a/contrib/lianad_config_example.toml +++ b/contrib/lianad_config_example.toml @@ -50,8 +50,13 @@ poll_interval_secs = 30 # In order to connect, it needs the address as a string, which can be # optionally prefixed with "ssl://" or "tcp://". If omitted, "tcp://" # will be assumed. +# `validate_domain` field is optional: used in case of SSL connection, +# if set to `false`, internal electrum client will not try to validate +# the domain associated to the certificate: it's useful in case of +# self-signed certificate. Its default value is `true`. # [electrum_config] # addr = "127.0.0.1:50001" +# validate_domain = false # # [bitcoind_config] diff --git a/liana-gui/src/app/state/settings/bitcoind.rs b/liana-gui/src/app/state/settings/bitcoind.rs index f4b5c8ce..5a8fe585 100644 --- a/liana-gui/src/app/state/settings/bitcoind.rs +++ b/liana-gui/src/app/state/settings/bitcoind.rs @@ -326,6 +326,7 @@ impl BitcoindSettings { } } } + view::SettingsEditMessage::ValidateDomainEdited(_) => {} view::SettingsEditMessage::BitcoindRpcAuthTypeSelected(auth_type) => { if !self.processing { self.selected_auth_type = auth_type; @@ -465,6 +466,7 @@ impl ElectrumSettings { daemon_config.bitcoin_backend = Some(lianad::config::BitcoinBackend::Electrum(ElectrumConfig { addr: self.addr.value.clone(), + validate_domain: self.electrum_config.validate_domain, })); self.processing = true; return Task::perform(async move { daemon_config }, |cfg| { @@ -473,6 +475,11 @@ impl ElectrumSettings { } } view::SettingsEditMessage::Clipboard(text) => return clipboard::write(text), + view::SettingsEditMessage::ValidateDomainEdited(b) => { + if !self.processing { + self.electrum_config.validate_domain = b; + } + } _ => {} }; Task::none() @@ -487,6 +494,7 @@ impl ElectrumSettings { cache.blockheight, &self.addr, self.processing, + self.electrum_config.validate_domain, ) } else { view::settings::electrum( diff --git a/liana-gui/src/app/view/message.rs b/liana-gui/src/app/view/message.rs index c2641c24..8aa88273 100644 --- a/liana-gui/src/app/view/message.rs +++ b/liana-gui/src/app/view/message.rs @@ -113,6 +113,7 @@ pub enum RemoteBackendSettingsMessage { pub enum SettingsEditMessage { Select, FieldEdited(&'static str, String), + ValidateDomainEdited(bool), BitcoindRpcAuthTypeSelected(RpcAuthType), Cancel, Confirm, diff --git a/liana-gui/src/app/view/settings.rs b/liana-gui/src/app/view/settings.rs index 932bd3e1..90131539 100644 --- a/liana-gui/src/app/view/settings.rs +++ b/liana-gui/src/app/view/settings.rs @@ -35,7 +35,7 @@ use crate::{ hw::HardwareWallet, node::{ bitcoind::{RpcAuthType, RpcAuthValues}, - electrum, + electrum::{self, validate_domain_checkbox}, }, }; @@ -626,6 +626,7 @@ pub fn electrum_edit<'a>( blockheight: i32, addr: &form::Value, processing: bool, + validate_domain: bool, ) -> Element<'a, SettingsEditMessage> { let mut col = Column::new().spacing(20); if is_configured_node_type && blockheight != 0 { @@ -658,6 +659,9 @@ pub fn electrum_edit<'a>( .push(separation().width(Length::Fill)); } + let checkbox = validate_domain_checkbox(addr, validate_domain, |b| { + SettingsEditMessage::ValidateDomainEdited(b) + }); col = col.push( Column::new() .push(text("Address:").bold().small()) @@ -669,6 +673,7 @@ pub fn electrum_edit<'a>( .size(P1_SIZE) .padding(5), ) + .push_maybe(checkbox) .push(text(electrum::ADDRESS_NOTES).size(P2_SIZE)) .spacing(5), ); diff --git a/liana-gui/src/installer/message.rs b/liana-gui/src/installer/message.rs index b4ffb57b..5be74a0f 100644 --- a/liana-gui/src/installer/message.rs +++ b/liana-gui/src/installer/message.rs @@ -126,6 +126,7 @@ pub enum DefineBitcoind { #[derive(Debug, Clone)] pub enum DefineElectrum { ConfigFieldEdited(electrum::ConfigField, String), + ValidDomainChanged(bool), } #[derive(Debug, Clone)] diff --git a/liana-gui/src/installer/step/node/electrum.rs b/liana-gui/src/installer/step/node/electrum.rs index 5978e2f1..6c412e03 100644 --- a/liana-gui/src/installer/step/node/electrum.rs +++ b/liana-gui/src/installer/step/node/electrum.rs @@ -14,9 +14,19 @@ use crate::{ node::electrum::ConfigField, }; -#[derive(Clone, Default)] +#[derive(Clone)] pub struct DefineElectrum { address: form::Value, + validate_domain: bool, +} + +impl Default for DefineElectrum { + fn default() -> Self { + Self { + address: Default::default(), + validate_domain: true, + } + } } impl DefineElectrum { @@ -38,6 +48,7 @@ impl DefineElectrum { crate::node::electrum::is_electrum_address_valid(&value); } }, + message::DefineElectrum::ValidDomainChanged(v) => self.validate_domain = v, }; }; Task::none() @@ -47,6 +58,7 @@ impl DefineElectrum { if self.can_try_ping() { ctx.bitcoin_backend = Some(lianad::config::BitcoinBackend::Electrum(ElectrumConfig { addr: self.address.value.clone(), + validate_domain: self.validate_domain, })); return true; } @@ -54,12 +66,15 @@ impl DefineElectrum { } pub fn view(&self) -> Element { - view::define_electrum(&self.address) + view::define_electrum(&self.address, self.validate_domain) } pub fn ping(&self) -> Result<(), Error> { let builder = electrum_client::Config::builder(); - let config = builder.timeout(Some(3)).build(); + let config = builder + .timeout(Some(3)) + .validate_domain(self.validate_domain) + .build(); let client = electrum_client::Client::from_config(&self.address.value, config) .map_err(|e| Error::Electrum(e.to_string()))?; client diff --git a/liana-gui/src/installer/view/mod.rs b/liana-gui/src/installer/view/mod.rs index 22388102..86dbf353 100644 --- a/liana-gui/src/installer/view/mod.rs +++ b/liana-gui/src/installer/view/mod.rs @@ -31,6 +31,7 @@ use liana_ui::{ widget::*, }; +use crate::node::electrum::validate_domain_checkbox; use crate::{ app::settings, hw::{is_compatible_with_tapminiscript, HardwareWallet, UnsupportedReason}, @@ -1246,7 +1247,15 @@ pub fn define_bitcoind<'a>( .into() } -pub fn define_electrum<'a>(address: &form::Value) -> Element<'a, Message> { +pub fn define_electrum<'a>( + address: &form::Value, + validate_domain: bool, +) -> Element<'a, Message> { + let checkbox = validate_domain_checkbox(address, validate_domain, |b| { + Message::DefineNode(DefineNode::DefineElectrum( + message::DefineElectrum::ValidDomainChanged(b), + )) + }); let col_address = Column::new() .push(text("Address:").bold()) .push( @@ -1262,7 +1271,8 @@ pub fn define_electrum<'a>(address: &form::Value) -> Element<'a, Message .size(text::P1_SIZE) .padding(10), ) - .push(text(electrum::ADDRESS_NOTES).size(text::P2_SIZE)) + .push_maybe(checkbox) + .push(text(electrum::ADDRESS_NOTES)) .spacing(10); Column::new().push(col_address).spacing(50).into() diff --git a/liana-gui/src/node/electrum.rs b/liana-gui/src/node/electrum.rs index 89886f9a..db55ee9f 100644 --- a/liana-gui/src/node/electrum.rs +++ b/liana-gui/src/node/electrum.rs @@ -1,13 +1,18 @@ use std::fmt; +use iced::{widget::checkbox, Element, Renderer}; +use liana_ui::{component::form, theme::Theme}; + #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum ConfigField { Address, } pub const ADDRESS_NOTES: &str = "Note: include \"ssl://\" as a prefix \ - for SSL connections. Be aware that self-signed \ - SSL certificates are currently not supported."; + for SSL connections."; + +pub const VALID_SSL_DOMAIN_NOTES: &str = "Do not validate SSL Domain \ + (check this only if you want to use a self-signed certificate)"; impl fmt::Display for ConfigField { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -17,6 +22,27 @@ impl fmt::Display for ConfigField { } } +pub fn validate_domain_checkbox<'a, F, M>( + addr: &form::Value, + value: bool, + closure: F, +) -> Option> +where + F: 'a + Fn(bool) -> M, + M: 'a, +{ + let checkbox = checkbox(VALID_SSL_DOMAIN_NOTES, !value).on_toggle(move |b| closure(!b)); + if addr.valid && is_ssl(&addr.value) { + Some(checkbox.into()) + } else { + None + } +} + +pub fn is_ssl(value: &str) -> bool { + value.starts_with("ssl://") +} + pub fn is_electrum_address_valid(value: &str) -> bool { let value_noprefix = if value.starts_with("ssl://") { value.replacen("ssl://", "", 1) diff --git a/lianad/Cargo.toml b/lianad/Cargo.toml index 1675a0e9..b2db1a53 100644 --- a/lianad/Cargo.toml +++ b/lianad/Cargo.toml @@ -26,6 +26,7 @@ liana = { path = "../liana" } miniscript = { version = "12.0", features = ["serde", "compiler", "base64"] } # For Electrum backend. + # We use a branch of the v1.0.0-alpha.13 tag, which corresponds to the bdk_electrum v0.15 crate, # in order to bump the electrum_client dependency to v0.21. This allows to use the "use-rustls-ring" # feature and avoid the default aws-ls-rs provider from rustls, which would break the reproducible build diff --git a/lianad/src/bitcoin/electrum/client.rs b/lianad/src/bitcoin/electrum/client.rs index bd1a3dd5..74cb2a4f 100644 --- a/lianad/src/bitcoin/electrum/client.rs +++ b/lianad/src/bitcoin/electrum/client.rs @@ -56,9 +56,13 @@ impl Client { /// Create a new client and perform sanity checks. pub fn new(electrum_config: &config::ElectrumConfig) -> Result { // First use a dummy config to check connectivity (no retries, short timeout). - let dummy_config = Config::builder().retry(0).timeout(Some(3)).build(); + let dummy_config = Config::builder() + .retry(0) + .validate_domain(electrum_config.validate_domain) + .timeout(Some(3)) + .build(); // Try to ping the server. - bdk_electrum::electrum_client::Client::from_config(&electrum_config.addr, dummy_config) + electrum_client::Client::from_config(&electrum_config.addr, dummy_config) .and_then(|dummy_client| dummy_client.ping()) .map_err(Error::Server)?; @@ -66,12 +70,13 @@ impl Client { let config = Config::builder() .retry(RETRY_LIMIT) .timeout(Some(RPC_SOCKET_TIMEOUT)) + .validate_domain(electrum_config.validate_domain) .build(); - let client = - bdk_electrum::electrum_client::Client::from_config(&electrum_config.addr, config) - .map_err(Error::Server)?; - let bdk_electrum_client = BdkElectrumClient::new(client); - Ok(Self(bdk_electrum_client)) + + let inner = electrum_client::Client::from_config(&electrum_config.addr, config) + .map_err(Error::Server)?; + let inner = BdkElectrumClient::new(inner); + Ok(Client(inner)) } pub fn chain_tip(&self) -> Result { diff --git a/lianad/src/config.rs b/lianad/src/config.rs index 12c37815..5f6cd0b8 100644 --- a/lianad/src/config.rs +++ b/lianad/src/config.rs @@ -123,12 +123,20 @@ pub struct BitcoindConfig { } /// Everything we need to know for talking to Electrum serenely. -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ElectrumConfig { /// The URL the Electrum's RPC is listening on. /// Include "ssl://" for SSL. otherwise TCP will be assumed. /// Can optionally prefix with "tcp://". pub addr: String, + /// If validate_domain == false, domain of ssl certificate will not be validated + /// (useful to allow usage of self signed certificates on local network) + #[serde(default = "default_validate_domain")] + pub validate_domain: bool, +} + +fn default_validate_domain() -> bool { + true } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -316,7 +324,7 @@ impl Config { mod tests { use std::path::PathBuf; - use super::{config_file_path, BitcoindConfig, BitcoindRpcAuth, Config}; + use super::*; // Test the format of the configuration file #[test] @@ -509,6 +517,41 @@ mod tests { .contains("`auth` must be 'user:password'")); } + // Test the format of the `electrum_config` section + #[test] + fn toml_electrum_config() { + // A valid config with `validate_domain` + let toml_str = r#" + addr = 'ssl://electrum.blockstream.info:60002' + validate_domain = false + "# + .trim_start() + .replace(" ", ""); + toml::from_str::(&toml_str).expect("Deserializing toml_str"); + let parsed = toml::from_str::(&toml_str).expect("Deserializing toml_str"); + let serialized = toml::to_string_pretty(&parsed).expect("Serializing to toml"); + assert_eq!(toml_str, serialized); + let expected = ElectrumConfig { + addr: "ssl://electrum.blockstream.info:60002".into(), + validate_domain: false, + }; + assert_eq!(parsed, expected,); + + // A valid config w/o `validate_domain` + let toml_str = r#" + addr = 'ssl://electrum.blockstream.info:60002' + "# + .trim_start() + .replace(" ", ""); + let parsed = toml::from_str::(&toml_str).expect("Deserializing toml_str"); + let expected = ElectrumConfig { + addr: "ssl://electrum.blockstream.info:60002".into(), + // `validate_domain` must default to true + validate_domain: true, + }; + assert_eq!(parsed, expected,); + } + #[test] fn config_directory() { let filepath = config_file_path().expect("Getting config file path");