Reloading a java.net.http.HttpClient's SSLContext - java

I've got a program that makes use of the java.net.http.HttpClient, which was introduced in Java 11, to connect and send requests to internal services. These services are mutually authenticated, both presenting certificates issued by an internal CA.
For example,
SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
KeyManager keys = /* load our cert and key */;
TrustManager trust = /* load our trusted CA */;
sslContext.init(keys, trust, secureRandom);
HttpClient.Builder builder = HttpClient.newBuilder().sslContext(sslContext);
HttpClient client = builder.build();
On our hosts, the client's certificate and private key get rotated pretty regularly, more often than the host or application gets a chance to restart. I'd like to be able to reload the HttpClient's SSLContext with the new cert/key pair while it's still running, but can't see any way to do so.
After the HttpClient has been built, it only provides an sslContext() getter to retrieve the SSLContext. It doesn't seem to have an API to set a new one.
Is there any other mechanism to achieve this?
(I'm thinking of something like Jetty's SslContextFactory#reload(SSLContext) method.)

I think this question is similar to How to renew keystore (SSLContext) in Spring Data Geode connections without restarting? the answer I have provided there is similar to this one.
This option is unfortunately not available by default. After you have supplied the SSLContext to the HttpClient and build the client you cannot change the SSLContext. You will need to create a new SSLContext and a new HttpClient, but there is a workaround which will do the trick to apply a reload/update.
I had the same challenge for one of my projects and I solved it by using a custom trustmanager and keymanager which wraps around the actual trustmanager and keymanager while having the capability of swapping the actual trustmanager and keymanager. So you can use the following setup if you still want to accomplish it without the need of recreating the HttpClient and SSLContext:
SSLFactory baseSslFactory = SSLFactory.builder()
.withDummyIdentityMaterial()
.withDummyTrustMaterial()
.withSwappableIdentityMaterial()
.withSwappableTrustMaterial()
.build();
HttpClient httpClient = HttpClient.newBuilder()
.sslParameters(sslFactory.getSslParameters())
.sslContext(sslFactory.getSslContext())
.build()
Runnable sslUpdater = () -> {
SSLFactory updatedSslFactory = SSLFactory.builder()
.withIdentityMaterial(Paths.get("/path/to/your/identity.jks"), "password".toCharArray())
.withTrustMaterial(Paths.get("/path/to/your/truststore.jks"), "password".toCharArray())
.build();
SSLFactoryUtils.reload(baseSslFactory, updatedSslFactory)
};
// initial update of ssl material to replace the dummies
sslUpdater.run();
// update ssl material every hour
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(sslUpdater, 1, 1, TimeUnit.HOURS);
// execute https request
HttpResponse<String> response = httpClient.send(aRequest, HttpResponse.BodyHandlers.ofString());
See here for the documentation of this option: Swapping KeyManager and TrustManager at runtime
And here for an actual working example: Example swapping certificates at runtime with HttpUrlConnection
And here for a server side example: Example swapping certificates at runtime with Spring Boot and Jetty Also other servers are possible such as Netty or Vert.x as long as they can either use SSLContext, SSLServerSocketFactory, TrustManager or KeyManager
You can add the library to your project with:
<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>sslcontext-kickstart</artifactId>
<version>7.4.8</version>
</dependency>
You can view the full documentation and other examples here: GitHub - SSLContext Kickstart
By the way I need to add a small disclaimer I am the maintainer of the library.

Related

Force client or server to restart SSL handshake (or expire SSL session)

I have a java client which connects to an HTTPS server (the server written in Java also). Here is the HttpClient setting in the client:
SSLContext ctx = SSLContext.getInstance("TLSv1.2");
keyManagers = ...; // Created from a PKIX KeyManagerFactory
trustManagers = ...; // Created from a PKIX TrustManagerFactory
ctx.init(keyManagers, trustManagers, new SecureRandom());
SSLContext.setDefault(ctx);
RequestConfig defaultRequestConfig = RequestConfig.custom()//
.setSocketTimeout(5000)//
.setConnectTimeout(5000)//
.setConnectionRequestTimeout(5000)//
.build();
httpClient = HttpClients.custom()//
.setSSLContext(ctx)//
.setDefaultRequestConfig(defaultRequestConfig)//
.setSSLHostnameVerifier(new NoopHostnameVerifier())//
.build();
The client certificate and trusted certificates are stored in a PKI token.
The client sends some HTTP requests to the server continuously. All things work fine. Now I want to force client (or server) to restart handshaking. In other words, I want to refresh SSL connection which causes to check server certificate periodically. Is there any way to do this?
I know about SSLSessionContext.setSessionTimeout(). But this will not refresh the current connection(s). It will force only new connections to do handshaking again.
For future readers.
I ask a similar question on security.stackexchange.com without details about programming. I had thought that the question may be a security issue. However, that question now is migrated from security.stackexchange.com to stackoverflow.com and has a convincing answer for me. I suggest referring to that: https://stackoverflow.com/a/55004572/5538979
You can clear the ssl caches with the following code snippet:
SSLContext sslContext = ...; // your initialised SSLContext
SSLSessionContext sslSessionContext = sslContext.getClientSessionContext();
Collections.list(sslContext.getClientSessionContext().getIds()).stream()
.map(sslSessionContext::getSession)
.filter(Objects::nonNull)
.forEach(SSLSession::invalidate);

Understanding SSL in Apache HttpClient (Java)

Here there is an example for custom SSL:
https://hc.apache.org/httpcomponents-client-ga/httpclient/examples/org/apache/http/examples/client/ClientCustomSSL.java
/**
* This example demonstrates how to create secure connections with a custom SSL
* context.
*/
public class ClientCustomSSL {
public final static void main(String[] args) throws Exception {
// Trust own CA and all self-signed certs
SSLContext sslcontext = SSLContexts.custom()
.loadTrustMaterial(new File("my.keystore"), "nopassword".toCharArray(),
new TrustSelfSignedStrategy())
.build();
// Allow TLSv1 protocol only
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslcontext,
new String[] { "TLSv1" },
null,
SSLConnectionSocketFactory.getDefaultHostnameVerifier());
CloseableHttpClient httpclient = HttpClients.custom()
.setSSLSocketFactory(sslsf)
.build();
try {
HttpGet httpget = new HttpGet("https://httpbin.org/");
System.out.println("Executing request " + httpget.getRequestLine());
CloseableHttpResponse response = httpclient.execute(httpget);
try {
HttpEntity entity = response.getEntity();
System.out.println("----------------------------------------");
System.out.println(response.getStatusLine());
EntityUtils.consume(entity);
} finally {
response.close();
}
} finally {
httpclient.close();
}
}
}
Why we need that? I've tested an HttpClient request without any SSL thing on it and I'm getting the correct response from HTTPS urls without errors.
What is the problem if I don't add any SSLContext?
And if it's important to make it more secure, what is this line?:
.loadTrustMaterial(new File("my.keystore"), "nopassword".toCharArray(),
it seems we need some file and also some password?
If you don't specify (a factory using) a context, Java (JSSE) uses a default context containing the default truststore, which defaults to the file JRE/lib/security/cacerts (or jssecacerts if present) unless overridden with system properties; see https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#CustomizingStores . Depending on whether you are using an Oracle-was-Sun Java package, an IBM or Apple package or Android system, or OpenJDK, this default truststore usually contains more or less the same set of public CAs as most OSes and browsers, like Verisign Symantec Digicert and GoDaddy and LetsEncrypt/Identrust. Whether you consider the default cacerts 'secure' is a choice for you to make; if not you can either change the contents of the default file, or have your code use a different file and to do the latter yes you must specify the filename of the keystore file and its password.
That example uses a custom store because it is an example of custom SSL. If it used the defaults, it would be an example of default SSL not an example of custom SSL. For many actual applications using the defaults is fine.
Aside: specifying only TLSv1 (meaning 1.0) for protocol is way out of date, and is likely to be considered insecure or at least borderline. It hasn't actually been broken outright like SSLv3 (and long ago SSLv2), because BEAST proved tamer than feared, but TLSv1.1 and 1.2 are now widely implemented and used, and 1.3 hopefully not too far away, so using 1.0 is widely considered substandard and for one example applicable to many people TLSv1.0 for payment-card transactions is prohibited outright as of last weekend.

Jetty: How to validate SSL client certs in application code?

I have a multi-tenant webservice which I want to use mutual SSL/TLS authentication as well as user authentication. This means that I need to resolve the user and the user's allowed certs, which can only occur after the SSL connection has been established. I will then use PKIXCertPathBuilderResult to valid the trust chain using the client certs passed in the request.
In Tomcat with the openssl connector, it's possible to use optional_no_ca mode, which requests a client cert but does not validate it.
With Jetty 9.x, I've tried configuring the following SslContextFactory options to no avail:
ValidateCerts=false
ValidatePeerCerts=false
TrustAll=true
How can this be achieved in Jetty 9.x?
Edit 2019: The requirement was to demand an SSL certificate from all client devices accessing the system. The validation of the certificate chain and other certificate attributes would then be performed by the application, which also has the ability to lookup missing cert roots from external sources.
This is in contrast to the norm - typically, application servers would perform cert-chain validation during the SSL connection setup using a pre-configured static list of known trusted CAs. If trust can not be found, the SSL connection is rejected.
While TrustAll seems to be the likely solution, it only works if no TrustStore and KeyStore is given. Then you can't connect using a regular client as the server has no certificate to give during the handshake.
To get a sensible trustAll mode, the only options seems to be to extend SslContextFactory:
package media.alu.jetty;
/**
* SslContextFactoryRelaxed is used to configure SSL connectors
* as well as HttpClient. It holds all SSL parameters and
* creates SSL context based on these parameters to be
* used by the SSL connectors.
*
* TrustAll really means trustAll!
*/
#ManagedObject
public class SslContextFactoryRelaxed extends SslContextFactory
{
private String _keyManagerFactoryAlgorithm = DEFAULT_KEYMANAGERFACTORY_ALGORITHM;
private String _trustManagerFactoryAlgorithm = DEFAULT_TRUSTMANAGERFACTORY_ALGORITHM;
#Override
protected TrustManager[] getTrustManagers(KeyStore trustStore, Collection<? extends CRL> crls) throws Exception
{
TrustManager[] managers = null;
if (trustStore != null)
{
if (isTrustAll()) {
managers = TRUST_ALL_CERTS;
}
// Revocation checking is only supported for PKIX algorithm
else if (isValidatePeerCerts() && "PKIX".equalsIgnoreCase(getTrustManagerFactoryAlgorithm()))
{
PKIXBuilderParameters pbParams = newPKIXBuilderParameters(trustStore, crls);
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(_trustManagerFactoryAlgorithm);
trustManagerFactory.init(new CertPathTrustManagerParameters(pbParams));
managers = trustManagerFactory.getTrustManagers();
}
else
{
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(_trustManagerFactoryAlgorithm);
trustManagerFactory.init(trustStore);
managers = trustManagerFactory.getTrustManagers();
}
}
return managers;
}
}
To use:
Follow Jetty documentation to configure SSL/TLS with client authentication
Compile code above against Jetty 9.x
Install jar in `$jetty.home/lib/ext'
Edit $jetty.home/etc/jetty-ssl-context.xml
i. Change:
<Configure id="sslContextFactory" class="org.eclipse.jetty.util.ssl.SslContextFactory">
to:
<Configure id="sslContextFactory" class="media.alu.jetty.SslContextFactoryRelaxed">
ii. Add <Set name="TrustAll">TRUE</Set> as child of <Configure id="sslContextFactory">
Why? JSSE already validates it. All you need to to is check the authorization of that user. By the time you get access to the certificate, it is already validated for integrity, non-expiry, and trust-anchoring, so you can believe that its SubjectDN refers to who it says it refers to, so all you have to do is decide what roles that SubjectDN has, if any.

JAVA Use two keystore same application

I'm trying to develop an application in java that basically gets a large number of data from REST-JSONs APIs and insert that on a DB. My problem is that two of the URLs I was working with, thrown errors about SSL certificates. To solve that, I created a jks file and inserted their certificates. The problem is, once I put that on my code, jvm only uses the certificates on that file, all others are rejected. So I want to make my application accepts jsons from either "cacerts.jks" and "custom.jks". Please don't tell me to add the certificates to the "teste.jks" (native from jvm), because the app won't run on my computer, I need the certificates on a sepparated file.
One more observation, I'm working with about 90 different URLs and I'm using the following line to indicate jvm, which keystore to use:
System.setProperty("javax.net.ssl.trustStore",System.getProperty("user.dir")+"/libs/teste.jks");
//or (for native one):
System.setProperty("javax.net.ssl.trustStore", "cacerts.jks");
Implement a subclass of javax.net.ssl.TrustManager or one of its subclasses like javax.net.ssl.X509TrustManager (MyTrustManager in the example below) and tweak the SSLContext to use it. Your trust manager implementation can do whatever you want with regard to validating/trusting certificates.
SSLContext context = SSLContext.getInstance("TLS");
MyTrustManager tm = new MyTrustManager();
SecureRandom sr = SecureRandom.getInstance("SHA1PRNG", "SUN");
context.init(null, new TrustManager[] { tm }, sr);
SSLContext.setDefault(context);
HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
Create two TrustManagers, one that uses the default truststore and one that uses your own, and initialize the SSLContext with both:
sslContext.init(null, new TrustManager[]{tm1, tm2}, null);
See the JSSE Reference Guide for further details.

Setting a client certificate as a request property in a Java HTTP connection?

I have a Java application that connects to another Java app through a socket with SSL, so my client JVM already has the -Djavax.net.ssl.keyStore and -Djavax.net.ssl.trustStore properties set.
This application needs to make some HTTP requests to a web server that requires client authentication. I can open the connection by using a URLConnection in Java which returns an HTTPSURLConnectionImpl.
The client certificate I want to present to the web server in the request is different than the one set as my JVM system property. Is there a way I can set a client cert. as a request property in the HTTPSURLConnectionImpl ?
Setting a SSL "client certificate" is not adequate directly through HTTPSURLConnectionImpl's request properties, because a digital signature is also required to prove you own the certificate. SSL already does all that automatically, so to makes sense to use that layer.
You have two ways to solve your issue going forward.
Through configuration
You can add you client key and certificate to your JVM KeyStore, it should be picked up at Runtime when the server asks for your client-side SSL authentication. (SSL/TLS is designed for that : the server will ask for a client certificate that is signed by it's trusted authority, which allows the SSL Engine to choose the right certificate, even when your KeyStore holds many).
Through Code
You can roll you own SSLContext using custom made KeyStore/TrustStores.
This is a bit complex (I won't elaborate on how to build Keystore instances in Java), but the gist of it is here :
public static void main(String[] args) throws Exception {
KeyStore clientKeyStore = ... // Whatever
KeyStore clientTrustStore = ... // Whatever you need to load
// We build the KeyManager (SSL client credentials we can send)
KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyFactory.init(clientKeyStore, "password".toCharArray());
KeyManager[] km = keyFactory.getKeyManagers();
// We build the TrustManager (Server certificates we trust)
TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustFactory.init(clientTrustStore);
TrustManager[] tm = trustFactory.getTrustManagers();
// We build a SSLContext with both our trust/key managers
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(km, tm, null);
SSLSocketFactory sslSf = sslContext.getSocketFactory();
// We prepare a URLConnection
URL url = new URL("https://www.google.com");
URLConnection urlConnection = url.openConnection();
// Before actually opening the sockets, we affect the SSLSocketFactory
HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) urlConnection;
httpsUrlConnection.setSSLSocketFactory(sslSf);
// Ready to go !
}

Categories