Node.js TLS接続でよくある問題と解決策: tls.TLSSocket編

2025-06-01

new tls.TLSSocket() は、Node.jsの tls (Transport Layer Security) モジュールが提供するクラスで、TLS (または SSL) プロトコルを用いた安全なネットワーク接続のためのソケットオブジェクトを新規に作成するためのコンストラクタです。

より具体的に言うと、これは通常の TCP ソケット(net.Socket)を拡張したもので、データの送受信を行う際に暗号化と認証の機能を提供します。これによって、クライアントとサーバー間でやり取りされる情報が第三者によって盗み見られたり、改ざんされたりするのを防ぐことができます。

new tls.TLSSocket() を使用する主な目的は以下の通りです。

  • TLS/SSL サーバーの実装
    安全な接続を受け付け、クライアントとの間で暗号化された通信を行うために使用します。
  • TLS/SSL クライアントの実装
    安全なサーバーに接続し、暗号化された通信を行うために使用します。

new tls.TLSSocket() を呼び出す際には、通常、以下のオプションを含むオブジェクトを引数として渡します。これらのオプションによって、TLS/SSL接続の振る舞いを細かく設定できます。

  • ALPNProtocols
    Application-Layer Protocol Negotiation (ALPN) で使用するプロトコルのリストを指定します。
  • SNICallback (サーバー側の場合)
    Server Name Indication (SNI) のコールバック関数を指定します。
  • rejectUnauthorized
    サーバー証明書の検証に失敗した場合に接続を拒否するかどうかを指定する真偽値です。
  • requestCert (サーバー側の場合)
    クライアント証明書を要求するかどうかを指定する真偽値です。
  • secureContext
    TLS/SSL の設定(証明書、秘密鍵、CA証明書など)を含む tls.SecureContext オブジェクトを指定します。
  • isServer (真偽値)
    ソケットがサーバー側であるか(true)、クライアント側であるか(false)を指定します。通常は server オプションが存在するかどうかで自動的に判断されます。
  • server (サーバー側の場合)
    tls.Server インスタンスを指定します。サーバーとして動作する場合に必要です。
  • socket (必須)
    既存の net.Socket オブジェクトを指定します。tls.TLSSocket は、この既存のソケットをラップしてTLS/SSL機能を追加します。

簡単な使用例(クライアント側)

const net = require('net');
const tls = require('tls');
const fs = require('fs');

const options = {
  host: 'example.com',
  port: 443,
  // CA証明書を指定(必要に応じて)
  ca: [fs.readFileSync('./ca.crt')]
};

const socket = net.connect(options, () => {
  const tlsSocket = new tls.TLSSocket(socket, {
    host: options.host,
    servername: options.host, // SNI (Server Name Indication) の設定
    // クライアント証明書が必要な場合は以下のように指定
    // cert: fs.readFileSync('./client.crt'),
    // key: fs.readFileSync('./client.key'),
    rejectUnauthorized: true // サーバー証明書の検証を行う
  });

  tlsSocket.on('secureConnect', () => {
    console.log('TLS接続が確立しました。');
    tlsSocket.write('Hello from client!\n');
  });

  tlsSocket.on('data', (data) => {
    console.log('サーバーからのデータ:', data.toString());
  });

  tlsSocket.on('end', () => {
    console.log('接続が閉じられました。');
  });

  tlsSocket.on('error', (err) => {
    console.error('TLSエラー:', err);
  });
});

この例では、まず net.connect() で通常の TCP ソケットを作成し、そのソケットを new tls.TLSSocket() に渡すことで、TLS/SSL 機能が追加された tlsSocket を作成しています。secureConnect イベントは、TLS/SSL のハンドシェイクが正常に完了したときに発生します。



一般的なエラーと原因、そしてトラブルシューティング

    • 原因
      指定されたホスト名とポート番号でサーバーがリスンしていない、またはファイアウォールで接続がブロックされている可能性があります。
    • トラブルシューティング
      • サーバーが起動しており、指定されたポートでリッスンしているか確認してください。
      • サーバー側のファイアウォール設定を確認し、接続を許可しているか確認してください。
      • クライアント側のファイアウォールやプロキシ設定も確認してください。
      • ホスト名が正しく解決されているか(DNSの問題がないか)確認してください。
  1. Error: self signed certificate (自己署名証明書エラー)

    • 原因
      サーバーが使用している証明書が信頼された認証局 (CA) によって署名されておらず、Node.js がデフォルトで自己署名証明書を信頼しないために発生します。
    • トラブルシューティング
      • 開発環境の場合
        tls.TLSSocket のオプションで rejectUnauthorized: false を設定することで、証明書の検証をスキップできます(本番環境では絶対に避けるべきです)。
      • 本番環境の場合
        • 信頼された CA によって署名された証明書を使用するようにサーバーを構成してください。
        • クライアント側で、サーバー証明書を発行した CA の証明書を ca オプションで明示的に指定してください。
        • 場合によっては、サーバー証明書の公開鍵を publicKey オプションで指定することも可能です。
  2. Error: unable to verify the first certificate (証明書チェーン検証エラー)

    • 原因
      サーバーが送信してきた証明書チェーンが完全ではなく、クライアントがルート CA まで辿って検証できない場合に発生します。
    • トラブルシューティング
      • サーバー側で、リーフ証明書だけでなく、中間 CA 証明書を含む完全な証明書チェーンを送信するように構成してください。
      • クライアント側で、ルート CA 証明書と必要に応じて中間 CA 証明書を ca オプションで指定してください。
  3. Error: TLS handshake timeout (TLSハンドシェイクタイムアウト)

    • 原因
      TLS/SSL のハンドシェイクが時間内に完了しなかった場合に発生します。ネットワークの遅延、サーバー側の負荷、またはクライアントとサーバーの間でサポートされている暗号スイートの不一致などが原因として考えられます。
    • トラブルシューティング
      • ネットワーク接続が安定しているか確認してください。
      • サーバーの負荷状況を確認してください。
      • クライアントとサーバーで共通してサポートされている暗号スイートがあるか確認してください。ciphers オプションで明示的に指定することも試してみてください。
      • タイムアウト時間を調整する必要があるかもしれません(Node.js のデフォルトのタイムアウト設定を確認してください)。
  4. Error: No shared cipher suite (共通の暗号スイートがない)

    • 原因
      クライアントとサーバーの間で、共通してサポートされている暗号化アルゴリズムの組み合わせ(暗号スイート)がない場合に発生します。
    • トラブルシューティング
      • クライアントとサーバーの設定を確認し、少なくとも一つの共通の暗号スイートが有効になっているか確認してください。
      • ciphers オプションを使用して、クライアントまたはサーバーが優先する暗号スイートのリストを明示的に指定してみてください。
  5. Error: Protocol "TLSv1.x" not supported or disabled (プロトコルがサポートされていないか無効)

    • 原因
      クライアントまたはサーバーが、接続しようとしている TLS/SSL プロトコルのバージョンをサポートしていないか、設定で無効になっている場合に発生します。
    • トラブルシューティング
      • minVersion および maxVersion オプションを使用して、許可する TLS/SSL プロトコルのバージョンを明示的に指定してみてください。ただし、古いプロトコルはセキュリティ上のリスクがあるため、慎重に検討してください。
  6. Error: Received fatal alert: handshake_failure (致命的なアラート: ハンドシェイク失敗)

    • 原因
      TLS/SSL ハンドシェイクの過程で、クライアントまたはサーバーが許容できないエラーを検出した場合に発生する一般的なエラーです。具体的な原因は、ログやネットワークキャプチャを確認する必要があります。
    • トラブルシューティング
      • クライアントとサーバーの TLS/SSL 設定(証明書、暗号スイート、プロトコルなど)が互いに互換性があるか確認してください。
      • サーバー側のエラーログを確認し、より詳細な情報がないか調べてください。
      • ネットワークキャプチャツール(Wiresharkなど)を使用して、TLS ハンドシェイクの詳細な流れを確認し、エラーが発生している箇所を特定してください。
  7. SNI (Server Name Indication) 関連の問題

    • 原因
      複数の仮想ホストが同じ IP アドレスで TLS/SSL を提供している場合、クライアントが接続先のホスト名をサーバーに通知しないと、サーバーが正しい証明書を提供できずエラーが発生することがあります。
    • トラブルシューティング
      • クライアント側の tls.TLSSocket オプションで servername を接続先のホスト名に正しく設定しているか確認してください。

トラブルシューティングの一般的なヒント

  • ドキュメントとコミュニティ
    Node.js の公式ドキュメントや、Stack Overflow などのコミュニティで情報を探してみてください。同様の問題に遭遇した人がいるかもしれません。
  • シンプルなテスト
    まずは最小限の設定で接続を試み、徐々に複雑な設定を追加していくことで、問題の箇所を特定しやすくなります。
  • ネットワークキャプチャ
    Wireshark などのツールを使用してネットワークトラフィックをキャプチャし、TLS ハンドシェイクの詳細な流れやエラーの内容を確認します。
  • 詳細なログ記録
    クライアントとサーバーの両方で、TLS/SSL 関連のログを有効にして、より詳細な情報を収集してください。Node.js の環境変数やライブラリのデバッグ機能を利用できる場合があります。
  • エラーメッセージをよく読む
    エラーメッセージには、問題の原因の手がかりが含まれていることが多いです。


簡単な TLS クライアントの例

この例では、自己署名証明書を使用している TLS サーバーに接続し、データを送信して受信します。

const net = require('net');
const tls = require('tls');
const fs = require('fs');

const options = {
  host: 'localhost',
  port: 8000,
  // サーバー証明書が自己署名の場合は rejectUnauthorized: false を設定
  // 本番環境では信頼された CA の証明書を使用すべきです
  rejectUnauthorized: false
};

const client = net.connect(options, () => {
  const tlsSocket = new tls.TLSSocket(client, {
    host: options.host,
    servername: options.host // SNI (Server Name Indication)
  });

  tlsSocket.on('secureConnect', () => {
    console.log('TLS接続が確立しました。');
    tlsSocket.write('クライアントからのメッセージです。\n');
  });

  tlsSocket.on('data', (data) => {
    console.log('サーバーからの応答:', data.toString());
    tlsSocket.end();
  });

  tlsSocket.on('end', () => {
    console.log('接続が閉じられました。');
  });

  tlsSocket.on('error', (err) => {
    console.error('TLSエラー:', err);
  });
});

client.on('error', (err) => {
  console.error('ソケットエラー:', err);
});

解説

  • client.on('error', ...): TCP ソケットのエラーが発生したときに発生するイベントです。
  • tlsSocket.on('error', ...): TLS/SSL 関連のエラーが発生したときに発生するイベントです。
  • tlsSocket.on('end', ...): 接続が閉じられたときに発生するイベントです。
  • tlsSocket.on('data', ...): サーバーから送信された暗号化されたデータを受信します。
  • tlsSocket.write(...): 暗号化されたデータをサーバーに送信します。
  • tlsSocket.on('secureConnect', ...): TLS/SSL ハンドシェイクが正常に完了したときに発生するイベントです。この時点で安全な通信が可能になります。
  • new tls.TLSSocket(client, { ... }): 既存の TCP ソケット (client) をラップし、TLS/SSL 機能を追加した tlsSocket を作成します。
    • host: 接続先のホスト名を指定します。
    • servername: SNI (Server Name Indication) をサーバーに通知するために、接続先のホスト名を指定します。これにより、サーバーは正しい証明書を提供できます。
    • rejectUnauthorized: false: 自己署名証明書を使用している場合の一時的な設定です。本番環境では、信頼された CA の証明書を使用し、rejectUnauthorized: true に設定すべきです。
  • net.connect(options, callback): 指定されたホストとポートへの TCP 接続を確立します。

簡単な TLS サーバーの例

この例では、クライアントからの TLS 接続を受け付け、データを受信して応答を送信します。自己署名証明書と秘密鍵が必要です。

const net = require('net');
const tls = require('tls');
const fs = require('fs');

const options = {
  key: fs.readFileSync('./server.key'), // サーバーの秘密鍵
  cert: fs.readFileSync('./server.crt'), // サーバーの証明書
  // クライアント証明書を要求する場合は以下を有効にする
  // requestCert: true,
  // 信頼する CA 証明書がある場合は以下を指定
  // ca: [fs.readFileSync('./ca.crt')]
};

const server = tls.createServer(options, (tlsSocket) => {
  console.log('クライアントが接続しました:', tlsSocket.remoteAddress, tlsSocket.remotePort);
  console.log('クライアント証明書:', tlsSocket.getPeerCertificate());

  tlsSocket.on('data', (data) => {
    console.log('クライアントからのデータ:', data.toString());
    tlsSocket.write('サーバーからの応答です。\n');
  });

  tlsSocket.on('end', () => {
    console.log('クライアントが切断しました。');
  });

  tlsSocket.on('error', (err) => {
    console.error('TLSソケットエラー:', err);
  });
});

server.listen(8000, () => {
  console.log('TLSサーバーがポート 8000 で起動しました。');
});

server.on('error', (err) => {
  console.error('TLSサーバーエラー:', err);
});

解説

  • server.on('error', ...): TLS サーバー全体のエラーが発生したときに発生するイベントです。
  • server.listen(port, callback): 指定されたポートでサーバーを起動し、クライアントからの接続を待ち受けます。
  • tlsSocket.getPeerCertificate(): 接続してきたクライアントの証明書情報を取得できます(requestCert: true が設定されている場合など)。
  • tlsSocket.remoteAddress, tlsSocket.remotePort: 接続してきたクライアントの IP アドレスとポート番号を取得できます。
  • tls.createServer(options, connectionListener): TLS/SSL を使用するサーバーを作成します。
    • options: サーバーの TLS 設定(秘密鍵、証明書など)を含むオブジェクトです。
      • key: サーバーの秘密鍵ファイルのパス。
      • cert: サーバーの証明書ファイルのパス。
      • requestCert: true: クライアントに証明書を要求します(オプション)。
      • ca: 信頼する CA 証明書ファイルのパスの配列(クライアント証明書を検証する場合に必要)。
    • connectionListener(tlsSocket): 新しい TLS 接続が確立されたときに呼び出されるコールバック関数です。tlsSocket は、個々の安全な接続を表す tls.TLSSocket オブジェクトです。

中間 CA 証明書を使用するクライアント

サーバーが中間 CA によって署名された証明書を使用している場合、クライアントはルート CA 証明書と中間 CA 証明書を提供する必要があります。

const net = require('net');
const tls = require('tls');
const fs = require('fs');

const options = {
  host: 'example.com', // 実際のホスト名に置き換えてください
  port: 443,
  // ルート CA 証明書と中間 CA 証明書を指定
  ca: [
    fs.readFileSync('./rootCA.crt'),
    fs.readFileSync('./intermediateCA.crt')
  ],
  rejectUnauthorized: true // 証明書の検証を有効にする
};

const client = net.connect(options, () => {
  const tlsSocket = new tls.TLSSocket(client, {
    host: options.host,
    servername: options.host
  });

  tlsSocket.on('secureConnect', () => {
    console.log('TLS接続が確立しました。');
    tlsSocket.write('Hello!\n');
  });

  tlsSocket.on('data', (data) => {
    console.log('Received:', data.toString());
    tlsSocket.end();
  });

  tlsSocket.on('error', (err) => {
    console.error('TLS Error:', err);
  });
});

解説

  • rejectUnauthorized: true は、証明書の検証を有効にするための重要な設定です。
  • ca オプションに、ルート CA 証明書と中間 CA 証明書の両方を配列として指定しています。これにより、Node.js はサーバーの証明書チェーンを正しく検証できます。

クライアント証明書を使用するサーバー

サーバーがクライアント証明書を要求する場合、クライアントは自身の証明書と秘密鍵を提供する必要があります。

サーバー側 (上記のサーバーの例で requestCert: true と ca を設定)

const options = {
  key: fs.readFileSync('./server.key'),
  cert: fs.readFileSync('./server.crt'),
  requestCert: true,
  ca: [fs.readFileSync('./clientCA.crt')] // クライアント証明書を発行した CA の証明書
};

クライアント側

const net = require('net');
const tls = require('tls');
const fs = require('fs');

const options = {
  host: 'localhost',
  port: 8000,
  cert: fs.readFileSync('./client.crt'), // クライアントの証明書
  key: fs.readFileSync('./client.key'), // クライアントの秘密鍵
  ca: [fs.readFileSync('./serverCA.crt')], // サーバー証明書を発行した CA の証明書
  rejectUnauthorized: true
};

const client = net.connect(options, () => {
  const tlsSocket = new tls.TLSSocket(client, {
    host: options.host,
    servername: options.host,
    cert: options.cert,
    key: options.key
  });

  tlsSocket.on('secureConnect', () => {
    console.log('TLS接続が確立しました (クライアント証明書あり)。');
    tlsSocket.write('Authenticated client!\n');
  });

  // ... (data, end, error イベントの処理は省略)
});
  • クライアント側
    cert オプションにクライアント自身の証明書、key オプションにクライアント自身の秘密鍵を指定します。また、サーバー証明書を検証するために、サーバー証明書を発行した CA の証明書を ca オプションで指定し、rejectUnauthorized: true を設定します。
  • サーバー側
    requestCert: true を設定し、信頼するクライアント証明書を発行した CA の証明書を ca オプションで指定します。


tls.connect() 関数

tls.connect() は、net.connect() を内部的に使用して TCP ソケットを確立し、そのソケットを自動的に tls.TLSSocket でラップして TLS/SSL 接続を開始する便利な関数です。クライアント側の TLS 接続をより簡単に記述できます。

const tls = require('tls');
const fs = require('fs');

const options = {
  host: 'example.com',
  port: 443,
  // 自己署名証明書の場合は rejectUnauthorized: false (本番環境では避ける)
  rejectUnauthorized: true,
  // 必要に応じて CA 証明書などを指定
  ca: [fs.readFileSync('./ca.crt')]
};

const tlsSocket = tls.connect(options, () => {
  console.log('TLS接続が確立しました。');
  tlsSocket.write('クライアントからのメッセージです。\n');
});

tlsSocket.on('data', (data) => {
  console.log('サーバーからの応答:', data.toString());
  tlsSocket.end();
});

tlsSocket.on('end', () => {
  console.log('接続が閉じられました。');
});

tlsSocket.on('error', (err) => {
  console.error('TLSエラー:', err);
});

解説

  • 返り値は tls.TLSSocket オブジェクトであり、new tls.TLSSocket() で作成した場合と同様にイベントリスナーを設定したり、データの送受信を行ったりできます。
  • tls.connect(options, connectListener): 指定されたオプションに基づいて TLS/SSL 接続を確立します。
    • options オブジェクトは、net.connect() のオプションに加えて、TLS/SSL 関連のオプション(servername, ca, cert, key, rejectUnauthorized など)を含めることができます。
    • connectListener は、TLS/SSL 接続が確立した後に呼び出されるコールバック関数です。

利点

  • TCP ソケットの作成と TLS ソケットの作成を 1 つの関数呼び出しで済ませることができ、コードが簡潔になります。

https モジュール (クライアント側)

HTTP over TLS (HTTPS) を扱う場合、https モジュールは tls.TLSSocket を直接操作するよりもはるかに高レベルで便利な API を提供します。HTTPS リクエストの作成、ヘッダーの処理、データの送受信などを抽象化してくれます。

const https = require('https');
const fs = require('fs');

const options = {
  hostname: 'example.com',
  port: 443,
  path: '/',
  method: 'GET',
  // 自己署名証明書の場合は rejectUnauthorized: false (本番環境では避ける)
  rejectUnauthorized: true,
  // 必要に応じて CA 証明書などを指定
  ca: [fs.readFileSync('./ca.crt')]
};

const req = https.request(options, (res) => {
  console.log('ステータスコード:', res.statusCode);
  console.log('ヘッダー:', res.headers);

  res.on('data', (chunk) => {
    console.log('データ:', chunk.toString());
  });

  res.on('end', () => {
    console.log('データ受信完了。');
  });
});

req.on('error', (error) => {
  console.error('HTTPSリクエストエラー:', error);
});

req.end();

解説

  • レスポンスオブジェクト (res) は、ステータスコード、ヘッダー、データストリームなどを提供します。
  • https.get(options, callback): GET リクエストに特化した便利な関数です。
  • https.request(options, callback): HTTPS リクエストを作成します。
    • options オブジェクトには、ホスト名、ポート、パス、HTTP メソッド、TLS/SSL 関連のオプションなどが含まれます。
    • callback は、サーバーからのレスポンスを受信したときに呼び出されます。

利点

  • HTTPS プロトコルの詳細(リクエストの作成、レスポンスの解析など)を意識せずに、簡単に HTTPS 通信を行うことができます。

http2 モジュール (クライアント側およびサーバー側)

HTTP/2 プロトコルを使用する場合、http2 モジュールは TLS/SSL を前提として動作することが多く、より効率的な通信を提供します。tls.TLSSocket を直接扱う必要はほとんどありません。

クライアント側の例

const http2 = require('http2');
const fs = require('fs');

const client = http2.connect('https://example.com', {
  // 自己署名証明書の場合は rejectUnauthorized: false (本番環境では避ける)
  rejectUnauthorized: true,
  ca: [fs.readFileSync('./ca.crt')]
}, () => {
  const req = client.request({ ':path': '/' });

  req.on('response', (headers, flags) => {
    console.log('ステータスコード:', headers[':status']);
    console.log('ヘッダー:', headers);
  });

  req.pipe(process.stdout);

  req.on('end', () => {
    client.close();
  });
});

client.on('error', (err) => console.error('HTTP/2 クライアントエラー:', err));

サーバー側の例

const http2 = require('http2');
const fs = require('fs');

const server = http2.createServer({
  key: fs.readFileSync('./server.key'),
  cert: fs.readFileSync('./server.crt')
});

server.on('stream', (stream, headers) => {
  stream.respond({
    'content-type': 'text/html; charset=utf-8',
    ':status': 200
  });
  stream.end('<h1>Hello from HTTP/2 server!</h1>');
});

server.listen(8443, () => {
  console.log('HTTP/2 サーバーがポート 8443 で起動しました。');
});

解説

  • ストリームベースの API を使用してリクエストとレスポンスを処理します。
  • http2.createServer(): HTTP/2 サーバーを作成します。TLS/SSL 設定(key, cert)が必要です。
  • http2.connect(): HTTP/2 サーバーへの接続を確立します。TLS/SSL がデフォルトで使用されます。

利点

  • 最新の HTTP/2 プロトコルの機能(多重化、ヘッダー圧縮など)を利用できます。

サードパーティのライブラリ

より高度な機能や特定のニーズに対応するために、サードパーティのライブラリも利用できます。例えば、WebSocket over TLS (WSS) を扱うためのライブラリなどがあります。これらのライブラリは、TLS/SSL のハンドリングを内部で行い、より使いやすい API を提供している場合があります。

  • 特定のプロトコル (WSS など)
    サードパーティのライブラリが役立つ場合があります。
  • HTTP/2 通信
    http2 モジュールが効率的な通信を可能にします。
  • HTTPS 通信
    https モジュールが高レベルな API を提供します。
  • クライアント側の簡単な TLS 接続
    tls.connect() が便利です。