NodeJS 速習チュートリアル

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.pemserver-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 365

5. 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 やセッション再開といった最適化技術を組み合わせることで、安全かつ高性能な通信インフラを構築することが可能になります。