SSL/TLS Sockets in Java: SSLSocket, Certificate Validation & Hostname Verification

Java's JSSE (Java Secure Socket Extension) wraps plain TCP sockets with TLS encryption. SSLSocket and SSLServerSocket provide the same stream-based API as their plain counterparts but negotiate a TLS session before any data is exchanged. This article covers SSLContext setup, certificate validation, hostname verification, and mutual TLS (mTLS).

Share

JSSE Architecture Overview

ComponentRole
SSLContextFactory for SSLSocketFactory and SSLServerSocketFactory; holds key material and trust anchors
KeyStoreRepository of certificates and private keys (JKS, PKCS12)
KeyManagerSelects which client certificate to present during handshake
TrustManagerDecides whether to trust the server's certificate chain
SSLSocket / SSLServerSocketTLS-wrapped stream sockets
SSLSessionNegotiated cipher suite, protocol version, peer certificates
HostnameVerifierValidates the server's hostname against the certificate CN/SAN

Using the Default SSLContext (Quick Start)

For connections to public HTTPS servers the default SSLContext trusts the JVM's built-in CA bundle: no configuration needed.

Java
import javax.net.ssl.*;
import java.io.*;

// Client connecting to a public server
SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();

try (SSLSocket socket = (SSLSocket) factory.createSocket("example.com", 443)) {
    socket.startHandshake();   // negotiate TLS

    SSLSession session = socket.getSession();
    System.out.println("Protocol : " + session.getProtocol());       // TLSv1.3
    System.out.println("Cipher   : " + session.getCipherSuite());
    System.out.println("Peer cert: " + session.getPeerCertificates()[0]);

    // use like a normal socket
    var out = new PrintWriter(socket.getOutputStream(), true);
    var in  = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    out.println("GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n");
    System.out.println(in.readLine());   // HTTP/1.1 200 OK
}

Custom SSLContext: Loading a KeyStore & TrustStore

When connecting to servers with self-signed or private CA certificates you must build a custom SSLContext.

Java
import java.security.*;
import javax.net.ssl.*;
import java.io.*;

public SSLContext buildSslContext(String keystorePath, char[] keystorePassword,
                                  String truststorePath, char[] truststorePassword)
        throws Exception {

    // 1. Load the KeyStore (contains our private key + certificate chain)
    KeyStore keyStore = KeyStore.getInstance("PKCS12");
    try (InputStream ks = new FileInputStream(keystorePath)) {
        keyStore.load(ks, keystorePassword);
    }

    // 2. Load the TrustStore (contains trusted CA certificates)
    KeyStore trustStore = KeyStore.getInstance("JKS");
    try (InputStream ts = new FileInputStream(truststorePath)) {
        trustStore.load(ts, truststorePassword);
    }

    // 3. Initialise KeyManager and TrustManager
    KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    kmf.init(keyStore, keystorePassword);

    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(trustStore);

    // 4. Build the SSLContext
    SSLContext ctx = SSLContext.getInstance("TLS");
    ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
    return ctx;
}
Java
// Use the custom context
SSLContext ctx = buildSslContext("client.p12", "keystorePass".toCharArray(),
                                 "truststore.jks", "trustPass".toCharArray());

SSLSocketFactory factory = ctx.getSocketFactory();
try (SSLSocket socket = (SSLSocket) factory.createSocket("internal-server", 8443)) {
    socket.startHandshake();
    // ... read/write
}

TLS Server with SSLServerSocket

Java
SSLContext ctx = buildSslContext("server.p12", "pass".toCharArray(),
                                 "truststore.jks", "pass".toCharArray());

SSLServerSocketFactory ssf = ctx.getServerSocketFactory();
try (SSLServerSocket server = (SSLServerSocket) ssf.createServerSocket(8443)) {
    // require TLS 1.2+
    server.setEnabledProtocols(new String[]{"TLSv1.2", "TLSv1.3"});

    System.out.println("TLS server on port 8443");
    while (true) {
        SSLSocket client = (SSLSocket) server.accept();
        new Thread(() -> {
            try (client;
                 var in  = new BufferedReader(new InputStreamReader(client.getInputStream()));
                 var out = new PrintWriter(client.getOutputStream(), true)) {
                out.println("Hello over TLS!");
                System.out.println("Client: " + in.readLine());
            } catch (IOException e) { e.printStackTrace(); }
        }).start();
    }
}

Hostname Verification

Hostname verification ensures the server's certificate matches the hostname you are connecting to: it prevents man-in-the-middle attacks even when the certificate is otherwise valid.

SSLSocket does not perform hostname verification by default: you must enable it explicitly using SSLParameters:

Java
SSLSocket socket = (SSLSocket) factory.createSocket("myserver.example.com", 443);

// Enable HTTPS-style hostname verification (checks CN and SANs)
SSLParameters params = socket.getSSLParameters();
params.setEndpointIdentificationAlgorithm("HTTPS");
socket.setSSLParameters(params);

socket.startHandshake();   // throws SSLHandshakeException if hostname doesn't match
Never disable hostname verification in production code. If you see code like setHostnameVerifier((h, s) -> true), it makes the connection vulnerable to MITM attacks.

Mutual TLS (mTLS): Client Certificate Authentication

In mTLS both the client and server authenticate with certificates. The server calls setNeedClientAuth(true) and the client must have a key loaded in its KeyManager.

Java
// Server: require client certificate
try (SSLServerSocket server = (SSLServerSocket) ssf.createServerSocket(8443)) {
    server.setNeedClientAuth(true);   // reject connections without a valid client cert

    SSLSocket client = (SSLSocket) server.accept();
    client.startHandshake();

    // inspect client identity
    java.security.cert.Certificate[] certs = client.getSession().getPeerCertificates();
    System.out.println("Client cert: " + certs[0]);
}
Java
// Client: must have a private key + certificate in its KeyStore
SSLContext ctx = buildSslContext("client.p12", "clientPass".toCharArray(),
                                 "server-ca.jks", "caPass".toCharArray());
SSLSocketFactory sf = ctx.getSocketFactory();
SSLSocket socket = (SSLSocket) sf.createSocket("server", 8443);
socket.startHandshake();   // presents client certificate automatically

Protocol & Cipher Suite Control

Java
SSLSocket socket = (SSLSocket) factory.createSocket("host", 443);

// Restrict to TLS 1.3 only
socket.setEnabledProtocols(new String[]{"TLSv1.3"});

// List available cipher suites
System.out.println(Arrays.toString(socket.getSupportedCipherSuites()));

// Enable only strong cipher suites
socket.setEnabledCipherSuites(new String[]{
    "TLS_AES_256_GCM_SHA384",
    "TLS_AES_128_GCM_SHA256",
    "TLS_CHACHA20_POLY1305_SHA256"
});
ProtocolRecommendation
TLSv1.3✅ Prefer: faster handshake, stronger security, no weak ciphers
TLSv1.2✅ Acceptable when TLS 1.3 not supported by peer
TLSv1.1❌ Deprecated (RFC 8996): disable explicitly
TLSv1.0❌ Deprecated (RFC 8996): disable explicitly
SSLv3❌ Broken (POODLE): never use

Generating Test Certificates with keytool

Quick commands to create a self-signed server certificate for local testing:

Shell
# Generate a self-signed server key + cert in PKCS12 format
keytool -genkeypair -alias server -keyalg RSA -keysize 2048 \
        -validity 365 -dname "CN=localhost,O=Test,C=US" \
        -keystore server.p12 -storetype PKCS12 -storepass changeit

# Export the server cert as PEM for use as a trust anchor
keytool -exportcert -alias server -keystore server.p12 \
        -storepass changeit -rfc -file server-cert.pem

# Import server cert into client's truststore
keytool -importcert -alias server-ca -file server-cert.pem \
        -keystore client-truststore.jks -storepass changeit -noprompt
For local development with self-signed certs, set the system property -Djavax.net.debug=ssl:handshake to see the full TLS handshake log: it shows which certificates are presented and which ciphers are negotiated.