9

Suppose we need to trust a self-signed SSL certificate. As an example, let's use https://self-signed.badssl.com/.

Since the signer is not a "proper" authority, Java doesn't trust it and refuses to connect to that server. However, after

$ cd $JAVA_HOME/jre/lib/security
$ keytool -import -trustcacerts -alias ... -file ... -keystore cacerts

and restart of the application, the following code works:

new URL ("https://self-signed.badssl.com/").openConnection ().getResponseCode ()

and returns 200 (OK), without throwing an exception. I.e. basic Java way of opening an HTTPS connection now works, since the certificate is now trusted.

However, this doesn't have any visible effect on javax.ws.rs Client (as implemented in Resteasy, at least) and I still get an exception:

javax.ws.rs.ProcessingException: Unable to invoke request
        at org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine.invoke(ApacheHttpClient4Engine.java:287)
        at org.jboss.resteasy.client.jaxrs.internal.ClientInvocation.invoke(ClientInvocation.java:407)
        at org.jboss.resteasy.client.jaxrs.internal.ClientInvocationBuilder.method(ClientInvocationBuilder.java:273)
        [...]
Caused by: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
        at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
        at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1949)
        at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:302)
        at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:296)
        at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1506)
        at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:216)
        at sun.security.ssl.Handshaker.processLoop(Handshaker.java:979)
        at sun.security.ssl.Handshaker.process_record(Handshaker.java:914)
        at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1062)
        at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375)
        at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403)
        at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387)
        at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:535)
        at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:403)
        at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:177)
        at org.apache.http.impl.conn.ManagedClientConnectionImpl.open(ManagedClientConnectionImpl.java:304)
        at org.apache.http.impl.client.DefaultRequestDirector.tryConnect(DefaultRequestDirector.java:611)
        at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:446)
        at org.apache.http.impl.client.AbstractHttpClient.doExecute(AbstractHttpClient.java:863)
        at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82)
        at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:57)
        at org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine.invoke(ApacheHttpClient4Engine.java:283)
        ... 90 more
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
        at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:387)
        at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:292)
        at sun.security.validator.Validator.validate(Validator.java:260)
        at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:324)
        at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:229)
        at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:124)
        at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1488)
        ... 107 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
        at sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:146)
        at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:131)
        at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:280)
        at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:382)
        ... 113 more

It seems as Resteasy doesn't take "standard" keystore into account. But I would rather like to have a central (machine-specific) place for additional trusted keys and not bother how exactly application uses them, with URL.openConnection or javax.ws.rs.

Question Is it possible to make javax.ws.rs Client use the same keystore as "normal" Java HTTPS connection mechanism?

cassiomolin
  • 124,154
  • 35
  • 280
  • 359
  • Please let me know if my answer works for you. – cassiomolin Jul 27 '16 at 15:38
  • Sure, it's just not trivial to implement and I'm busy with other tasks right now. A question: currently I don't depend on any particular implementation, so at compilation time I only have the interfaces available. How would I compile against Resteasy, not just have it supplied at runtime by the webserver? –  Jul 28 '16 at 08:59
  • My current answer is tailored to RESTEasy because your questions mentions your are using RESTEasy as JAX-RS implementation. In order to use `ResteasyClientBuilder`, you will need to add the [`resteasy-client`](https://mvnrepository.com/artifact/org.jboss.resteasy/resteasy-client) dependency. You can add that dependency as `provided` scope if you are using a container that provides RESTEasy, such as WildFly. – cassiomolin Jul 28 '16 at 09:11
  • Well, if you have an answer that is implementation-independent, that would be even better, of course. We use Gradle, which lacks provided scope for dependencies. What would happen if we have our own Resteasy inside the application? Will it be just preferred to what Wildfly provides? –  Jul 28 '16 at 09:37
  • Unfortunatelly, I am not aware of any implementation independent solution. The key point here is to have the `ResteasyClientBuilder` in your classpath. It can come from your RESTEasy or from the RESTEasy provided by WildFly. – cassiomolin Jul 28 '16 at 09:42
  • For some reason, setting the `SSLContext` in the `ClientBuilder` won't work for RESTEasy: `Client client = ClientBuilder.newBuilder().sslContext(createSSLContext()).build();`. But it works when using `ResteasyClientBuilder`. – cassiomolin Jul 28 '16 at 09:59
  • 1
    Thank you, I'll look into it when I have time. –  Jul 28 '16 at 10:04
  • Please check my update. `SSLContext.getDefault()` will make things simpler. But there might be a bug in RESTEasy. The following instruction works for Jersey but doesn't work for RESTEasy: `Client client = ClientBuilder.newBuilder().sslContext(SSLContext.getDefault()).build();`. – cassiomolin Jul 28 '16 at 10:24
  • Have you tried to configure the truststore of WildFly ? see https://docs.jboss.org/author/display/WFLY8/Detailed+Configuration#DetailedConfiguration-%7B%7B%3Ctruststore%2F%3E%7D%7D – Nicolas Henneaux Aug 03 '16 at 12:12
  • No. I tried now, adding ``, but it doesn't seem to have any effect. I tried on both "ManagementRealm" and "ApplicationRealm". Or does it have to be a separate keystore? –  Aug 03 '16 at 13:40

1 Answers1

9

Setting the SSL context when creating the Client instance

In the ClientBuilder API there's a method that allows you to set the SSLContext:

public abstract ClientBuilder sslContext(SSLContext sslContext)

Set the SSL context that will be used when creating secured transport connections to server endpoints from web targets created by the client instance that is using this SSL context. The SSL context is expected to have all the security infrastructure initialized, including the key and trust managers.

Setting a SSL context instance resets any key store or trust store values previously specified.

Parameters:

sslContext - secure socket protocol implementation which acts as a factory for secure socket factories or SSL engines. Must not be null.

Returns:

an updated client builder instance.

Throws:

NullPointerException - in case the sslContext parameter is null.

Assuming you have added the certificate to cacerts trust store, you could use the default SSLContext when creating your Client instance.

Client client = ClientBuilder.newBuilder().sslContext(SSLContext.getDefault()).build();

It should be enough. However, for some reason, the above piece of code does not work with RESTEasy, but does work with Jersey. It's very likely it's a RESTEasy bug.

The standard solution does not work with RESTEasy. What should I do?

The RESTEasy documentation states the following:

Network communication between the client and server is handled in RESTEasy, by default, by HttpClient (4.x) from the Apache HttpComponents project. [...]

RESTEasy and HttpClient make reasonable default decisions so that it is possible to use the client framework without ever referencing HttpClient, but for some applications it may be necessary to drill down into the HttpClient details. [...]

To customize the HttpClient used by RESTEeasy, do the following:

HttpClient httpClient = HttpClientBuilder.create()
                                         .setSslcontext(SSLContext.getDefault())
                                         .build();

ApacheHttpClient4Engine engine = new ApacheHttpClient4Engine(httpClient);
Client client = new ResteasyClientBuilder().httpEngine(engine).build();

Then you can perform the request:

Response response = client.target("https://self-signed.badssl.com/").request().get();
System.out.println(response.getStatus());

Are there any alternatives to the SSL context?

Instead of using the SSLContext when creating your Client, you could load a KeyStore. To load cacerts trust store, you can do the following:

String filename = System.getProperty("java.home") + 
        "/lib/security/cacerts".replace('/', File.separatorChar);
FileInputStream is = new FileInputStream(filename);
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
String password = "changeit";
keystore.load(is, password.toCharArray());

The cacerts' default password is changeit.

Then create your Client instance using one of the following approaches:

Client client = ClientBuilder.newBuilder().trustStore(keystore).build();
Client client = ClientBuilder.newBuilder().keyStore(keystore, password).build();

The issue is that it doesn't work with RESTEasy, but does work with Jersey.


The solutions mentioned above were tested against the following JAX-RS Client API implementations:

cassiomolin
  • 124,154
  • 35
  • 280
  • 359
  • 1
    It works, but unfortunately not on Resteasy 4.2 (Wildfly 8) which we still use on production, because there is no `HttpClientBuilder` in that version. On later versions (e.g. in Wildfly 9) it does make REST clients trust certificates in standard Java keystore. In general, looks like a bug in Resteasy. I had tried the standard solution before asking this question, but it failed on Resteasy, so I thought it was somehow wrong — but it's correct and works on other implementations. –  Jul 29 '16 at 08:46
  • @doublep What do you mean with *Resteasy 4.2*? – cassiomolin Jul 29 '16 at 08:55
  • Eh, not Resteasy, but Apache HttpComponents, sorry. –  Jul 29 '16 at 09:56
  • I am using this [ ClientBuilder.newBuilder().keyStore(keystore, password).build();] the keystore get in the httpresponse(in different format), but i can make the keystore again from the http response, can you plz help me out this.@cassiomolin – VG__ Dec 11 '19 at 23:10