Core Java

Java HTTPS with Multiple Client Certificates

This article explains how to implement Java SSL with multiple client certificates, enabling per-connection certificate selection for secure mutual TLS authentication. It covers practical approaches using multiple SSLContext instances and custom KeyManager routing, along with examples for both HttpsURLConnection and Apache HttpComponents to ensure flexibility and dynamic certificate switching without requiring the application to restart.

1. Create Keystores and Truststore

This section creates a private CA, signs server/client CSRs, imports the CA and signed certs into PKCS#12 keystores, and prepares matching truststores. We’ll do everything from a terminal using keytool and openssl.

Create a Private Certificate Authority (CA)

# Generate a private key for the CA
openssl genrsa -out ca-key.pem 2048

# Generate a self-signed root certificate for the CA
openssl req -x509 -new -nodes -key ca-key.pem -sha256 -days 3650 \
  -out ca-cert.pem \
  -subj "/C=US/ST=CA/L=SanFrancisco/O=ExampleCA/OU=Dev/CN=ExampleRootCA"

Generate Key Pair and Certificate for the Server

# Generate a private key for the server
openssl genrsa -out server-key.pem 2048

# Generate a certificate signing request (CSR) for the server
openssl req -new -key server-key.pem -out server.csr \
  -subj "/C=US/ST=CA/L=SanFrancisco/O=ExampleServer/OU=Dev/CN=localhost"

# Sign the server CSR with the CA certificate
openssl x509 -req -in server.csr -CA ca-cert.pem -CAkey ca-key.pem \
  -CAcreateserial -out server-cert.pem -days 365 -sha256

Generate Key Pair and Certificate for the Client

# Generate a private key for the client
openssl genrsa -out client-key.pem 2048

# Generate a certificate signing request (CSR) for the client
openssl req -new -key client-key.pem -out client.csr \
  -subj "/C=US/ST=CA/L=SanFrancisco/O=ExampleClient/OU=Dev/CN=client"

# Sign the client CSR with the CA certificate
openssl x509 -req -in client.csr -CA ca-cert.pem -CAkey ca-key.pem \
  -CAcreateserial -out client-cert.pem -days 365 -sha256

Package Server Certificate and Key into PKCS12 Keystore

openssl pkcs12 -export \
  -in server-cert.pem \
  -inkey server-key.pem \
  -certfile ca-cert.pem \
  -out server-keystore.p12 \
  -name server \
  -password pass:password

Package Client Certificate and Key into PKCS12 Keystore

openssl pkcs12 -export \
  -in client-cert.pem \
  -inkey client-key.pem \
  -certfile ca-cert.pem \
  -out client-keystore.p12 \
  -name client \
  -password pass:password

Create a Truststore and Import the CA Certificate

keytool -importcert \
  -file ca-cert.pem \
  -keystore truststore.p12 \
  -storetype PKCS12 \
  -alias myca \
  -storepass password \
  -noprompt

We create a CA root, then generate CSRs from Java keystores and sign them with the CA. We import the CA first into each keystore, then import the signed certificate to complete the chain. Both the server and clients use truststores that contain the CA root.

2. Simulating Two HTTPS Servers

We will use Java’s built-in HttpsServer to simulate both endpoints, each requiring mTLS.

public class SecureServer {

    private static final Logger LOGGER = Logger.getLogger(SecureServer.class.getName());
    private static final String CERTS_DIR = new File("/absolute/path/to/certs").getAbsolutePath();
    private static final String SERVER_KEYSTORE = "server-keystore.p12";
    private static final String SERVER_TRUSTSTORE = "truststore.p12";
    private static final String PASSWORD = "password";

    public static void main(String[] args) throws Exception {
        startHttpsServer("service1", 8443, "Hello from Service1");
        startHttpsServer("service2", 9443, "Hello from Service2");
        LOGGER.info("Servers started. Press Ctrl+C to stop.");
    }

    private static void startHttpsServer(String host, int port, String message) throws Exception {
        // Load server keystore
        KeyStore ks = KeyStore.getInstance("PKCS12");
        try (FileInputStream fis = new FileInputStream(new File(CERTS_DIR, SERVER_KEYSTORE))) {
            ks.load(fis, PASSWORD.toCharArray());
        }

        KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
        kmf.init(ks, PASSWORD.toCharArray());

        // Load truststore
        KeyStore ts = KeyStore.getInstance("PKCS12");
        try (FileInputStream fis = new FileInputStream(new File(CERTS_DIR, SERVER_TRUSTSTORE))) {
            ks.load(fis, PASSWORD.toCharArray());
        }

        TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
        tmf.init(ts);

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

        // Create and configure the server
        HttpsServer server = HttpsServer.create(new InetSocketAddress(port), 0);
        server.setHttpsConfigurator(new HttpsConfigurator(sslContext) {
            public void configure(HttpsParameters params) {
                SSLContext c = getSSLContext();
                SSLEngine engine = c.createSSLEngine();
                params.setNeedClientAuth(true);
                params.setCipherSuites(engine.getEnabledCipherSuites());
                params.setProtocols(engine.getEnabledProtocols());
                params.setSSLParameters(c.getDefaultSSLParameters());
            }
        });

        // Simple handler
        server.createContext("/", exchange -> {
            String response = message;
            LOGGER.info("Received request on " + host);
            exchange.sendResponseHeaders(200, response.length());
            try (OutputStream os = exchange.getResponseBody()) {
                os.write(response.getBytes());
            }
        });

        new Thread(server::start).start();
        LOGGER.info(host + " listening on port " + port);
    }
}

This Java program creates two HTTPS servers with mutual TLS (mTLS) using the built-in HttpsServer.
One runs on port 8443 ("service1") and the other on 9443 ("service2"), each responding with a simple greeting.

The startHttpsServer method loads the server’s keystore (private key + certificate) and truststore (trusted client certificates), initialises SSL via SSLContext, and configures HttpsConfigurator to require client authentication. When run, both endpoints (https://localhost:8443/ and https://localhost:9443/) require valid client certificates before responding.

If the configuration is correct and both the keystore and truststore files are in place, start the server. The expected output when it runs is:

Aug 14, 2025 3:39:31 P.M. com.jcg.example.SecureServer startHttpsServer
INFO: service1 listening on port 8443
Aug 14, 2025 3:39:32 P.M. com.jcg.example.SecureServer startHttpsServer
INFO: service2 listening on port 9443
Aug 14, 2025 3:39:32 P.M. com.jcg.example.SecureServer main
INFO: Servers started. Press Ctrl+C to stop.

3. Client Certificate Routing via KeyManager and TrustManager

If we need one shared HttpClient that dynamically picks the certificate based on the target host, we can write a custom KeyManager. In this approach, we create a custom KeyManager and TrustManager to dynamically choose which client certificate to use based on the target service. This allows us to reuse the same HttpClient while securely routing requests to different services with different SSL credentials.

public class RoutingKeyManager implements X509KeyManager {

    private final Map<String, X509KeyManager> keyManagers;

    public RoutingKeyManager(Map<String, X509KeyManager> keyManagers) {
        this.keyManagers = keyManagers;
    }

    @Override
    public String[] getClientAliases(String keyType, Principal[] issuers) {
        return null;
    }

    @Override
    public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
        String host = socket.getInetAddress().getHostName();
        X509KeyManager km = keyManagers.get(host);
        return (km != null) ? km.chooseClientAlias(keyType, issuers, socket) : null;
    }

    @Override
    public String[] getServerAliases(String keyType, Principal[] issuers) {
        return null;
    }

    @Override
    public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
        return null;
    }

    @Override
    public X509Certificate[] getCertificateChain(String alias) {
        for (X509KeyManager km : keyManagers.values()) {
            X509Certificate[] chain = km.getCertificateChain(alias);
            if (chain != null) {
                return chain;
            }
        }
        return null;
    }

    @Override
    public PrivateKey getPrivateKey(String alias) {
        for (X509KeyManager km : keyManagers.values()) {
            PrivateKey key = km.getPrivateKey(alias);
            if (key != null) {
                return key;
            }
        }
        return null;
    }
}

This code sets up SSL contexts where the KeyManager chooses the appropriate client certificate for each connection, and the TrustManager ensures that only trusted servers are accepted. The advantage is that certificate switching is handled automatically without creating separate clients.

RoutingHttpClient.java

This class implements the HTTP client that leverages the custom KeyManager and TrustManager. It builds a single HttpClient capable of making secure calls to multiple services, each requiring its own certificate.

public class RoutingHttpClient {

    private static final String CERTS_DIR = "/absolute/path/to/certs";
    private static final String PASSWORD = "password";

    public static void main(String[] args) throws Exception {

        // Map host - KeyManager
        Map<String, X509KeyManager> kmMap = new HashMap<>();
        kmMap.put("localhost", loadKeyManager("client-keystore.p12"));
        kmMap.put("localhost", loadKeyManager("client-keystore2.p12"));

        X509KeyManager routingKM = new RoutingKeyManager(kmMap);
        X509TrustManager tm = loadTrustManager("truststore.p12");

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(new KeyManager[]{routingKM}, new TrustManager[]{tm}, null);

        SSLParameters sslParams = sslContext.getDefaultSSLParameters();
        sslParams.setEndpointIdentificationAlgorithm(null);

        HttpClient client = HttpClient.newBuilder()
                .sslContext(sslContext)
                .sslParameters(sslParams)
                .build();

        HttpRequest req1 = HttpRequest.newBuilder()
                .uri(URI.create("https://localhost:8443"))
                .GET()
                .build();

        HttpRequest req2 = HttpRequest.newBuilder()
                .uri(URI.create("https://localhost:9443"))
                .GET()
                .build();

        HttpResponse res1 = client.send(req1, HttpResponse.BodyHandlers.ofString());
        System.out.println("Response from Service1: " + res1.body());

        HttpResponse res2 = client.send(req2, HttpResponse.BodyHandlers.ofString());
        System.out.println("Response from Service2: " + res2.body());
    }

    private static X509KeyManager loadKeyManager(String p12File) throws Exception {
        KeyStore ks = KeyStore.getInstance("PKCS12");
        try (FileInputStream fis = new FileInputStream(CERTS_DIR + "/" + p12File)) {
            ks.load(fis, PASSWORD.toCharArray());
        }
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(ks, PASSWORD.toCharArray());
        return (X509KeyManager) kmf.getKeyManagers()[0];
    }

    private static X509TrustManager loadTrustManager(String p12File) throws Exception {
        KeyStore ts = KeyStore.getInstance("PKCS12");
        try (FileInputStream fis = new FileInputStream(CERTS_DIR + "/" + p12File)) {
            ts.load(fis, PASSWORD.toCharArray());
        }
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        tmf.init(ts);
        return (X509TrustManager) tmf.getTrustManagers()[0];
    }
}

Here, the HTTP client is configured once with the custom SSL context. When a request is made, the routing logic decides which certificate to present based on the request’s destination.

Example Output:

Response from Service1: Hello from Service1
Response from Service2: Hello from Service2

4. Apache HTTP Components Client Example

This approach demonstrates how to use Apache HttpComponents to load a unique keystore and truststore for each connection.

public class ApacheHttpComponentsClient {

    private static final String CERTS_DIR = "/absolute/path/to/certs";
    private static final String PASSWORD = "password"; 

    private static CloseableHttpClient createHttpsClient() throws Exception {
        char[] passwordArray = PASSWORD.toCharArray();

        SSLContext sslContext = SSLContextBuilder.create()
                .loadTrustMaterial(Paths.get(CERTS_DIR, "truststore.p12"), passwordArray)
                .loadKeyMaterial(Paths.get(CERTS_DIR, "server-keystore.p12"), passwordArray, passwordArray)
                .build();

        var connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
                .setTlsSocketStrategy(new DefaultClientTlsStrategy(sslContext))
                .build();

        return HttpClients.custom()
                .setConnectionManager(connectionManager)
                .build();
    }

    public static void main(String[] args) {
        try (CloseableHttpClient client = createHttpsClient()) {

            HttpGet request1 = new HttpGet("https://localhost:8443/test1");
            client.execute(request1, response -> {
                if (response.getCode() == HttpStatus.SC_OK) {
                    System.out.println("Request to localhost1 successful.");
                } else {
                    System.out.println("Request to localhost1 failed with status: " + response.getCode());
                }
                return null;
            });

            HttpGet request2 = new HttpGet("https://localhost:9443/test2");
            client.execute(request2, response -> {
                if (response.getCode() == HttpStatus.SC_OK) {
                    System.out.println("Request to localhost2 successful.");
                } else {
                    System.out.println("Request to localhost2 failed with status: " + response.getCode());
                }
                return null;
            });

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

The class defines constants for the certificates directory (CERTS_DIR) and password (PASSWORD). The createHttpsClient() method loads the truststore and keystore into an SSLContext. This context is passed into PoolingHttpClientConnectionManagerBuilder.create(), which creates a builder for an HTTP connection manager that supports pooling (reusing TCP connections for efficiency).

The setTlsSocketStrategy(new DefaultClientTlsStrategy(sslContext)) method configures the manager to use the provided SSLContext for HTTPS connections with mutual TLS, and build() finalises the manager’s creation.

In main() method, a CloseableHttpClient is built using createHttpsClient(). Two HTTPS GET requests are sent to https://localhost:8443, and responses are validated by checking the HTTP status codes.

5. Conclusion

In this article, we explored how to configure and use Apache HttpComponents to perform HTTPS requests with mutual TLS authentication by loading a specific keystore and truststore. We also covered an alternative approach using a custom KeyManager and TrustManager to dynamically route certificates per connection.

6. Download the Source Code

This article explored implementing Java SSL with multiple client certificates.

Download
You can download the full source code of this example here: java ssl multiple client certificates

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button