[MQ学習④]javaアプリからのMQIチャネル接続をSSL/TLS暗号化する

# はじめに

以下でjavaアプリからMQIClient経由でIBM MQとやり取りする手順を見た。
この発展として、キューマネージャー側に証明書を入れて、MQIチャネル接続をSSL/TLS暗号化する手順を試す。
下記の環境が構築されている前提で、SSL/TLS暗号化に必要な追加手順を記載する。

https://qiita.com/alphajinsei/items/8750c885adbb08096332

公式の記載は以下だが十分な情報とは言えず、主な出展はchat gptである点ご留意ください。

https://www.ibm.com/docs/ja/ibm-mq/9.2?topic=client-specifying-that-mqi-channel-uses-ssltls


完成系は以下のイメージ。オレンジの箇所を設定追加していく。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1523824/224644a8-7134-d519-fa00-90bda05301c1.png)




# 自己署名証明書(オレオレ証明書)を作成する

### CSR・秘密鍵を作成

```shell
[root@my-instance cert]# pwd
/root/cert
[root@my-instance cert]# openssl req -newkey rsa:2048 -nodes -keyout mqserver.key -out mqserver.csr -subj "/CN=QM1"
Generating a 2048 bit RSA private key
............................+++
.............................+++
writing new private key to 'mqserver.key'
-----
[root@my-instance cert]# ll
合計 8
-rw-r--r--. 1 root root  883  2月  8 13:50 mqserver.csr
-rw-r--r--. 1 root root 1704  2月  8 13:50 mqserver.key
```

### CSRに署名して自己署名証明書を作成

1年間有効なオレオレ証明書を作成。
```shell
[root@my-instance cert]# openssl x509 -req -in mqserver.csr -signkey mqserver.key -out mqserver.crt -days 365
Signature ok
subject=/CN=QM1
Getting Private key
[root@my-instance cert]# ll
合計 12
-rw-r--r--. 1 root root  960  2月  8 13:51 mqserver.crt   ★
-rw-r--r--. 1 root root  883  2月  8 13:50 mqserver.csr
-rw-r--r--. 1 root root 1704  2月  8 13:50 mqserver.key
```

# javaアプリに読み込ませるトラストストアをホストOS側に設定

Java クライアントではJKS(Java KeyStore)の形式で証明書を管理する。
keytool(JDKに同梱されているjava用の証明書管理ツール)を利用してトラストストア (truststore.jks) を作成し、先ほど作成したオレオレ証明書を登録する。
ホストOSの`/root/cert/truststore.jks`として配置。

```shell
[root@my-instance cert]# keytool -import -trustcacerts -alias mqserver -file mqserver.crt -keystore truststore.jks -storepass password
所有者: CN=QM1
発行者: CN=QM1
シリアル番号: 9a37b67239f760cf
有効期間の開始日: Sat Feb 08 13:51:01 JST 2025終了日: Sun Feb 08 13:51:01 JST 2026
証明書のフィンガプリント:
         SHA1: 73:9D:E9:4C:A6:EA:C1:63:8E:7E:D3:21:1B:86:D5:D9:42:A2:8F:74
         SHA256: BE:B8:5F:EF:F1:4C:05:22:20:8B:AB:24:35:C6:7B:5D:3B:C4:E0:9A:F7:AB:71:0C:2B:62:F1:49:53:A0:A4:02
署名アルゴリズム名: SHA256withRSA
サブジェクト公開鍵アルゴリズム: 2048ビットRSA鍵
バージョン: 1
この証明書を信頼しますか。 [いいえ]:  y
証明書がキーストアに追加されました
[root@my-instance cert]# ll
合計 16
-rw-r--r--. 1 root root  960  2月  8 13:51 mqserver.crt
-rw-r--r--. 1 root root  883  2月  8 13:50 mqserver.csr
-rw-r--r--. 1 root root 1704  2月  8 13:50 mqserver.key
-rw-r--r--. 1 root root 1046  2月  8 13:53 truststore.jks   ★
```

# MQ側に証明書・秘密鍵を登録する

MQコンテナの`/var/mqm/qmgrs/QM1/ssl`配下にキーストアを作成して証明書・秘密鍵を登録するまでを行う。

### 証明書・秘密鍵をPKCS#12形式に変換
MQ側に証明書・秘密鍵を配置する下準備として、証明書(.crt), 秘密鍵(.key)の形式を、
PKCS#12(.p12)形式に変換する。

この後MQ側でキーストアに証明書・秘密鍵を登録するが、その際に利用するコマンド`runmqakm`がPKCS#12形式しか受け付けないため。（要出展）

```shell
# ホストOS側で実施。(opensslコマンドがMQコンテナ内に存在しないため仕方なく)
[root@my-instance cert]# openssl pkcs12 -export -out mqserver.p12 -inkey mqserver.key -in mqserver.crt -name ibmwebspheremqqm1 -password pass:mqpass
[root@my-instance cert]# ll
合計 20
-rw-r--r--. 1 root root  960  2月  8 13:51 mqserver.crt
-rw-r--r--. 1 root root  883  2月  8 13:50 mqserver.csr
-rw-r--r--. 1 root root 1704  2月  8 13:50 mqserver.key
-rw-r--r--. 1 root root 2368  2月  8 14:02 mqserver.p12   ★
-rw-r--r--. 1 root root 1046  2月  8 13:53 truststore.jks
```

作成したPKCS#12形式のファイル`mqserver.p12`は、MQコンテナの`/var/mqm/qmgrs/QM1/ssl`配下に配置しておく。
```shell
# ホストOS側からMQコンテナへファイルをコピー
[root@my-instance cert]# docker cp mqserver.p12 QM1:/var/mqm/qmgrs/QM1/ssl

# MQコンテナ側に配置された
bash-4.4$ ll /var/mqm/qmgrs/QM1/ssl
total 4
-rw-r--r--. 1 root root 2368 Feb  8 05:02 mqserver.p12　★
```


### MQ側で利用するキーストアを作成

MQ側で利用するキーストアを`/var/mqm/qmgrs/QM1/ssl/mqserver.kdb`として作成。
`-stash`オプションを指定することで、キーストアのパスワード(`mqpass`)はstashファイルに難読化して保存される。(のでキーストアの中身を確認する際にパスワード入力不要になる）

```shell
bash-4.4$ pwd
/var/mqm/qmgrs/QM1/ssl
bash-4.4$ runmqakm -keydb -create -db mqserver.kdb -pw mqpass -type cms -stash
bash-4.4$ ll
total 20
-rw-------. 1 1001 root   88 Feb  8 05:08 mqserver.crl ★
-rw-------. 1 1001 root   88 Feb  8 05:08 mqserver.kdb ★★
-rw-r--r--. 1 root root 2368 Feb  8 05:02 mqserver.p12
-rw-------. 1 1001 root   88 Feb  8 05:08 mqserver.rdb ★
-rw-------. 1 1001 root  193 Feb  8 05:08 mqserver.sth ★
```

### PKCS#12形式の証明書・秘密鍵をキーストアに登録

```shell
bash-4.4$ runmqakm -cert -import -file mqserver.p12 -target mqserver.kdb -stashed -label ibmwebspheremqqm1 -type pkcs12 -pw mqpass
bash-4.4$  runmqakm -cert -list -db mqserver.kdb -stashed
Certificates found
* default, - personal, ! trusted, # secret key
-       ibmwebspheremqqm1
```
登録確認まで問題なし。
* 登録の状態がpersonal certificate(`-`)の状態になっていることが重要で、もしここがtrusted(`!`)になっているとキューマネージャーの証明書として認識されないらしい（1敗）（要出展）。
* 証明書ラベルの設定はIBM MQのデフォルトのネーミング`ibmwebspheremqqm1`を踏襲する。（ここを自由に設定したい場合、すぐ下で登場するMQ側で指定可能な証明書ラベル名と命名を合わせる必要がありそう。）

https://www.ibm.com/docs/ja/ibm-mq/9.3?topic=repository-digital-certificate-labels-understanding-requirements#q014340___title__5


# MQ本体の設定

### キューマネージャーの設定

キューマネージャーのSSLキー・リポジトリの設定を`/var/mqm/qmgrs/QM1/ssl/mqserver`とする。
※ディレクトリ名`/var/mqm/qmgrs/QM1/ssl`でもキーストア名`/var/mqm/qmgrs/QM1/ssl/mqserver.kdb`でもない微妙な指定である点注意。(1敗）

他はデフォルトのまま。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1523824/b159e067-8c85-9cc9-120d-3113a04f205e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1523824/efd7821e-a05f-f1d7-2a97-9a7b77377f5e.png)

### サーバ接続チャネルの設定

以下を明示的に指定する。他はデフォルト。

* チャネルの暗号使用(SSLCIPH)：`ANY_TLS12_OR_HIGHER`
    * チャネルの暗号使用(SSLCIPH)を最初は`TLS_RSA_WITH_AES_256_CBC_SHA256`で試していたが、MQコンテナ側かホストOS側かのどちらかが対応していないようでうまくいかず。（N敗）
* SSL認証(SSLCAUTH)：`オプション`
    * SSL認証とあるがSSL**クライアント**認証の有無の指定。これが必須になっていると、クライアント側も証明書を配置しての相互認証が必要になってしまい、今はサーバ側の証明書だけなのでオフにする。（1敗）


実機では以下で確認できる。
```bash
bash-4.4$ echo "DISPLAY QMGR SSLKEYR" | runmqsc QM1
5724-H72 (C) Copyright IBM Corp. 1994, 2021.
Starting MQSC for queue manager QM1.


     1 : DISPLAY QMGR SSLKEYR
AMQ8408I: Display Queue Manager details.
   QMNAME(QM1)
   SSLKEYR(/var/mqm/qmgrs/QM1/ssl/mqserver)
One MQSC command read.
No commands have a syntax error.
All valid MQSC commands were processed.
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1523824/081b8d82-fc58-b974-f002-90b58fc89f81.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1523824/64c8e60a-3688-2226-bf8c-59c795b9aca3.png)


実機では以下で確認できる。
```shell
bash-4.4$ echo "DISPLAY CHANNEL(DEV.APP.SVRCONN) SSLCIPH SSLCAUTH" | runmqsc QM1
5724-H72 (C) Copyright IBM Corp. 1994, 2021.
Starting MQSC for queue manager QM1.


     1 : DISPLAY CHANNEL(DEV.APP.SVRCONN) SSLCIPH SSLCAUTH
AMQ8414I: Display Channel details.
   CHANNEL(DEV.APP.SVRCONN)                CHLTYPE(SVRCONN)
   SSLCAUTH(OPTIONAL)                      SSLCIPH(ANY_TLS12_OR_HIGHER)
One MQSC command read.
No commands have a syntax error.
All valid MQSC commands were processed.
```


# javaコードにSSL/TLSの設定を追記

https://qiita.com/alphajinsei/items/8750c885adbb08096332

上記で作成したjavaコードに以下２か所の追記が必要。
１．先ほど作成したJKS形式のトラストストアを読み込む設定

```java
System.setProperty("javax.net.ssl.trustStore", "/root/cert/truststore.jks");
System.setProperty("javax.net.ssl.trustStorePassword", "password");
``` 

２．暗号スイートを指定する設定
```java
cf.setStringProperty(WMQConstants.WMQ_SSL_CIPHER_SUITE, "*TLS12ORHIGHER");
```


コードの全量は以下。

```java
[root@my-instance MQClient]# pwd
/root/MQClient
[root@my-instance MQClient]# cat com/ibm/mq/samples/jms/JmsPutGet.java
/*
* (c) Copyright IBM Corporation 2018
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.ibm.mq.samples.jms;

import java.io.Console;
import javax.jms.Destination;
import javax.jms.JMSConsumer;
import javax.jms.JMSContext;
import javax.jms.JMSException;
import javax.jms.JMSProducer;
import javax.jms.TextMessage;

import com.ibm.msg.client.jms.JmsConnectionFactory;
import com.ibm.msg.client.jms.JmsFactoryFactory;
import com.ibm.msg.client.wmq.WMQConstants;

/**
 * A minimal and simple application for Point-to-point messaging.
 *
 * Application makes use of fixed literals, any customisations will require
 * re-compilation of this source file. Application assumes that the named queue
 * is empty prior to a run.
 *
 * Notes:
 *
 * API type: JMS API (v2.0, simplified domain)
 *
 * Messaging domain: Point-to-point
 *
 * Provider type: IBM MQ
 *
 * Connection mode: Client connection
 *
 * JNDI in use: No
 *
 */
public class JmsPutGet {

        // System exit status value (assume unset value to be 1)
        private static int status = 1;

        // Create variables for the connection to MQ
        private static final String HOST = "localhost"; // Host name or IP address
        private static final int PORT = 1414; // Listener port for your queue manager
        private static final String CHANNEL = "DEV.APP.SVRCONN"; // Channel name
        private static final String QMGR = "QM1"; // Queue manager name
        private static final String APP_USER = "app"; // User name that application uses to connect to MQ
        private static final String APP_PASSWORD = "passw0rd"; // Password that the application uses to connect to MQ
        private static final String QUEUE_NAME = "DEV.QUEUE.1"; // Queue that the application uses to put and get messages to and from


        /**
         * Main method
         *
         * @param args
         */
        public static void main(String[] args) {
                System.setProperty("javax.net.ssl.trustStore", "/root/cert/truststore.jks"); //★追記
                System.setProperty("javax.net.ssl.trustStorePassword", "password"); //★追記
                // Sanity check main() arguments and warn user
                if (args.length > 0) {
                        System.out.println("\n!!!! WARNING: You have provided arguments to the Java main() function. JVM arguments (such as -Djavax.net.ssl.trustStore) must be passed before the main class or .jar you wish to run.\n\n");
                        Console c = System.console();
                        System.out.println("Press the Enter key to continue");
                        c.readLine();
                }

                // Variables
                JMSContext context = null;
                Destination destination = null;
                JMSProducer producer = null;
                JMSConsumer consumer = null;



                try {
                        // Create a connection factory
                        JmsFactoryFactory ff = JmsFactoryFactory.getInstance(WMQConstants.WMQ_PROVIDER);
                        JmsConnectionFactory cf = ff.createConnectionFactory();

                        // Set the properties
                        cf.setStringProperty(WMQConstants.WMQ_HOST_NAME, HOST);
                        cf.setIntProperty(WMQConstants.WMQ_PORT, PORT);
                        cf.setStringProperty(WMQConstants.WMQ_CHANNEL, CHANNEL);
                        cf.setIntProperty(WMQConstants.WMQ_CONNECTION_MODE, WMQConstants.WMQ_CM_CLIENT);
                        cf.setStringProperty(WMQConstants.WMQ_QUEUE_MANAGER, QMGR);
                        cf.setStringProperty(WMQConstants.WMQ_APPLICATIONNAME, "JmsPutGet (JMS)");
                        cf.setBooleanProperty(WMQConstants.USER_AUTHENTICATION_MQCSP, true);
                        cf.setStringProperty(WMQConstants.USERID, APP_USER);
                        cf.setStringProperty(WMQConstants.PASSWORD, APP_PASSWORD);
                        cf.setStringProperty(WMQConstants.WMQ_SSL_CIPHER_SUITE, "*TLS12ORHIGHER");  //★追記

                        // Create JMS objects
                        context = cf.createContext();
                        destination = context.createQueue("queue:///" + QUEUE_NAME);

                        long uniqueNumber = System.currentTimeMillis() % 1000;
                        TextMessage message = context.createTextMessage("Your lucky number today is " + uniqueNumber);

                        producer = context.createProducer();
                        producer.send(destination, message);
                        System.out.println("Sent message:\n" + message);


                        try {
                                System.out.println("60秒停止します");
                                Thread.sleep(60000);
                                System.out.println("一時停止を解除しました。");
                        } catch (InterruptedException e){
                                e.printStackTrace();
                        }


                        consumer = context.createConsumer(destination); // autoclosable
                        String receivedMessage = consumer.receiveBody(String.class, 15000); // in ms or 15 seconds

                        System.out.println("\nReceived message:\n" + receivedMessage);

                        context.close();

                        recordSuccess();
                } catch (JMSException jmsex) {
                        recordFailure(jmsex);
                }

                System.exit(status);

        } // end main()

        /**
         * Record this run as successful.
         */
        private static void recordSuccess() {
                System.out.println("SUCCESS");
                status = 0;
                return;
        }

        /**
         * Record this run as failure.
         *
         * @param ex
         */
        private static void recordFailure(Exception ex) {
                if (ex != null) {
                        if (ex instanceof JMSException) {
                                processJMSException((JMSException) ex);
                        } else {
                                System.out.println(ex);
                        }
                }
                System.out.println("FAILURE");
                status = -1;
                return;
        }

        /**
         * Process a JMSException and any associated inner exceptions.
         *
         * @param jmsex
         */
        private static void processJMSException(JMSException jmsex) {
                System.out.println(jmsex);
                Throwable innerException = jmsex.getLinkedException();
                if (innerException != null) {
                        System.out.println("Inner exception(s):");
                }
                while (innerException != null) {
                        System.out.println(innerException);
                        innerException = innerException.getCause();
                }
                return;
        }

}
```

モジュール等含めると以下の構成。
```shell
[root@my-instance MQClient]# find -ls
74710118    4 drwxr-xr-x   3 root     root         4096  2月  8 13:48 .
74908148 8148 -rw-r--r--   1 root     root      8339503 12月  8 05:45 ./com.ibm.mq.allclient-9.3.0.0.jar
74908149   64 -rw-r--r--   1 root     root        64009 12月  8 05:45 ./javax.jms-api-2.0.1.jar
74908150   72 -rw-r--r--   1 root     root        70939 12月  8 05:45 ./json-20220320.jar
204651028    0 drwxr-xr-x   3 root     root           17 12月  8 05:48 ./com
10132479    0 drwxr-xr-x   3 root     root           16 12月  8 05:48 ./com/ibm
74908151    0 drwxr-xr-x   3 root     root           21 12月  8 05:48 ./com/ibm/mq
142048921    0 drwxr-xr-x   3 root     root           17 12月  8 05:48 ./com/ibm/mq/samples
208213851    0 drwxr-xr-x   2 root     root          102  2月  8 14:38 ./com/ibm/mq/samples/jms
208213862    8 -rw-r--r--   1 root     root         4765  2月  8 13:47 ./com/ibm/mq/samples/jms/JmsPutGet.class
208213854    8 -rw-r--r--   1 root     root         6102  2月  8 14:38 ./com/ibm/mq/samples/jms/JmsPutGet.java
```


# javaアプリの実行

コンパイルして問題なく実行できることを確認。

```shell
[root@my-instance MQClient]# javac -cp ./com.ibm.mq.allclient-9.3.0.0.jar:./javax.jms-api-2.0.1.jar:./json-20220320.jar com/ibm/mq/samples/jms/JmsPutGet.java
[root@my-instance MQClient]# java -cp ./com.ibm.mq.allclient-9.3.0.0.jar:./javax.jms-api-2.0.1.jar:./json-20220320.jar:. com.ibm.mq.samples.jms.JmsPutGet
Sent message:

  JMSMessage class: jms_text
  JMSType:          null
  JMSDeliveryMode:  2
  JMSDeliveryDelay: 0
  JMSDeliveryTime:  1738993494654
  JMSExpiration:    0
  JMSPriority:      4
  JMSMessageID:     ID:414d5120514d31202020202020202020c7e0a66701ae0040
  JMSTimestamp:     1738993494654
  JMSCorrelationID: null
  JMSDestination:   queue:///DEV.QUEUE.1
  JMSReplyTo:       null
  JMSRedelivered:   false
    JMSXAppID: JmsPutGet (JMS)
    JMSXDeliveryCount: 0
    JMSXUserID: app
    JMS_IBM_PutApplType: 28
    JMS_IBM_PutDate: 20250208
    JMS_IBM_PutTime: 05445483
Your lucky number today is 545
60秒停止します
一時停止を解除しました。

Received message:
Your lucky number today is 545
SUCCESS
```

途中にキューがエンキューされている様子も確認できた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1523824/c6b95b00-da37-c374-93e3-0d81c4a63016.png)


コネクションを貼っている最中のサーバ接続チャネルの状態は以下。
暗号化スイートの値に`SSLCIPH(TLS_CHACHA20_POLY1305_SHA256)`が入っており、SSLのネゴシエーションの結果この暗号スイートが利用されていることがわかる。

```shell
bash-4.4$ echo "DISPLAY CHSTATUS(DEV.APP.SVRCONN)  SSLCIPH " | runmqsc QM1
5724-H72 (C) Copyright IBM Corp. 1994, 2021.
Starting MQSC for queue manager QM1.


     1 : DISPLAY CHSTATUS(DEV.APP.SVRCONN)  SSLCIPH
AMQ8417I: Display Channel Status details.
   CHANNEL(DEV.APP.SVRCONN)                CHLTYPE(SVRCONN)
   CONNAME(172.17.0.1)                     CURRENT
   SSLCIPH(TLS_CHACHA20_POLY1305_SHA256)   STATUS(RUNNING)
   SUBSTATE(RECEIVE)
One MQSC command read.
No commands have a syntax error.
All valid MQSC commands were processed.
```



# おわりに

公式情報が少ないのがつらい。

終わった後に以下の神の記事を発見した、もっと早く出会えていれば・・・
https://www.pulsarintegration.com/MQBasic/MQBasic12.html