Add UI, Model, and BitcoinService support to use insecure SSL. Dependent on BDK-FFI that exposes validate_domain parameter in the electrum client contstructor

This commit is contained in:
Nick Klockenga 2026-03-30 17:02:32 -04:00
parent bd847a9a57
commit 5ec22bb4fc
No known key found for this signature in database
GPG Key ID: D32B8BF28121ADF6
6 changed files with 92 additions and 8 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.")
}
}
}