From 5ec22bb4fce2f8f396928beba7ef6741b83dc563 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Mon, 30 Mar 2026 17:02:32 -0400 Subject: [PATCH] Add UI, Model, and BitcoinService support to use insecure SSL. Dependent on BDK-FFI that exposes validate_domain parameter in the electrum client contstructor --- hellbender/Models/WalletProfile.swift | 5 ++- hellbender/Services/BitcoinService.swift | 11 +++-- hellbender/Services/ElectrumConfig.swift | 5 ++- .../ViewModels/SetupWizardViewModel.swift | 2 + .../Views/Main/Settings/WalletInfoView.swift | 35 +++++++++++++++- .../Setup/ElectrumServerSetupSection.swift | 42 ++++++++++++++++++- 6 files changed, 92 insertions(+), 8 deletions(-) diff --git a/hellbender/Models/WalletProfile.swift b/hellbender/Models/WalletProfile.swift index 97de4a4..4cb0984 100644 --- a/hellbender/Models/WalletProfile.swift +++ b/hellbender/Models/WalletProfile.swift @@ -17,6 +17,7 @@ final class WalletProfile { var electrumPort: Int var electrumSSL: Int // 0 = network default, 1 = TCP, 2 = SSL var blockExplorerHost: String // empty = mempool.space + var electrumAllowInsecureSSL: Bool = false var privacyMode: Bool = false @Relationship(deleteRule: .cascade, inverse: \CosignerInfo.wallet) @@ -35,6 +36,7 @@ final class WalletProfile { electrumHost: String = "", electrumPort: Int = 0, electrumSSL: Int = 0, + electrumAllowInsecureSSL: Bool = false, blockExplorerHost: String = "", privacyMode: Bool = false ) { @@ -51,6 +53,7 @@ final class WalletProfile { self.electrumHost = electrumHost self.electrumPort = electrumPort self.electrumSSL = electrumSSL + self.electrumAllowInsecureSSL = electrumAllowInsecureSSL self.blockExplorerHost = blockExplorerHost self.privacyMode = privacyMode cosigners = [] @@ -69,7 +72,7 @@ final class WalletProfile { case 2: true // SSL default: net.usesSSL // 0 = network default } - return ElectrumConfig(host: host, port: port, useSSL: ssl) + return ElectrumConfig(host: host, port: port, useSSL: ssl, allowInsecureSSL: electrumAllowInsecureSSL) } var multisigDescription: String { diff --git a/hellbender/Services/BitcoinService.swift b/hellbender/Services/BitcoinService.swift index 03d2303..6254157 100644 --- a/hellbender/Services/BitcoinService.swift +++ b/hellbender/Services/BitcoinService.swift @@ -264,7 +264,8 @@ final class BitcoinService { addToLog("Connecting to Electrum: \(config.url)") do { let url = config.url - electrumClient = try await Task.detached { try ElectrumClient(url: url) }.value + let validateDomain = !config.allowInsecureSSL + electrumClient = try await Task.detached { try ElectrumClient(url: url, validateDomain: validateDomain) }.value electrumConnectionError = nil addToLog("Electrum client initialized") } catch { @@ -301,7 +302,8 @@ final class BitcoinService { let config = profile.electrumConfig addToLog("Re-initializing Electrum client: \(config.url)") let reconnectURL = config.url - electrumClient = try await Task.detached { try ElectrumClient(url: reconnectURL) }.value + let validateDomain = !config.allowInsecureSSL + electrumClient = try await Task.detached { try ElectrumClient(url: reconnectURL, validateDomain: validateDomain) }.value electrumConnectionError = nil } @@ -453,13 +455,14 @@ final class BitcoinService { @discardableResult func testElectrumConnection(config: ElectrumConfig) async throws -> UInt32 { // Pre-check SSL certificate before handing off to BDK - if config.useSSL { + if config.useSSL, !config.allowInsecureSSL { try await Self.validateTLSCertificate(host: config.host, port: config.port) } let url = config.url + let validateDomain = !config.allowInsecureSSL let header = try await Task.detached { - let client = try ElectrumClient(url: url) + let client = try ElectrumClient(url: url, validateDomain: validateDomain) return try client.blockHeadersSubscribe() }.value return UInt32(header.height) diff --git a/hellbender/Services/ElectrumConfig.swift b/hellbender/Services/ElectrumConfig.swift index d004aba..403445c 100644 --- a/hellbender/Services/ElectrumConfig.swift +++ b/hellbender/Services/ElectrumConfig.swift @@ -4,21 +4,24 @@ struct ElectrumConfig: Equatable { var host: String var port: UInt16 var useSSL: Bool + var allowInsecureSSL: Bool var url: String { let proto = useSSL ? "ssl" : "tcp" return "\(proto)://\(host):\(port)" } - init(host: String, port: UInt16, useSSL: Bool) { + init(host: String, port: UInt16, useSSL: Bool, allowInsecureSSL: Bool = false) { self.host = host self.port = port self.useSSL = useSSL + self.allowInsecureSSL = allowInsecureSSL } init(network: BitcoinNetwork) { host = network.defaultElectrumHost ?? "" port = network.defaultElectrumPort useSSL = network.usesSSL + allowInsecureSSL = false } } diff --git a/hellbender/ViewModels/SetupWizardViewModel.swift b/hellbender/ViewModels/SetupWizardViewModel.swift index 43fe01e..c0235e4 100644 --- a/hellbender/ViewModels/SetupWizardViewModel.swift +++ b/hellbender/ViewModels/SetupWizardViewModel.swift @@ -50,6 +50,7 @@ final class SetupWizardViewModel { var electrumHost: String = "" var electrumPort: String = "" var electrumSSL: Int = 0 // 0 = network default, 1 = TCP, 2 = SSL + var electrumAllowInsecureSSL: Bool = false /// Returns an error message if the descriptor contains keys that don't match the selected network, nil otherwise. var descriptorNetworkMismatchError: String? { @@ -449,6 +450,7 @@ final class SetupWizardViewModel { electrumHost: electrumHost.trimmingCharacters(in: .whitespaces), electrumPort: Int(electrumPort) ?? 0, electrumSSL: electrumSSL, + electrumAllowInsecureSSL: electrumAllowInsecureSSL, blockExplorerHost: blockExplorerHost.trimmingCharacters(in: .whitespaces) ) diff --git a/hellbender/Views/Main/Settings/WalletInfoView.swift b/hellbender/Views/Main/Settings/WalletInfoView.swift index 58d0c3e..c0b32f4 100644 --- a/hellbender/Views/Main/Settings/WalletInfoView.swift +++ b/hellbender/Views/Main/Settings/WalletInfoView.swift @@ -23,6 +23,7 @@ struct WalletInfoView: View { @State private var connectionTestResult: String? @State private var blockExplorerText: String = "" @State private var initialElectrumConfig: ElectrumConfig? + @State private var showInsecureSSLAlert = false @State private var showDescriptorQR = false @State private var showDeleteConfirmation = false @State private var showDescriptorPDF = false @@ -200,7 +201,12 @@ struct WalletInfoView: View { default: wallet.bitcoinNetwork.usesSSL ? 2 : 1 } }, - set: { wallet.electrumSSL = $0 } + set: { + wallet.electrumSSL = $0 + if $0 != 2 { + wallet.electrumAllowInsecureSSL = false + } + } )) { Text("TCP").tag(1) Text("SSL").tag(2) @@ -208,6 +214,24 @@ struct WalletInfoView: View { .pickerStyle(.segmented) } + if wallet.electrumSSL == 2 || (wallet.electrumSSL == 0 && wallet.bitcoinNetwork.usesSSL) { + Toggle(isOn: Binding( + get: { wallet.electrumAllowInsecureSSL }, + set: { newValue in + if newValue { + showInsecureSSLAlert = true + } else { + wallet.electrumAllowInsecureSSL = false + } + } + )) { + Text("Allow insecure SSL") + .font(.hbBody(13)) + .foregroundStyle(Color.hbTextPrimary) + } + .tint(Color.hbBitcoinOrange) + } + if let result = connectionTestResult { Text(result) .font(.hbBody(13)) @@ -426,6 +450,14 @@ struct WalletInfoView: View { } } } + .alert("Allow Insecure SSL?", isPresented: $showInsecureSSLAlert) { + Button("Cancel", role: .cancel) {} + Button("Allow", role: .destructive) { + wallet.electrumAllowInsecureSSL = true + } + } message: { + Text("This removes the requirement to verify that the server is who it claims to be. The connection will still be encrypted, but self-signed, expired, or invalid certificates will be accepted.") + } .sheet(isPresented: $showEditCosigners) { EditCosignersView(wallet: wallet) } @@ -460,6 +492,7 @@ struct WalletInfoView: View { wallet.electrumHost = "" wallet.electrumPort = 0 wallet.electrumSSL = 0 + wallet.electrumAllowInsecureSSL = false electrumHostText = "" electrumPortText = "" connectionTestResult = nil diff --git a/hellbender/Views/Setup/ElectrumServerSetupSection.swift b/hellbender/Views/Setup/ElectrumServerSetupSection.swift index d3a4389..e8a78a6 100644 --- a/hellbender/Views/Setup/ElectrumServerSetupSection.swift +++ b/hellbender/Views/Setup/ElectrumServerSetupSection.swift @@ -2,6 +2,15 @@ import SwiftUI struct ElectrumServerSetupSection: View { @Bindable var viewModel: SetupWizardViewModel + @State private var showInsecureSSLAlert = false + + private var isSSLSelected: Bool { + switch viewModel.electrumSSL { + case 1: false + case 2: true + default: viewModel.network.usesSSL + } + } var body: some View { VStack(spacing: 12) { @@ -50,7 +59,12 @@ struct ElectrumServerSetupSection: View { default: viewModel.network.usesSSL ? 2 : 1 } }, - set: { viewModel.electrumSSL = $0 } + set: { + viewModel.electrumSSL = $0 + if $0 != 2 { + viewModel.electrumAllowInsecureSSL = false + } + } )) { Text("TCP").tag(1) Text("SSL").tag(2) @@ -59,6 +73,24 @@ struct ElectrumServerSetupSection: View { } } + if isSSLSelected { + Toggle(isOn: Binding( + get: { viewModel.electrumAllowInsecureSSL }, + set: { newValue in + if newValue { + showInsecureSSLAlert = true + } else { + viewModel.electrumAllowInsecureSSL = false + } + } + )) { + Text("Allow insecure SSL") + .font(.hbBody(13)) + .foregroundStyle(Color.hbTextPrimary) + } + .tint(Color.hbBitcoinOrange) + } + if viewModel.network.defaultElectrumHost != nil { Text("Leave blank to use defaults for \(viewModel.network.displayName)") .font(.hbBody(11)) @@ -70,6 +102,14 @@ struct ElectrumServerSetupSection: View { } } .hbCard() + .alert("Allow Insecure SSL?", isPresented: $showInsecureSSLAlert) { + Button("Cancel", role: .cancel) {} + Button("Allow", role: .destructive) { + viewModel.electrumAllowInsecureSSL = true + } + } message: { + Text("This removes the requirement to verify that the server is who it claims to be. The connection will still be encrypted, but self-signed, expired, or invalid certificates will be accepted.") + } } }