Merge #1500: gui: allow to connect to an electrum server w/ a self signed certificate

308355322cd62b4e98a91de40c92070c3a8ae011 ci: add openssl to 'windows_latest' (pythcoiner)
09bb450b1fbd39aabec61719b9e67e12180641ba gui: allow user to not validate the ssl domain for an electrum server (pythcoiner)
efb23300dae06702459e07334ab458bfcc54b3e6 lianad(electrum): add an option to not validate SSL domain in order to work w/ self signed certificates (pythcoiner)

Pull request description:

  closes #1300
  The issue about connecting to an electrum certificate using `rustls` have been fixed [upstream](https://github.com/bitcoindevkit/bdk/issues/1598) but in order to beneficiate from it we have to update `bdk_electrum` and `rust-bitvoin` dependencies.
  Meanwhile, this PR introduce a workaround: the initial issue is related to `electrum-client` `use-rustls` feature and `use-openssl` feature is not reexported by `bdk_electrum` but we can use `electrum-client` crate directly and use `use-openssl` feature by this way:
   - [x] use `electrum-client` directly w/ `use-openssl`
   - [x] add and option to opt-out of ssl domain validation
   - [x] let user change the `validate_domain` values in `Settings` menu.

  ![image](https://github.com/user-attachments/assets/8314b89b-bfd2-4dc8-b331-ee980c3b24d5)

   Note: ssl://testnet.aranguren.org:51002 electrum server can be used to test this PR

ACKs for top commit:
  jp1ac4:
    ACK 308355322c.

Tree-SHA512: 28139ef6c6073045b413303c725c0d6c83b193d89c7b39edfb1d10108a1551c1ac9fdf20c941bc770d62f0075fb47ffa305b4975ea38e1840ae3d631ca9e39e6
This commit is contained in:
edouardparis 2025-01-02 10:24:31 +01:00
commit 2d2d080888
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
13 changed files with 204 additions and 21 deletions

View File

@ -41,6 +41,14 @@ jobs:
toolchain: ${{ matrix.toolchain }} toolchain: ${{ matrix.toolchain }}
override: true override: true
profile: minimal profile: minimal
- name: Install OpenSSL (windows)
if: matrix.os == 'windows-latest'
run: |
choco install openssl.light --no-progress
echo "C:\Program Files\OpenSSL" >> $env:GITHUB_PATH
echo "C:\Program Files\OpenSSL\bin" >> $env::GITHUB_PATH
echo "OPENSSL_DIR=C:\Program Files\OpenSSL" >> $env:GITHUB_ENV
- name: Test on Rust ${{ matrix.toolchain }} (only Windows) - name: Test on Rust ${{ matrix.toolchain }} (only Windows)
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
run: cargo test --verbose --no-default-features run: cargo test --verbose --no-default-features

65
Cargo.lock generated
View File

@ -1143,7 +1143,7 @@ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation 0.9.4", "core-foundation 0.9.4",
"core-graphics-types 0.1.3", "core-graphics-types 0.1.3",
"foreign-types", "foreign-types 0.5.0",
"libc", "libc",
] ]
@ -1156,7 +1156,7 @@ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"core-foundation 0.10.0", "core-foundation 0.10.0",
"core-graphics-types 0.2.0", "core-graphics-types 0.2.0",
"foreign-types", "foreign-types 0.5.0",
"libc", "libc",
] ]
@ -1537,6 +1537,7 @@ dependencies = [
"byteorder", "byteorder",
"libc", "libc",
"log", "log",
"openssl",
"rustls", "rustls",
"serde", "serde",
"serde_json", "serde_json",
@ -1824,6 +1825,15 @@ dependencies = [
"ttf-parser 0.19.2", "ttf-parser 0.19.2",
] ]
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared 0.1.1",
]
[[package]] [[package]]
name = "foreign-types" name = "foreign-types"
version = "0.5.0" version = "0.5.0"
@ -1831,7 +1841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [ dependencies = [
"foreign-types-macros", "foreign-types-macros",
"foreign-types-shared", "foreign-types-shared 0.3.1",
] ]
[[package]] [[package]]
@ -1845,6 +1855,12 @@ dependencies = [
"syn 2.0.87", "syn 2.0.87",
] ]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "foreign-types-shared" name = "foreign-types-shared"
version = "0.3.1" version = "0.3.1"
@ -3112,6 +3128,7 @@ dependencies = [
"backtrace", "backtrace",
"bdk_electrum", "bdk_electrum",
"dirs 5.0.1", "dirs 5.0.1",
"electrum-client",
"fern", "fern",
"jsonrpc 0.17.0", "jsonrpc 0.17.0",
"liana", "liana",
@ -3372,7 +3389,7 @@ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"block", "block",
"core-graphics-types 0.1.3", "core-graphics-types 0.1.3",
"foreign-types", "foreign-types 0.5.0",
"log", "log",
"objc", "objc",
"paste", "paste",
@ -3798,6 +3815,44 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openssl"
version = "0.10.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"foreign-types 0.3.2",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "openssl-sys"
version = "0.9.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "option-ext" name = "option-ext"
version = "0.2.0" version = "0.2.0"
@ -5118,7 +5173,7 @@ dependencies = [
"core-graphics 0.24.0", "core-graphics 0.24.0",
"drm", "drm",
"fastrand", "fastrand",
"foreign-types", "foreign-types 0.5.0",
"js-sys", "js-sys",
"log", "log",
"memmap2 0.9.5", "memmap2 0.9.5",

View File

@ -50,8 +50,13 @@ poll_interval_secs = 30
# In order to connect, it needs the address as a string, which can be # In order to connect, it needs the address as a string, which can be
# optionally prefixed with "ssl://" or "tcp://". If omitted, "tcp://" # optionally prefixed with "ssl://" or "tcp://". If omitted, "tcp://"
# will be assumed. # 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] # [electrum_config]
# addr = "127.0.0.1:50001" # addr = "127.0.0.1:50001"
# validate_domain = false
# #
# #
[bitcoind_config] [bitcoind_config]

View File

@ -323,6 +323,7 @@ impl BitcoindSettings {
} }
} }
} }
view::SettingsEditMessage::ValidateDomainEdited(_) => {}
view::SettingsEditMessage::BitcoindRpcAuthTypeSelected(auth_type) => { view::SettingsEditMessage::BitcoindRpcAuthTypeSelected(auth_type) => {
if !self.processing { if !self.processing {
self.selected_auth_type = auth_type; self.selected_auth_type = auth_type;
@ -462,6 +463,7 @@ impl ElectrumSettings {
daemon_config.bitcoin_backend = daemon_config.bitcoin_backend =
Some(lianad::config::BitcoinBackend::Electrum(ElectrumConfig { Some(lianad::config::BitcoinBackend::Electrum(ElectrumConfig {
addr: self.addr.value.clone(), addr: self.addr.value.clone(),
validate_domain: self.electrum_config.validate_domain,
})); }));
self.processing = true; self.processing = true;
return Command::perform(async move { daemon_config }, |cfg| { return Command::perform(async move { daemon_config }, |cfg| {
@ -470,6 +472,11 @@ impl ElectrumSettings {
} }
} }
view::SettingsEditMessage::Clipboard(text) => return clipboard::write(text), view::SettingsEditMessage::Clipboard(text) => return clipboard::write(text),
view::SettingsEditMessage::ValidateDomainEdited(b) => {
if !self.processing {
self.electrum_config.validate_domain = b;
}
}
_ => {} _ => {}
}; };
Command::none() Command::none()
@ -484,6 +491,7 @@ impl ElectrumSettings {
cache.blockheight, cache.blockheight,
&self.addr, &self.addr,
self.processing, self.processing,
self.electrum_config.validate_domain,
) )
} else { } else {
view::settings::electrum( view::settings::electrum(

View File

@ -89,6 +89,7 @@ pub enum RemoteBackendSettingsMessage {
pub enum SettingsEditMessage { pub enum SettingsEditMessage {
Select, Select,
FieldEdited(&'static str, String), FieldEdited(&'static str, String),
ValidateDomainEdited(bool),
BitcoindRpcAuthTypeSelected(RpcAuthType), BitcoindRpcAuthTypeSelected(RpcAuthType),
Cancel, Cancel,
Confirm, Confirm,

View File

@ -32,7 +32,7 @@ use crate::{
hw::HardwareWallet, hw::HardwareWallet,
node::{ node::{
bitcoind::{RpcAuthType, RpcAuthValues}, bitcoind::{RpcAuthType, RpcAuthValues},
electrum, electrum::{self, validate_domain_checkbox},
}, },
}; };
@ -563,6 +563,7 @@ pub fn electrum_edit<'a>(
blockheight: i32, blockheight: i32,
addr: &form::Value<String>, addr: &form::Value<String>,
processing: bool, processing: bool,
validate_domain: bool,
) -> Element<'a, SettingsEditMessage> { ) -> Element<'a, SettingsEditMessage> {
let mut col = Column::new().spacing(20); let mut col = Column::new().spacing(20);
if is_configured_node_type && blockheight != 0 { if is_configured_node_type && blockheight != 0 {
@ -595,6 +596,9 @@ pub fn electrum_edit<'a>(
.push(separation().width(Length::Fill)); .push(separation().width(Length::Fill));
} }
let checkbox = validate_domain_checkbox(addr, validate_domain, |b| {
SettingsEditMessage::ValidateDomainEdited(b)
});
col = col.push( col = col.push(
Column::new() Column::new()
.push(text("Address:").bold().small()) .push(text("Address:").bold().small())
@ -606,6 +610,7 @@ pub fn electrum_edit<'a>(
.size(P1_SIZE) .size(P1_SIZE)
.padding(5), .padding(5),
) )
.push_maybe(checkbox)
.push(text(electrum::ADDRESS_NOTES).size(P2_SIZE)) .push(text(electrum::ADDRESS_NOTES).size(P2_SIZE))
.spacing(5), .spacing(5),
); );

View File

@ -81,6 +81,7 @@ pub enum DefineBitcoind {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum DefineElectrum { pub enum DefineElectrum {
ConfigFieldEdited(electrum::ConfigField, String), ConfigFieldEdited(electrum::ConfigField, String),
ValidDomainChanged(bool),
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@ -14,9 +14,19 @@ use crate::{
node::electrum::ConfigField, node::electrum::ConfigField,
}; };
#[derive(Clone, Default)] #[derive(Clone)]
pub struct DefineElectrum { pub struct DefineElectrum {
address: form::Value<String>, address: form::Value<String>,
validate_domain: bool,
}
impl Default for DefineElectrum {
fn default() -> Self {
Self {
address: Default::default(),
validate_domain: true,
}
}
} }
impl DefineElectrum { impl DefineElectrum {
@ -38,6 +48,7 @@ impl DefineElectrum {
crate::node::electrum::is_electrum_address_valid(&value); crate::node::electrum::is_electrum_address_valid(&value);
} }
}, },
message::DefineElectrum::ValidDomainChanged(v) => self.validate_domain = v,
}; };
}; };
Command::none() Command::none()
@ -47,6 +58,7 @@ impl DefineElectrum {
if self.can_try_ping() { if self.can_try_ping() {
ctx.bitcoin_backend = Some(lianad::config::BitcoinBackend::Electrum(ElectrumConfig { ctx.bitcoin_backend = Some(lianad::config::BitcoinBackend::Electrum(ElectrumConfig {
addr: self.address.value.clone(), addr: self.address.value.clone(),
validate_domain: self.validate_domain,
})); }));
return true; return true;
} }
@ -54,12 +66,15 @@ impl DefineElectrum {
} }
pub fn view(&self) -> Element<Message> { pub fn view(&self) -> Element<Message> {
view::define_electrum(&self.address) view::define_electrum(&self.address, self.validate_domain)
} }
pub fn ping(&self) -> Result<(), Error> { pub fn ping(&self) -> Result<(), Error> {
let builder = electrum_client::Config::builder(); 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) let client = electrum_client::Client::from_config(&self.address.value, config)
.map_err(|e| Error::Electrum(e.to_string()))?; .map_err(|e| Error::Electrum(e.to_string()))?;
client client

View File

@ -29,6 +29,7 @@ use liana_ui::{
widget::*, widget::*,
}; };
use crate::node::electrum::validate_domain_checkbox;
use crate::{ use crate::{
hw::{is_compatible_with_tapminiscript, HardwareWallet, UnsupportedReason}, hw::{is_compatible_with_tapminiscript, HardwareWallet, UnsupportedReason},
installer::{ installer::{
@ -1134,7 +1135,15 @@ pub fn define_bitcoind<'a>(
.into() .into()
} }
pub fn define_electrum<'a>(address: &form::Value<String>) -> Element<'a, Message> { pub fn define_electrum<'a>(
address: &form::Value<String>,
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() let col_address = Column::new()
.push(text("Address:").bold()) .push(text("Address:").bold())
.push( .push(
@ -1150,7 +1159,8 @@ pub fn define_electrum<'a>(address: &form::Value<String>) -> Element<'a, Message
.size(text::P1_SIZE) .size(text::P1_SIZE)
.padding(10), .padding(10),
) )
.push(text(electrum::ADDRESS_NOTES).size(text::P2_SIZE)) .push_maybe(checkbox)
.push(text(electrum::ADDRESS_NOTES))
.spacing(10); .spacing(10);
Column::new().push(col_address).spacing(50).into() Column::new().push(col_address).spacing(50).into()

View File

@ -1,13 +1,18 @@
use std::fmt; use std::fmt;
use iced::{widget::checkbox, Element, Renderer};
use liana_ui::{component::form, theme::Theme};
#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum ConfigField { pub enum ConfigField {
Address, Address,
} }
pub const ADDRESS_NOTES: &str = "Note: include \"ssl://\" as a prefix \ pub const ADDRESS_NOTES: &str = "Note: include \"ssl://\" as a prefix \
for SSL connections. Be aware that self-signed \ for SSL connections.";
SSL certificates are currently not supported.";
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 { impl fmt::Display for ConfigField {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 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<String>,
value: bool,
closure: F,
) -> Option<Element<'a, M, Theme, Renderer>>
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 { pub fn is_electrum_address_valid(value: &str) -> bool {
let value_noprefix = if value.starts_with("ssl://") { let value_noprefix = if value.starts_with("ssl://") {
value.replacen("ssl://", "", 1) value.replacen("ssl://", "", 1)

View File

@ -28,6 +28,7 @@ miniscript = { version = "11.0", features = ["serde", "compiler", "base64"] }
# For Electrum backend. This is the latest version with the same bitcoin version as # For Electrum backend. This is the latest version with the same bitcoin version as
# the miniscript dependency. # the miniscript dependency.
bdk_electrum = { version = "0.14" } bdk_electrum = { version = "0.14" }
electrum-client = { version = "0.19", features = ["use-openssl"] }
# Don't reinvent the wheel # Don't reinvent the wheel
dirs = "5.0" dirs = "5.0"

View File

@ -7,10 +7,11 @@ use bdk_electrum::{
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}, spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
BlockId, ChainPosition, ConfirmationHeightAnchor, TxGraph, BlockId, ChainPosition, ConfirmationHeightAnchor, TxGraph,
}, },
electrum_client::{self, Config, ElectrumApi},
ElectrumExt, ElectrumExt,
}; };
use electrum_client::{self, Config, ElectrumApi};
use super::utils::{ use super::utils::{
block_id_from_tip, height_i32_from_usize, height_usize_from_i32, outpoints_from_tx, block_id_from_tip, height_i32_from_usize, height_usize_from_i32, outpoints_from_tx,
}; };
@ -56,9 +57,13 @@ impl Client {
/// Create a new client and perform sanity checks. /// Create a new client and perform sanity checks.
pub fn new(electrum_config: &config::ElectrumConfig) -> Result<Self, Error> { pub fn new(electrum_config: &config::ElectrumConfig) -> Result<Self, Error> {
// First use a dummy config to check connectivity (no retries, short timeout). // 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. // 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()) .and_then(|dummy_client| dummy_client.ping())
.map_err(Error::Server)?; .map_err(Error::Server)?;
@ -66,10 +71,10 @@ impl Client {
let config = Config::builder() let config = Config::builder()
.retry(RETRY_LIMIT) .retry(RETRY_LIMIT)
.timeout(Some(RPC_SOCKET_TIMEOUT)) .timeout(Some(RPC_SOCKET_TIMEOUT))
.validate_domain(electrum_config.validate_domain)
.build(); .build();
let client = let client = electrum_client::Client::from_config(&electrum_config.addr, config)
bdk_electrum::electrum_client::Client::from_config(&electrum_config.addr, config) .map_err(Error::Server)?;
.map_err(Error::Server)?;
Ok(Self(client)) Ok(Self(client))
} }

View File

@ -122,12 +122,20 @@ pub struct BitcoindConfig {
} }
/// Everything we need to know for talking to Electrum serenely. /// Everything we need to know for talking to Electrum serenely.
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ElectrumConfig { pub struct ElectrumConfig {
/// The URL the Electrum's RPC is listening on. /// The URL the Electrum's RPC is listening on.
/// Include "ssl://" for SSL. otherwise TCP will be assumed. /// Include "ssl://" for SSL. otherwise TCP will be assumed.
/// Can optionally prefix with "tcp://". /// Can optionally prefix with "tcp://".
pub addr: String, 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)] #[derive(Debug, Clone, Deserialize, Serialize)]
@ -286,7 +294,7 @@ impl Config {
mod tests { mod tests {
use std::path::PathBuf; use std::path::PathBuf;
use super::{config_file_path, BitcoindConfig, BitcoindRpcAuth, Config}; use super::*;
// Test the format of the configuration file // Test the format of the configuration file
#[test] #[test]
@ -479,6 +487,41 @@ mod tests {
.contains("`auth` must be 'user:password'")); .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::<ElectrumConfig>(&toml_str).expect("Deserializing toml_str");
let parsed = toml::from_str::<ElectrumConfig>(&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::<ElectrumConfig>(&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] #[test]
fn config_directory() { fn config_directory() {
let filepath = config_file_path().expect("Getting config file path"); let filepath = config_file_path().expect("Getting config file path");