From 46d444615c4eafc5ba7593cb45bd49ac123dc240 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 19 Feb 2026 15:35:03 +0200 Subject: [PATCH] implement tofu certificate pinning for tls bitcoin core connections --- .../com/sparrowwallet/sparrow/io/Storage.java | 13 +++- .../sparrow/net/ElectrumServer.java | 7 +++ .../sparrow/net/TcpOverTlsTransport.java | 15 +++-- .../cormorant/bitcoind/BitcoindTransport.java | 61 +++++++++++-------- 4 files changed, 63 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java index 51147fa0..36917662 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java @@ -7,6 +7,7 @@ import com.sparrowwallet.drongo.wallet.MnemonicException; import com.sparrowwallet.drongo.wallet.StandardAccount; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.net.ServerType; import com.sparrowwallet.sparrow.SparrowWallet; import javafx.concurrent.ScheduledService; import javafx.concurrent.Service; @@ -504,7 +505,7 @@ public class Storage { public static File getCertificateFile(String host) { File certsDir = getCertsDir(); - File[] certs = certsDir.listFiles((dir, name) -> name.equals(host)); + File[] certs = certsDir.listFiles((dir, name) -> name.equals(getCertName(host))); if(certs != null && certs.length > 0) { return certs[0]; } @@ -513,7 +514,7 @@ public class Storage { } public static void saveCertificate(String host, Certificate cert) { - try(FileWriter writer = new FileWriter(new File(getCertsDir(), host))) { + try(FileWriter writer = new FileWriter(new File(getCertsDir(), getCertName(host)))) { writer.write("-----BEGIN CERTIFICATE-----\n"); writer.write(Base64.getEncoder().encodeToString(cert.getEncoded()).replaceAll("(.{64})", "$1\n")); writer.write("\n-----END CERTIFICATE-----\n"); @@ -524,6 +525,14 @@ public class Storage { } } + private static String getCertName(String host) { + if(Config.get().getServerType() == ServerType.BITCOIN_CORE) { + return host + ".bitcoind"; + } + + return host; + } + static File getCertsDir() { File certsDir = new File(getSparrowDir(), CERTS_DIR); if(!certsDir.exists()) { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 9112d128..d6fc103b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -31,6 +31,7 @@ import javafx.util.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.net.ssl.SSLHandshakeException; import java.io.*; import java.nio.charset.StandardCharsets; import java.util.*; @@ -1435,6 +1436,12 @@ public class ElectrumServer { bwtStartLock.unlock(); } } + } catch(IllegalStateException e) { + if(e.getCause() instanceof SSLHandshakeException) { + throw new TlsServerException(Config.get().getCoreServer().getHostAndPort(), e.getCause()); + } else { + throw e; + } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java index 9554ae2d..6d6fdfd8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java @@ -23,7 +23,7 @@ public class TcpOverTlsTransport extends TcpTransport { public TcpOverTlsTransport(HostAndPort server) throws NoSuchAlgorithmException, KeyManagementException, CertificateException, KeyStoreException, IOException { super(server); - TrustManager[] trustManagers = getTrustManagers(Storage.getCertificateFile(server.getHost())); + TrustManager[] trustManagers = getTrustManagers(Storage.getCertificateFile(server.getHost()), server.getHost()); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustManagers, new SecureRandom()); @@ -34,7 +34,7 @@ public class TcpOverTlsTransport extends TcpTransport { public TcpOverTlsTransport(HostAndPort server, File crtFile) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { super(server); - TrustManager[] trustManagers = getTrustManagers(crtFile); + TrustManager[] trustManagers = getTrustManagers(crtFile, server.getHost()); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustManagers, null); @@ -60,7 +60,7 @@ public class TcpOverTlsTransport extends TcpTransport { } } - private TrustManager[] getTrustManagers(File crtFile) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { + public static TrustManager[] getTrustManagers(File crtFile, String host) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { if(crtFile == null) { return new TrustManager[] { new X509TrustManager() { @@ -79,7 +79,7 @@ public class TcpOverTlsTransport extends TcpTransport { try { certs[0].checkValidity(); } catch(CertificateExpiredException e) { - if(Storage.getCertificateFile(server.getHost()) == null) { + if(Storage.getCertificateFile(host) == null) { throw new UnknownCertificateExpiredException(e.getMessage(), certs[0]); } } @@ -88,7 +88,10 @@ public class TcpOverTlsTransport extends TcpTransport { }; } - Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(new FileInputStream(crtFile)); + Certificate certificate; + try(FileInputStream fis = new FileInputStream(crtFile)) { + certificate = CertificateFactory.getInstance("X.509").generateCertificate(fis); + } if(certificate instanceof X509Certificate) { try { X509Certificate x509Certificate = (X509Certificate)certificate; @@ -98,7 +101,7 @@ public class TcpOverTlsTransport extends TcpTransport { //These will usually be self-signed certificates that users may not have the expertise to renew } catch(CertificateException e) { crtFile.delete(); - return getTrustManagers(null); + return getTrustManagers(null, host); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindTransport.java index 0ceda59b..b7e9f8bf 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindTransport.java @@ -4,7 +4,9 @@ import com.github.arteam.simplejsonrpc.client.Transport; import com.sparrowwallet.drongo.Network; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.io.Server; +import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.net.Protocol; +import com.sparrowwallet.sparrow.net.TcpOverTlsTransport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,8 +16,7 @@ import java.net.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; +import java.security.cert.Certificate; import java.util.Base64; public class BitcoindTransport implements Transport { @@ -27,6 +28,7 @@ public class BitcoindTransport implements Transport { private File cookieFile; private Long cookieFileTimestamp; private String bitcoindAuthEncoded; + private SSLSocketFactory sslSocketFactory; public BitcoindTransport(Server bitcoindServer, String bitcoindWallet, String bitcoindAuth) { this(bitcoindServer, bitcoindWallet); @@ -57,9 +59,10 @@ public class BitcoindTransport implements Transport { HttpURLConnection connection = proxy != null && Protocol.isOnionAddress(bitcoindServer) ? (HttpURLConnection)bitcoindUrl.openConnection(proxy) : (HttpURLConnection)bitcoindUrl.openConnection(); if(connection instanceof HttpsURLConnection httpsURLConnection) { - SSLSocketFactory sslSocketFactory = getTrustAllSocketFactory(); + SSLSocketFactory sslSocketFactory = getSSLSocketFactory(); if(sslSocketFactory != null) { httpsURLConnection.setSSLSocketFactory(sslSocketFactory); + httpsURLConnection.setHostnameVerifier((_, _) -> true); } } @@ -81,6 +84,19 @@ public class BitcoindTransport implements Transport { } int statusCode = connection.getResponseCode(); + + if(connection instanceof HttpsURLConnection httpsConn && Storage.getCertificateFile(bitcoindServer.getHost()) == null) { + try { + Certificate[] certs = httpsConn.getServerCertificates(); + if(certs.length > 0) { + Storage.saveCertificate(bitcoindServer.getHost(), certs[0]); + sslSocketFactory = null; + } + } catch(SSLPeerUnverifiedException e) { + log.warn("Could not retrieve certificate for saving", e); + } + } + if(statusCode == 401) { throw new IOException((cookieFile == null ? "User/pass" : "Cookie file") + " authentication failed"); } @@ -138,29 +154,24 @@ public class BitcoindTransport implements Transport { return bitcoindDir; } - private SSLSocketFactory getTrustAllSocketFactory() { - TrustManager[] trustAllCerts = new TrustManager[] { - new X509TrustManager() { - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - - public void checkClientTrusted(X509Certificate[] certs, String authType) throws CertificateException { - } - - public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException { - } - } - }; - - try { - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, trustAllCerts, null); - return sslContext.getSocketFactory(); - } catch (Exception e) { - log.error("Error creating SSL socket factory", e); + private SSLSocketFactory getSSLSocketFactory() { + if(sslSocketFactory == null) { + sslSocketFactory = createSSLSocketFactory(); } - return null; + return sslSocketFactory; + } + + private SSLSocketFactory createSSLSocketFactory() { + try { + String host = bitcoindServer.getHost(); + TrustManager[] trustManagers = TcpOverTlsTransport.getTrustManagers(Storage.getCertificateFile(host), host); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustManagers, null); + return sslContext.getSocketFactory(); + } catch(Exception e) { + log.error("Error creating SSL socket factory", e); + return null; + } } }