Without really knowing what SNI was I tried to get some insight with the test-program shown below.
I don't know the output from the openssl s_client
command, but the test-program might prove to be a starting point. When the javax.net.debug
output is turned on a lot of output is dumped of which only a few lines are relevant (see also the comments). That is a bit annoying and I do not have an easy solution for that. The TrustAllServers
class can be reworked to inspect the certificates you expect to receive from the server (a.ka. host) for a particular domain. There might be other options (e.g. the socket's handshake methods) but this is as far as I got.
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedTrustManager;
// https://stackoverflow.com/questions/56005883/java-equivalent-to-openssl-s-client-command
// Please use latest Java 8 version, bugs are around in earlier versions.
public class ServerNameTest {
public static void main(String[] args) {
// SSL debug options, see https://stackoverflow.com/q/23659564/3080094 and https://access.redhat.com/solutions/973783
// System.setProperty("javax.net.debug", "all");
// System.setProperty("javax.net.debug", "ssl:handshake");
// System.setProperty("jsse.enableSNIExtension", "true"); // "true" is the default
try {
ServerNameTest sn = new ServerNameTest();
// This will show 2 different server certificate chains.
// Note this is a random server - please pick your own one.
sn.test("major.io", "rackerhacker.com");
sn.test("major.io", "major.io");
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Done");
}
/*
* With javax.net.debug output you should see something like:
* <pre>
* *** ClientHello
* ...
* Extension server_name, server_name: [type=host_name (0), value=DOMAIN;]
* ...
* *** ServerHello
* ...
* Extension server_name, server_name:
* ...
* </pre>
* Note that if the server does not provide a value for server_name,
* it does not actually mean the server does not support SNI/server_name (see https://serverfault.com/a/506303)
*/
void test(String host, String domain) throws Exception {
SSLParameters sslParams = new SSLParameters();
if (domain != null && !domain.isEmpty()) {
sslParams.setServerNames(Arrays.asList(new SNIHostName(domain)));
}
// Only for webservers: set endpoint algorithm to HTTPS
sslParams.setEndpointIdentificationAlgorithm("HTTPS");
SSLSocketFactory sslsf = serverTrustingSSLFactory();
try (SSLSocket socket = (SSLSocket) sslsf.createSocket()) {
socket.setSSLParameters(sslParams);
socket.setSoTimeout(3_000);
System.out.println("Connecting to " + host + " for domain " + domain);
socket.connect(new InetSocketAddress(host, 443), 3_000);
// Trigger actual connection by getting the session.
socket.getSession();
System.out.println("Connected to remote " + socket.getRemoteSocketAddress());
try (BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))) {
try (OutputStream out = socket.getOutputStream()) {
System.out.println(">> OPTIONS");
out.write("OPTIONS * HTTP/1.1\r\n\r\n".getBytes(StandardCharsets.UTF_8));
System.out.println("<< " + input.readLine());
}
} catch (Exception e) {
System.err.println("No line read: " + e);
}
}
}
SSLSocketFactory serverTrustingSSLFactory() throws Exception {
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, trustManager(), null);
return ctx.getSocketFactory();
}
TrustManager[] trustManager() throws Exception {
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init( (KeyStore) null);
// Must use "extended" type versus the default javax.net.ssl.X509TrustManager,
// otherwise the error "No subject alternative DNS name matching" keeps showing up.
X509ExtendedTrustManager defaultManager = null;
for (TrustManager trustManager : tmf.getTrustManagers()) {
if (trustManager instanceof X509ExtendedTrustManager) {
defaultManager = (X509ExtendedTrustManager) trustManager;
break;
}
}
if (defaultManager == null) {
throw new RuntimeException("Cannot find default X509ExtendedTrustManager");
}
return new TrustManager[] { new TrustAllServers(defaultManager) };
}
static void printChain(X509Certificate[] chain) {
try {
for (int i = 0; i < chain.length; i++) {
X509Certificate cert = chain[i];
System.out.println("Cert[" + i + "] " + cert.getSubjectX500Principal() + " :alt: " + cert.getSubjectAlternativeNames());
}
} catch (Exception e) {
e.printStackTrace();
}
}
static class TrustAllServers extends X509ExtendedTrustManager {
final X509ExtendedTrustManager defaultManager;
public TrustAllServers(X509ExtendedTrustManager defaultManager) {
this.defaultManager = defaultManager;
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
try {
defaultManager.checkServerTrusted(chain, authType);
} catch (Exception e) {
System.err.println("Untrusted server: " + e);
}
printChain(chain);
}
public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
try {
defaultManager.checkServerTrusted(chain, authType, socket);
} catch (Exception e) {
System.err.println("Untrusted server for socket: " + e);
}
printChain(chain);
}
public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException {
try {
defaultManager.checkServerTrusted(chain, authType, engine);
} catch (Exception e) {
System.err.println("Untrusted server for engine: " + e);
}
printChain(chain);
}
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
defaultManager.checkClientTrusted(chain, authType);
}
public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
defaultManager.checkClientTrusted(chain, authType, socket);
}
public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException {
defaultManager.checkClientTrusted(chain, authType, engine);
}
public X509Certificate[] getAcceptedIssuers() {
return defaultManager.getAcceptedIssuers();
}
}
}