40

In Python I was using requests like this:

requests.put(
              webdavURL, 
              auth=(tUsername, tPassword), 
              data=webdavFpb, 
              verify=False, 
              cert=("/path/to/file.pem", "/path/to/file.key"))

Easy as pie.

Now I need to implement the same thing in Java using Apache HttpClient. How can I pass a client certificate when making requests using HttpClient?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
daniels
  • 18,416
  • 31
  • 103
  • 173

3 Answers3

60

I think the main difference is that in java, you usually put the key and the certificate to a key store and use it from there. Like you mention often people do want to use a separate library for it, like mentioned httpcomponents client (just like you're using requests library in your python example).

Here's an example of using a client certificate from a key store, using the previously mentioned library:

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.junit.Test;

import javax.net.ssl.SSLContext;
import java.io.InputStream;
import java.security.KeyStore;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

public class MyClientCertTest {

    private static final String KEYSTOREPATH = "/clientkeystore.jks"; // or .p12
    private static final String KEYSTOREPASS = "keystorepass";
    private static final String KEYPASS = "keypass";

    KeyStore readStore() throws Exception {
        try (InputStream keyStoreStream = this.getClass().getResourceAsStream(KEYSTOREPATH)) {
            KeyStore keyStore = KeyStore.getInstance("JKS"); // or "PKCS12"
            keyStore.load(keyStoreStream, KEYSTOREPASS.toCharArray());
            return keyStore;
        }
    }
    @Test
    public void readKeyStore() throws Exception {
        assertNotNull(readStore());
    }
    @Test
    public void performClientRequest() throws Exception {
        SSLContext sslContext = SSLContexts.custom()
                .loadKeyMaterial(readStore(), KEYPASS.toCharArray()) // use null as second param if you don't have a separate key password
                .build();

        HttpClient httpClient = HttpClients.custom().setSSLContext(sslContext).build();
        HttpResponse response = httpClient.execute(new HttpGet("https://slsh.iki.fi/client-certificate/protected/"));
        assertEquals(200, response.getStatusLine().getStatusCode());
        HttpEntity entity = response.getEntity();

        System.out.println("----------------------------------------");
        System.out.println(response.getStatusLine());
        EntityUtils.consume(entity);
    }
}

Maven pom for dependency versions:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.acme</groupId>
    <artifactId>httptests</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.3</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.9</version>
                <!-- this is not needed, but useful if you want to debug what's going
                     on with your connection -->
                <configuration>
                    <argLine>-Djavax.net.debug=all</argLine>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

I've also published a simple test page for testing a client certificate.


Just to demonstrate that it can be done, below is an example of using client certificate just using standard java api, without extra libraries.

import org.junit.Test;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.KeyStore;

public class PlainJavaHTTPS2Test {

    @Test
    public void testJKSKeyStore() throws Exception {
        final String KEYSTOREPATH = "clientkeystore.jks";
        final char[] KEYSTOREPASS = "keystorepass".toCharArray();
        final char[] KEYPASS = "keypass".toCharArray();

        try (InputStream storeStream = this.getClass().getResourceAsStream(KEYSTOREPATH)) {
            setSSLFactories(storeStream, "JKS", KEYSTOREPASS, KEYPASS);
        }
        testPlainJavaHTTPS();
    }
    @Test
    public void testP12KeyStore() throws Exception {
        final String KEYSTOREPATH = "clientkeystore.p12";
        final char[] KEYSTOREPASS = "keystorepass".toCharArray();
        final char[] KEYPASS = "keypass".toCharArray();

        try (InputStream storeStream = this.getClass().getResourceAsStream(KEYSTOREPATH)) {
            setSSLFactories(storeStream, "PKCS12", KEYSTOREPASS, KEYPASS);
        }
        testPlainJavaHTTPS();
    }
    private static void setSSLFactories(InputStream keyStream, String keystoreType, char[] keyStorePassword, char[] keyPassword) throws Exception
    {
        KeyStore keyStore = KeyStore.getInstance(keystoreType);

        keyStore.load(keyStream, keyStorePassword);

        KeyManagerFactory keyFactory =
                KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());

        keyFactory.init(keyStore, keyPassword);

        KeyManager[] keyManagers = keyFactory.getKeyManagers();

        SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(keyManagers, null, null);
        SSLContext.setDefault(sslContext);
    }

    public void testPlainJavaHTTPS() throws Exception {
        String httpsURL = "https://slsh.iki.fi/client-certificate/protected/";
        URL myUrl = new URL(httpsURL);
        HttpsURLConnection conn = (HttpsURLConnection)myUrl.openConnection();
        try (InputStream is = conn.getInputStream()) {
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);

            String inputLine;

            while ((inputLine = br.readLine()) != null) {
                System.out.println(inputLine);
            }
        }
    }
}

And here's a third version with least amount of code, but which relies on the fact that a) keystore is a file on disk, not within jar, and b) key password must be identical to keystore password.

import org.junit.BeforeClass;
import org.junit.Test;

import java.net.URL;
import java.io.*;
import javax.net.ssl.HttpsURLConnection;

public class PlainJavaHTTPSTest {

    @BeforeClass
    public static void setUp() {
        System.setProperty("javax.net.ssl.keyStore", "/full/path/to/clientkeystore-samepassword.jks");
        System.setProperty("javax.net.ssl.keyStorePassword", "keystorepass");
    }

    @Test
    public void testPlainJavaHTTPS() throws Exception {
        String httpsURL = "https://slsh.iki.fi/client-certificate/protected/";
        URL myUrl = new URL(httpsURL);
        HttpsURLConnection conn = (HttpsURLConnection)myUrl.openConnection();
        try (InputStream is = conn.getInputStream()) {
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);

            String inputLine;

            while ((inputLine = br.readLine()) != null) {
                System.out.println(inputLine);
            }
        }
    }
}

The properties set above in code can of course be also given as startup parameters, -Djavax.net.ssl.keyStore=/full/path/to/clientkeystore-samepassword.jks and -Djavax.net.ssl.keyStorePassword=keystorepass.

eis
  • 51,991
  • 13
  • 150
  • 199
  • 1
    If the keystore contains multiple keys, how does the code know which one to use? – David Brossard Aug 23 '19 at 14:08
  • 1
    @DavidBrossard https://stackoverflow.com/questions/23527426/how-is-the-ssl-client-certificate-chosen-when-there-are-multiple-matching-certif – eis Aug 24 '19 at 15:26
  • I have been trying for days to get a cert code to work; I have tried this code multiple ways and still no effect. I don't have a jks or p12 file just the /C/Program Files/Java/jdk1.8.0_271/jre/lib/security/cacerts file- i get "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target" I have tried many SO posts -https://stackoverflow.com/questions/17712417/how-to-configure-truststore-for-javax-net-ssl-truststore-on-windows; does this only work with .jks/.p12 files; how do I get those? please help. – ASheppardWork Apr 02 '21 at 18:43
  • @ASheppardWork sounds like you don't have a client certificate if you only have cacerts file, so it's not related to this question or this code? – eis Apr 02 '21 at 21:24
10

If you want to use the Apache HTTP client instead of the Java HTTP client, you have to provide to SSLFactory your keystore and configure DefaultHTTPClient to use it in the HTTPS protocol.

You can find a working example here.

I hope that helps.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Bosko Mijin
  • 3,287
  • 3
  • 32
  • 45
  • can you give some pointers to use Java HTTP Client? I am having difficulty with Apache one. – Ravi Jul 07 '16 at 06:18
  • 8
    I may be mistaken, but that example seems to be only about using a custom truststore (e.g. to trust a certificate which would not normally be trusted), rather than providing a client certificate to authenticate the client. – Matt Sheppard May 03 '17 at 07:10
  • 1
    This does not answer the question. This will only add a custom truststore, not a client certificate. – sstendal Sep 07 '17 at 13:47
  • 2
    This example is about a custom trust store. This doesn't show how to provide a client certificate. – Kanishka Dilshan Sep 21 '20 at 09:58
0

Using the fluent API of Apache HttpClient 5.2 it would look like this:

System.setProperty("javax.net.ssl.keyStore", "/full/path/to/clientkeystore-samepassword.p12");
System.setProperty("javax.net.ssl.keyStorePassword", "password");
System.setProperty("javax.net.ssl.keyStoreType", "PKCS12");
System.setProperty("javax.net.debug", "ssl"); // very verbose debug

String url = "https://example.org/client-certificate/protected/";
        
Content content = Request.get(url)
  .execute()
  .returnContent();
System.out.println(content);
Falko Menge
  • 788
  • 7
  • 16
  • Hi - Can you provide more information? How can we can actually build a working client that does 2-way from the above? Thanks! – user555303 Sep 01 '23 at 04:19