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.

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');