allow to connect to an electrum server w/ a self signed certificate"

This reverts commit b7b52d73bec0522af6dd08e9018868841b9363ca.
This commit is contained in:
pythcoiner 2025-03-19 11:56:05 +01:00
parent dd864905cd
commit df1fb7f8f5
No known key found for this signature in database
GPG Key ID: C1048AEEDF303B88
12 changed files with 148 additions and 25 deletions

19
Cargo.lock generated
View File

@ -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",

View File

@ -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]

View File

@ -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(

View File

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

View File

@ -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<String>,
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),
);

View File

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

View File

@ -14,9 +14,19 @@ use crate::{
node::electrum::ConfigField,
};
#[derive(Clone, Default)]
#[derive(Clone)]
pub struct DefineElectrum {
address: form::Value<String>,
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<Message> {
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

View File

@ -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<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()
.push(text("Address:").bold())
.push(
@ -1262,7 +1271,8 @@ pub fn define_electrum<'a>(address: &form::Value<String>) -> 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()

View File

@ -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<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 {
let value_noprefix = if value.starts_with("ssl://") {
value.replacen("ssl://", "", 1)

View File

@ -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

View File

@ -56,9 +56,13 @@ impl Client {
/// Create a new client and perform sanity checks.
pub fn new(electrum_config: &config::ElectrumConfig) -> Result<Self, Error> {
// 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<BlockChainTip, Error> {

View File

@ -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::<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]
fn config_directory() {
let filepath = config_file_path().expect("Getting config file path");