implement tofu certificate pinning for tls bitcoin core connections

This commit is contained in:
Craig Raw
2026-02-19 15:35:03 +02:00
parent 78fe55787b
commit 46d444615c
4 changed files with 63 additions and 33 deletions
@@ -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()) {
@@ -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;
}
}
}
@@ -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);
}
}
@@ -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;
}
}
}