Webflux + Webclient + Netty, how to use SslContext with multiple client certificates please? - java

Small question regarding Netty, Spring Webflux, and how to send http requests to multiples downstream systems, when each of the downstream require mTLS and a different client certificate is required to send requests to each please?
What I have so far in my Java 11 Spring Webflux 2.4.2 app for sending request is:
#Bean
#Primary
public WebClient getWebClient() {
return WebClient.create().mutate().defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).clientConnector(new ReactorClientHttpConnector(HttpClient.create().wiretap(true).secure(sslContextSpec -> sslContextSpec.sslContext(getSslContext())))).build();
}
And for the Netty SslContext (it is not an apache SSLContext btw)
public SslContext getSslContext() {
try {
final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
try (InputStream file = new FileInputStream(keyStorePath)) {
final KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(file, keyStorePassPhrase.toCharArray());
keyManagerFactory.init(keyStore, keyPassPhrase.toCharArray());
}
final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
try (InputStream trustStoreFile = new FileInputStream(trustStorePath)) {
final KeyStore trustStore = KeyStore.getInstance(trustStoreType);
trustStore.load(trustStoreFile, trustStorePassPhrase.toCharArray());
trustManagerFactory.init(trustStore);
}
return SslContextBuilder.forClient().keyManager(keyManagerFactory).trustManager(trustManagerFactory).build();
} catch (CertificateException | NoSuchAlgorithmException | IOException | KeyStoreException | UnrecoverableKeyException e) {
return null;
}
}
This is even working perfectly fine when we only need to send request to only one downstream.
This is even working if there are multiple downstream, and they accept the same client certificate!
But problem arise when each downstream requires me to use their respective client certificate.
May I ask how to achieve this please?
Thank you

Option 1
The most straightforward solution would be using a specific client for each downstream api. And having each client configured with their specific client key and trust material.
Option 2
But your question is: how to use SslContext with multiple client certificates please?
So I want to give you some code examples to have a working setup. But the short answer is: yes it is possible!
The long answer is that you need some additional configuration to make it working. Basically what you need to do is create a keymanagerfactory from your keystore-1 and get the keymanager from the keymanagerfactory and repeat that for the other two keystores. Afterwords you will have 3 keymanagers. The next step is to have a special kind of keymanager which can be supplied to the Netty SslContext. This special kind of keymanager has the ability to iterate through the 3 keymanagers which you have created earlier and it will select the correct key material to communicate with the server. What you need is a CompositeKeyManager and CompositeTrustManager which is mentioned at the following stackoverflow answer here: Registering multiple keystores in JVM
The actual code snippet will be the below. I disregarded the loading file with inputstream and creating the keystore file and creating the keymanagerfactory as you already know how to do that.
KeyManager keyManagerOne = keyManagerFactoryOne.getKeyManagers()[0]
KeyManager keyManagerTwo = keyManagerFactoryTwo.getKeyManagers()[0]
KeyManager keyManagerThree = keyManagerFactoryThree.getKeyManagers()[0]
List<KeyManager> keyManagers = new ArrayList<>();
keyManagers.add(keyManagerOne);
keyManagers.add(keyManagerTwo);
keyManagers.add(keyManagerThree);
CompositeX509KeyManager baseKeyManager = new CompositeX509KeyManager(keyManagers);
//repeat the same for the trust material
TrustManager trustManagerOne = trustManagerFactoryOne.getTrustManagers()[0]
TrustManager trustManagerTwo = trustManagerFactoryTwo.getTrustManagers()[0]
TrustManager trustManagerThree = trustManagerFactoryThree.getTrustManagers()[0]
List<TrustManager> trustManagers = new ArrayList<>();
trustManagers.add(trustManagerOne);
trustManagers.add(trustManagerTwo);
trustManagers.add(trustManagerThree);
CompositeX509TrustManager baseTrustManager = new CompositeX509TrustManager(trustManagers);
SslContext sslContext = SslContextBuilder.forClient()
.keyManager(baseKeyManager)
.trustManager(baseTrustManager)
.build();
And the above code should give you the capability of using multiple key and trust for a single client. This client will be able to communicate with the different downstream api's with the different key and trust material.
The downside of this setup is that you require to copy and paste the CompositeKeyManager and CompositeTrustManager into your code base and that the setup is a bit verbose. Java does not provide something out of the box for this use-case.
Option 3
If you want a a bit simpeler setup I would suggest you the code snippet below:
import io.netty.handler.ssl.SslContext;
import nl.altindag.ssl.SSLFactory;
import nl.altindag.ssl.util.NettySslUtils;
public class App {
public static void main(String[] args) {
SSLFactory sslFactory = SSLFactory.builder()
.withIdentityMaterial(keyStorePathOne, password)
.withIdentityMaterial(keyStorePathTwo, password)
.withIdentityMaterial(keyStorePathThree, password)
.withTrustMaterial(trustStorePathOne, password)
.withTrustMaterial(trustStorePathTwo, password)
.withTrustMaterial(trustStorePathThree, password)
.build();
SslContext sslContext = NettySslUtils.forClient(sslFactory).build();
}
}
I need to provide some disclaimer, I am the maintainer of the library of the code snippet above. The library is available here: GitHub - SSLContext Kickstart and it uses the same CompositeKeyManager and CompositeTrustManager under the covers which I mentioned earlier for option 2.
And you can add it to your pom with the following snippet:
<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>sslcontext-kickstart-for-netty</artifactId>
<version>7.4.9</version>
</dependency>

Related

Convert Program to use Secure Socket Connection via java-websockets library

I am attempting to convert an existing application that uses the org.java-websocket library to allow the webserver to communicate using https instead of the previous http. After researching, the only example I was able to find is here:
https://github.com/TooTallNate/Java-WebSocket/blob/master/src/main/example/SSLServerExample.java
public static void main(String[] args) throws Exception {
ChatServer chatserver = new ChatServer(
8887); // Firefox does allow multible ssl connection only via port 443 //tested on FF16
// load up the key store
String STORETYPE = "JKS";
String KEYSTORE = Paths.get("src", "test", "java", "org", "java_websocket", "keystore.jks")
.toString();
String STOREPASSWORD = "storepassword";
String KEYPASSWORD = "keypassword";
KeyStore ks = KeyStore.getInstance(STORETYPE);
File kf = new File(KEYSTORE);
ks.load(new FileInputStream(kf), STOREPASSWORD.toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, KEYPASSWORD.toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(ks);
SSLContext sslContext = null;
sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
chatserver.setWebSocketFactory(new DefaultSSLWebSocketServerFactory(sslContext));
chatserver.start();
}
The only problem with this is that I'm hesitant because it seems to be wanting you to access the keystore.jks and provide to the store password + keypassword and also seems to expect the KeyStore file to be on the running computer (or somewhere released with the software). Isn't this a security risk?
I already have the jar signed with the keystore, perhaps there is nothing more that I need to do? Can someone point me to a different example if this is not the way to do this?
This keystore I'm using is the one provided to us by an external company to our company to sign our java applications. Perhaps this is not the keystore I should be using and need to make one for this single app independently of that one?

Use specific keystore for JMS

We have the requirement to use SSL client certificate for a JMS connection to an IBM MQ server. I already asked a question specifically for Websphere MQ but then I learned that this is mainly the job of JSSE and can be configured via Java System Properties (e.g. -Djavax.net.ssl.keyStore=<location of keyStore>).
But since there are already active keystores for other parts of the application within our WildFly 9 AS, I'm looking for a way to enable a specific keystore just for the JMS part - can this be done?
Yes it is possible for an MQ classes for JMS application to use a specific keystore and truststore when creating secure connections to a queue manager.
By default, the MQ classes for JMS will use the standard javax.net.ssl System Properties to determine which certificate store to use as the key and trust stores. However, you can customise this by building your own javax.net.ssl.SSLSocketFactory object that gets set on the JMS Connection Factory used by your application.
See the Knowledge Center for further details:
https://www.ibm.com/support/knowledgecenter/SSFKSJ_9.0.0/com.ibm.mq.dev.doc/q032450_.htm
This typically means you have to programmatically build or update a JMS Connection Factory within application code, rather than via administration only and updating a JNDI definition - which is somewhat unfortunate.
I know you have stated you are using WildFly as your application server of choice, but just for your awareness, WebSphere Application Server (WSAS) allows you to configure a JMS Connection Factory within JNDI and have a separate SSL/TLS configuration (containing certificate store information, Cipher Suites etc) that can be associated with the JMS resources. WSAS will then take care of creating the SSLSocketFactory and setting it appropriately on the JMS Connection Factory when an application uses it to create a JMS Connection or Context.
As such, you continue to define your resources (JMS and SSL) administratively via the WSAS Administration Console or wsadmin scripting without having to insert specific logic within the application to do this, which is obviously preferred.
WildFly (and other JEE app servers) might offer similar functionality, but I do not know.
This maybe a little too late, but may help others. I was able to get it to work using JmsFactoryFactory,MQConnectionFactory, JKS trustStore and keyStore generated from a Certificate with the following code:
try {
JmsFactoryFactory factoryFactory = JmsFactoryFactory.getInstance(WMQConstants.WMQ_PROVIDER);
jmsConnectionFactory = (MQConnectionFactory) factoryFactory.createConnectionFactory();
SSLContext sslContext = createSSlContext();
setSSLSystemProperties();
jmsConnectionFactory.setSSLSocketFactory(sslContext.getSocketFactory());
// Set the properties
jmsConnectionFactory.setStringProperty(WMQConstants.WMQ_HOST_NAME, hostName);
jmsConnectionFactory.setIntProperty(WMQConstants.WMQ_PORT, port);
jmsConnectionFactory.setStringProperty(WMQConstants.WMQ_CHANNEL, channel);
jmsConnectionFactory.setIntProperty(WMQConstants.WMQ_CONNECTION_MODE, WMQConstants.WMQ_CM_CLIENT);
jmsConnectionFactory.setStringProperty(WMQConstants.WMQ_APPLICATIONNAME, APPLICATION_NAME);
jmsConnectionFactory.setBooleanProperty(WMQConstants.USER_AUTHENTICATION_MQCSP, false);
jmsConnectionFactory.setStringProperty(WMQConstants.WMQ_SSL_CIPHER_SUITE, cipherSuite);
return jmsConnectionFactory;
} catch (JMSException ex) {}
SSL Context
private SSLContext createSSlContext() throws NoSuchAlgorithmException {
SSLContext sslContext = SSLContext.getInstance("TLS");
try {
// Load KeyStore
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(toInputStream(keyStorePath), keyStorePassword.toCharArray());
// Load TrustStore
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(toInputStream(trustStorePath), trustStorePassword.toCharArray());
// Set KeyManger from keyStore
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(keyStore, keyStorePassword.toCharArray());
// Set TrustManager from trustStore
TrustManagerFactory trustFact = TrustManagerFactory.getInstance("SunX509");
trustFact.init(trustStore);
// Set Context to TLS and initialize it
sslContext.init(kmf.getKeyManagers(), trustFact.getTrustManagers(), null);
return sslContext;
} catch (Exception ex) {
LOG.error("Unable to load the SSL Config", ex);
}
return sslContext;
}
I've never worked with IBM MQ but i solved the similar task for various application containers and databases. As i can see from documentation it's possible to specify custom ssl connection factory for MQ using this method MQConnectionFactory.setSSLSocketFactory().
So yes, it's definitely possible to address your requirements and basically your task is to build a dedicated ssl socket factory for MQ connections.
Here is code snippets for this:
Utility class for generating in-memory keystores and truststores.
Java keyloader supports only pkcs8 private keys out of the box. To load pem keys some external library like BouncyCastle should be used.
It's possible to generate pkcs8 keys from pem keys using openssl.
public class KeystoreGenerator {
private KeystoreGenerator() {
}
public static KeyStore generateTrustStore(CertificateEntry certificateEntry) throws Exception {
return generateTrustStore(Collections.singletonList(certificateEntry));
}
public static KeyStore generateTrustStore(Collection<CertificateEntry> certificateEntries) throws Exception {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
for (CertificateEntry certificateEntry : certificateEntries) {
Certificate certificate = certFactory.generateCertificate(certificateEntry.getCertificate());
keyStore.setCertificateEntry(certificateEntry.getAlias(), certificate);
}
return keyStore;
}
public static KeyStore generateKeystore(PrivateKeyCertificateEntry privateKeyCertificateEntry) throws Exception {
return generateKeystore(Collections.singletonList(privateKeyCertificateEntry));
}
public static KeyStore generateKeystore(Collection<PrivateKeyCertificateEntry> privateKeyCertificateEntries) throws Exception {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
for (PrivateKeyCertificateEntry privateKeyCertificateEntry : privateKeyCertificateEntries) {
Certificate certificate = certFactory.generateCertificate(privateKeyCertificateEntry.getCertificate());
keyStore.setCertificateEntry(privateKeyCertificateEntry.getAlias(), certificate);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(IOUtils.toByteArray(privateKeyCertificateEntry.getKey()));
PrivateKey privateKey = keyFactory.generatePrivate(spec);
keyStore.setKeyEntry(privateKeyCertificateEntry.getAlias(), privateKey,
privateKeyCertificateEntry.getPassword(), new Certificate[]{certificate});
}
return keyStore;
}
public static class CertificateEntry {
private final InputStream certificate;
private final String alias;
// constructor, getters and setters
}
public static class PrivateKeyCertificateEntry {
private final InputStream key;
private final InputStream certificate;
private final String alias;
private final char[] password;
// constructor, getters and setters
}
}
The next code creates ssl socket factory for MQ using dedicated keystore and truststore. This code loads keys and certificates from disk as class path resources. It's also possible to store them only in memory using OS environment variables and some extra effort during client application deployment.
public SSLSocketFactory generateMqSSLSocketFactory() throws Exception {
KeyStore keyStore = KeystoreGenerator.generateKeystore(new KeystoreGenerator.PrivateKeyCertificateEntry(
getClass().getResourceAsStream("/keys/mq-client-key.pkcs8"),
getClass().getResourceAsStream("/keys/mq-client-certificate.pem"),
"mq_client", "changeit".toCharArray()
));
// Generate keystore to authorize client on server
KeyStore trustStore = KeystoreGenerator.generateTrustStore(new KeystoreGenerator.CertificateEntry(
getClass().getResourceAsStream("/keys/mq-server-certificate.pem"), "mq_server"));
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, "changeit".toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
return sslContext.getSocketFactory();
}
And then set this ssl socket factory to mq connection factory using MQConnectionFactory.setSSLSocketFactory() method. Seems that IBM MQ is a proprietary library so unfortunately i can't test it, but i guess such configuration should work.

Java - verifying certificate with system TrustStore

Premise: I have a certificate and I want to verify that the system 'trusts' this certificate (signed by a trusted root CA by Java / Operating System)
I have found some varying solutions on how to accomplish this.
Option 1:
Use SSL classes to derive trust.
TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmfactory.init((KeyStore) null);
for (TrustManager trustManager : tmfactory.getTrustManagers()) {
if (trustManager instanceof X509TrustManager) {
try {
((X509TrustManager) trustManager).checkClientTrusted(new X509Certificate[] {new JcaX509CertificateConverter().getCertificate(holder)}, "RSA");
System.out.println("This certificate is trusted by a Root CA");
} catch (CertificateException e) {
e.printStackTrace();
}
}
}
Since this approach relies heavily on SSL classes (which are not needed by the current project) we are looking for alternatives.
Option 2:
Load Java's cacertsfile into a keystore and check each 'most-trusted' certificate against my certificate for equality.
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());
// This class retrieves the most-trusted CAs from the keystore
PKIXParameters params = new PKIXParameters(keystore);
// Get the set of trust anchors, which contain the most-trusted CA certificates
Set<X509Certificate> rootCertificates = params.getTrustAnchors().parallelStream().map(TrustAnchor::getTrustedCert).collect(Collectors.toSet());
return rootCertificates.contains(holderX509);
The problem with this approach is that it requires a password to verify integrity of the JKS encoded file. While the SSL one seemingly does not (or rather uses System.getProperty("javax.net.ssl.trustStorePassword") which again is heavily tied to SSL.
Question: Does there exist a solution that is in between manually loading certificates from a file and pure SSL? I feel as if there should be some class that I can call to simply verify the system trust of a certificate without having to jump through a couple hoops.
After reading Beginning Cryptography With Java by David Hook I have produced the following example to verify a certificate chain (which accomplishes the original goal of using the system truststore to verify Root CA's)
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509", new BouncyCastleProvider());
InputStream is = new ByteArrayInputStream(some bytes in an array);
CertPath certPath = certificateFactory.generateCertPath(is, "PKCS7"); // Throws Certificate Exception when a cert path cannot be generated
CertPathValidator certPathValidator = CertPathValidator.getInstance("PKIX", new BouncyCastleProvider());
PKIXParameters parameters = new PKIXParameters(KeyTool.getCacertsKeyStore());
PKIXCertPathValidatorResult validatorResult = (PKIXCertPathValidatorResult) certPathValidator.validate(certPath, parameters); // This will throw a CertPathValidatorException if validation fails
This also accomplishes the goal of not having to use SSL classes - instead Java security classes / algorithms are used.
Short of downloading a third-party library, there probably isn't another alternative.
Why are you trying to avoid the "SSL" library? It's part of the standard library and so puts no burden on your program.
In any case, certificate verification is a big part of SSL. I doubt anyone's gone to the trouble of creating a library that does so without also implementing some substantial subset of the SSL protocol. There's just no real reason to do so.

TrustStore and Keystore during 2 way SSL validation

I have been unable to find a solution to this problem elsewhere so I am hoping someone here can provide some insight. My setup below:
keystore, myKeys.jks:
mine-private, 3/6/2014, PrivateKeyEntry
mine-trusted, 3/6/2014, trustedCertEntry
trust store, myTrust.jks:
trusted-cert-1, 3/6/2014, trusterCertEntry
trusted-cert-2, 3/6/2014, trusterCertEntry
mine-trusted, 3/6/2014, trustedCertEntry <-- this is mine
What ends up happening is I get a message stating that my client has not been authenticated. Let me know if there is more information necessary
Responses to questions:
First off: what classes/library are you using? Simply the default https in java?
Apache HTTP Client, code below:
HttpClient client = new HttpClient();
GetMethod method = new GetMethod("https://foo.bar.baz/rest");
client.executeMethod(method);
Secondly: how exactly are you registering the keystore/truststore? You need a custom SSLContext for this.
Don't think so, but could be wrong
-Djavax.net.ssl.trustStore="path/to/myTrust.jks"
-Djavax.net.ssl.trustStorePassword="password"
-Djavax.net.ssl.keyStore="path/to/myKeys.jks"
-Djavax.net.ssl.keyStorePassword="password"
First off: what classes/library are you using? Simply the default https in java?
Secondly: how exactly are you registering the keystore/truststore? You need a custom SSLContext for this.
Initial example:
SSLContext context = SSLContext.getInstance();
KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyFactory.init(keyStore, password);
TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustFactory.init(trustStore);
context.init(keyFactory.getKeyManagers(), trustFactory.getTrustManagers(), null);
Most libraries that I know support setting a custom SSLContext or SSLSocketFactory which can be obtained from the context.
I have written a sample that does the exact same thing. You can find the particular code in [1].
[1] https://github.com/wso2/carbon-identity/blob/v5.0.7/components/authentication-framework/org.wso2.carbon.identity.application.authentication.endpoint.util/src/main/java/org/wso2/carbon/identity/application/authentication/endpoint/util/TenantMgtAdminServiceClient.java#L155

How can I effect 'known_hosts' vs self-certified servers for my app's SSL usage?

Oh wise and noble Oracle,
I'm adding SSL to a TCP client I've written on my Android phone. I can
successfully connect to servers with properly signed certificates, and I can
connect to self-certifying hosts by cooking up a TrustManager implementation
that always thinks everything is fine.
I now have a decorator TrustManager capturing the certificates (before
delegating to its decoratee) for self-certifying hosts and presenting them for
my breathless perusal, but what I can't work out is how to implement ssh's
behaviour of warning that a host is unknown and offering to remember it for
next time - and doing so.
I presumed all I needed was to store the public key - as ssh does with
known_hosts - and re-represent it, but with this code and 'sslTrust' holding
the public key:
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null); // initialise!
ks.setKeyEntry("dbentry", Base64.decode(sslTrust, Base64.NO_WRAP), null);
tmf.init(ks);
tms = tmf.getTrustManagers();
ss.stm = new SnoopyTrustManager((X509TrustManager) tms[0]);
// ...
SLContext context = SSLContext.getInstance("SSL");
context.init(null, new TrustManager[] { ss.stm } , null);
ss.factory = context.getSocketFactory();
// ...
SocketFactory factory = ss.getFactory();
mSocket = factory.createSocket(host, port);
attempting to establish a connection results in
SSLHandshakeException: InvalidAlgorithmParameterException: trustAnchors.isEmpty()
which is fair enough: I don't know how to cook things up from the certificate
offered by the remote server. I'm also fairly sure this isn't how I tell a
TrustManager about a remote server's public key anyway.
Since the site is self-certifying, I imagine could probably just verify that
the public keys match in a trivial TrustManager, but I'd like to understand
how this 'should' be done - adding a CA on a per-connection basis, since
I won't trust that CA for anything else.
You need to use your own trust store on pre-ICS version, and add the serer's certificates to it on first error. Subsequent connections will load it from the trust store and thus trust the remote certificate. This is not a complete solution, but here's one way to do it (code on Github), along with some discussion:
http://nelenkov.blogspot.jp/2011/12/using-custom-certificate-trust-store-on.html

Categories

Resources