From 5d91f033c09363058146cc958b7b713d18d5fe66 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 2 Apr 2021 12:27:04 +0200 Subject: [PATCH] add ssl server certificate pinning --- .../sparrowwallet/sparrow/AppController.java | 6 +- .../sparrowwallet/sparrow/AppServices.java | 25 +++++ .../sparrow/event/ConnectionFailedEvent.java | 24 +++++ .../com/sparrowwallet/sparrow/io/Storage.java | 34 ++++++ .../sparrowwallet/sparrow/net/Protocol.java | 14 +-- .../sparrow/net/ProxyTcpOverTlsTransport.java | 4 +- .../sparrow/net/TcpOverTlsTransport.java | 101 +++++++++++++----- .../sparrow/net/TcpTransport.java | 3 + .../sparrow/net/TlsServerException.java | 59 ++++++++++ .../sparrow/net/TorTcpOverTlsTransport.java | 4 +- .../ServerPreferencesController.java | 28 ++++- 11 files changed, 254 insertions(+), 48 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/net/TlsServerException.java diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index c8c547bc..babd0c69 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -76,6 +76,7 @@ public class AppController implements Initializable { public static final double TAB_LABEL_GRAPHIC_OPACITY_INACTIVE = 0.8; public static final double TAB_LABEL_GRAPHIC_OPACITY_ACTIVE = 0.95; public static final String LOADING_TRANSACTIONS_MESSAGE = "Loading wallet, select Transactions tab to view..."; + public static final String CONNECTION_FAILED_PREFIX = "Connection failed: "; @FXML private MenuItem saveTransaction; @@ -1451,8 +1452,7 @@ public class AppController implements Initializable { @Subscribe public void connectionFailed(ConnectionFailedEvent event) { - String reason = event.getException().getCause() != null ? event.getException().getCause().getMessage() : event.getException().getMessage(); - String status = "Connection failed: " + reason; + String status = CONNECTION_FAILED_PREFIX + event.getMessage(); statusUpdated(new StatusEvent(status)); } @@ -1465,7 +1465,7 @@ public class AppController implements Initializable { @Subscribe public void disconnection(DisconnectionEvent event) { serverToggle.setDisable(false); - if(!AppServices.isConnecting() && !AppServices.isConnected() && !statusBar.getText().startsWith("Connection error")) { + if(!AppServices.isConnecting() && !AppServices.isConnected() && !statusBar.getText().startsWith(CONNECTION_FAILED_PREFIX)) { statusUpdated(new StatusEvent("Disconnected")); } if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) { diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 15c99599..89c8f926 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -208,6 +208,31 @@ public class AppServices { connectionService.setRestartOnFailure(false); } + if(failEvent.getSource().getException() instanceof TlsServerException && failEvent.getSource().getException().getCause() != null) { + TlsServerException tlsServerException = (TlsServerException)failEvent.getSource().getException(); + connectionService.setRestartOnFailure(false); + if(tlsServerException.getCause().getMessage().contains("PKIX path building failed")) { + File crtFile = Config.get().getElectrumServerCert(); + if(crtFile != null) { + AppServices.showErrorDialog("SSL Handshake Failed", "The configured server certificate at " + crtFile.getAbsolutePath() + " did not match the certificate provided by the server at " + tlsServerException.getServer().getHost() + "." + + "\n\nThis may indicate a man-in-the-middle attack!" + + "\n\nChange the configured server certificate if you would like to proceed."); + } else { + crtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost()); + if(crtFile != null) { + Optional optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " appears to have changed." + + "\n\nThis may indicate a man-in-the-middle attack!" + + "\n\nDo you still want to proceed?", ButtonType.NO, ButtonType.YES); + if(optButton.isPresent() && optButton.get() == ButtonType.YES) { + crtFile.delete(); + Platform.runLater(() -> restartService(connectionService)); + return; + } + } + } + } + } + onlineProperty.removeListener(onlineServicesListener); onlineProperty.setValue(false); onlineProperty.addListener(onlineServicesListener); diff --git a/src/main/java/com/sparrowwallet/sparrow/event/ConnectionFailedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/ConnectionFailedEvent.java index a693720d..244765b2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/ConnectionFailedEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/ConnectionFailedEvent.java @@ -1,5 +1,7 @@ package com.sparrowwallet.sparrow.event; +import com.sparrowwallet.sparrow.net.TlsServerException; + public class ConnectionFailedEvent { private final Throwable exception; @@ -10,4 +12,26 @@ public class ConnectionFailedEvent { public Throwable getException() { return exception; } + + public String getMessage() { + if(exception instanceof TlsServerException) { + return exception.getMessage(); + } + + Throwable cause = (exception.getCause() != null ? exception.getCause() : exception); + cause = (cause.getCause() != null ? cause.getCause() : cause); + String message = splitCamelCase(cause.getClass().getSimpleName().replace("Exception", "Error")); + return message + " (" + cause.getMessage() + ")"; + } + + static String splitCamelCase(String s) { + return s.replaceAll( + String.format("%s|%s|%s", + "(?<=[A-Z])(?=[A-Z][a-z])", + "(?<=[^A-Z])(?=[A-Z])", + "(?<=[A-Za-z])(?=[^A-Za-z])" + ), + " " + ); + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java index 9b9668a3..d59f96ba 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java @@ -24,6 +24,8 @@ import java.lang.reflect.Type; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.*; @@ -44,6 +46,7 @@ public class Storage { public static final String WINDOWS_SPARROW_DIR = "Sparrow"; public static final String WALLETS_DIR = "wallets"; public static final String WALLETS_BACKUP_DIR = "backup"; + public static final String CERTS_DIR = "certs"; public static final String HEADER_MAGIC_1 = "SPRW1"; private static final int BINARY_HEADER_LENGTH = 28; public static final String TEMP_BACKUP_EXTENSION = "tmp"; @@ -384,6 +387,37 @@ public class Storage { return walletsDir; } + public static File getCertificateFile(String host) { + File certsDir = getCertsDir(); + File[] certs = certsDir.listFiles((dir, name) -> name.equals(host)); + if(certs.length > 0) { + return certs[0]; + } + + return null; + } + + public static void saveCertificate(String host, Certificate cert) { + try(FileWriter writer = new FileWriter(new File(getCertsDir(), host))) { + writer.write("-----BEGIN CERTIFICATE-----\n"); + writer.write(Base64.getEncoder().encodeToString(cert.getEncoded()).replaceAll("(.{64})", "$1\n")); + writer.write("\n-----END CERTIFICATE-----\n"); + } catch(CertificateEncodingException e) { + log.error("Error encoding PEM certificate", e); + } catch(IOException e) { + log.error("Error writing PEM certificate", e); + } + } + + static File getCertsDir() { + File certsDir = new File(getSparrowDir(), CERTS_DIR); + if(!certsDir.exists()) { + certsDir.mkdirs(); + } + + return certsDir; + } + static File getSparrowDir() { if(Network.get() != Network.MAINNET) { return new File(getSparrowHome(), Network.get().getName()); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java b/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java index 8a332649..7cb68ff2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java @@ -39,7 +39,7 @@ public enum Protocol { }, SSL { @Override - public Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException { + public Transport getTransport(HostAndPort server) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { if(isOnionAddress(server)) { return new TorTcpOverTlsTransport(server); } @@ -57,7 +57,7 @@ public enum Protocol { } @Override - public Transport getTransport(HostAndPort server, HostAndPort proxy) throws NoSuchAlgorithmException, KeyManagementException { + public Transport getTransport(HostAndPort server, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { return new ProxyTcpOverTlsTransport(server, proxy); } @@ -68,27 +68,27 @@ public enum Protocol { }, HTTP { @Override - public Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException { + public Transport getTransport(HostAndPort server) { throw new UnsupportedOperationException("No transport supported for HTTP"); } @Override - public Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + public Transport getTransport(HostAndPort server, File serverCert) { throw new UnsupportedOperationException("No transport supported for HTTP"); } @Override - public Transport getTransport(HostAndPort server, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + public Transport getTransport(HostAndPort server, HostAndPort proxy) { throw new UnsupportedOperationException("No transport supported for HTTP"); } @Override - public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) { throw new UnsupportedOperationException("No transport supported for HTTP"); } }; - public abstract Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException; + public abstract Transport getTransport(HostAndPort server) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException; public abstract Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException; diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ProxyTcpOverTlsTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/ProxyTcpOverTlsTransport.java index be5e46c3..781b8753 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ProxyTcpOverTlsTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ProxyTcpOverTlsTransport.java @@ -18,7 +18,7 @@ public class ProxyTcpOverTlsTransport extends TcpOverTlsTransport { private final HostAndPort proxy; - public ProxyTcpOverTlsTransport(HostAndPort server, HostAndPort proxy) throws KeyManagementException, NoSuchAlgorithmException { + public ProxyTcpOverTlsTransport(HostAndPort server, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { super(server); this.proxy = proxy; } @@ -34,7 +34,7 @@ public class ProxyTcpOverTlsTransport extends TcpOverTlsTransport { Socket underlying = new Socket(new Proxy(Proxy.Type.SOCKS, proxyAddr)); underlying.connect(new InetSocketAddress(server.getHost(), server.getPortOrDefault(DEFAULT_PORT))); SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(underlying, proxy.getHost(), proxy.getPortOrDefault(DEFAULT_PROXY_PORT), true); - sslSocket.startHandshake(); + startHandshake(sslSocket); return sslSocket; } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java index f45f4f6a..004358b8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java @@ -1,6 +1,9 @@ package com.sparrowwallet.sparrow.net; import com.google.common.net.HostAndPort; +import com.sparrowwallet.sparrow.io.Storage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.net.ssl.*; import java.io.File; @@ -8,35 +11,23 @@ import java.io.FileInputStream; import java.io.IOException; import java.net.Socket; import java.security.*; +import java.security.cert.*; import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; public class TcpOverTlsTransport extends TcpTransport { + private static final Logger log = LoggerFactory.getLogger(TcpOverTlsTransport.class); + public static final int DEFAULT_PORT = 50002; protected final SSLSocketFactory sslSocketFactory; - public TcpOverTlsTransport(HostAndPort server) throws NoSuchAlgorithmException, KeyManagementException { + public TcpOverTlsTransport(HostAndPort server) throws NoSuchAlgorithmException, KeyManagementException, CertificateException, KeyStoreException, IOException { super(server); - TrustManager[] trustAllCerts = new TrustManager[]{ - new X509TrustManager() { - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - - public void checkClientTrusted(X509Certificate[] certs, String authType) { - } - - public void checkServerTrusted(X509Certificate[] certs, String authType) { - } - } - }; + TrustManager[] trustManagers = getTrustManagers(Storage.getCertificateFile(server.getHost())); SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, trustAllCerts, new SecureRandom()); + sslContext.init(null, trustManagers, new SecureRandom()); this.sslSocketFactory = sslContext.getSocketFactory(); } @@ -44,25 +35,77 @@ public class TcpOverTlsTransport extends TcpTransport { public TcpOverTlsTransport(HostAndPort server, File crtFile) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { super(server); - Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(new FileInputStream(crtFile)); - - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - keyStore.load(null, null); - keyStore.setCertificateEntry("electrumx", certificate); - - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(keyStore); + TrustManager[] trustManagers = getTrustManagers(crtFile); SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, trustManagerFactory.getTrustManagers(), null); + sslContext.init(null, trustManagers, null); sslSocketFactory = sslContext.getSocketFactory(); } + private TrustManager[] getTrustManagers(File crtFile) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { + if(crtFile == null) { + return 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 { + if(certs.length == 0) { + throw new CertificateException("No server certificate provided"); + } + + certs[0].checkValidity(); + } + } + }; + } + + Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(new FileInputStream(crtFile)); + if(certificate instanceof X509Certificate) { + try { + X509Certificate x509Certificate = (X509Certificate)certificate; + x509Certificate.checkValidity(); + } catch(CertificateException e) { + crtFile.delete(); + return getTrustManagers(null); + } + } + + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, null); + keyStore.setCertificateEntry("electrum-server", certificate); + + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + + return trustManagerFactory.getTrustManagers(); + } + protected Socket createSocket() throws IOException { SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(server.getHost(), server.getPortOrDefault(DEFAULT_PORT)); - sslSocket.startHandshake(); - + startHandshake(sslSocket); return sslSocket; } + + protected void startHandshake(SSLSocket sslSocket) throws IOException { + sslSocket.addHandshakeCompletedListener(event -> { + if(Storage.getCertificateFile(server.getHost()) == null) { + try { + Certificate[] certs = event.getPeerCertificates(); + if(certs.length > 0) { + Storage.saveCertificate(server.getHost(), certs[0]); + } + } catch(SSLPeerUnverifiedException e) { + log.warn("Attempting to retrieve certificate for unverified peer", e); + } + } + }); + + sslSocket.startHandshake(); + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java index 62cd1e35..af69fcf0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java @@ -10,6 +10,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.SocketFactory; +import javax.net.ssl.SSLHandshakeException; import java.io.*; import java.net.Socket; import java.nio.charset.StandardCharsets; @@ -192,6 +193,8 @@ public class TcpTransport implements Transport, Closeable { try { socket = createSocket(); running = true; + } catch(SSLHandshakeException e) { + throw new TlsServerException(server, e); } catch(IOException e) { throw new ServerException(e); } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TlsServerException.java b/src/main/java/com/sparrowwallet/sparrow/net/TlsServerException.java new file mode 100644 index 00000000..08a47295 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/TlsServerException.java @@ -0,0 +1,59 @@ +package com.sparrowwallet.sparrow.net; + +import com.google.common.net.HostAndPort; +import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.io.Storage; + +import java.io.File; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; + +public class TlsServerException extends ServerException { + private final HostAndPort server; + + public TlsServerException(HostAndPort server) { + this.server = server; + } + + public TlsServerException(HostAndPort server, String message) { + super(message); + this.server = server; + } + + public TlsServerException(HostAndPort server, Throwable cause) { + this(server, getMessage(cause, server), cause); + } + + public TlsServerException(HostAndPort server, String message, Throwable cause) { + super(message, cause); + this.server = server; + } + + public HostAndPort getServer() { + return server; + } + + private static String getMessage(Throwable cause, HostAndPort server) { + if(cause != null) { + if(cause.getMessage().contains("PKIX path building failed")) { + File configCrtFile = Config.get().getElectrumServerCert(); + File savedCrtFile = Storage.getCertificateFile(server.getHost()); + if(configCrtFile != null) { + return "Provided server certificate from " + server.getHost() + " did not match configured certificate at " + configCrtFile.getAbsolutePath(); + } else if(savedCrtFile != null) { + return "Provided server certificate from " + server.getHost() + " did not match previously saved certificate at " + savedCrtFile.getAbsolutePath(); + } + + return "Provided server certificate from " + server.getHost() + " was invalid: " + (cause.getCause() != null ? cause.getCause().getMessage() : cause.getMessage()); + } else if(cause.getCause() instanceof CertificateNotYetValidException) { + return cause.getMessage().replace("NotBefore:", "Certificate only valid from"); + } else if(cause.getCause() instanceof CertificateExpiredException) { + return cause.getMessage().replace("NotAfter:", "Certificate expired at"); + } + + return cause.getMessage(); + } + + return "SSL Handshake Error"; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TorTcpOverTlsTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TorTcpOverTlsTransport.java index 5fc8d638..c16cf03a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TorTcpOverTlsTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TorTcpOverTlsTransport.java @@ -17,7 +17,7 @@ import java.security.cert.CertificateException; public class TorTcpOverTlsTransport extends TcpOverTlsTransport { private static final Logger log = LoggerFactory.getLogger(TorTcpOverTlsTransport.class); - public TorTcpOverTlsTransport(HostAndPort server) throws NoSuchAlgorithmException, KeyManagementException { + public TorTcpOverTlsTransport(HostAndPort server) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { super(server); } @@ -42,7 +42,7 @@ public class TorTcpOverTlsTransport extends TcpOverTlsTransport { } SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, server.getHost(), server.getPortOrDefault(DEFAULT_PORT), true); - sslSocket.startHandshake(); + startHandshake(sslSocket); return sslSocket; } diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java index 3dbd7d26..55ce1873 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java @@ -11,6 +11,7 @@ import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.net.*; import javafx.application.Platform; import javafx.beans.value.ChangeListener; @@ -35,13 +36,13 @@ import org.slf4j.LoggerFactory; import tornadofx.control.Field; import tornadofx.control.Form; -import javax.net.ssl.SSLHandshakeException; import java.io.File; import java.io.FileInputStream; import java.security.cert.CertificateFactory; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.List; +import java.util.Optional; import java.util.Random; public class ServerPreferencesController extends PreferencesDetailController { @@ -545,10 +546,27 @@ public class ServerPreferencesController extends PreferencesDetailController { private void showConnectionFailure(Throwable exception) { log.error("Connection error", exception); String reason = exception.getCause() != null ? exception.getCause().getMessage() : exception.getMessage(); - if(exception.getCause() != null && exception.getCause() instanceof SSLHandshakeException) { - reason = "SSL Handshake Error\n" + reason; - } - if(exception.getCause() != null && exception.getCause() instanceof TorControlError && exception.getCause().getMessage().contains("Failed to bind")) { + if(exception instanceof TlsServerException && exception.getCause() != null) { + TlsServerException tlsServerException = (TlsServerException)exception; + if(exception.getCause().getMessage().contains("PKIX path building failed")) { + File configCrtFile = Config.get().getElectrumServerCert(); + File savedCrtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost()); + if(configCrtFile == null && savedCrtFile != null) { + Optional optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " appears to have changed." + + "\n\nThis may indicate a man-in-the-middle attack!" + + "\n\nDo you still want to proceed?", ButtonType.NO, ButtonType.YES); + if(optButton.isPresent()) { + if(optButton.get() == ButtonType.YES) { + savedCrtFile.delete(); + Platform.runLater(this::startElectrumConnection); + return; + } + } + } + } + + reason = tlsServerException.getMessage() + "\n\n" + reason; + } else if(exception.getCause() != null && exception.getCause() instanceof TorControlError && exception.getCause().getMessage().contains("Failed to bind")) { reason += "\nIs a Tor proxy already running on port " + TorService.PROXY_PORT + "?"; }