How do I pass the client certificate with HTTP client?
Asked Answered
O

2

10

I want to use mutual SSL authentication between service A and B. I'm currently implementing passing the client certificate from service A in Java. I'm using Apache DefaultHttpClient to execute my requests. I was able to retrieve the client certificate for my service A from an internal credential manager and I keep it as an array of bytes.

DefaultHttpClient client = new DefaultHttpClient();
byte [] certificate = localCertManager.retrieveCert();

I have very little experience in this area and I'd appreciate your help!

I thought maybe it should be somehow passed through arguments in the HTTP client or maybe in the headers.

How do I pass the client certificate with HTTP client?

Obmutescence answered 30/8, 2013 at 21:36 Comment(1)
E
9

You need to tell an SSLSocketFactory (org.apache.http, not javax) about your keystore, and configure your DefaultHTTPClient to use it for https connections.

An example is here: http://hc.apache.org/httpcomponents-client-ga/httpclient/examples/org/apache/http/examples/client/ClientCustomSSL.java

Epaminondas answered 30/8, 2013 at 22:1 Comment(3)
This is vaguely correct to start, but the linked example confuses things by not using client certificates. It actually sets alternative CA certs for trusting the server being connected to instead, but could appear to someone not intimately familiar with SSLContexts to be a correct example. I followed this and took significant time to disentangle that loadTrustMaterial won't work.Catheycathi
the link is deadSamy
This is a link with probably the same example: javatips.net/api/uw-android-master/UWPreloader/…Rennes
P
22

The client certificate is sent during the TLS handshake when establishing a connection and can't be sent via HTTP within that connection.

The communication is layered like this:

  • HTTP (application-layer protocol) within
  • TLS (presentation-layer protocol) within
  • TCP (transport-layer protocol) within
  • IP (network-layer protocol)

You need to send the client certificate during the TLS handshake before anything HTTP (methods, headers, URLs, request bodies) is available to be influenced. The server will not accept a client certificate sent later.

I'm recommending switching from DefaultHttpClient (deprecated) to CloseableHttpClient which works more cleanly with try-with-resources.

Apache HttpClient 4.5 makes Mutual TLS reasonably convenient. This answer has been tested with Apache HttpClient 4.5.3.

The essential starting point is using loadKeyMaterial to load your client certicate and it's key (the client keypair) into the SSLContext:

SSLContext sslContext = SSLContexts.custom().loadKeyMaterial(
                MutualHttpsMain.class.getResource(TEST_CLIENT_KEYSTORE_RESOURCE),
                storePassword, keyPassword,
                (aliases, socket) -> aliases.keySet().iterator().next()
        ).build();

And finally building an HTTP client with that socket factory:

CloseableHttpClient httpclient = HttpClients
        .custom().setSSLContext(sslContext).build();

With that client, all your requests can be executed with Mutual TLS authentication implied:

CloseableHttpResponse closeableHttpResponse = httpclient.execute(
        new HttpGet(URI.create("https://mutual-tls.example.com/")));

Here's a full runnable example of mutual TLS with Apache HttpClient:

import org.apache.http.HttpEntity;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;

import javax.net.ssl.SSLContext;
import java.io.Console;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.security.GeneralSecurityException;

public class MutualHttpsMain {
    private static final String TEST_URL = "https://mutual-tls.example.com/";
    private static final String TEST_CLIENT_KEYSTORE_RESOURCE = "/mutual-tls-keystore.p12";

    public static void main(String[] args) throws GeneralSecurityException, IOException {
        Console console = System.console();
        char[] storePassword = console.readPassword("Key+Keystore password: ");
        char[] keyPassword = storePassword;
        SSLContext sslContext = SSLContexts.custom().loadKeyMaterial(
                MutualHttpsMain.class.getResource(TEST_CLIENT_KEYSTORE_RESOURCE),
                storePassword, keyPassword,
                (aliases, socket) -> aliases.keySet().iterator().next()
        ).build();
        try (CloseableHttpClient httpclient = HttpClients
                .custom().setSSLContext(sslContext).build();
             CloseableHttpResponse closeableHttpResponse = httpclient.execute(
                    new HttpGet(URI.create(TEST_URL)))) {
            console.writer().println(closeableHttpResponse.getStatusLine());
            HttpEntity entity = closeableHttpResponse.getEntity();
            try (InputStream content = entity.getContent();
                 ReadableByteChannel src = Channels.newChannel(content);
                 WritableByteChannel dest = Channels.newChannel(System.out)) {
                ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024);
                while (src.read(buffer) != -1) {
                    buffer.flip();
                    dest.write(buffer);
                    buffer.compact();
                }
                buffer.flip();
                while (buffer.hasRemaining())
                    dest.write(buffer);
            }
        }
    }
}

It's generally better to use Gradle or Maven to run something like this, but in the interest of keeping this Yak shave as minimal as possible I'm providing baseline JDK instructions for building and running this.

Download JARs from the following pages:

Save the full example above as MutualHttpsMain.java.

Copy your PKCS#12 to mutual-tls-keystore.p12 in the same directory.

Compile it as follows (on macOS/Linux/*nix-likes):

javac MutualHttpsMain.java -cp httpclient-4.5.3.jar:httpcore-4.4.8.jar

Or on Windows:

javac MutualHttpsMain.java -cp httpclient-4.5.3.jar;httpcore-4.4.8.jar

Run as follows (on macOS/Linux/*nix-likes):

java -cp httpclient-4.5.3.jar:commons-codec-1.10.jar:commons-logging-1.2.jar:httpcore-4.4.8.jar:. MutualHttpsMain

Run as follows (on Windows):

java -cp httpclient-4.5.3.jar;commons-codec-1.10.jar;commons-logging-1.2.jar;httpcore-4.4.8.jar;. MutualHttpsMain
Ponder answered 19/10, 2017 at 2:24 Comment(9)
@magnus has a concise example mutual TLS java HTTP client example here https://mcmap.net/q/102177/-java-https-client-certificate-authenticationCatheycathi
password, password, confusing. Better rename them to keyPassword and keystorePassword respectively.Leontine
@Leontine fair point. What do you think of my edit to address the confusion?Catheycathi
@Leontine is keyPassword the same thing as the certificate's password?Celibacy
As far as I can tell, PKCS#12 doesn't support distinction between the storePassword and keyPassword, and in practice in my experience they must be the same.Catheycathi
certificate should not be protected with password - it's public information. Keystore password - password from file, where keys, certificates and key-pairs and other things stored. Private key as usual protected with password. But it is different password. Keeping this passwords same - it is like live in multy-flats house, and have same key for house entrance and for each flat in it. It's safe if all belong to you. However, I'm not security engineer, it's just my understanding and it may be wrong.Leontine
Given that it's mutual TLS, the client needs both the private key and the certificate, so encrypting the file that contains them is a sensible security choice. In the case of this application, on startup the password-based encrypted PKCS#12 file with the keypair within it was downloaded from an Amazon S3 bucket encrypted with a KMS customer-managed key (CMK) and the password was in Amazon Secrets Manager. It is a file with credentials for a single client, so multi-tenancy is not a concern in this case, and shouldn't normally be a concern where this kind of code would be used as an MTLS client.Catheycathi
Hi @AlainO'Dea - I am trying your example, but when I run the MutualHttpsMain, I am getting exceptions "Exception in thread "main" javax.net.ssl.SSLException at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:133)" and "Caused by: java.util.NoSuchElementException at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1512)"Socio
Hi @AlainO'Dea et al - in the vein of "paying it back" - I just found the solution to the problem I was having (the NoSuchElementException) - see #57601784 - the system I was running on was defaulting to java and javac 11, so I explicitly included path to 1.8.xxx javac and java and recompiled AND run on the 1.8.xxx, and now it worked!!Socio
E
9

You need to tell an SSLSocketFactory (org.apache.http, not javax) about your keystore, and configure your DefaultHTTPClient to use it for https connections.

An example is here: http://hc.apache.org/httpcomponents-client-ga/httpclient/examples/org/apache/http/examples/client/ClientCustomSSL.java

Epaminondas answered 30/8, 2013 at 22:1 Comment(3)
This is vaguely correct to start, but the linked example confuses things by not using client certificates. It actually sets alternative CA certs for trusting the server being connected to instead, but could appear to someone not intimately familiar with SSLContexts to be a correct example. I followed this and took significant time to disentangle that loadTrustMaterial won't work.Catheycathi
the link is deadSamy
This is a link with probably the same example: javatips.net/api/uw-android-master/UWPreloader/…Rennes

© 2022 - 2024 — McMap. All rights reserved.