diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 3f54cb21..ec4fafb8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -328,6 +328,9 @@ public class AppServices { "\n\nChange the configured server certificate if you would like to proceed."); } else { crtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost()); + if(crtFile == null) { + crtFile = Storage.getCaCertificateFile(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 be simply due to a certificate renewal, or it may indicate a man-in-the-middle attack." + diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java index 36917662..b17b0736 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java @@ -504,8 +504,23 @@ public class Storage { } public static File getCertificateFile(String host) { - File certsDir = getCertsDir(); - File[] certs = certsDir.listFiles((dir, name) -> name.equals(getCertName(host))); + return findCertFile(getCertName(host)); + } + + public static void saveCertificate(String host, Certificate cert) { + writeCertPem(getCertName(host), cert); + } + + public static File getCaCertificateFile(String host) { + return findCertFile(host + ".cacert"); + } + + public static void saveCaCertificate(String host, Certificate cert) { + writeCertPem(host + ".cacert", cert); + } + + private static File findCertFile(String filename) { + File[] certs = getCertsDir().listFiles((dir, name) -> name.equals(filename)); if(certs != null && certs.length > 0) { return certs[0]; } @@ -513,8 +528,8 @@ public class Storage { return null; } - public static void saveCertificate(String host, Certificate cert) { - try(FileWriter writer = new FileWriter(new File(getCertsDir(), getCertName(host)))) { + private static void writeCertPem(String filename, Certificate cert) { + try(FileWriter writer = new FileWriter(new File(getCertsDir(), filename))) { writer.write("-----BEGIN CERTIFICATE-----\n"); writer.write(Base64.getEncoder().encodeToString(cert.getEncoded()).replaceAll("(.{64})", "$1\n")); writer.write("\n-----END CERTIFICATE-----\n"); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java index 6d6fdfd8..06cd3da0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java @@ -1,6 +1,7 @@ package com.sparrowwallet.sparrow.net; import com.google.common.net.HostAndPort; +import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Storage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,7 +24,12 @@ public class TcpOverTlsTransport extends TcpTransport { public TcpOverTlsTransport(HostAndPort server) throws NoSuchAlgorithmException, KeyManagementException, CertificateException, KeyStoreException, IOException { super(server); - TrustManager[] trustManagers = getTrustManagers(Storage.getCertificateFile(server.getHost()), server.getHost()); + TrustManager[] trustManagers; + if(Storage.getCaCertificateFile(server.getHost()) != null) { + trustManagers = getCaTrustManagers(); + } else { + trustManagers = getTrustManagers(Storage.getCertificateFile(server.getHost()), server.getHost()); + } SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustManagers, new SecureRandom()); @@ -97,8 +103,11 @@ public class TcpOverTlsTransport extends TcpTransport { X509Certificate x509Certificate = (X509Certificate)certificate; x509Certificate.checkValidity(); } catch(CertificateExpiredException e) { - //Allow expired certificates so long as they have been previously used or explicitly approved - //These will usually be self-signed certificates that users may not have the expertise to renew + if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER) { + crtFile.delete(); + return getTrustManagers(null, host); + } + //Allow expired certificates for private servers where users may not have the expertise to renew } catch(CertificateException e) { crtFile.delete(); return getTrustManagers(null, host); @@ -127,7 +136,11 @@ public class TcpOverTlsTransport extends TcpTransport { try { Certificate[] certs = event.getPeerCertificates(); if(certs.length > 0) { - Storage.saveCertificate(server.getHost(), certs[0]); + if(isCaSigned(certs)) { + Storage.saveCaCertificate(server.getHost(), certs[0]); + } else { + Storage.saveCertificate(server.getHost(), certs[0]); + } } } catch(SSLPeerUnverifiedException e) { log.warn("Attempting to retrieve certificate for unverified peer", e); @@ -139,14 +152,42 @@ public class TcpOverTlsTransport extends TcpTransport { } protected boolean shouldSaveCertificate() { - //Avoid saving the certificates for public servers - they change often, encourage approval complacency, and there is little a user can do to check - for(PublicElectrumServer publicElectrumServer : PublicElectrumServer.getServers()) { - if(publicElectrumServer.getServer().getHost().equals(server.getHost())) { + return Storage.getCertificateFile(server.getHost()) == null && Storage.getCaCertificateFile(server.getHost()) == null; + } + + private static TrustManager[] getCaTrustManagers() throws NoSuchAlgorithmException, KeyStoreException { + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore)null); + return tmf.getTrustManagers(); + } + + private static boolean isCaSigned(Certificate[] certs) { + try { + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore)null); + + X509TrustManager defaultTm = null; + for(TrustManager tm : tmf.getTrustManagers()) { + if(tm instanceof X509TrustManager) { + defaultTm = (X509TrustManager)tm; + break; + } + } + + if(defaultTm == null) { return false; } - } - return Storage.getCertificateFile(server.getHost()) == null; + X509Certificate[] x509Certs = new X509Certificate[certs.length]; + for(int i = 0; i < certs.length; i++) { + x509Certs[i] = (X509Certificate)certs[i]; + } + + defaultTm.checkServerTrusted(x509Certs, "RSA"); + return true; + } catch(Exception e) { + return false; + } } @Override diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TlsServerException.java b/src/main/java/com/sparrowwallet/sparrow/net/TlsServerException.java index 08a47295..0a081686 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TlsServerException.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TlsServerException.java @@ -42,6 +42,8 @@ public class TlsServerException extends ServerException { 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(); + } else if(Storage.getCaCertificateFile(server.getHost()) != null) { + return "Provided server certificate from " + server.getHost() + " failed CA validation"; } return "Provided server certificate from " + server.getHost() + " was invalid: " + (cause.getCause() != null ? cause.getCause().getMessage() : cause.getMessage()); diff --git a/src/main/java/com/sparrowwallet/sparrow/settings/ServerSettingsController.java b/src/main/java/com/sparrowwallet/sparrow/settings/ServerSettingsController.java index 06a932d0..fbc3c178 100644 --- a/src/main/java/com/sparrowwallet/sparrow/settings/ServerSettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/settings/ServerSettingsController.java @@ -670,6 +670,9 @@ public class ServerSettingsController extends SettingsDetailController { if(exception.getCause().getMessage().contains("PKIX path building failed")) { File configCrtFile = Config.get().getElectrumServerCert(); File savedCrtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost()); + if(savedCrtFile == null) { + savedCrtFile = Storage.getCaCertificateFile(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!" + diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/settings/ServerTestDialog.java b/src/main/java/com/sparrowwallet/sparrow/terminal/settings/ServerTestDialog.java index 3b39fc7d..80bf2891 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/settings/ServerTestDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/settings/ServerTestDialog.java @@ -185,6 +185,9 @@ public class ServerTestDialog extends DialogWindow { if(exception.getCause().getMessage().contains("PKIX path building failed")) { File configCrtFile = Config.get().getElectrumServerCert(); File savedCrtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost()); + if(savedCrtFile == null) { + savedCrtFile = Storage.getCaCertificateFile(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!" +