add hostname verification for ca-validated tls connections
What changed, and why it matters
This commit fixes a bug in Sparrow Wallet's encrypted network connections. When the wallet connected to a server through a proxy (like Tor), it accidentally checked the proxy's identity instead of the actual server's identity. It also adds proper hostname verification for connections that use standard certificate authorities. Without these checks, a malicious actor controlling a proxy or network path could trick the wallet into trusting the wrong server, potentially exposing transaction data or balances.
Users should upgrade to a Sparrow Wallet release containing this commit. Operators of public Electrum servers or backend services should ensure their TLS certificates match the hostnames clients expect. Developers should review other transport paths for similar proxy/TLS hostname mismatches and consider adding tests that fail when hostname verification is disabled.
Security signals we found
Missing hostname verification in CA-validated TLS connections
Wrong hostname passed to TLS layer when connecting through SOCKS proxy
Potential man-in-the-middle exposure for proxied TLS connections
Addition of endpoint identification algorithm (HTTPS) for CA-trusted sockets
Onion addresses explicitly excluded from hostname verification
Evidence from the diff
The patch corrects two TLS validation issues in TcpOverTlsTransport and ProxyTcpOverTlsTransport. First, in ProxyTcpOverTlsTransport, the SSLSocketFactory.createSocket() call previously passed proxy.getHost()/proxy.getPort() as the hostname parameter, which is used by the TLS stack for hostname verification and SNI. It now correctly passes server.getHost()/server.getPort(). Second, the patch introduces a usingCaTrust flag and enables endpoint identification (setEndpointIdentificationAlgorithm(“HTTPS”)) during the handshake for CA-validated connections to non-onion hosts. This ensures hostname verification is performed when relying on the system/CA trust store, matching standard TLS behavior. The onion-address exclusion is intentional because .onion services use self-signed certificates and different trust semantics.
Changed components
src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.javasrc/main/java/com/sparrowwallet/sparrow/net/ProxyTcpOverTlsTransport.javaInspect captured patch +11 / −1
diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ProxyTcpOverTlsTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/ProxyTcpOverTlsTransport.java
index d865f87..135f163 100644
--- a/src/main/java/com/sparrowwallet/sparrow/net/ProxyTcpOverTlsTransport.java
+++ b/src/main/java/com/sparrowwallet/sparrow/net/ProxyTcpOverTlsTransport.java
@@ -33,7 +33,7 @@ public class ProxyTcpOverTlsTransport extends TcpOverTlsTransport {
InetSocketAddress proxyAddr = new InetSocketAddress(proxy.getHost(), proxy.getPortOrDefault(DEFAULT_PROXY_PORT));
socket = new Socket(new Proxy(Proxy.Type.SOCKS, proxyAddr));
socket.connect(InetSocketAddress.createUnresolved(server.getHost(), server.getPortOrDefault(getDefaultPort())));
- socket = sslSocketFactory.createSocket(socket, proxy.getHost(), proxy.getPortOrDefault(DEFAULT_PROXY_PORT), true);
+ socket = sslSocketFactory.createSocket(socket, server.getHost(), server.getPortOrDefault(getDefaultPort()), true);
startHandshake((SSLSocket)socket);
}
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java
index 06cd3da..791626d 100644
--- a/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java
+++ b/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java
@@ -20,6 +20,7 @@ public class TcpOverTlsTransport extends TcpTransport {
public static final int PAD_TO_MULTIPLE_OF_BYTES = 96;
protected final SSLSocketFactory sslSocketFactory;
+ protected final boolean usingCaTrust;
public TcpOverTlsTransport(HostAndPort server) throws NoSuchAlgorithmException, KeyManagementException, CertificateException, KeyStoreException, IOException {
super(server);
@@ -27,8 +28,10 @@ public class TcpOverTlsTransport extends TcpTransport {
TrustManager[] trustManagers;
if(Storage.getCaCertificateFile(server.getHost()) != null) {
trustManagers = getCaTrustManagers();
+ this.usingCaTrust = true;
} else {
trustManagers = getTrustManagers(Storage.getCertificateFile(server.getHost()), server.getHost());
+ this.usingCaTrust = false;
}
SSLContext sslContext = SSLContext.getInstance("TLS");
@@ -40,6 +43,7 @@ public class TcpOverTlsTransport extends TcpTransport {
public TcpOverTlsTransport(HostAndPort server, File crtFile) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
super(server);
+ this.usingCaTrust = false;
TrustManager[] trustManagers = getTrustManagers(crtFile, server.getHost());
SSLContext sslContext = SSLContext.getInstance("TLS");
@@ -148,6 +152,12 @@ public class TcpOverTlsTransport extends TcpTransport {
}
});
+ if(usingCaTrust && !Protocol.isOnionAddress(server)) {
+ SSLParameters sslParameters = sslSocket.getSSLParameters();
+ sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
+ sslSocket.setSSLParameters(sslParameters);
+ }
+
sslSocket.startHandshake();
}
Why this scored 74/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.