Contents

TLS (Transport Layer Security) authenticates and encrypts network connections. Java maps TLS concepts to these classes:

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.
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.
// ❌ 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');