mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 05:06:45 +00:00
add ssl server certificate pinning
This commit is contained in:
parent
771bd1545c
commit
5d91f033c0
11 changed files with 254 additions and 48 deletions
|
@ -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_INACTIVE = 0.8;
|
||||||
public static final double TAB_LABEL_GRAPHIC_OPACITY_ACTIVE = 0.95;
|
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 LOADING_TRANSACTIONS_MESSAGE = "Loading wallet, select Transactions tab to view...";
|
||||||
|
public static final String CONNECTION_FAILED_PREFIX = "Connection failed: ";
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private MenuItem saveTransaction;
|
private MenuItem saveTransaction;
|
||||||
|
@ -1451,8 +1452,7 @@ public class AppController implements Initializable {
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void connectionFailed(ConnectionFailedEvent event) {
|
public void connectionFailed(ConnectionFailedEvent event) {
|
||||||
String reason = event.getException().getCause() != null ? event.getException().getCause().getMessage() : event.getException().getMessage();
|
String status = CONNECTION_FAILED_PREFIX + event.getMessage();
|
||||||
String status = "Connection failed: " + reason;
|
|
||||||
statusUpdated(new StatusEvent(status));
|
statusUpdated(new StatusEvent(status));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1465,7 +1465,7 @@ public class AppController implements Initializable {
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void disconnection(DisconnectionEvent event) {
|
public void disconnection(DisconnectionEvent event) {
|
||||||
serverToggle.setDisable(false);
|
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"));
|
statusUpdated(new StatusEvent("Disconnected"));
|
||||||
}
|
}
|
||||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||||
|
|
|
@ -208,6 +208,31 @@ public class AppServices {
|
||||||
connectionService.setRestartOnFailure(false);
|
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<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!" +
|
||||||
|
"\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.removeListener(onlineServicesListener);
|
||||||
onlineProperty.setValue(false);
|
onlineProperty.setValue(false);
|
||||||
onlineProperty.addListener(onlineServicesListener);
|
onlineProperty.addListener(onlineServicesListener);
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package com.sparrowwallet.sparrow.event;
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.net.TlsServerException;
|
||||||
|
|
||||||
public class ConnectionFailedEvent {
|
public class ConnectionFailedEvent {
|
||||||
private final Throwable exception;
|
private final Throwable exception;
|
||||||
|
|
||||||
|
@ -10,4 +12,26 @@ public class ConnectionFailedEvent {
|
||||||
public Throwable getException() {
|
public Throwable getException() {
|
||||||
return exception;
|
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])"
|
||||||
|
),
|
||||||
|
" "
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,8 @@ import java.lang.reflect.Type;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
@ -44,6 +46,7 @@ public class Storage {
|
||||||
public static final String WINDOWS_SPARROW_DIR = "Sparrow";
|
public static final String WINDOWS_SPARROW_DIR = "Sparrow";
|
||||||
public static final String WALLETS_DIR = "wallets";
|
public static final String WALLETS_DIR = "wallets";
|
||||||
public static final String WALLETS_BACKUP_DIR = "backup";
|
public static final String WALLETS_BACKUP_DIR = "backup";
|
||||||
|
public static final String CERTS_DIR = "certs";
|
||||||
public static final String HEADER_MAGIC_1 = "SPRW1";
|
public static final String HEADER_MAGIC_1 = "SPRW1";
|
||||||
private static final int BINARY_HEADER_LENGTH = 28;
|
private static final int BINARY_HEADER_LENGTH = 28;
|
||||||
public static final String TEMP_BACKUP_EXTENSION = "tmp";
|
public static final String TEMP_BACKUP_EXTENSION = "tmp";
|
||||||
|
@ -384,6 +387,37 @@ public class Storage {
|
||||||
return walletsDir;
|
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() {
|
static File getSparrowDir() {
|
||||||
if(Network.get() != Network.MAINNET) {
|
if(Network.get() != Network.MAINNET) {
|
||||||
return new File(getSparrowHome(), Network.get().getName());
|
return new File(getSparrowHome(), Network.get().getName());
|
||||||
|
|
|
@ -39,7 +39,7 @@ public enum Protocol {
|
||||||
},
|
},
|
||||||
SSL {
|
SSL {
|
||||||
@Override
|
@Override
|
||||||
public Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException {
|
public Transport getTransport(HostAndPort server) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||||
if(isOnionAddress(server)) {
|
if(isOnionAddress(server)) {
|
||||||
return new TorTcpOverTlsTransport(server);
|
return new TorTcpOverTlsTransport(server);
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ public enum Protocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
return new ProxyTcpOverTlsTransport(server, proxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,27 +68,27 @@ public enum Protocol {
|
||||||
},
|
},
|
||||||
HTTP {
|
HTTP {
|
||||||
@Override
|
@Override
|
||||||
public Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException {
|
public Transport getTransport(HostAndPort server) {
|
||||||
throw new UnsupportedOperationException("No transport supported for HTTP");
|
throw new UnsupportedOperationException("No transport supported for HTTP");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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");
|
throw new UnsupportedOperationException("No transport supported for HTTP");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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");
|
throw new UnsupportedOperationException("No transport supported for HTTP");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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");
|
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;
|
public abstract Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException;
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ public class ProxyTcpOverTlsTransport extends TcpOverTlsTransport {
|
||||||
|
|
||||||
private final HostAndPort proxy;
|
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);
|
super(server);
|
||||||
this.proxy = proxy;
|
this.proxy = proxy;
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ public class ProxyTcpOverTlsTransport extends TcpOverTlsTransport {
|
||||||
Socket underlying = new Socket(new Proxy(Proxy.Type.SOCKS, proxyAddr));
|
Socket underlying = new Socket(new Proxy(Proxy.Type.SOCKS, proxyAddr));
|
||||||
underlying.connect(new InetSocketAddress(server.getHost(), server.getPortOrDefault(DEFAULT_PORT)));
|
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 sslSocket = (SSLSocket) sslSocketFactory.createSocket(underlying, proxy.getHost(), proxy.getPortOrDefault(DEFAULT_PROXY_PORT), true);
|
||||||
sslSocket.startHandshake();
|
startHandshake(sslSocket);
|
||||||
|
|
||||||
return sslSocket;
|
return sslSocket;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package com.sparrowwallet.sparrow.net;
|
package com.sparrowwallet.sparrow.net;
|
||||||
|
|
||||||
import com.google.common.net.HostAndPort;
|
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 javax.net.ssl.*;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -8,35 +11,23 @@ import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.security.*;
|
import java.security.*;
|
||||||
|
import java.security.cert.*;
|
||||||
import java.security.cert.Certificate;
|
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 {
|
public class TcpOverTlsTransport extends TcpTransport {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(TcpOverTlsTransport.class);
|
||||||
|
|
||||||
public static final int DEFAULT_PORT = 50002;
|
public static final int DEFAULT_PORT = 50002;
|
||||||
|
|
||||||
protected final SSLSocketFactory sslSocketFactory;
|
protected final SSLSocketFactory sslSocketFactory;
|
||||||
|
|
||||||
public TcpOverTlsTransport(HostAndPort server) throws NoSuchAlgorithmException, KeyManagementException {
|
public TcpOverTlsTransport(HostAndPort server) throws NoSuchAlgorithmException, KeyManagementException, CertificateException, KeyStoreException, IOException {
|
||||||
super(server);
|
super(server);
|
||||||
|
|
||||||
TrustManager[] trustAllCerts = new TrustManager[]{
|
TrustManager[] trustManagers = getTrustManagers(Storage.getCertificateFile(server.getHost()));
|
||||||
new X509TrustManager() {
|
|
||||||
public X509Certificate[] getAcceptedIssuers() {
|
|
||||||
return new X509Certificate[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkClientTrusted(X509Certificate[] certs, String authType) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkServerTrusted(X509Certificate[] certs, String authType) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||||
sslContext.init(null, trustAllCerts, new SecureRandom());
|
sslContext.init(null, trustManagers, new SecureRandom());
|
||||||
|
|
||||||
this.sslSocketFactory = sslContext.getSocketFactory();
|
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 {
|
public TcpOverTlsTransport(HostAndPort server, File crtFile) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||||
super(server);
|
super(server);
|
||||||
|
|
||||||
Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(new FileInputStream(crtFile));
|
TrustManager[] trustManagers = getTrustManagers(crtFile);
|
||||||
|
|
||||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
|
||||||
keyStore.load(null, null);
|
|
||||||
keyStore.setCertificateEntry("electrumx", certificate);
|
|
||||||
|
|
||||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
|
||||||
trustManagerFactory.init(keyStore);
|
|
||||||
|
|
||||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||||
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
|
sslContext.init(null, trustManagers, null);
|
||||||
|
|
||||||
sslSocketFactory = sslContext.getSocketFactory();
|
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 {
|
protected Socket createSocket() throws IOException {
|
||||||
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(server.getHost(), server.getPortOrDefault(DEFAULT_PORT));
|
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(server.getHost(), server.getPortOrDefault(DEFAULT_PORT));
|
||||||
sslSocket.startHandshake();
|
startHandshake(sslSocket);
|
||||||
|
|
||||||
return 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.net.SocketFactory;
|
import javax.net.SocketFactory;
|
||||||
|
import javax.net.ssl.SSLHandshakeException;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
@ -192,6 +193,8 @@ public class TcpTransport implements Transport, Closeable {
|
||||||
try {
|
try {
|
||||||
socket = createSocket();
|
socket = createSocket();
|
||||||
running = true;
|
running = true;
|
||||||
|
} catch(SSLHandshakeException e) {
|
||||||
|
throw new TlsServerException(server, e);
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
throw new ServerException(e);
|
throw new ServerException(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ import java.security.cert.CertificateException;
|
||||||
public class TorTcpOverTlsTransport extends TcpOverTlsTransport {
|
public class TorTcpOverTlsTransport extends TcpOverTlsTransport {
|
||||||
private static final Logger log = LoggerFactory.getLogger(TorTcpOverTlsTransport.class);
|
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);
|
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 sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, server.getHost(), server.getPortOrDefault(DEFAULT_PORT), true);
|
||||||
sslSocket.startHandshake();
|
startHandshake(sslSocket);
|
||||||
|
|
||||||
return sslSocket;
|
return sslSocket;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
|
||||||
import com.sparrowwallet.sparrow.event.*;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.io.Storage;
|
||||||
import com.sparrowwallet.sparrow.net.*;
|
import com.sparrowwallet.sparrow.net.*;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
|
@ -35,13 +36,13 @@ import org.slf4j.LoggerFactory;
|
||||||
import tornadofx.control.Field;
|
import tornadofx.control.Field;
|
||||||
import tornadofx.control.Form;
|
import tornadofx.control.Form;
|
||||||
|
|
||||||
import javax.net.ssl.SSLHandshakeException;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
public class ServerPreferencesController extends PreferencesDetailController {
|
public class ServerPreferencesController extends PreferencesDetailController {
|
||||||
|
@ -545,10 +546,27 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||||
private void showConnectionFailure(Throwable exception) {
|
private void showConnectionFailure(Throwable exception) {
|
||||||
log.error("Connection error", exception);
|
log.error("Connection error", exception);
|
||||||
String reason = exception.getCause() != null ? exception.getCause().getMessage() : exception.getMessage();
|
String reason = exception.getCause() != null ? exception.getCause().getMessage() : exception.getMessage();
|
||||||
if(exception.getCause() != null && exception.getCause() instanceof SSLHandshakeException) {
|
if(exception instanceof TlsServerException && exception.getCause() != null) {
|
||||||
reason = "SSL Handshake Error\n" + reason;
|
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<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!" +
|
||||||
|
"\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;
|
||||||
}
|
}
|
||||||
if(exception.getCause() != null && exception.getCause() instanceof TorControlError && exception.getCause().getMessage().contains("Failed to bind")) {
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 + "?";
|
reason += "\nIs a Tor proxy already running on port " + TorService.PROXY_PORT + "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue