Contents
- TLS Concepts & Java Components
- KeyStore — Loading & Creating
- keytool — Managing Certificates
- Building an SSLContext
- Secure HttpClient
- Mutual TLS (mTLS)
- Custom TrustManager
- Best Practices & Common Mistakes
TLS (Transport Layer Security) authenticates and encrypts network connections. Java maps TLS concepts to these classes:
- KeyStore — an in-memory database of keys and certificates (backed by .jks, .p12, or .pem files)
- KeyManager / KeyManagerFactory — selects the server's own certificate to present to clients (or the client's cert in mTLS)
- TrustManager / TrustManagerFactory — decides whether to trust a remote peer's certificate
- SSLContext — top-level factory that ties together key/trust managers and protocol version (TLSv1.3)
- SSLSocketFactory / SSLEngine — creates TLS-wrapped sockets or NIO-style engines from an SSLContext
Use PKCS12 (.p12) format for new keystores — it is the modern standard and the JDK default since Java 9. The older JKS format is proprietary and being phased out.
The class below shows the implementation. Key points are highlighted in the inline comments.
import java.io.*;
import java.security.*;
import java.security.cert.*;
// Load an existing PKCS12 keystore from classpath/file
KeyStore ks = KeyStore.getInstance("PKCS12");
try (InputStream is = new FileInputStream("/certs/server.p12")) {
char[] password = "changeit".toCharArray();
ks.load(is, password);
}
// Inspect contents
Enumeration<String> aliases = ks.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
System.out.println(alias + " — " + ks.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class));
}
// Load a PEM certificate into a KeyStore (e.g., from a CA bundle)
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Certificate caCert;
try (InputStream is = new FileInputStream("/certs/ca.crt")) {
caCert = cf.generateCertificate(is);
}
KeyStore trustStore = KeyStore.getInstance("PKCS12");
trustStore.load(null, null); // empty store
trustStore.setCertificateEntry("my-ca", caCert);
// Create a new empty keystore and save it
KeyStore newKs = KeyStore.getInstance("PKCS12");
newKs.load(null, null);
try (OutputStream os = new FileOutputStream("new.p12")) {
newKs.store(os, "newpassword".toCharArray());
}
The JDK ships with keytool for common certificate operations — no external tools needed for development and testing.
# Generate a self-signed certificate + private key in a new PKCS12 keystore
keytool -genkeypair \
-alias server \
-keyalg RSA -keysize 2048 \
-validity 365 \
-storetype PKCS12 \
-keystore server.p12 \
-storepass changeit \
-dname "CN=localhost, OU=Dev, O=MyOrg, L=City, ST=State, C=US"
# Export the certificate as PEM (for importing into trust stores or sharing with clients)
keytool -exportcert -alias server -keystore server.p12 \
-storepass changeit -rfc -file server.crt
# Import a CA certificate into a trust store
keytool -importcert -alias my-ca -file ca.crt \
-keystore truststore.p12 -storepass trustpass -noprompt
# List contents of a keystore
keytool -list -v -keystore server.p12 -storepass changeit
# Generate a Certificate Signing Request (CSR) for a real CA
keytool -certreq -alias server -keystore server.p12 \
-storepass changeit -file server.csr
An SSLContext requires a KeyManager[] (own identity) and a TrustManager[] (trusted peers). Either can be null to use JVM defaults.
import javax.net.ssl.*;
// --- Load keystore (server identity) ---
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (InputStream is = new FileInputStream("server.p12")) {
keyStore.load(is, "changeit".toCharArray());
}
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); // "SunX509"
kmf.init(keyStore, "changeit".toCharArray());
// --- Load truststore (trusted CAs) ---
KeyStore trustStore = KeyStore.getInstance("PKCS12");
try (InputStream is = new FileInputStream("truststore.p12")) {
trustStore.load(is, "trustpass".toCharArray());
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
// --- Build SSLContext ---
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
// Use TLSv1.3 only (Java 11+)
SSLParameters params = sslContext.getDefaultSSLParameters();
params.setProtocols(new String[]{"TLSv1.3"});
Java 11's HttpClient accepts an SSLContext directly, replacing the global HttpsURLConnection defaults:
import java.net.http.*;
import java.net.URI;
// Build client with custom SSLContext (e.g., trusting a private CA)
HttpClient client = HttpClient.newBuilder()
.sslContext(sslContext) // from previous section
.sslParameters(sslContext.getDefaultSSLParameters())
.version(HttpClient.Version.HTTP_2)
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://internal.mycompany.com/api/data"))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + ": " + response.body());
In mTLS both sides present certificates. The server loads a keystore (its own cert) and a truststore (trusted client CAs). The client does the same in reverse.
// SERVER SIDE — Spring Boot application.yml equivalent, done programmatically
SSLContext serverCtx = SSLContext.getInstance("TLS");
serverCtx.init(serverKmf.getKeyManagers(), serverTmf.getTrustManagers(), null);
SSLServerSocketFactory factory = serverCtx.getServerSocketFactory();
SSLServerSocket serverSocket = (SSLServerSocket) factory.createServerSocket(8443);
serverSocket.setNeedClientAuth(true); // ← enforce client certificate
// CLIENT SIDE — must present its own certificate
SSLContext clientCtx = SSLContext.getInstance("TLS");
clientCtx.init(clientKmf.getKeyManagers(), // client's private key + cert
clientTmf.getTrustManagers(), // trust the server's CA
null);
HttpClient mtlsClient = HttpClient.newBuilder()
.sslContext(clientCtx)
.build();
For Spring Boot mTLS, set server.ssl.client-auth=need in application.properties along with server.ssl.trust-store pointing to the CA bundle that signed client certificates.
Implement X509TrustManager when you need custom certificate validation logic — e.g., pinning a specific certificate fingerprint, trusting self-signed certs in dev, or adding extra checks beyond the standard chain validation.
import javax.net.ssl.X509TrustManager;
import java.security.cert.*;
// ✅ Certificate pinning — reject certs whose SHA-256 fingerprint doesn't match
public class PinningTrustManager implements X509TrustManager {
private static final String EXPECTED_FINGERPRINT =
"AB:CD:12:34:..."; // SHA-256 hex of the leaf cert
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
// 1. Delegate to the default trust manager for CA chain validation
defaultTrustManager.checkServerTrusted(chain, authType);
// 2. Additionally pin the leaf certificate fingerprint
String actual = getFingerprint(chain[0]);
if (!EXPECTED_FINGERPRINT.equalsIgnoreCase(actual)) {
throw new CertificateException("Certificate pinning failed. Got: " + actual);
}
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException { /* not used for outbound calls */ }
@Override
public X509Certificate[] getAcceptedIssuers() {
return defaultTrustManager.getAcceptedIssuers();
}
private String getFingerprint(X509Certificate cert) throws CertificateException {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(cert.getEncoded());
StringBuilder sb = new StringBuilder();
for (int i = 0; i < digest.length; i++) {
if (i > 0) sb.append(':');
sb.append(String.format("%02X", digest[i]));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
throw new CertificateException(e);
}
}
}
Never use a trust-all TrustManager (checkServerTrusted that does nothing) in production. It disables certificate validation entirely, making connections vulnerable to man-in-the-middle attacks. Restrict it to isolated integration tests.
The class below shows the implementation. Key points are highlighted in the inline comments.
// ❌ WRONG — trust-all TrustManager (only ever for throwaway test code)
SSLContext insecure = SSLContext.getInstance("TLS");
insecure.init(null, new TrustManager[]{ new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] c, String a) {}
public void checkServerTrusted(X509Certificate[] c, String a) {}
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
}}, null);
// ✅ CORRECT — always validate chains; load only the specific CA you trust
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); // most strict algorithm
tmf.init(trustStore);
// ❌ WRONG — setting the default SSLContext globally (affects all code in the JVM)
SSLContext.setDefault(myCustomContext);
// ✅ CORRECT — pass SSLContext explicitly to each client
HttpClient client = HttpClient.newBuilder().sslContext(myCustomContext).build();
// ❌ WRONG — hardcoding passwords in source code
char[] password = "changeit".toCharArray();
// ✅ CORRECT — read from environment variables, Vault, or AWS Secrets Manager
char[] password = System.getenv("KEYSTORE_PASSWORD").toCharArray();
// ✅ TLSv1.3 only — disable older vulnerable protocols
SSLParameters params = new SSLParameters();
params.setProtocols(new String[]{"TLSv1.3"});
params.setCipherSuites(new String[]{
"TLS_AES_128_GCM_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_CHACHA20_POLY1305_SHA256"
});
// ✅ Always close keystores' input streams and zero out password arrays after use
Arrays.fill(password, '\0');