Node.js TLS/SSL モジュール
1. TLS/SSL とは?
Transport Layer Security (TLS) とその前身である Secure Socket Layer (SSL) は、コンピュータネットワーク上でセキュアな通信を提供するためのプロトコルです。これらは以下の 3 つを保証します:
- 機密性 (Privacy): 通信が暗号化され、盗聴を防止します。
- データ完全性 (Data integrity): メッセージの内容が改ざんされた場合に検知できます。
- 認証 (Authentication): 通信相手の身元を確認できます。
TLS/SSL は、主に以下の用途で使用されます:
- Web ブラウジング (HTTPS)
- メール送信 (SMTP, IMAP, POP3)
- インスタントメッセージング
- VoIP (Voice over IP)
- API 通信
2. TLS モジュールの使用方法
Node.js で TLS モジュールを使用するには、require でインポートします:
const tls = require('tls');3. TLS サーバー
基本的な TLS サーバーを作成する方法は以下の通りです:
const tls = require('tls');
const fs = require('fs');
const path = require('path');
// TLS 証明書を含むサーバーオプション
const options = {
key: fs.readFileSync(path.join(__dirname, 'server-key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'server-cert.pem')),
// クライアント証明書を要求する(オプション)
requestCert: true,
// 認証されていない証明書による接続を拒否するか(オプション)
rejectUnauthorized: false
};
// TLS サーバーを作成
const server = tls.createServer(options, (socket) => {
console.log('サーバーに接続されました',
socket.authorized ? '認証済み' : '未認証');
// データのエンコーディングを設定
socket.setEncoding('utf8');
// 入力データのハンドル
socket.on('data', (data) => {
console.log('受信データ:', data);
// データをエコーバック
socket.write(`あなたの発言: ${data}`);
});
// ソケット終了のハンドル
socket.on('end', () => {
console.log('ソケットが終了しました');
});
// ウェルカムメッセージを送信
socket.write('TLS サーバーへようこそ!\n');
});
// TLS サーバーを起動
const port = 8000;
server.listen(port, () => {
console.log(`TLS サーバーがポート ${port} で稼働中です`);
});この例を実行するには、証明書ファイル(server-key.pem と server-cert.pem)が必要です。開発目的であれば、OpenSSL を使用して自己署名証明書を生成できます。
4. 開発用の自己署名証明書の生成
OpenSSL を使用して、開発・テスト用の自己署名証明書を生成するコマンド例です:
# CA (認証局) 証明書の生成
openssl genrsa -out ca-key.pem 2048
openssl req -new -x509 -key ca-key.pem -out ca-cert.pem -days 365
# サーバー証明書の生成
openssl genrsa -out server-key.pem 2048
openssl req -new -key server-key.pem -out server-csr.pem
openssl x509 -req -in server-csr.pem -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -days 365
# クライアント証明書の生成(オプション:相互認証用)
openssl genrsa -out client-key.pem 2048
openssl req -new -key client-key.pem -out client-csr.pem
openssl x509 -req -in client-csr.pem -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -days 3655. TLS クライアント
TLS サーバーに接続するクライアントを作成します:
const tls = require('tls');
const fs = require('fs');
const path = require('path');
// クライアントオプション
const options = {
// 相互認証用(オプション)
key: fs.readFileSync(path.join(__dirname, 'client-key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'client-cert.pem')),
// Server Name Indication (SNI) 用のサーバー名
servername: 'localhost',
// サーバーを検証するための CA 証明書(オプション)
ca: fs.readFileSync(path.join(__dirname, 'ca-cert.pem')),
// 認証されていない証明書を拒否する
rejectUnauthorized: true
};
// サーバーに接続
const client = tls.connect(8000, 'localhost', options, () => {
// 認証状態を確認
console.log('クライアントが接続されました',
client.authorized ? '認証済み' : '未認証');
if (!client.authorized) {
console.log('拒否理由:', client.authorizationError);
}
// サーバーにデータを送信
client.write('TLS クライアントからのメッセージです!');
});
// 受信データのエンコーディングを設定
client.setEncoding('utf8');
// 受信データのハンドル
client.on('data', (data) => {
console.log('サーバーからの受信:', data);
// 別のメッセージを送信
client.write('元気ですか?');
});
// エラーハンドリング
client.on('error', (error) => {
console.error('接続エラー:', error);
});
// 接続終了のハンドル
client.on('end', () => {
console.log('サーバーが接続を終了しました');
});
// 5秒後に接続を閉じる
setTimeout(() => {
console.log('接続を閉じています');
client.end();
}, 5000);6. サーバーおよびクライアントのオプション
tls.createServer() と tls.connect() は、TLS 接続を構成するための様々なオプションを受け取ります:
6.1 共通オプション
- key: PEM 形式の秘密鍵
- cert: PEM 形式の証明書
- ca: 信頼された CA 証明書
- ciphers: 暗号スイートの指定文字列
- minVersion: 許可する最小 TLS バージョン
- maxVersion: 許可する最大 TLS バージョン
6.2 サーバー固有のオプション
- requestCert: クライアントに証明書を要求するかどうか
- rejectUnauthorized: 無効な証明書を持つクライアントを拒否するか
- SNICallback: クライアントからの SNI を処理する関数
6.3 クライアント固有のオプション
- servername: SNI 用のサーバー名
- checkServerIdentity: サーバーのホスト名を検証する関数
- session: TLS セッションを含む Buffer インスタンス
const tls = require('tls');
const fs = require('fs');
// 詳細なサーバーオプションの例
const serverOptions = {
// キーと証明書
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
// 認証局
ca: [fs.readFileSync('ca-cert.pem')],
// プロトコルバージョンの制御
minVersion: 'TLSv1.2',
maxVersion: 'TLSv1.3',
// 暗号の制御
ciphers: 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384',
// クライアント認証
requestCert: true,
rejectUnauthorized: true,
// Server Name Indication (SNI) の処理
SNICallback: (servername, cb) => {
// サーバー名ごとに異なる証明書を適用
if (servername === 'example.com') {
cb(null, tls.createSecureContext({
key: fs.readFileSync('example-key.pem'),
cert: fs.readFileSync('example-cert.pem')
}));
} else {
// デフォルトの証明書
cb(null, tls.createSecureContext({
key: fs.readFileSync('default-key.pem'),
cert: fs.readFileSync('default-cert.pem')
}));
}
}
};7. セキュアな HTTP サーバー (HTTPS)
TLS モジュールを直接使用することもできますが、HTTPS サーバーの場合、Node.js は TLS 上に構築された高レベルの https モジュールを提供しています:
const https = require('https');
const fs = require('fs');
const path = require('path');
// HTTPS サーバーオプション
const options = {
key: fs.readFileSync(path.join(__dirname, 'server-key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'server-cert.pem'))
};
// HTTPS サーバーを作成
https.createServer(options, (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h1>セキュア HTTPS サーバー</h1><p>この接続は TLS を使用して暗号化されています。</p>');
}).listen(443, () => {
console.log('HTTPS サーバーがポート 443 で稼働中です');
});HTTPS モジュールは、セキュアな HTTP サーバーを作成するためのより便利な方法を提供しますが、内部的には TLS モジュールを使用しています。
8. Express での TLS 利用
Express を使用して HTTPS サーバーを作成することも可能です:
const express = require('express');
const https = require('https');
const fs = require('fs');
const path = require('path');
const app = express();
// ルートの定義
app.get('/', (req, res) => {
res.send('<h1>セキュア Express アプリ</h1><p>この接続は TLS で暗号化されています。</p>');
});
app.get('/api/data', (req, res) => {
res.json({
message: 'これは機密データです',
timestamp: new Date()
});
});
// HTTPS サーバーオプション
const options = {
key: fs.readFileSync(path.join(__dirname, 'server-key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'server-cert.pem'))
};
// Express アプリを使用して HTTPS サーバーを起動
const port = 443;
https.createServer(options, app).listen(port, () => {
console.log(`セキュア Express アプリがポート ${port} で稼働中です`);
});9. 証明書の検証
TLS は、サーバー(および任意でクライアント)の身元を確認するために証明書を使用します。以下は、カスタムの証明書検証を実装する例です:
const tls = require('tls');
const fs = require('fs');
// カスタム検証関数
function validateCertificate(cert) {
// 基本的な証明書情報の表示
console.log('証明書サブジェクト:', cert.subject);
console.log('証明書発行者:', cert.issuer);
console.log('有効期間開始:', cert.valid_from);
console.log('有効期間終了:', cert.valid_to);
// 証明書の有効期限チェック
const now = new Date();
const validFrom = new Date(cert.valid_from);
const validTo = new Date(cert.valid_to);
if (now < validFrom || now > validTo) {
return { valid: false, reason: '証明書の有効期間外です' };
}
return { valid: true };
}
// カスタム検証を含む TLS クライアントオプション
const options = {
ca: [fs.readFileSync('ca-cert.pem')],
checkServerIdentity: (hostname, cert) => {
// まず独自のカスタムルールで証明書をチェック
const validationResult = validateCertificate(cert);
if (!validationResult.valid) {
return new Error(validationResult.reason);
}
// 次にホスト名が証明書と一致するか検証
const certCN = cert.subject.CN;
if (hostname !== certCN &&
(!cert.subjectaltname || !cert.subjectaltname.includes(hostname))) {
return new Error(`証明書名の不一致: ${hostname} !== ${certCN}`);
}
// 証明書は有効
return undefined;
}
};10. TLS セッションの再開
セッション再開(Session Resumption)を利用すると、フルハンドシェイクを実行せずにサーバーに再接続できるため、パフォーマンスが向上します:
const tls = require('tls');
const fs = require('fs');
const path = require('path');
const serverOptions = {
key: fs.readFileSync(path.join(__dirname, 'server-key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'server-cert.pem')),
// セッション再開の有効化
sessionTimeout: 300, // セッションタイムアウト(秒)
ticketKeys: Buffer.from('0123456789abcdef0123456789abcdef'), // キー暗号化用の 32 バイト
};
const server = tls.createServer(serverOptions, (socket) => {
console.log('クライアントが接続されました');
// 再開されたセッションかどうかを確認
if (socket.isSessionReused()) {
console.log('セッションが再利用されました!');
} else {
console.log('新規セッションです');
}
socket.on('data', (data) => {
socket.write('返信メッセージです!');
});
});
server.listen(8443, () => {
console.log('TLS サーバーがポート 8443 でリッスン中');
// 1回目のクライアント接続
connectClient(() => {
// 2回目の接続 - セッション再開が使用されるはず
connectClient();
});
});
let savedSession = null;
function connectClient(callback) {
const clientOptions = {
rejectUnauthorized: false,
session: savedSession // 保存されたセッションがあれば使用
};
const client = tls.connect(8443, 'localhost', clientOptions, () => {
console.log('クライアント接続完了。認証状態:', client.authorized);
console.log('セッション再開を使用:', client.isSessionReused());
// 将来の接続のためにセッションを保存
savedSession = client.getSession();
client.write('ハロー、サーバー!');
setTimeout(() => {
client.end();
if (callback) setTimeout(callback, 100);
}, 100);
});
}11. Server Name Indication (SNI)
SNI を使用すると、単一の IP アドレスとポート上で、異なるホスト名に対して異なる証明書を提示できます:
const tls = require('tls');
const fs = require('fs');
const path = require('path');
const serverOptions = {
SNICallback: (servername, cb) => {
console.log(`SNI リクエスト: ${servername}`);
// ホスト名に基づいた異なる証明書コンテキスト
if (servername === 'example.com') {
const context = tls.createSecureContext({
key: fs.readFileSync(path.join(__dirname, 'example.com-key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'example.com-cert.pem'))
});
cb(null, context);
}
else if (servername === 'another.com') {
const context = tls.createSecureContext({
key: fs.readFileSync(path.join(__dirname, 'another.com-key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'another.com-cert.pem'))
});
cb(null, context);
}
else {
// デフォルト証明書
const context = tls.createSecureContext({
key: fs.readFileSync(path.join(__dirname, 'default-key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'default-cert.pem'))
});
cb(null, context);
}
},
// フォールバック用のデフォルト設定
key: fs.readFileSync(path.join(__dirname, 'default-key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'default-cert.pem'))
};
const server = tls.createServer(serverOptions, (socket) => {
socket.write(`${socket.servername || 'unknown'} へ接続されました!\n`);
socket.end();
});
server.listen(8443, () => {
console.log('TLS SNI サーバーがポート 8443 で稼働中');
});12. 高度な証明書管理
12.1 証明書チェーンと複数の CA
信頼の連鎖(Chain of Trust)を構築するために、中間証明書や複数の CA を指定します:
const caCerts = [
fs.readFileSync(path.join(__dirname, 'ca1-cert.pem')),
fs.readFileSync(path.join(__dirname, 'ca2-cert.pem')),
fs.readFileSync(path.join(__dirname, 'intermediate-cert.pem'))
];
const serverOptions = {
key: fs.readFileSync(path.join(__dirname, 'server-key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'server-cert.pem')),
ca: caCerts, // CA 証明書の配列
requestCert: true,
rejectUnauthorized: true
};12.2 CRL による証明書の失効チェック
証明書失効リスト (CRL) を使用して、無効化された証明書を拒否します:
// CRL の読み込みとチェックロジック(簡略化)
const crl = fs.readFileSync('revoked-certs.pem');
const server = tls.createServer({
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
requestCert: true,
rejectUnauthorized: true,
checkServerIdentity: (host, cert) => {
// 実際の実装ではシリアル番号を CRL と照合します
const revokedSerials = ['0123456789ABCDEF'];
if (revokedSerials.includes(cert.serialNumber)) {
return new Error('証明書が失効しています');
}
return undefined;
}
});13. Let's Encrypt による自動証明書管理
実務では、Let's Encrypt を使用して証明書の取得と更新を自動化するのが一般的です:
// コンセプトを示す簡略化された CertManager クラス
class TLSCertManager {
// ... (domain, email 等のプロパティ)
async getCertificates() {
// 証明書の存在と有効期限を確認
if (this.certsValid()) {
return this.loadCerts();
}
// certbot 等を使用して新規取得
return await this.obtainCertificates();
}
async obtainCertificates() {
// 本番環境では greenlock や acme 等のライブラリを使用します
console.log('Let\'s Encrypt から新規証明書を取得中...');
// execSync('certbot ...') などの処理
}
}14. セキュリティ・ベストプラクティス
本番環境で TLS を使用する際は、以下の構成を推奨します:
14.1 強力な TLS バージョンの使用
const options = {
minVersion: 'TLSv1.2',
// TLS 1.0 および 1.1 を明示的に無効化
secureOptions: crypto.constants.SSL_OP_NO_TLSv1 |
crypto.constants.SSL_OP_NO_TLSv1_1
};14.2 強力な暗号スイートの構成
const options = {
ciphers: [
'TLS_AES_256_GCM_SHA384',
'TLS_CHACHA20_POLY1305_SHA256',
'TLS_AES_128_GCM_SHA256',
'ECDHE-RSA-AES256-GCM-SHA384'
].join(':')
};14.3 Perfect Forward Secrecy (PFS) の利用
ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) を含む暗号スイートを使用します。
14.4 OCSP ステープリングの実装
サーバーが証明書の有効性をあらかじめ検証し、クライアントに提示することでプライバシーとパフォーマンスを向上させます。
15. 実戦的なユースケース
15.1 HTTP/2 による API ゲートウェイ
TLS をベースとした HTTP/2 サーバーにより、高パフォーマンスなルーティングを実現します。
15.2 リアルタイム・データストリーミング
セキュアなストリーム接続を維持し、クライアントへデータをブロードキャストします。
16. HTTP Strict Transport Security (HSTS) の利用
ブラウザに対して HTTPS 経由のみのアクセスを強制します:
// Express アプリケーションでの例
app.use((req, res, next) => {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
next();
});17. まとめ
Node.js の TLS/SSL モジュールは、セキュアなアプリケーション構築のための基盤を提供します。証明書の適切な管理、モダンなプロトコルバージョンの採用、そして SNI やセッション再開といった最適化技術を組み合わせることで、安全かつ高性能な通信インフラを構築することが可能になります。