mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-08-10 08:37:05 +00:00
implement tofu certificate pinning for tls bitcoin core connections
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+36
-25
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user