はじめに
以下でjavaアプリからMQIClient経由でIBM MQとやり取りする手順を見た。
この発展として、キューマネージャー側に証明書を入れて、MQIチャネル接続をSSL/TLS暗号化する手順を試す。
下記の環境が構築されている前提で、SSL/TLS暗号化に必要な追加手順を記載する。
公式の記載は以下だが十分な情報とは言えず、主な出展はchat gptである点ご留意ください。
完成系は以下のイメージ。オレンジの箇所を設定追加していく。
自己署名証明書(オレオレ証明書)を作成する
CSR・秘密鍵を作成
[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年間有効なオレオレ証明書を作成。
[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
として配置。
[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形式しか受け付けないため。(要出展)
# ホスト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
配下に配置しておく。
# ホスト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ファイルに難読化して保存される。(のでキーストアの中身を確認する際にパスワード入力不要になる)
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形式の証明書・秘密鍵をキーストアに登録
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側で指定可能な証明書ラベル名と命名を合わせる必要がありそう。)
MQ本体の設定
キューマネージャーの設定
キューマネージャーのSSLキー・リポジトリの設定を/var/mqm/qmgrs/QM1/ssl/mqserver
とする。
※ディレクトリ名/var/mqm/qmgrs/QM1/ssl
でもキーストア名/var/mqm/qmgrs/QM1/ssl/mqserver.kdb
でもない微妙な指定である点注意。(1敗)
他はデフォルトのまま。
サーバ接続チャネルの設定
以下を明示的に指定する。他はデフォルト。
- チャネルの暗号使用(SSLCIPH):
ANY_TLS12_OR_HIGHER
- チャネルの暗号使用(SSLCIPH)を最初は
TLS_RSA_WITH_AES_256_CBC_SHA256
で試していたが、MQコンテナ側かホストOS側かのどちらかが対応していないようでうまくいかず。(N敗)
- チャネルの暗号使用(SSLCIPH)を最初は
- SSL認証(SSLCAUTH):
オプション
- SSL認証とあるがSSLクライアント認証の有無の指定。これが必須になっていると、クライアント側も証明書を配置しての相互認証が必要になってしまい、今はサーバ側の証明書だけなのでオフにする。(1敗)
実機では以下で確認できる。
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.
実機では以下で確認できる。
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の設定を追記
上記で作成したjavaコードに以下2か所の追記が必要。
1.先ほど作成したJKS形式のトラストストアを読み込む設定
System.setProperty("javax.net.ssl.trustStore", "/root/cert/truststore.jks");
System.setProperty("javax.net.ssl.trustStorePassword", "password");
2.暗号スイートを指定する設定
cf.setStringProperty(WMQConstants.WMQ_SSL_CIPHER_SUITE, "*TLS12ORHIGHER");
コードの全量は以下。
[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;
}
}
モジュール等含めると以下の構成。
[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アプリの実行
コンパイルして問題なく実行できることを確認。
[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
途中にキューがエンキューされている様子も確認できた。
コネクションを貼っている最中のサーバ接続チャネルの状態は以下。
暗号化スイートの値にSSLCIPH(TLS_CHACHA20_POLY1305_SHA256)
が入っており、SSLのネゴシエーションの結果この暗号スイートが利用されていることがわかる。
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