自己署名証明書（オレオレ証明書）によるHTTPSローカル環境の構築

こんにちは。

開発環境において、より本番に近い環境で開発をしたいなどHTTPSでの通信を必要とする場面に出くわすことがあると思う。
（WebRTCをローカル環境で試す際に、HTTPSの環境が必要とのことだったが、localhost~であればHTTPSでなくても問題なかった...）

そのような場合にローカル環境（localhost）でHTTPS環境を構築する方法を備忘録として、記述する。


# 目次
* やりたいこと
* HTTPSとは？
* 自己署名証明書とは？
* 対象環境
* 手順
* おわり

# やりたいこと
ローカル環境で、自己署名証明書（オレオレ証明書）を発行して、それを元にHTTPS通信を行う。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3468943/bd021eab-0816-02a1-ab08-592053806bc1.png)

# HTTPSとは？
WebブラウザとWebサイト間でデータを送受信するために使用されるプロトコルであるHTTPをSSL/TLS（暗号化通信）により、通信内容を暗号化したプロトコルのこと。

HTTPSを利用して、通信を行うことで通信内容の改竄／盗聴から守ることが出来る。

HTTPS通信の大まかな通信の流れは下記。
1. Webブラウザで、WebサーバにHTTPSでアクセス
2. WebサーバからSSL証明書と公開鍵が送られてくる
3. Webブラウザは、あらかじめ登録されている証明書（ルート証明書）とサーバから送られてきた証明書を検証
3. 有効な証明書だと分かった場合には、「共通鍵（セッションキー）」を生成する
生成した共通鍵を、SSL証明書に含まれている公開鍵で暗号化して、Webサーバに返送する
返送された「共通鍵」は、Webサーバ側で保持している秘密鍵で復号され、取り出される。
これで、お互いに共通鍵を所持している状態になる。

Webブラウザは、共通鍵でデータを暗号化して、Webサーバに送る
Webサーバは、共通鍵を使用して通信データを復号して、データを取得する。

# 自己署名証明書とは？
デジタル署名の発行形態の一つ。
本来ならTLS／SSL証明書は中間認証局（CA）が署名を行うが、認証局に依頼せず、ウェブサイトの責任者が自身で発行した秘密鍵を用いて署名を行う。

自己署名によるTLS／SSL証明書の作成の流れは下記。
1. 秘密鍵を作成する
1. 秘密鍵とそのペアの公開鍵を作成する
1. 署名要求証明書（CSR）を作成する。
1. 署名要求証明書（CSR）に対して、秘密鍵で署名する

# 対象環境
* Windows10
* Node.js（Express）
* OpenSSL

# 手順

## 自己署名による証明書と秘密鍵を作成
今回は自己署名証明書を作成して、HTTPSで通信を行う。（OpenSSL）

〇秘密鍵を作成
* genrsa : RSA秘密鍵を生成する
* -out : 出力ファイル指定
* numbits : 生成する秘密鍵のサイズ。デフォルトで2048。
```bash
$ openssl genrsa -out server.key 2048
```

〇証明書署名要求（CSR）の作成
* req : PKCS形式の証明書要求（CSR）を作成する
* -new : 新しい証明書要求を生成する
* -out : 出力ファイル指定
* -key :証明書署名要求（CSR）に署名するための秘密鍵を指定する
```bash
$ openssl req -out server.csr -key server.key -new
Country Name (2 letter code) [XX]: # Enter
State or Province Name (full name) []: # Enter
Locality Name (eg, city) [Default City]: # Enter
Organization Name (eg, company) [Default Company Ltd]:  # Enter
Organizational Unit Name (eg, section) []: # Enter
Common Name (eg, your name or your server's hostname) []: # Enter
Email Address []: # Enter

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []: #Enter
An optional company name []: # Enter
```
「# Enter」の部分は、任意で入力のこと。

〇自己署名証明書の作成
証明書署名要求（CSR）から自己署名証明書を作成する。
有効期限は、3650日で作成する。
* x509 : 証明書署名要求（CSR）から自己署名証明書を作成する
* -req : PKCS#10の証明書署名要求（CSR）を読み取る
* -in : 読み取る証明書署名要求（CSR）を指定する
* -days : 証明書の有効期限の日数を指定。デフォルトでは30日
* -signkey : -keyのエイリアス。証明書に署名するための秘密鍵を指定する
* -out : 出力ファイルの指定
* -extfile : 証明書に追加する情報を記述しているファイルの指定

Chromeを使用している場合は、「保護されていない通信」と表示されるため、SAN.txtを作成する。（この対応をしなくても、HTTPS接続は可能。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3468943/c9d5ed55-31f4-d226-01b1-3a288d43df34.png)

Chromeは、ドメイン名のチェックをCommon Nameで行わず、SAN（Subject Alternative Name）を参照しているとのこと。
従って、SAN.txtに適用するホスト名を指定する。
```SAN.txt
subjectAltName = DNS:localhost
```
SAN.txtを自己署名証明書に情報を加えて、証明書署名要求（CSR）から自己署名証明書を作成する。
SAN.txtがあるディレクトリで下記コマンドを実行。
```bash
$ openssl x509 -req -days 3650 -signkey server.key -in server.csr -out server.crt -extfile SAN.txt
```

server.keyとserver.crtが作成されていることを確認。


## サーバアプリケーションを構築する
作成した証明書と秘密鍵を元に、HTTPS通信で「Hello World!」と表示する

```javascript:index.js
const express = require('express');
const app = express();
const fs = require('fs');

const server = require('https').createServer(
  {
    key: fs.readFileSync('./server.key'),
    cert: fs.readFileSync('./server.crt'),
    passphrase: 'password',
  },
  app
);

// GET
app.get('/', (req, res) => {
  res.send('Hello World!');
});

server.listen(3000, () => {
  console.log('Server listening on port 3000');
});
```

サーバを起動して、ブラウザで[https://localhost:3000](https://localhost:3000)にアクセスして、「Hello World!」が表示されることを確認する。


# おわり
自己署名証明書を作成して、それを元にローカルでHTTPS環境を構築した。
開発の際に使用しなくても、一度構築してみると証明書の仕組みなどとても勉強になると感じた。

# 参考資料
* [公式　openssl-genrsa](https://www.openssl.org/docs/manmaster/man1/openssl-genrsa.html)
* [公式　openssl-req](https://www.openssl.org/docs/manmaster/man1/openssl-req.html)
* [公式　openssl-x509](https://www.openssl.org/docs/manmaster/man1/openssl-x509.html)
* [Chrome Deprecates Subject CN Matching](https://textslashplain.com/2017/03/10/chrome-deprecates-subject-cn-matching/)