use ca validation or tofu pinning for public servers depending on certificate type
This commit is contained in:
parent
46d444615c
commit
a478edfad7
@ -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<ButtonType> 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." +
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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<ButtonType> 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!" +
|
||||
|
||||
@ -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<ButtonType> 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!" +
|
||||
|
||||
Loading…
Reference in New Issue
Block a user