開発環境をhttps化するmkcertの仕組み

# 記事の趣旨

ローカル開発環境をhttps化する際に、 [mkcert](https://github.com/FiloSottile/mkcert) というライブラリを使うと、簡単に証明書を発行してhttps環境を構築することができました。
この「証明書」とは何か、についてよく知らなかったため、書籍等を読んで調べてみました。

**参考書籍**

食べる！SSL！ ―HTTPS環境構築から始めるSSL入門
https://www.amazon.co.jp/dp/B00PHC4480

nginx実践ガイド
https://www.amazon.co.jp/dp/4295000728

# HTTPS通信の仕組み
HTTPS（HTTP over TLS/SSL）は、HTTPプロトコルをTLS/SSLプロトコルで安全を保証して使用するプロトコルです。

## SSL
SSL（Secure Socket Layer）/ TLS（Transport Layer Security）は、通信の暗号化、改ざん検知、通信相手の認証を行う仕組みです。現在はプロトコルとしては主にTLSが使われていますが、慣例的にTLSのことも含めてSSLと総称されています。

## 暗号化方式
SSL等で使われる暗号方式には以下の種類があります。

### 公開鍵暗号方式
秘密鍵と公開鍵のペアを使った暗号化手法。
送信側が通信相手の公開鍵を使って暗号化したメッセージを送信し、受信側は自身の秘密鍵でメッセージを復号します。

### 共通鍵暗号方式
双方が同じ鍵を使って暗号化し、同じ鍵で復号します。

### ハイブリッド暗号方式
SSLでは、上記2つの暗号方式を組み合わせて利用します。メインの通信の暗号化には共通鍵暗号を用いますが、その共通鍵を共有する際に公開鍵・秘密鍵を用います。

**共通鍵暗号を使用する理由**
メインの通信も公開鍵暗号方式で暗号化すれば良いのではないかと思うかもしれませんが、公開鍵暗号は計算量が多いため、大きなデータが流れる部分には計算量の少ない共通鍵暗号が使われています。

## SSL通信の概要
SSLによる通信の大まかな流れは以下の図のようになります。

**2021/08/22追記:**
コメントでご指摘いただきましたが、この共通鍵の交換方式（サーバーの公開鍵・秘密鍵で共通鍵の元を暗号・復号化する方式）は2021年現在では使われなくなっているようです。
本記事でメインに扱いたい「サーバー証明書」の役割については変わらないため、一旦図はそのままにさせていただきます。

図1. SSL通信の流れ
![SSL通信の流れ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504515/a6a12dad-ff8e-8a65-7935-9962532fca8f.jpeg)


※上記では省略していますが、やりとりするメッセージをハッシュ化して互いに検証することで改ざんを検知することなども行われています。

## 証明書とは
図1に登場するサーバー証明書とは、「公開鍵」が「被証明者のもの」であることを証明するものです。証明者（認証局）が、証明者の秘密鍵で署名することで、正当性が証明されます。

### 証明書の階層構造
証明書は、一般に `ルート認証局 → 中間認証局 → サーバー証明書` のような形の階層構造を持ちます。

図2. Qiitaのサーバー証明書（2021/08/16時点）
![Qiitaのサーバー証明書（2021/08/16時点）](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504515/d1b8f764-b670-91bf-b478-145ff70aa844.png)

サーバー証明書の正当性は中間認証局が保証し、中間認証局の正当性はルート認証局が保証します。
ルート認証局は、自身が正当性を保証する自己証明書になっていますが、監査や実績等により「信頼できる証明書」と認められるものは、ブラウザの証明書リストに元々登録されています。

図3. 登録されている証明書リスト（mac）
![登録されている証明書リスト（mac）](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504515/2dcccfd7-5422-1b98-0b7b-c54e154ad91a.png)

### 証明書の検証
ブラウザは、図1の②で通信相手のサーバー証明書を受け取ると、その階層構造をたどって署名を検証していきます。最終的にルート認証局が上述の証明書リストに含まれているか、署名が正しいかを検証します。

検証に成功した場合は次の手順（④）に進みますが、検証できなかった場合、chromeでは以下のような警告画面が表示されます。

図4. 証明書が検証できなかった場合の警告画面
![証明書が検証できなかった場合の警告画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504515/5b15d9a1-f854-ae14-00bb-ad45b010c261.png)

（上記画面で「thisisunsafe」とタイプすると、警告を無視して進むことができます）

### 開発環境用の自己証明書
本番環境で用いるサーバー証明書は、通常第三者の認証局（パブリック認証局）に証明してもらいますが、開発環境では、自己証明書を使ってSSL通信を行うこともできます。

#### 自己証明書
被証明者が自身の秘密鍵で署名した証明書を自己証明書（オレオレ証明書）と呼びます。
そのままではブラウザで警告が出るため、その証明書を証明書リストに追加する必要があります。

#### 自己認証局
自身の秘密鍵で署名したルート証明書を作成し、自身を認証局としてサーバー証明書を発行するということもできます。
この場合、ブラウザの警告を防ぐために証明書リストに追加するのは、ルート証明書だけでよくなります。

# mkcertの仕組み
以上を踏まえて、[mkcert](https://github.com/FiloSottile/mkcert) により開発環境用の証明書を発行する際の動きを見ていきます。

## mkcertのインストール（macの場合）

```
$ brew install mkcert
```

## 自己認証局の作成、証明書リストへの追加

```
$ mkcert -install
```

上記のコマンドで、以下の2つが行われます。

**1. 自己認証局用の証明書・秘密鍵の作成**
`/Users/ユーザー名/Library/Application\ Support/mkcert/` 配下に以下のファイルが作成されます。（このパスは `mkcert -CAROOT` コマンドによって確認できます）

```
rootCA.pem （ルート証明書）
rootCA-key.pem （秘密鍵）
```

**2. 証明書を証明書リストに追加**
作成したルート証明書が「信頼できる証明書」として、自動で証明書リストに追加されます。

図5. 証明書リストに追加された自己認証局（macの場合）
![証明書リストに追加された自己認証局](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504515/ba49a22d-8bb9-4116-1db1-f64ceb2b621e.png)

## サーバー証明書の発行
上で作成した自己認証局によって正当性が保証されたサーバー証明書を発行します。
以下の例では、example.comというドメインに対してサーバー証明書・秘密鍵を発行します。

```
$ mkcert example.com
```

上記コマンドにより、その場に以下のファイルが作成されます。

```
example.com.pem （サーバー証明書）
example.com-key.pem （秘密鍵）
```

これらをnginxなどの開発用webサーバーに配置し、適切な設定をすることで、そのサーバーに対してhttpsで通信することができるようになります。

# mkcertを使わない例

## macのGUI
macの場合はGUI上で認証局の作成、証明書の発行を行うこともできます。

簡単に作成できますが、作成した証明書と秘密鍵をファイルに書き出す際に、pem形式を選ぶことができず、自分で変換が必要なようでした。

**参考記事**
MacでGUIを使ってオレオレルート認証局を作る
https://mashi.hatenablog.com/entry/2019/04/14/183705

## OpenSSLで作成
OpenSSLは、SSL/TLS関連の様々な関数を実装しているソフトで、Linuxやmacには最初からインストールされています。秘密鍵の生成や、証明書の中身の確認など、証明書の作成以外にも便利な機能が揃っています。

自己認証局やサーバー証明書の作成について、以下の記事で分かりやすく説明されていました。

**参考記事**
How to create & sign SSL/TLS certificates
https://dev.to/techschoolguru/how-to-create-sign-ssl-tls-certificates-2aai