-->

Use specific keystore for JMS

2020-07-22 10:56发布

问题:

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?

回答1:

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.



回答2:

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.