Skip to main content

Node.jsでSSL/TLSピンニングを実装する方法

著者
Headshot of Nwani Victory

Nwani Victory

blog feature multithreading

2023年8月29日

0 分で読めます

脅威アクターによる中間者(MITM)攻撃が行われるなか、SSL/TLS証明書があるというだけでは、受信接続を信頼する根拠にならなくなっています。そのため、開発者の間では、接続の真正性と完全性を証明するための追加対策として、証明書ピンニングまたは公開鍵ピンニングとも呼ばれるSSL/TLSピンニングの採用が進んでいます。

Node.jsアプリケーションでは、SSL/TLSピンニングによってクライアントとサーバー間の通信を攻撃者が傍受・改ざんできないようにし、セキュリティをさらに強化できます。

この記事では、証明書ピンニングのメリットとNode.jsアプリケーションでの使用例を紹介します。

SSL/TLSピンニングを使うべきタイミングと理由

SSL/TLSピンニングは、MITM攻撃などの証明書関連の攻撃から保護するセキュリティ機構です。Node.jsアプリケーションなどのクライアントが、事前に検証されたデジタル証明書を持つサーバーにのみ接続するようにします。この手法では、ホストアプリケーションに特定の証明書または公開鍵を保存して使用し、サーバーの公開鍵または証明書と照合します。証明書や公開鍵は環境変数を使ってアプリケーションにハードコードすることも、柔軟性を高めるために外部のキーボールトサービスに保存することもできます。

証明書ピンニングでは、ピン留めした証明書と一致しないリクエストを、HTTPSを使用している場合でも不正なものとみなして終了するため、MITM攻撃のリスクを低減できます。また、証明書の偽造や改ざん、認証局(CA)の侵害など、その他の証明書関連の脆弱性の防止にも役立ちます。

Stack Overflow開発者アンケートによると、開発者の47.12%がNode.jsを使用しており、サーバーサイドアプリケーションの標準的なWeb技術となっています。HTTPSなど、Node.jsに組み込まれたモジュールは、ピン留めした証明書を検証することでアプリケーションの保護をさらに強化します。証明書ピンニングを使用するNode.jsアプリケーションは、TLSハンドシェイク中に提示された証明書を、事前に定義した証明書または公開鍵と照合します。一致しない場合は、トランスポート層でリクエストを終了します。

証明書ピンニングの使用例の1つが、モバイル端末上の金融アプリケーションです。こうした端末に対するMITM攻撃は、クレジットカード情報や連絡先情報などの機密性の高い個人情報が送信される可能性があるため、所有者に深刻な被害をもたらしかねません。Android Pieなどの新しいモバイルOSバージョンには、アプリケーションが安全な接続のみを確立するようにするセキュリティ機能が備わっていますが、脅威アクターは自己署名証明書を生成して端末にアクセスするフィッシング攻撃を仕掛ける可能性があります。証明書ピンニングを使えば、端末が自己署名証明書を信頼していても、アプリケーションが不正な接続を許可しないようにできます。

SSL/TLSピンニングの実装に向けた準備

クライアントがサーバーにネットワークリクエストを送信すると、トランスポート層がSSL/TLSプロトコルを使用して安全なハンドシェイクを開始します。サーバーは、暗号化と認証に使用する公開鍵を含むデジタル証明書を提示します。ハンドシェイク中に、アプリケーションは提示された証明書からサーバーの公開鍵を取得します。

アプリケーションは、取得した公開鍵を、事前設定またはハードコードされた公開鍵のコピーと照合します。公開鍵がローカルに保存されたコピーと一致すれば、接続は安全とみなされ、通信を続行できます。一致しない場合、アプリケーションはリンクを終了するか、セキュリティポリシーに応じてアラートを出す、機密データの送信を拒否するなど、適切な措置を取れます。

CAは、安全な接続を確立するためのSSL/TLS証明書を提供し、証明書ピンニングのプロセスで重要な役割を果たします。CAは証明書に署名し、Webベースのアプリケーションではブラウザーが、またはOSでは信頼チェーンがその証明書を検証します。証明書ピンニングは、証明書が信頼できることを確認するための追加手段です。

TLSはNode.jsに組み込まれたモジュールで、ホストから公開鍵フィールドを含む証明書オブジェクトを抽出できます。以下のコードは、TLSモジュールを使ってホストにリクエストを送信し、証明書をオブジェクトとして取得する方法を示しています。

const tls = require("tls");
const host = "example.com";
const port = 443;

const socket = tls.connect(port, host, () => { 
const certificate = socket.getPeerCertificate();
 const publicKey = certificate.pubkey;

 console.log("Public Key:", publicKey);
 console.log("Certificate:", certificate);
});

socket.on("error", (error) => {
 console.error("Connection error:", error);
});

このコードでは、getPeerCertificateメソッドが証明書の抽出を担います。また、getPeerX509Certificateメソッドを使うと、ピア証明書をx509Certificateオブジェクト形式で取得できます。

Node.jsでSSL/TLSピンニングを実装する

Node.jsアプリケーションでSSL/TLSピンニングを使用するには、リクエスト設定で鍵または証明書を指定する必要があります。HTTPSでは、ca、cert、keyの各プロパティをhttps.requestのオプションに指定することで、SSL/TLSピンニングを実現できます。証明書をピン留めする場合、<c>ca</c>プロパティには、信頼されたPEM(Privacy-Enhanced Mail)形式の証明書またはCA証明書を含む配列を指定します。<c>key</c>プロパティには、信頼されたサーバー証明書に対応する秘密鍵を含む配列を指定します。

Snyk Codeは、開発者を第一に考えた静的アプリケーションセキュリティテスト(SAST)ツールで、開発中にコードベースのセキュリティ脆弱性をリアルタイムで特定します。コマンドラインインターフェース(CLI)コマンド、継続的インテグレーション/継続的デプロイ(CI/CD)パイプライン向けのソフトウェア開発キット(SDK)、Webインターフェース、統合開発環境(IDE)用拡張機能、コードエディターから利用できます。Node.jsプロジェクトでは、SSL/TLS証明書への対応が不足している場合に、Snyk Codeが検出して警告できます。

SSL/TLS対応の不足を特定するには、以下のcode testコマンドを使い、ターミナルからプロジェクトのコードベースをスキャンします。snyk ignore --file-path=<file_path>コマンドを使うと、Node.jsパッケージを含むnode_modulesディレクトリなど、Snykのスキャン対象に含めたくないファイルやディレクトリを除外できます。

snyk code test

SSL/TLSピンニングにはセキュリティ上のメリットがありますが、エラー処理を適切に行わずに実装すると、深刻な問題を招く恐れもあります。ピン留めした証明書が失効または期限切れになった場合など、想定外の状況に対処するためのフェイルセーフ計画を用意する必要があります。エラー処理があれば、証明書または公開鍵を確認できないときにアプリケーション全体がクラッシュするのを防げます。監査を実行すると、そうしたケースをログに記録できます。

まず、snyk-tlsという名前のディレクトリを作成し、次のコマンドを実行します。

npm init -y

以下のコードは、Node.jsネイティブのhttpsモジュールで作成したGETリクエストにおいて、証明書の公開鍵フィンガープリントと一致する公開鍵フィンガープリントをピン留めする例です。公開鍵フィンガープリントが一致しない場合、コードはリクエストを中止します。試すにはindex.jsファイルを作成し、次のコードを追加してください。

const https = require("https");

const PINNED_CERT_FINGERPRINTS = [
    "44:0C:58:51:4C:73:7C:67:DA:A2:72:29:81:68:CD:FC:51:B5:79:65:66:F0:55:FA:55:C4:45:30:BB:DD:09:82",
];

const options = {
    hostname: "google.com",
    port: 443,
    path: "/",
    method: "GET",
    headers: {
        "User-Agent": "Node.js/https"
    }
};

const req = https.request(options, (res) => {
    res.on("data", d => {
        console.log("Request succeeded and received a response after certificate pinning validation")
    });
});

req.on("error", console.error);

req.on("socket", (socket) => {
    socket.on("secureConnect", () => {
        // Uses the sha256 fingerprint hash of the certificate instead of the certificate itself or
        // the public key info. The one downside to this is that if/when the certificate
        // changes the fingerprint will too and the PINNED_CERT_FINGERPRINTS will need to be updated.
        // Reference: https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning
        const fingerprint256 = socket.getPeerCertificate().fingerprint256;

        if (!PINNED_CERT_FINGERPRINTS.includes(fingerprint256)) {
            req.emit("error", new Error("The certificate fingerprint received does not match any pinned certificate fingerprints"));
            return req.destroy();
        } else {
            console.log("All OK. Server matched one of our pinned certificate fingerprints");
        }
    });
});

req.end();

上記のコードでは、PINNED_CERT_FINGERPRINTS配列を確認し、公開証明書のフィンガープリントが許可されたフィンガープリントのいずれかと一致するかを判定します。フィンガープリントが配列に含まれていない場合、destroyメソッドがリクエストを終了します。

本番環境では、コードをリポジトリにプッシュしたときに公開鍵が露出しないよう、公開鍵または証明書を環境変数か別のキーボールトサービスに保存する必要があります。

SSL/TLSピンニングのテストと保守

ピンニングの実装が有効かどうかをテストするには、MITM攻撃をシミュレーションするのが効果的です。MitmproxyやWireshackなどのツールを使うと、テスト用ホストのネットワークリクエストを監視、傍受、プロキシするテスト環境を構築できます。

SSL/TLSピンニングの有効性をテストするには、ピン留めした証明書を無効にし、プロキシ経由でMITM攻撃を実行して、ピンニングなしでのアプリケーションの動作をベースラインとして記録します。次に、ピンニングを有効にしてテストを繰り返し、アプリケーションがプロキシを検出して接続を終了できるか確認します。

証明書の鍵ハッシュを比較する方法でも、SSL/TLSピンニングをテストできます。証明書ハッシュは証明書固有の値です。暗号学的ハッシュ関数が任意のサイズのデータを、証明書を表す固定サイズの値に変換するため、証明書の検証に適しています。Node.jsのcryptoモジュールを使えば、ホストの証明書からハッシュを生成し、期待する公開鍵ハッシュと比較できます。

SSL/TLSピンニングをテストする3つ目の方法は、Nmapなどのネットワークスキャンツールを使ってSSLポートスキャンを実行することです。Nmapには、指定したターゲットのSSLポートをスキャンするssl-enum-chipersコマンドがあります。

ピン留めした証明書や公開鍵を保守・更新することは、通信チャネルの信頼性、セキュリティ、完全性を維持するうえで不可欠です。常に最新の状態に保つことで、証明書の侵害、期限切れ、失効に伴うリスクを軽減しながら、安全で認証された接続を確保できます。

ターミナルでsnyk-tlsフォルダーを開き、以下のコマンドを実行します。

echo -n | openssl s_client -connect google.com:443 -servername google.com | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > cert.pem

このコマンドは、google.comの公開証明書を含むcert.pemファイルを生成します。

次に、プロジェクトフォルダーにmain.jsファイルを作成し、以下のコードを貼り付けます。

// import the required modules
const tls = require('node:tls');
const https = require('node:https');
const { exec } = require('child_process');

// setting up the variables
const certPath = 'cert.pem';
const command = `openssl x509 -noout -in ${certPath} -fingerprint -sha256`;

// generate SHA256 fingerprint of the certificate
const generate_SHA256_fingerprint = () => {
 return new Promise((resolve, reject) => {
   exec(command, (error, stdout, stderr) => {
     if (error) {
       reject(new Error('Error executing OpenSSL command: ' + error));
       return;
     }

     if (stderr) {
       reject(new Error('OpenSSL command returned an error: ' + stderr));
       return;
     }

     resolve(stdout.split('=')[1].trim());
   });
 });
};

// verify the certificate
const verifyCertificate = async () => {
 try {
   const cert256 = await generate_SHA256_fingerprint();
   console.log('Pinned Certificate Fingerprint:', cert256);

   const options = {
     hostname: 'google.com',
     port: 443,
     path: '/',
     method: 'GET',
     checkServerIdentity: function (host, cert) {
       // Make sure the certificate is issued to the host we are connected to
       const err = tls.checkServerIdentity(host, cert);
       if (err) {
         return err;
       }

       // Pin the exact certificate
       if (cert.fingerprint256 !== cert256) {
         const msg = 'Certificate verification error: ' +
           `The certificate of '${cert.subject.CN}' ` +
           'does not match our pinned fingerprint';
         return new Error(msg);
       }
     },
   };

   options.agent = new https.Agent(options);
   const req = https.request(options, (res) => {
     console.log('All OK. Server matched our pinned cert');
   });

   req.on('error', (e) => {
     console.error(e.message);
   });

   req.end();
 } catch (error) {
   console.error('Error generating SHA256 fingerprint:', error);
 }
};

verifyCertificate();

上記のコードでは、必要なモジュールをインポートし、certPathとcommandという2つの変数を定義しています。certPathは証明書ファイルへのパスを定義します。commandには、証明書のSHA-256フィンガープリントを生成するシェルコマンドを格納します。

generate_SHA256_fingerprint()関数は、exec関数を使って、証明書のSHA-256フィンガープリントを生成するopensslコマンドを実行します。この関数はフィンガープリントを返すPromiseを返します。

verifyCertificate()関数は、HTTPSリクエストを設定するためのオプションオブジェクトを定義します。このオブジェクトでは、hostname、port、path、methodを指定します。また、サーバー証明書を検証するcheckServerIdentity()関数も定義します。この関数は、証明書が接続先ホストのものであることを確認し、フィンガープリントがピン留めした値と一致するかをチェックします。

このスクリプトをテストするには、ターミナルでnode main.jsコマンドを実行します。次のような出力が表示されます。

All OK. Server matched our pinned cert

ピン留めした証明書または鍵を更新するには、APIファイルを開き、opensslファイルを生成するcert.pemコマンドを実行します。外部のボールトサービスにピン留めした証明書または公開鍵を保存している場合は、アプリケーションコードを変更せずに、ボールトサービス内の値を編集できます。ピン留めした証明書の更新が必要となる状況は次のとおりです。

  • 証明書または公開鍵の期限切れ

  • データ漏えい

  • 発行元による失効

  • ユーザーのコンプライアンスポリシーで定められた、一定期間ごとの証明書ローテーション

まとめ

この記事では、Node.jsアプリケーションでSSL/TLSピンニングを実装する方法を解説しました。SSL/TLSピンニングでは、サーバーの公開鍵または証明書をローカルに保存したコピーと照合することで、セキュリティをさらに強化します。MITM攻撃などの証明書関連の脆弱性からアプリケーションを保護するのに役立ちます。

Node.jsにはHTTPSなど、証明書ピンニングに対応した組み込みモジュールがあります。TLSハンドシェイク中に公開鍵を照合し、一致しない場合は接続を終了することで、SSL/TLSピンニングを適用できます。また、証明書や公開鍵の期限切れ、発行元による失効といった想定外の状況で接続できなくなるのを避けるため、適切なエラー処理も欠かせません。攻撃のシミュレーションや鍵ハッシュの比較、証明書または鍵の更新を通じてアプリケーションのSSL/TLSピンニングをテストすることは、有効性を確保するためのベストプラクティスです。公開鍵を安全に保管することも重要です。

SSL/TLSピンニングを活用することで、Node.jsアプリケーション全体のセキュリティを強化し、ユーザーに信頼性と安全性の高い体験を提供できます。

Capture the Flagを始めよう

オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。

続きを読む

blog feature snyk container custom base recommendations
Blog

Evo Agentic AppSecのご紹介:AI開発のスピードに対応するセキュリティ

AI駆動型開発のスピードに、従来のセキュリティワークフローは追いついていません。SnykのAgentic AppSecは、セキュリティエージェントが脆弱性を継続的に検出、修正、検証し、人が基準を定められるようにします。

blog feature ai violet
Blog

VulnBump:AIエージェントはビルドを壊さずに脆弱な依存関係を修正できるか?

VulnBumpは、実際の依存関係アップグレード307件を対象にAIエージェントをテストします。脆弱性が解消され、ビルドとテストが成功した場合にのみ、修正としてカウントされます。

Blog

フロンティアモデルは脆弱性を発見した。攻撃者だけがエクスプロイトチェーンを見つけた。

静的解析で欠陥は見つかりましたが、ライブ攻撃テストで侵害につながる連鎖を実証できたのは唯一でした。Evo COS、Claude Security、Claude Code Securityを比較します。