はじめに
仕事でmTLSの疎通確認する環境を作る必要があったので、自環境内での疎通確認の方法を調べてたんですが、
サーバー認証の検証方法を書いてくださってるサイトはあるものの、
クライアントも認証する場合の環境周りの構築方法が書いてあるサイトさんは少なくて、結構困ったので、備忘も兼ねて記事にすることとしました。
この記事でできること
- mTLSの最小構成をローカルで再現できる
- サーバー証明書・クライアント証明書を自己署名CAで発行できる
- "どのファイルをどこに渡すか" が分かる
- ついでにTLSの最小構成も作れるようになる(はず)
対象読者
- ローカル環境で mTLS の一連の流れ を短時間で体験したい方
- mTLS を試したい人
- 証明書の SAN / EKU まわりでつまずいた経験がある人
- mkcert / OpenSSL の最小コマンド をコピペで動かして把握したい人
- 本番導入前に ローカルで再現→原因切り分け をしたい人
前提知識(あると理解が速い)
- TLS と証明書の基礎知識(CN・SAN・CA・CSR・鍵ペア)
- コマンドラインの基本操作(PowerShell/ターミナル)
想定環境
-
OS: Windows 10/11(WSLやmacOSでもほぼ同様)
-
必要ツール:
-
mkcert
※ なくても作れますが、あった方が楽なので、今回はmkcert使って作成します。 - OpenSSL( Shining Light Productions版 )
確認用
- PostmanなどのAPIツール
- 検証用サーバー(node.jsサーバーで良ければ、作り方記載してます。)
-
mkcert
用語ざっくり
CA: 証明書に「この相手は本物ですよ」というハンコを押す認証局。GMOグローバルサインなどが有名。
CSR: 証明書の申請書。鍵ペアに「CNやSANなどの情報」を添えてCAへ提出。
SAN: 証明書の対象名(DNS/IP)。サーバー証明書では必須。
EKU: Extended Key Usage。サーバーはserverAuth、クライアントはclientAuthを付ける。
証明書を作る
各種証明書の作成していきます。
に各ファイルの説明があるので、先に全体像把握したい方は、そちらを参照してください。
ただ、手元にファイルあった方がわかりやすいとは思います。
0.作成するファイル
中間認証局のファイル
- rootCA.pem
- rootCA-key.pem
サーバー側のファイル
- server.key
- server.csr
- server.ext (san.cnfでも可)
- server.crt
クライアント側のファイル
- client.key
- client.csr
- client.ext (san.cnfでも可)
- client.crt
1. 自己署名CAを用意(mkcert)
mkcert -install でローカル用のCAを作り、OSの信頼ストアに登録します。
windowsだと通常:
-
C:\Users\user\AppData\Local\mkcert\配下に
rootCA.pemとrootCA-key.pemが作成されます。
メモ: ここで作るのは「開発用のローカルCA」です。実運用では公的CAを利用します。
mkcert -install
以降の記載は、 rootCA.pem と rootCA-key.pemのあるディレクトリがカレントディレクトリになっていること前提になりますので、作業しやすい場所に移すことをお勧めします。
2. サーバー証明書の発行
2-1. 鍵とCSRを作る
# 秘密鍵(2048で十分。必要なら4096でも)
openssl genrsa -out server.key 2048
# CSR(Common Name は例として localhost)
openssl req -new -key server.key -out server/server.csr -subj "/CN=localhost"
2-2. 拡張設定(SAN/EKU)
server.ext を作成して、作業中のディレクトリに配置します。
今回はlocalhostにサーバーもある想定で記載していますが、
別のIPのサーバーとの接続を想定する場合は、IPアドレスなどをIP.1/IP.2に記載します。
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
IP.1 = 127.0.0.1
2-3. CAでサインしてサーバー証明書を発行
openssl x509 -req -in server.csr -CA rootCA.pem -CAkey rootCA-key.pem `
-out server.crt -days 365 -sha256 -CAcreateserial -extfile server.ext
3. クライアント証明書の発行
3-1. 鍵とCSR
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr -subj "/CN=test-client"
3-2. 拡張設定(EKU=clientAuth)
client.ext を作成:
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
3-3. CAでサインしてクライアント証明書を発行
openssl x509 -req -in client.csr -CA rootCA.pem -CAkey rootCA-key.pem `
-out client.crt -days 365 -sha256 -CAcreateserial -extfile client.ext
4. 動作確認
テストサーバーを立ち上げ、Postman等でAPI実行して疎通できたら成功です!
後はちゃんと認証できてることを、ファイル入れ替えたりして確認してください。
- クライアントの
.keyとか.crtを新しく作って入れ替えたり - サーバーの
.keyとか.crtを新しく作って入れ替えたり - 別のCA作ってみて、別のCAで認証したサーバーの証明書作ってみたり
色々してみて、暗号化技術の理解を深めていきたいですね。
5.テスト用node.jsコードサンプル
テスト用サーバーをnode.jsでのサンプルコードを配置しておきますので、参考程度に利用していただければと思います。
node.jsでのサーバー作成・起動の方法に関しては、わかりやすく説明してくださっているサイトさん等がいっぱいありますので、そちらを参照していただければと思います。
こことか
const express = require('express');
const app = express();
const https = require('https');
const fs = require('fs');
const path = require('path');
// サーバーの公開キー
const SERVER_CERT = path.join('', 'C:\\Users\\user\\AppData\\Local\\mkcert\\server.crt');
// サーバーの秘密キー
const SERVER_KEY = path.join('', 'C:\\Users\\user\\AppData\\Local\\mkcert\\server.key');
// CAの秘密キー
const CLIENT_CA = path.join('', 'C:\\Users\\user\\AppData\\Local\\mkcert\\rootCA.pem');
const options = {
cert: fs.readFileSync(SERVER_CERT), // サーバの証明書
key: fs.readFileSync(SERVER_KEY), // サーバの秘密鍵
ca: [fs.readFileSync(CLIENT_CA)], // クライアント証明書に署名したCAの証明書
rejectUnauthorized: false, // クライアント認証に失敗してもrejectしない
requestCert: true, // クライアント認証を実施
};
// クライアント認証を行うためのExpressミドルウェア
// req.socket.getPeerCertificate() は requestCert: true の場合にのみ有効
app.use((req, res, next) => {
const clientCert = req.socket.getPeerCertificate(true);
console.log('clientCert:' + clientCert)
console.log('req:'+ req)
if (req.client.authorized) { // rejectUnauthorized: true が設定されている場合、認証成功すると true
console.log('[Express Middleware] Client Certificate Authorized!');
console.log(` Subject: ${clientCert.subject.CN || 'N/A'}`);
console.log(` Issuer: ${clientCert.issuer.CN || 'N/A'}`);
// さらに証明書の内容をチェックしたい場合はここで実装
// 例: 特定のCNを持つクライアントのみを許可するなど
next(); // 次のミドルウェアまたはルートハンドラへ
} else {
// rejectUnauthorized: false の場合はここに来る可能性がある
// rejectUnauthorized: true の場合は通常ここまで到達しない (TLSハンドシェイクで拒否されるため)
console.error('[Express Middleware] Client Certificate Unauthorized!');
console.error(` Authorization Error: ${req.client.authorizationError || 'Unknown Error'}`);
res.status(401).send('Client certificate required and not authorized.');
}
});
app.use(express.json());
app.post('/test', (req, res) => {
console.log('Request Header:', req.headers);
console.log('[Express POST /submit] Request Body:', req.body);
res.json({
message: 'Data received via POST successfully!',
receivedData: req.body,
clientCertCN: req.socket.getPeerCertificate(true).subject.CN || 'N/A'
});
})
// POSTリクエスト用のエンドポイント
app.post('/ex/v1/get_members', (req, res) => {
console.log('[Express POST /submit] Request Body:', req.body);
// ここでリクエストボディを処理するロジックを記述
res.json({
message: 'Data received via POST successfully!',
receivedData: req.body,
clientCertCN: req.socket.getPeerCertificate(true).subject.CN || 'N/A'
});
});
// ルートパスやその他のGETリクエスト
app.get('/', (req, res) => {
res.send('Welcome to the Express HTTPS server!');
});
// HTTPSサーバーの作成
const serverInstance = https.createServer(options, app); // ここでExpress appを渡す
// エラーハンドリング (以前追加した部分)
serverInstance.on('tlsClientError', (err, socket) => {
console.error('[Server TLS Client Error] An error occurred during TLS handshake from client:');
console.error(` Error Code: ${err.code}`);
console.error(` Error Message: ${err.message}`);
if (err.fatal) {
console.error(' Fatal: true (connection closed)');
}
});
serverInstance.on('clientError', (err, socket) => {
// This event fires for generic client connection errors, not necessarily TLS errors
console.error('[Server Client Error] An error occurred from client connection:');
console.error(` Error: ${err.message}`);
// Only try to end the socket if it's still writable
if (!socket.writableEnded) {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
}
});
serverInstance.listen(8444, () => {
console.log('HTTPS listening on 8444....');
});
8. 最小コマンドだけのチートシート
# CA(mkcert)
mkcert -install
copy $env:LOCALAPPDATA\mkcert\rootCA.pem .
copy $env:LOCALAPPDATA\mkcert\rootCA-key.pem .
# Server
openssl genrsa -out server/server.key 2048
openssl req -new -key server/server.key -out server/server.csr -subj "/CN=localhost"
# server.ext を用意してから
openssl x509 -req -in server/server.csr -CA rootCA.pem -CAkey rootCA-key.pem -out server/server.crt -days 365 -sha256 -CAcreateserial -extfile server/server.ext
# Client
openssl genrsa -out client/client.key 2048
openssl req -new -key client/client.key -out client/client.csr -subj "/CN=test-client"
# client.ext を用意してから
openssl x509 -req -in client/client.csr -CA rootCA.pem -CAkey rootCA-key.pem -out client/client.crt -days 365 -sha256 -CAcreateserial -extfile client/client.ext
# PKCS#12
openssl pkcs12 -export -inkey server/server.key -in server/server.crt -certfile rootCA.pem -name server -out server/server-keystore.p12
openssl pkcs12 -export -inkey client/client.key -in client/client.crt -certfile rootCA.pem -name client -out client/client.p12
# truststore(Java用)
keytool -import -trustcacerts -file rootCA.pem -alias local-dev-ca -keystore truststore.p12 -storetype PKCS12
その他補足
-
PEMよりP12の方が、実務上は扱いやすいです
openssl利用等すれば、簡単にp12(PKCS12)に変更できるので、そちらの方がファイルの扱いがしやすいです- 今回は、テスト対象のシステムのJVM上でp12使う上でのバグが影響して使えなかったので、p12でのテストではない方法でこの記事を記載しています (気が向いたら自環境でp12でのテスト行ってみて、記事を追記しようと思います。)
- ただ、根本的にやっていることは同じなので、こちらで一回実装してみて損はないのかなと思います