How do I pass the client certificate with HTTP client?

36,302

Solution 1

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

Solution 2

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
Share:
36,302
agerrr
Author by

agerrr

Updated on August 04, 2022

Comments

  • agerrr
    agerrr almost 2 years

    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?

  • agerrr
    agerrr almost 11 years
    Cool, this was really helpful!
  • ThanksForYourHelp
    ThanksForYourHelp over 6 years
    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.
  • ThanksForYourHelp
    ThanksForYourHelp over 6 years
    @magnus has a concise example mutual TLS java HTTP client example here stackoverflow.com/a/32513368/154527
  • degr
    degr over 3 years
    password, password, confusing. Better rename them to keyPassword and keystorePassword respectively.
  • ThanksForYourHelp
    ThanksForYourHelp over 3 years
    @degr fair point. What do you think of my edit to address the confusion?
  • leftyloosie
    leftyloosie over 3 years
    @degr is keyPassword the same thing as the certificate's password?
  • ThanksForYourHelp
    ThanksForYourHelp over 3 years
    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.
  • degr
    degr over 3 years
    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.
  • ThanksForYourHelp
    ThanksForYourHelp over 3 years
    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.
  • leonardkraemer
    leonardkraemer over 3 years
    the link is dead
  • José Manuel Blasco
    José Manuel Blasco almost 3 years
    This is a link with probably the same example: javatips.net/api/uw-android-master/UWPreloader/…