AWS lambda(Node.js)でオレオレ証明書(self-signed)を一時的に信頼してSSL通信を行う方法


はじめに

AWS lambdaのNode.js(https標準モジュール)で実装した、WEBサイトへhttpsのリクエストを投げる処理で、以下の2つのエラーが発生した際の対応についての記事です。
※急いでいる方、ソースコードだけ見たい方はここから見ればOKです


これ何

調べたところ、リクエスト先のWEBサーバから送出されているサーバ証明書に対してのnodejs内部での検証失敗のため生じたエラーでした。
2つのエラーの違いは、
* 自分自身が署名(オレオレ証明書(self-signed))
* サーバ証明書の発行元が信頼されていないか
👉2つのエラーは共に、Node.jsとして信頼1していないCA証明書にチェーンしていたため生じていました。

環境

  • AWS lambda
  • Node.js ランタイム12.x
  • 接続イメージ [AWS lambda]-->(WAN HTTP over SSL)-->[WEBサーバ]

対処案

3つ考えましたが、妥協点で私の環境では3つ目で実装しましたので以下にまとめます。

1.WEBサーバ側にちゃんとしたサーバ証明書2に変更してもらう

この対応ができたらこんな記事はいらない気もする。
自局署名の問題については、ネット上でも従前から議論されおり3、本記事をご覧になっている方でも既知のことと思います。WEBサーバ管理者に対しては、セキュリティの観点で懸念点を伝えてさしあげる程度にしました。

2.TLSハンドシェイクエラーを無視・無効にする

NODE_TLS_REJECT_UNAUTHORIZEDを環境変数に定義し、値を0とすれば検証自体がdisableになる模様。これは、curlでいうところの--insecureオプションと類似していますが、検証を全て無視するため有効期限やトラストアンカーとのチェーン等々を丸っとすっ飛ばす模様。(詳細は未検証のため割愛)
https://nodejs.org/api/cli.html#cli_node_tls_reject_unauthorized_value

3.オレオレ証明書(self-signed)を一時的に信頼する

公式ドキュメントに書いてありました。
https://nodejs.org/api/tls.html#tls_tls_connect_options_callback
具体的には、通信先のWEBサーバのオレオレ証明書(サーバ証明書自体)ないしは、サーバ証明書のissuerとなっている現状信頼されていない自局CA証明書(pem形式)を取得し、https.requestのoptionにcaを追加するというものです。以下にサンプルを用いて実装例を示しています。

※今回の実装ではpemファイルの読み込みを、諸般の事情によりlambdaのみで完結したかったため、環境変数に事前に設定して、そこから読み込ませるという方法を使っています。(S3に入れて読み込むという方法もありかと思います。)

3-1.pemファイルを1行化する

環境変数にpemを入れるため、改行を¥nに置換します。

[work@localhost]$ awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' cacert.pem
-----BEGIN CERTIFICATE-----\nAIIDQjCCAiHCEQCQzvg6BX4eF(中略)v2wk3xtME7i8Jb2aUQH9vFVYuXUN2\nUO+j8l1OA4p1ew0kCsit2HOn\n-----END CERTIFICATE-----\n [work@localhost]$

余談ですが、当初は、pem形式って改行まで含んで形式であるということを理解しておらず、改行を単純に削除して環境変数に入れていたためエラーが発生して、ここで地味に悩みました。ちなみに、pemの改行は、Windows形式(CR+LF)、Unix形式(LF)どちらで良い模様なのでCRを削除後に置換している。4

3-2.AWS lamda環境変数の設定

上コマンドで表示された内容を環境変数にCA_PEMとして保存します。

3-3. 実際のスクリプト(sample)

const https = require('https');

//ここで環境変数からcacertにPEMを読み込んでいる
var cacert = process.env['CA_PEM'].replace(/\\n/g, '\n');

exports.handler = (event, context) => {
    //caを追加してcacertを設定
    const options = {
      protocol: 'https:',
      host: 'example.com',
      path: '/index.html',
      port: 443,
      method: 'GET',
      timeout: 8000,
      ca: cacert,
      headers: {
        'User-Agent': 'AWS-lambda',
      }
    };

    let req =  https.request(options, (res) => {
      res.setEncoding('utf8');
      let body = '';
      res.on('data', (chunk) => {
        body += chunk;
      });
      res.on('end', () => {
        console.log(body);
      });
    });
    req.end();
}

3-4. エラー解消。取れました

Response:
null

Request ID:
"XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"

Function Logs:
START RequestId: XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX Version: $LATEST
2020-04-16T14:26:29.930Z    XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX INFO
<!DOCTYPE html>
<html lang="jp">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello</title>
</head>
<body>
    This is test page ;>
</body>
</html>

END RequestId: XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
REPORT RequestId: XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
Duration: 210.24 ms Billed Duration: 300 ms Memory Size: 128 MB Max Memory Used: 68 MB  

  1. Node.jsのSSL通信時のトラストアンカーってなんだろ?と思い調べた。https://github.com/nodejs/node/blob/bf7409e9740ce602b09e088aac70b7c817f5d27c/doc/guides/maintaining-root-certs.md 読んでみると、このソースに書いてあるよとのこと。Mozilla NSSのトラストアンカーと合わせているみたいですね、定期的にメンテはされている模様。ここに自己署名をここに入れるのは影響が大きそうです。 

  2. Node.jsのトラストアンカー1に含まれるCAに直接または間接的に署名されているサーバ証明書のこと。 

  3. Qiitaですと、こちらの記事がとても参考になりました。オレオレ証明書を使いたがる人を例を用いて説得する 

  4. 出典:https://stackoverflow.com/questions/57870914/how-to-create-a-single-line-x509-certificate-that-can-be-parsed-by-openssl-comma