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

# はじめに
AWS lambdaのNode.js(https標準モジュール)で実装した、WEBサイトへhttpsのリクエストを投げる処理で、以下の２つのエラーが発生した際の対応についての記事です。
※急いでいる方、ソースコードだけ見たい方は[ここから見ればOK](#3オレオレ証明書self-signedを一時的に信頼する)です

<img width="578" alt="スクリーンショット 2020-04-16 22.24.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124955/aa01a421-acaf-f0e7-4eed-5232638b7c19.png">
<img width="585" alt="スクリーンショット 2020-04-16 18.31.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124955/0130f234-454d-e2b8-3def-5519a01b2a3c.png">

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

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

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

# 対処案
３つ考えましたが、妥協点で私の環境では[３つ目](#3サーバ証明書を署名している自局ca証明書を-一時的に信頼する)で実装しましたので以下にまとめます。

## 1.WEBサーバ側にちゃんとしたサーバ証明書[^2]に変更してもらう
[^2]: Node.jsのトラストアンカー[^1]に含まれるCAに直接または間接的に署名されているサーバ証明書のこと。
~~この対応ができたらこんな記事はいらない気もする。~~
自局署名の問題については、ネット上でも従前から議論されおり[^3]、本記事をご覧になっている方でも既知のことと思います。WEBサーバ管理者に対しては、セキュリティの観点で懸念点を伝えてさしあげる程度にしました。
[^3]: Qiitaですと、こちらの記事がとても参考になりました。[オレオレ証明書を使いたがる人を例を用いて説得する](https://qiita.com/Sheile/items/dc91128e8918fc823562)

## 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に置換します。

```sh
[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]
[^4]: 出典:https://stackoverflow.com/questions/57870914/how-to-create-a-single-line-x509-certificate-that-can-be-parsed-by-openssl-comma

### 3-2.AWS lamda環境変数の設定
[上コマンド](#3-1pemの改行を置換)で表示された内容を環境変数にCA_PEMとして保存します。
<img width="869" alt="スクリーンショット 2020-04-16 14.43.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124955/7609c6c5-909a-024f-9c88-0ebe84ea5873.png">

### 3-3. 実際のスクリプト(sample)
```javascript
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	
```