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