目次


サーバー証明書を Bluemix の Node.js アプリケーションで検証する

Comments

(「No man is an island (何人も孤立した島のようではありません)」という一節が有名な) ジョン・ダンの詩では触れていませんが、アプリケーションも孤立した島のようではありません。ほとんどの場合、アプリケーションはリモート・サーバーと通信して情報を交換しなければなりません。情報の交換を容易にするために、Node.js には各種のプロトコルを使ってサーバーと通信するためのパッケージがいくつもあります。

しかしながら、リモート・サーバーを使用すると、なりすましのリスクを伴います。つまり、アタッカーが正規のパートナーの振りをして、情報を盗んだり改ざんしたりする可能性があるのです。そのような問題を防ぐために、私たちは証明書を使用しています。これについては、PDF として入手できる IBM のホワイト・ペーパー「IBM Global Security Kit 7.0: Managing certificates」で説明されています。この記事では、Bluemix 内で実行されている皆さんの Node.js アプリケーション内から証明書を使用する方法を説明します。

アプリケーションを作成するために必要となるもの

アプリを実行するコードを入手する

この記事では、アタッカーが正規のパートナーの振りをできないように、Bluemix 内で実行されている Node.js アプリケーション内から証明書を使用する方法を説明します。

デモ・アプリケーション

このアプリケーションにユーザーがサーバーの URL を入力すると、アプリケーションがそのサーバーと通信して、サーバーの証明書を取得します。また、このアプリケーションを使用して自己署名証明書をチェックすることもできます。

HTTPS リクエストを送信する

HTTPS プロトコルは、おそらく世界で最も重要なアプリケーション・レベルのプロトコルのセキュアなバージョンです。これは、ユーザー・インターフェースとしても、REST を使用したプログラム間の通信手段としても使用されます。Node.js アプリケーションが HTTPS クライアントとして機能するためには、HTTPS パッケージを使用します。

// Issue http requests
var https = require("https");
.
.
.
var httpsReq = https.request(req.query.url, function(httpsRes) {…});

HTTPS リクエストを作成するプロセスには、2 つのステップがあります。最初に HTTPS パッケージをインスタンス化し、次に https.request を使用してリクエストを発行します。https.request 関数は、2 つの引数を取ります。1 番目の引数には、サーバーの URL を指定することができます (後で説明しますが、この引数をオプションの配列にすることもできます)。2 番目の引数は、アプリケーションがレスポンスを受け取ったときに呼び出される関数です。

httpsReq.end();

Express では、リクエストがまだ開かれている状態で応答することは許可されません。従って、リクエストの処理が完了したという、HTTPS パッケージの上記関数が呼び出されると、リクエストの送信が行われます。

httpsReq.on("error", function(err) {…});

上記の呼び出しは、エラー・ハンドラーをセットアップします。有効な証明書だけでなく、無効な証明書も確認する必要があるため、この呼び出しは重要です。

証明書を取得する

HTTPS リクエスト (サンプル・プログラムでは、httpsReq) には、socket という名前のフィールドが含まれています。これは通信ソケットであり、フィールドのタイプは tls.TLSSocket です。証明書を取得するには、関数 getPeerCertificate() を使用します。この関数はブール値を引数に取り、その値が true であれば、その証明書だけでなく、その証明書を証明する証明書チェーンの全体を返します。

サンプル・プログラムでは、この処理は関数 socketCert2HTML の中で行われます。

if (socket == null)
  return "<b>No socket</b>";
var cert = socket.getPeerCertificate(false);
if (cert == null)
  return "<b>No certificate in the socket</b>";

この関数は最初にいくつかのエラー条件をチェックします。この関数はエラー・ハンドラーからも呼び出されるため、ソケットがまったくない場合や、ソケットに証明書がない場合もあります。

var auth = ";

if (socket.authorized)
    auth = "<b>Certificate is legitimate</b>";
  else
    auth = "<b>Certificate looks fake</b><br />" + socket.authorizationError;

証明書が正式なものであるかどうか (つまり、その環境で信頼されている認証局から発行された有効な証明書であるかどうか) をチェックするには、socket.authorized を使用することができます。証明書に問題がある場合、その問題の性質が socket.authorizationError で説明されます。

return auth + "<br /><pre>" + JSON.stringify(cert, null, 4) + "</pre>";

証明書自体は、JSON フォーマットで表示可能な構造体です。以下に、Bluemix 自体の証明書の一部を抜粋します。

{
    "subject": {
        "C": "US",
        "ST": "New York",
        "L": "Armonk",
        "O": "International Business Machines Corporation",
        "CN": "*.ng.bluemix.net"
    },
    "issuer": {
        "C": "US",
        "O": "DigiCert Inc",
        "CN": "DigiCert SHA2 Secure Server CA"
    },
    "subjectaltname": "DNS:*.ng.bluemix.net, DNS:ng.bluemix.net",
    "infoAccess": {
        "OCSP - URI": [
            "http://ocsp.digicert.com"
        ],
        "CA Issuers - URI": [
            "http://cacerts.digicert.com/DigiCertSHA2SecureServerCA.crt"
        ]
    },
    "modulus": "E02DABBF2337B98C42D572...",
    "exponent": "10001",
    "valid_from": "Sep 29 00:00:00 2014 GMT",
    "valid_to": "Nov  8 12:00:00 2017 GMT",
    "fingerprint": "97:F4:45:FA:7B:9A:12:98:FB:18:5F:76:80:19:02:A3:84:FE:4D:74",
    "ext_key_usage": [
        "1.3.6.1.5.5.7.3.1",
        "1.3.6.1.5.5.7.3.2"
    ],
    "serialNumber": "06380DF0F0AF299B5AD75BBB851F7301",
    "raw": {
        "type": "Buffer",
        "data": […]
    }
}

コード内で証明書をチェックする

ここまでで証明書の詳細な情報を得る方法がわかったので、これらの詳細情報を使用して、サーバー・ソフトウェアに頼らずに独自のコードによって「自らの手で」証明書をチェックすることができます。

理由

ここで皆さんの理解が深まるように、私が手間をかけてこの記事を執筆した理由を説明しておきます。皆さんに socket.authorized をチェックするよう指示することもできましたが、実のところ、その必要さえありません。証明書が有効なものであると認識されなかった場合、HTTPS リクエストによってエラーがスローされるからです。

それも事実ですが、証明書を自分でチェックするのには、以下の理由があります。

  • 特定のアプリケーションでは、認証局の信頼性に頼ることができないほど、セキュリティーが重要になります。一部の認証局には、過去に間違いを犯したり、不適切な証明書を発行したりした歴史があります。認証局が無条件で中間証明書を発行し、その認証局の名前で証明書を発行することを許可していたケースさえあります。
  • 接続先としているのは、内部サーバーです。その場合、認証局 (CA) からの証明書に費用をかけるのではなく、無料の自己署名証明書を使用することができます。けれども、そのような証明書が自動的に認識されることはないため、アプリケーションに手作業でインポートしなければなりません。例えば https://certs.simple-tech.com では自己署名証明書を使用していますが、Node.js ではその証明書によってエラーが生成されます。
  • 接続先のサーバーが使用している認証局を Bluemix が認識しない場合があります。例えば、https://www.us.army.mil の米国政府による証明書は Bluemix が認識しないため、エラーが発生します。

方法

証明書をチェックする最も簡単な方法は、フィンガープリント値を使用することです。フィンガープリント値は暗号化チェックサムであり、その値は証明書の重要なフィールドのすべてに依存しています。

このフィンガープリントが機能することを確認するには、アプリケーションの 2 番目のパネルでホスト名を入力してください。ホスト名が certs.simple-tech.com (または IP アドレス 129.41.135.12) であれば、フィンガープリントが一致して証明書が受け入れられますが、そうでなければ証明書は拒否されます。

認証局を信頼せずに HTTPS リクエストを発行する

最初に解決しなければならない問題は、https.request() に自動的には検証できない証明書の URL が指定されると、この関数がエラー・メッセージを出力して終了するという問題です。自己署名証明書と、不明な認証局から発行された証明書は、有効な証明書として認識されません。この問題を解決するには、URL ストリングではなく連想配列として接続パラメーターを指定し、追加のパラメーターを使用できるようにします。

// Check if the certificate matches ceerts.simple-tech.com
app.get("/check_self_signed", function(req, res) {
  var httpsReq = https.request({
    hostname: req.query.hostname,
    port: 443,
    path: "/",
    method: "GET",
    rejectUnauthorized: false   // To avoid an error, because the cert is
                                // unauthorized
  }, function(httpsRes) {…});
});

接続パラメーターのほとんどは、一般に URL を構成する標準的な HTTP 値です。例外 rejectUnauthorized はブール値であり、これを使用して不適切な証明書のエラーを無効にすることができます。

フィンガープリントを検証する

証明書には、人間が読んで理解できるストリングとしてフィンガープリントが記載されます。フィンガープリントを検証するには、単に既知のフィンガープリントと同じであるかどうかをチェックするだけです。

function(httpsRes) {
      if (httpsReq.socket.getPeerCertificate().fingerprint ==
        "D6:0A:10:CB:C5:B3:F9:B6:A8:89:05:F2:50:28:5E:17:75:A6:98:76")
        res.send("This is certs.simple-tech.com");
      else
        res.send("This is a different server");
    }

上記の単純なコードでは、証明書の有効期限をチェックしていないことに注意してください。有効期限をチェックする必要がある場合は、JavaScript の Date.now() 関数を使用します。

フィンガープリントを将来簡単に変更できるようにする

プログラマーにとっては、コード内に直接フィンガープリントを保管するのが簡単ですが、そうしてしまうと、サーバー上でフィンガープリントが変更された場合、運用チームがその変更を適用するのが大変になります。フィンガープリントを将来簡単に変更できるプロパティーとして保管する方法については、「Bluemix と MEAN スタックを使用して自動投稿 Facebook アプリケーションを作成する: パート 3 サーバーにユーザーの代理を務めさせる」のステップ 5 を参照してください。

失効した証明書

証明書を含む秘密鍵が開示された時点で、その証明書は有効な期間がまだ残っているとしても使用できなくなります。証明書自体を変更することはできないため、失効した証明書をクライアントに通知するために、認証局では証明書失効リスト (CRL) を使用して、OCSP (open certificate status protocol) によって証明書失効に関する情報を提供しています。

あいにく、この記事を執筆している時点 (2015年 10月) では、Bluemix Node.js 環境で OCSP のサポートを使用することはできなさそうです。それを確認するには、https://test-sspev.verisign.com:2443/ のステータスをチェックしてください。このアドレスで使用している証明書は、クライアント開発者がコードをチェックできるように意図的に失効した状態になっています。ほとんどのブラウザーでは、この URL にアクセスすることはできません。

しかし、Node.js の証明書チェッカーを使用すれば、このサイトにアクセスすることができます。

Node.js からサイトにアクセスすると表示される画面のスクリーンショット
Node.js からサイトにアクセスすると表示される画面のスクリーンショット

このことがまだ当てはまるかどうかをチェックするには、このリンクをクリックしてください。これによって表示される以下のような内容のコードは、問題が解決される前のものです。

HTTPS オブジェクトを拡張する

認識されない証明書を使用している多数のサイトにアクセスするために、HTTPS を使用するとしたら、リクエストごとにフィンガープリントを追加するよりも、フィンガープリントの機能をオブジェクトにカプセル化したほうが簡単です。その場合、オブジェクトはできるだけ HTTPS に似たものにするのが理想的です。

// Wrapper around https to use a fingerprint instead of the normal
// certificate verification. This wrapper replicates the two
// client functions in https, https.request() and https.get().
var fingerprintHttps = {

  request: function(requestUrl, fingerprint, handler) {
	…
  },

  get: function(requestUrl, fingerprint, handler) {
	…
  }
};

最初のステップでは、該当する関数を含める構造体を作成します。JavaScript の関数は他のあらゆるタイプの変数と同じように処理されるため、構文はまったく同じです。HTTPS にはクライアント・サイドの関数として https.request()https.get() の 2 つがあるため、両方とも、この構造体に実装します。

    var opts;

    // Is requestURL a string URL we need to parse, or already an array of options?
    if(typeof requestUrl == "string")
      opts = url.parse(requestUrl);
    else
      opts = requestUrl;

    // Add the parameter to disable automatic checking of certificate
    // validity
    opts.rejectUnauthorized = false;

HTTPS クライアント関数は、1 番目の引数としてストリングまたは構造体を取ることができます。この例の場合、構造体を使用して rejectAuthorized = false を指定する必要があります。それには、引数がストリングであるかどうかをチェックします。引数がストリングの場合は、引数を解析して構造体にします。すでに構造体になっている場合は、その構造体を使用します。

JavaScript において変数の型を取得するには、上記に示されているように typeof 演算子を使用します。

    // Issue the actual request
    var httpsReq = https.request(opts,
      function(httpsRes) {
        if (httpsReq.socket.getPeerCertificate().fingerprint == fingerprint)
          handler(httpsRes);
        else
          httpsReq.emit("error", new Error("Fingerprint mismatch"));
      });

実際のリクエストを発行する部分は、かなり単純です。目新しい唯一の点は、httpsReq.emit() 関数を呼び出すところです。前に説明したように、エラー・ハンドラーを HTTPS オブジェクトに登録するには、.on("error", <handler>) 構文を使用します。HTTPS オブジェクトは EventEmitter のインスタンスであるため、.emit() を使用することで、.on() リスナーで処理するイベントを作成することができます。

注: 説明のために、この実装は単純化されています。実際の実装では、HTTPS オブジェクトによって発生する可能性のあるすべてのイベント・タイプに対してリスナーを用意し、それらのどのイベントが発生した場合でも、fingerprintHttps オブジェクトに登録された、そのイベントに対応するすべてのリスナーにイベントを伝搬することになります。あるいは、単純なラッパーの代わりに、実際の継承を使用するという方法もあります。

セキュリティーとタイミング

これまでのところ、この例での実装は単純かつ簡潔であり、セキュリティーの問題を招く恐れがあります。この実装での処理の順序は以下のとおりです。

  1. リモート・サーバーに対して HTTPS リクエストを送信します (https.request() 呼び出し)。
  2. コールバック関数でレスポンスを受け取ります。
  3. 証明書のフィンガープリントをチェックします。
  4. レスポンスを処理します。

この処理の順序で問題となるのは、サーバーを識別する前にリクエストを送信している点です。捏造されたレスポンスを無視することは可能ですが、それでもまだ、機密情報が含まれるリクエストをなりすましサーバーが受け取る可能性があります。アプリケーションによっては、そのような事態は許容されません。

HTTPS パッケージには、セキュア接続がセットアップされた時点で呼び出すコールバックを指定する場所はありませんが、リクエスト・プロセスのさまざまな時点で発行されるイベントがあります (詳細については、Node.js v4.2.1 の資料を参照してください)。これらのイベントを利用すれば、リクエストで証明書を受け取るとすぐに、リクエストの残りの部分が送信される前に、証明書の処理を行うことができます。

  httpsReq.on("socket", function() {
    // We are connected to a socket now

    httpsReq.socket.on("secureConnect", function() {
      // Now we are connected with encryption

上記のコードには、2 つのイベント・ハンドラーがあります。最初の httpsReq.on("socket", function() {…}); は、リクエストに対してソケットが作成された時点で呼び出されます。しかし、この時点で証明書をチェックするのは早すぎます。このイベント・ハンドラーが呼び出された後に、クライアントはリモート・サーバーと通信して証明書を取得し、トンネルを作成します。従って、このイベント・ハンドラーの中には、ソケットが secureConnect イベントを発行すると呼び出される別のイベント・ハンドラーがあります。トンネルが作成されて、この 2 番目のイベントが発生した時点で、証明書が利用可能になります。

      res.send("Fingerprint:" +
        httpsReq.socket.getPeerCertificate().fingerprint);

      // Pretend the fingerprint does not match, and abort the request
      httpsReq.abort();

2 番目のイベント・ハンドラーに含まれるコードは、たいした処理をしません。ユーザーにフィンガープリントを送り返して、何かが起こっていることを示してから、そのフィンガープリントが不正であると見なして終了するだけです。この機能を fingerprintHttps オブジェクトに統合する作業は、読者の皆さんに演習として残しておきます。

まとめ

これで皆さんは、HTTPS サーバーの証明書が Bluemix で認識されるかどうかに関わらず、独自のコードからセキュアに HTTPS サーバーを使用できるようになるはずです。LDAP などの他のクライアントも tls.TLSSocket を使用するので、これらのクライアントも同様の方法で保護することができます。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=セキュリティ, Cloud computing
ArticleID=1025833
ArticleTitle=サーバー証明書を Bluemix の Node.js アプリケーションで検証する
publish-date=01282016