Node.js イベント駆動:OCSPRequest イベントハンドラの書き方

2025-06-01

もう少し詳しく説明します。

OCSP とはその役割

OCSP は、X.509 デジタル証明書の失効ステータスをリアルタイムで確認するためのプロトコルです。従来の証明書失効リスト (CRL) と比較して、よりタイムリーで効率的な失効確認方法を提供します。

'OCSPRequest' イベントが発生する状況

このイベントは、TLS/SSL ハンドシェイク中に、サーバーがクライアントから提示された証明書の有効性をOCSPで確認しようとする場合に発生します。通常、サーバー側で OCSP ステーピング(OCSP Stapling)が有効になっていない場合に、サーバー自身が OCSP レスポンダーにリクエストを送信して確認を行います。

イベントハンドラの役割

'OCSPRequest' イベントが発生すると、イベントリスナーとして登録された関数が呼び出されます。このイベントハンドラは通常、以下の処理を行うために使用されます。

  1. OCSP リクエストのカスタマイズ
    デフォルトの OCSP リクエストをカスタマイズする必要がある場合に、リクエストの内容を変更できます。
  2. OCSP レスポンスの処理
    サーバーが OCSP レスポンダーから受け取ったレスポンスを処理し、証明書の失効ステータスを確認します。
  3. エラー処理
    OCSP リクエストの送信やレスポンスの処理中にエラーが発生した場合の処理を記述します。

イベントオブジェクト

'OCSPRequest' イベントのイベントハンドラには、通常、以下の情報を含むオブジェクトが渡されます。

  • res (Buffer)
    OCSP レスポンダーから受信した OCSP レスポンスを格納するための空の Buffer オブジェクト。イベントハンドラ内でこの Buffer にレスポンスデータを書き込む必要があります。
  • req (Buffer)
    送信される OCSP リクエストの ASN.1 エンコードされた内容を含む Buffer オブジェクト。

簡単なコード例

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

const server = tls.createServer({
  cert: fs.readFileSync('server-cert.pem'),
  key: fs.readFileSync('server-key.pem'),
  requestCert: true, // クライアント証明書を要求する
  rejectUnauthorized: true // 無効なクライアント証明書を拒否する
}, (socket) => {
  console.log('クライアントが接続しました。');
});

server.on('OCSPRequest', (req, res) => {
  console.log('OCSP リクエストを受信しました。');
  // ここで OCSP リクエストを処理し、レスポンスを res に書き込む処理を実装します。
  // 実際の実装は OCSP レスポンダーとの通信などが必要になります。
  // 簡単な例として、常に "good" なステータスを返すダミーのレスポンスを作成することも可能です。
  // res.end(dummyOCSPResponse);
});

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

この例では、tls.createServer で作成されたサーバーが 'OCSPRequest' イベントをリッスンしています。クライアントが接続し、クライアント証明書を提示すると、サーバーは OCSP リクエストを送信しようとし、その際に 'OCSPRequest' イベントが発生します。イベントハンドラ内では、受信した OCSP リクエスト (req) を確認し、OCSP レスポンダーからレスポンスを取得して (res) に書き込む処理を実装する必要があります。

  • 実運用では、OCSP クライアントのライブラリを利用することが一般的です。
  • 実際に動作する OCSP クライアントの実装は複雑になる場合があります。Node.js の標準モジュールだけで完全に実装するには、ASN.1 のエンコード・デコードやネットワーク通信の知識が必要です。


一般的なエラーとトラブルシューティング

    • エラー内容
      サーバーが指定された OCSP レスポンダーに接続できない。ネットワークの問題、ファイアウォールの設定ミス、または OCSP レスポンダーのダウンなどが原因として考えられます。
    • トラブルシューティング
      • サーバーから OCSP レスポンダーへのネットワーク接続を確認してください (ping, telnet など)。
      • ファイアウォールが OCSP レスポンダーへのアクセスをブロックしていないか確認してください。
      • OCSP レスポンダーの稼働状況を確認してください。
      • OCSP レスポンダーの URL が正しく設定されているか確認してください。
  1. OCSP リクエストの形式不正

    • エラー内容
      'OCSPRequest' イベントで受け取った req オブジェクト(OCSP リクエスト)の形式が OCSP レスポンダーが期待する形式と異なる。これは、Node.js の内部実装の問題である可能性は低いですが、もしカスタムでリクエストを生成している場合は注意が必要です。
    • トラブルシューティング
      • 通常は Node.js の TLS モジュールが正しくリクエストを生成するため、このエラーに遭遇することは稀です。もしカスタム処理を行っている場合は、OCSP の RFC に準拠したリクエストを生成しているか確認してください。
  2. OCSP レスポンスの処理エラー

    • エラー内容
      OCSP レスポンダーから受信した res オブジェクト(OCSP レスポンス)の処理に失敗する。レスポンスの形式が不正、署名の検証に失敗するなどが考えられます。
    • トラブルシューティング
      • 受信した res の内容をログ出力して確認し、OCSP レスポンダーからのレスポンスが正常であるか確認してください。
      • OCSP レスポンスの署名検証に必要な CA 証明書が正しく設定されているか確認してください。
      • OCSP レスポンスの ASN.1 デコード処理に誤りがないか確認してください(もし手動でデコードしている場合)。
  3. OCSP レスポンスのタイムアウト

    • エラー内容
      OCSP レスポンダーからのレスポンスが時間内に返ってこない。ネットワークの遅延や OCSP レスポンダーの負荷が高いなどが原因として考えられます。
    • トラブルシューティング
      • ネットワークの状況を確認してください。
      • OCSP レスポンダーのレスポンスタイムが遅くないか確認してください。
      • 必要に応じて、OCSP リクエストのタイムアウト値を調整することを検討してください(Node.js の TLS オプションで設定できる場合があります)。
  4. OCSP レスポンダーが失効情報を返さない

    • エラー内容
      OCSP レスポンダーにリクエストを送信しても、証明書の失効ステータスに関する情報がレスポンスに含まれていない。
    • トラブルシューティング
      • OCSP レスポンダーの設定を確認し、要求された証明書の失効情報を提供できるように構成されているか確認してください。
      • リクエストしている証明書に対して、OCSP レスポンダーが有効な情報を保持しているか確認してください。
  5. イベントハンドラ内でのエラー

    • エラー内容
      'OCSPRequest' イベントに登録したハンドラ関数内で例外が発生し、処理が中断される。
    • トラブルシューティング
      • イベントハンドラ内のコードを注意深くレビューし、例外が発生する可能性のある箇所に try...catch ブロックを追加してエラーハンドリングを行ってください。
      • エラーログを確認し、発生した例外の詳細を把握してください。
  6. OCSP ステーピングとの混同

    • 誤解
      'OCSPRequest' イベントは常に発生するものだと考えている。
    • 説明
      サーバー側で OCSP ステーピングが有効になっている場合、サーバーは自身の証明書の OCSP レスポンスをクライアントに TLS ハンドシェイク時に送信するため、通常クライアント証明書の検証のために 'OCSPRequest' イベントは発生しません。このイベントが発生するのは、サーバーがクライアント証明書の検証のために OCSP リクエストを送信しようとする場合です。

トラブルシューティングのヒント

  • OCSP クライアントライブラリの利用
    自力で OCSP クライアントを実装するのではなく、信頼できるサードパーティの OCSP クライアントライブラリを利用することを検討してください。これにより、複雑な処理を安全かつ効率的に行うことができます。
  • ネットワーク監視
    Wireshark などのネットワーク監視ツールを使用して、サーバーと OCSP レスポンダー間の通信をキャプチャし、リクエストとレスポンスの内容を確認するのも有効な手段です。
  • ログ出力
    'OCSPRequest' イベントハンドラ内で、送信するリクエスト (req) の内容や、受信したレスポンス (res) の内容をログ出力するようにしてください。エラー発生時の原因究明に役立ちます。


基本的なサーバーでの 'OCSPRequest' イベントの捕捉とログ出力

これは、'OCSPRequest' イベントが発生したことを確認し、リクエストの内容をログ出力する基本的な例です。実際には、ここで OCSP レスポンダーとの通信とレスポンスの処理を行う必要があります。

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

const server = tls.createServer({
  cert: fs.readFileSync('server-cert.pem'),
  key: fs.readFileSync('server-key.pem'),
  ca: [fs.readFileSync('ca-cert.pem')], // クライアント証明書の検証に必要な CA 証明書
  requestCert: true, // クライアント証明書を要求する
  rejectUnauthorized: true // 無効なクライアント証明書を拒否する
}, (socket) => {
  console.log('クライアントが接続しました。');
});

server.on('OCSPRequest', (req, res) => {
  console.log('--- OCSP リクエストを受信しました ---');
  console.log('OCSP リクエスト (Buffer):', req);

  // ここで OCSP レスポンダーにリクエストを送信し、
  // レスポンスを解析して res に書き込む処理を実装する必要があります。
  // この例では、簡略化のため空のレスポンスを送信します。
  res.end(Buffer.from([]));
});

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

このコードでは、TLS サーバーがクライアント証明書を要求するように設定されています (requestCert: true)。クライアントが証明書を提示すると、サーバーは OCSP でその有効性を確認しようとし、'OCSPRequest' イベントが発生します。イベントハンドラでは、受信した OCSP リクエスト (req) の Buffer の内容をログ出力し、簡略化のため空の Bufferres.end() で送信しています。実際には、ここで OCSP レスポンダーとの通信処理を実装する必要があります。

ダミーの OCSP レスポンスを返す例

OCSP レスポンダーとの実際の通信を伴わない、常に「good」(有効)なステータスを返すダミーの OCSP レスポンスを作成する例です。これはテストや開発環境での利用を想定しています。

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

// 簡略化されたダミーの OCSP レスポンス (ASN.1 エンコードされている必要があります)
// 実際には、OCSP レスポンダーからのレスポンスを模倣した正しい形式の Buffer を作成する必要があります。
const dummyOCSPResponse = Buffer.from('MIIB...省略...'); // 実際の OCSP レスポンスの ASN.1 エンコードされたデータ

const server = tls.createServer({
  cert: fs.readFileSync('server-cert.pem'),
  key: fs.readFileSync('server-key.pem'),
  ca: [fs.readFileSync('ca-cert.pem')],
  requestCert: true,
  rejectUnauthorized: true
}, (socket) => {
  console.log('クライアントが接続しました。');
});

server.on('OCSPRequest', (req, res) => {
  console.log('--- OCSP リクエストを受信しました (ダミーレスポンス) ---');
  res.end(dummyOCSPResponse); // ダミーのレスポンスを送信
});

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

この例では、dummyOCSPResponse に事前に用意した OCSP レスポンスの Bufferres.end() で送信しています。実際の OCSP レスポンスは ASN.1 でエンコードされており、その構造は複雑です。

OCSP クライアントライブラリの利用例 (概念的)

Node.js の標準モジュールだけで完全な OCSP クライアントを実装するのは複雑なため、通常はサードパーティのライブラリを利用します。以下は、そのようなライブラリの利用を想定した概念的なコードです。

const tls = require('tls');
const fs = require('fs');
// 例: 'ocsp' という名前の OCSP クライアントライブラリを仮定
const ocsp = require('ocsp');

const server = tls.createServer({
  cert: fs.readFileSync('server-cert.pem'),
  key: fs.readFileSync('server-key.pem'),
  ca: [fs.readFileSync('ca-cert.pem')],
  requestCert: true,
  rejectUnauthorized: true
}, (socket) => {
  console.log('クライアントが接続しました。');
});

server.on('OCSPRequest', (req, res) => {
  console.log('--- OCSP リクエストを受信しました (ライブラリ利用) ---');

  // OCSP リクエストをライブラリに渡して処理する
  ocsp.request({
    request: req,
    responderUrl: 'http://ocsp.example.com' // OCSP レスポンダーの URL
  }, (error, ocspResponse) => {
    if (error) {
      console.error('OCSP リクエストエラー:', error);
      // エラー処理 (例: 証明書を拒否するなど)
      res.end(Buffer.from([])); // エラー時は空のレスポンスを返すか、適切なエラーレスポンスを生成
      return;
    }

    if (ocspResponse) {
      console.log('OCSP レスポンス:', ocspResponse);
      res.end(ocspResponse); // OCSP レスポンスをクライアントに送信
    } else {
      console.log('OCSP レスポンスがありません。');
      res.end(Buffer.from([]));
    }
  });
});

server.listen(8000, () => {
  console.log('サーバーがポート 8000 で起動しました。');
});
  • エラーハンドリング
    OCSP リクエストの送信やレスポンスの処理中にエラーが発生した場合の適切なエラーハンドリングが重要です。
  • ASN.1 の扱い
    OCSP リクエストとレスポンスは ASN.1 という形式でエンコードされています。Node.js の標準モジュールだけでは ASN.1 のエンコード・デコードは難しいため、ライブラリの利用が推奨されます。
  • OCSP レスポンダーの URL
    実際に OCSP リクエストを送信するには、対象の証明書を発行した CA の OCSP レスポンダーの URL を知る必要があります。これは通常、証明書自体に含まれています。


OCSP ステーピング (OCSP Stapling) の利用

  • コード例
  • 欠点
    • サーバー側で OCSP レスポンダーから定期的にレスポンスを取得し、管理する必要があります。
    • クライアントが OCSP ステーピングに対応している必要があります(多くのモダンなブラウザや TLS クライアントは対応しています)。
  • 利点
    • クライアントが OCSP レスポンダーに直接問い合わせる必要がないため、クライアント側のネットワーク負荷や遅延を軽減できます。
    • クライアントはサーバーから提供された最新の失効情報を利用できます。
  • Node.js での設定
    tls.createServer のオプションで requestOCSPtrue に設定することで、サーバーは自身の証明書の OCSP レスポンスを要求し、ステーピングを試みます。
const tls = require('tls');
const fs = require('fs');

const server = tls.createServer({
  cert: fs.readFileSync('server-cert.pem'),
  key: fs.readFileSync('server-key.pem'),
  ca: [fs.readFileSync('ca-cert.pem')],
  requestCert: true,
  rejectUnauthorized: true,
  requestOCSP: true // OCSP ステーピングを有効にする
}, (socket) => {
  console.log('クライアントが接続しました。');
});

server.on('tlsClientError', (err, socket) => {
  console.error('TLS クライアントエラー:', err);
});

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

この例では、requestOCSP: true を設定することで、サーバーは自身の証明書の OCSP ステーピングを試みます。これにより、クライアント証明書の検証のために 'OCSPRequest' イベントを明示的に処理する必要がなくなる可能性があります。

証明書失効リスト (CRL) の利用

  • コード例 (概念的 - CRL のダウンロードと基本的なチェック)
  • 欠点
    • CRL のサイズが大きくなる可能性があり、ダウンロードや解析に時間がかかることがあります。
    • 失効情報が CRL の更新頻度に依存するため、リアルタイム性に劣る場合があります。
    • CRL の管理や更新の仕組みを自身で実装する必要があります。
  • 利点
    • OCSP レスポンダーへのリアルタイムな問い合わせが不要なため、ネットワークの依存性が低くなります。
  • Node.js での設定
    Node.js の TLS モジュールは、CRL を直接的に扱うための高レベルな API を提供していません。CRL を利用する場合は、自身で CRL をダウンロード・解析し、クライアント証明書のシリアル番号と照合する処理を実装する必要があります。ASN.1 パーサーなどのライブラリを利用することになるでしょう。
const tls = require('tls');
const fs = require('fs');
const https = require('https');
// 例: ASN.1 パーサーライブラリを仮定
// const asn1 = require('asn1-parser');

const crlUrl = 'http://example.com/ca.crl'; // CA の CRL 配布 URL

let revokedCertificates = [];

// CRL をダウンロードして解析する関数 (実際には複雑な処理が必要です)
function downloadAndParseCRL(callback) {
  https.get(crlUrl, (res) => {
    let rawData = '';
    res.on('data', (chunk) => { rawData += chunk; });
    res.on('end', () => {
      // ここで rawData を ASN.1 パーサーで解析し、失効した証明書のシリアル番号のリストを取得する
      // revokedCertificates = parseCRL(rawData);
      console.log('CRL をダウンロードしました (解析は省略)');
      callback();
    });
  }).on('error', (err) => {
    console.error('CRL のダウンロードエラー:', err);
    callback(err);
  });
}

tls.createServer({
  cert: fs.readFileSync('server-cert.pem'),
  key: fs.readFileSync('server-key.pem'),
  ca: [fs.readFileSync('ca-cert.pem')],
  requestCert: true,
  rejectUnauthorized: false // ここでは、失効チェックに失敗しても接続を拒否しない例
}, (socket) => {
  const clientCert = socket.getPeerCertificate();
  if (clientCert && clientCert.serialNumber) {
    const isRevoked = revokedCertificates.includes(clientCert.serialNumber);
    if (isRevoked) {
      console.log(`クライアント証明書 (${clientCert.serialNumber}) は失効しています。`);
      socket.destroy(new Error('Certificate Revoked'));
    } else {
      console.log(`クライアント証明書 (${clientCert.serialNumber}) は有効です (CRL チェック)。`);
    }
  }
  console.log('クライアントが接続しました。');
});

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

この例は CRL の利用の概念を示すものであり、実際の CRL のダウンロード、解析、失効確認の実装はより複雑になります。

  • コード例 (概念的 - 外部 API 呼び出し)
  • 欠点
    • 外部サービスへの依存が発生します。
    • サービスによっては費用が発生する場合があります。
    • ネットワーク経由での通信が必要になるため、遅延が発生する可能性があります。
  • 利点
    • 失効確認の複雑な処理を外部サービスに任せることができ、アプリケーション側の負担を軽減できます。
    • より高度な認証・認可ポリシーを適用できる場合があります。
  • Node.js での設定
    クライアント証明書をサービスに送信し、API などを通じて検証結果を受け取ります。
const tls = require('tls');
const fs = require('fs');
const https = require('https');

const verificationServiceUrl = 'https://auth.example.com/verify-certificate';

tls.createServer({
  cert: fs.readFileSync('server-cert.pem'),
  key: fs.readFileSync('server-key.pem'),
  ca: [fs.readFileSync('ca-cert.pem')],
  requestCert: true,
  rejectUnauthorized: false // 検証結果に基づいて拒否するかどうかを後で決定
}, (socket) => {
  const clientCert = socket.getPeerCertificate();
  if (clientCert && clientCert.raw) {
    const postData = JSON.stringify({ certificate: clientCert.raw.toString('base64') });

    const req = https.request(verificationServiceUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': postData.length,
      },
    }, (res) => {
      let data = '';
      res.on('data', (chunk) => { data += chunk; });
      res.on('end', () => {
        try {
          const result = JSON.parse(data);
          if (result.valid) {
            console.log('クライアント証明書は有効です (外部サービス)。');
          } else {
            console.log('クライアント証明書は無効です (外部サービス)。理由:', result.reason);
            socket.destroy(new Error('Certificate Invalid'));
          }
        } catch (error) {
          console.error('検証サービスからの応答の解析エラー:', error);
          socket.destroy(new Error('Verification Error'));
        }
      });
    }).on('error', (err) => {
      console.error('検証サービスへのリクエストエラー:', err);
      socket.destroy(new Error('Verification Service Unavailable'));
    });

    req.write(postData);
    req.end();
  } else {
    console.log('クライアント証明書が存在しません。');
    socket.end();
  }
});

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