今度こそopensslコマンドを理解して使いたい (1) ルートCAをスクリプトで作成する

# これまでの反省
OpenSSLを使ってオレオレ証明書を作った経験は何度かあるのですが、先人がネットで紹介されていた手順のとおりに操作しただけで、各サブコマンドの機能や設定、オプションの意味など何も理解していませんでした。

![ham_cert.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169872/e99734e9-cd8d-d3df-88cd-e979ec93de5d.png)

今回、多数のプライベートCAとクライアント証明書をスクリプトで自動作成することになったので、これを機会にopensslコマンドの勉強を始めました。

## なぜopensslコマンドが難しいのか
個人的に難しいと思う点をまとめたらこんなにありました…

* コマンド（サブコマンド）の数が多く、しかも機能が重複している
    OpenSSLコマンド（1.0.2）: https://www.openssl.org/docs/man1.0.2/man1/

* 同じ目的のための方法がいろいろある
    例えば証明書発行を [秘密鍵を作成] → [署名要求を作成] → [署名して証明書を発行] と3手順で行う方法もあれば、1手順で全部を行う方法もあります。

* 設定ファイルが難解
    構造は一般的なINI形式ですが、どのセクションの設定がどの場合に使われるかが分かりにくいです。

* コマンドのオプションが難解
    コマンド（サブコマンド）ごとに定義されているオプションが多く、また設定ファイル内の設定値との関係も複雑です。

* そもそも公開鍵暗号技術が難解
    こればかりは勉強するしかありません。

## 設定ファイル（openssl.cnf）のセクションに関する誤解
恥ずかしながら公式ドキュメントを読んで、設定ファイル内のセクションの意味を初めて知りました。

例えば以下の部分を見て、`default_ca = CA_default`が下の`[ CA_default ]`セクションを指定していることは分かります。
しかし最初の`[ ca ]`セクションはどこからも指定されていません。

```Ini:/etc/pki/tls/openssl.cnf
[ ca ]
default_ca  = CA_default       # The default ca section

[ CA_default ]
```

私はこれを誤解して、`[ ca ]`セクションは CAを作る時に自動的に参照されると思っていました。
しかし実際には**`[ ca ]`セクションは`ca`コマンドが参照するセクション**であって、CAを作ることとは関係ありません。
同様に、`[ req ]`セクションは`req`コマンドが参照するセクションです。

例えばデフォルトの`openssl.cnf`で`ca`コマンドを実行すると、各セクションは以下のように参照されます。

1. `openssl ca`コマンドを実行すると
2. セクション`[ ca ]`が参照される  
    この中で`default_ca = CA_default`と指定されているので
3. セクション`[ CA_default ]`が参照される  
    この中で`policy = policy_match`と`x509_extensions = usr_cert`が指定されているので
4. セクション`[ policy_match ]`と拡張セクション`[ usr_cert ]`が参照される

こんなことも知らずに使っていたとは…

# ルートCAを作る
まずは ルートCA（認証局）を作るところから始めます。

## 要件
少しハードルが高くなりますが、以下の要件で進めたいと思います。

* 自動処理
    opensslコマンドを普通に使用するとパスフレーズや識別名が対話入力になるが、これをスクリプトで自動化したい

* 設定ファイルは1つ
    CA用／サーバー用などで設定ファイルを作り分けると設定値の管理が煩雑になるので、設定ファイルを1つにしたい

* CAの秘密鍵にパスフレーズを設定する
    パスフレーズを削除する例が多いが、署名時のパスフレーズ入力操作も含めて自動化したい

## 環境とバージョン
CentOS 7.6を使用するので、OpenSSLのバージョンは1.0.2を使用します。

* OpenSSL Manpages for 1.0.2: https://www.openssl.org/docs/man1.0.2/

英語のドキュメントを読むのが嫌いな人は、よろしければこちらもご一読ください。

* [IT系の技術文書なら英語でも簡単に読めることを知らないと損をすると思う](https://qiita.com/3244/items/41a27dd13d76dc7048a4)

## 'CA'スクリプトのCA作成処理
まずCAの作り方のお手本として、公式スクリプトの`'CA'`（CentOSの場合は`/etc/pki/tls/misc/CA`）を参考にします。新規CAを作成する時には`-newca`オプションを指定するので、実行されるコードは以下の部分になります。

```Bash:/etc/pki/tls/misc/CA
-newca)
    # if explicitly asked for or it doesn't exist then setup the directory
    # structure that Eric likes to manage things
    NEW="1"
    if [ "$NEW" -o ! -f ${CATOP}/serial ]; then
        # create the directory hierarchy
        mkdir -p ${CATOP}
        mkdir -p ${CATOP}/certs
        mkdir -p ${CATOP}/crl
        mkdir -p ${CATOP}/newcerts
        mkdir -p ${CATOP}/private
        touch ${CATOP}/index.txt
    fi
    if [ ! -f ${CATOP}/private/$CAKEY ]; then
        echo "CA certificate filename (or enter to create)"
        read FILE

        # ask user for existing CA certificate
        if [ "$FILE" ]; then
            cp_pem $FILE ${CATOP}/private/$CAKEY PRIVATE
            cp_pem $FILE ${CATOP}/$CACERT CERTIFICATE
            RET=$?
            if [ ! -f "${CATOP}/serial" ]; then
                $X509 -in ${CATOP}/$CACERT -noout -next_serial \
                    -out ${CATOP}/serial
            fi
        else
            echo "Making CA certificate ..."
            $REQ -new -keyout ${CATOP}/private/$CAKEY \
                -out ${CATOP}/$CAREQ
            $CA -create_serial -out ${CATOP}/$CACERT $CADAYS -batch \
                -keyfile ${CATOP}/private/$CAKEY -selfsign \
                -extensions v3_ca \
                -infiles ${CATOP}/$CAREQ
            RET=$?
        fi
    fi
    ;;
```

既存の証明書を処理する部分を除いて変数を展開し、新しいスクリプトとして保存します。

```Bash:/etc/pki/tls/misc/newca.sh
CATOP=/etc/pki/CA

# 秘密鍵の生成から署名要求の作成までを行う
openssl req -new -keyout ${CATOP}/private/cakey.pem \
    -out ${CATOP}/careq.pem

# 上の署名要求を受け取って証明書を発行する
openssl ca -create_serial -out ${CATOP}/cacert.pem -days 1095 -batch \
    -keyfile ${CATOP}/private/cakey.pem -selfsign \
    -extensions v3_ca \
    -infiles ${CATOP}/careq.pem
```
処理の内容は以下の2つです。

1. `req`コマンドを使って、秘密鍵の生成から署名要求の作成までを行う
    * このままでは、秘密鍵のパスフレーズを対話形式で入力する必要があります
    * 署名要求の識別名（国、組織、コモンネームなど）も対話形式で入力する必要があります
2. `ca`コマンドを使って署名要求を受け取り、署名して証明書を発行する
    * `-days`オプションで証明書の有効期限を指定しているので、`openssl.cnf`の`default_days`の設定値は無視されます
    * `-extensions`オプションで`openssl.cnf`内の拡張セクション`[ v3_ca ]`を参照することが明示的に指定されています
    * 拡張セクション`[ v3_ca ]`内で`basicConstraints = CA:true`が設定されているので、発行された証明書はCAで署名用として使用できるようになります

## 公式ドキュメントをよく読む

* `req`コマンド manpage: https://www.openssl.org/docs/man1.0.2/man1/openssl-req.html

ページの前半にコマンドオプション、後半に設定ファイルのオプション、識別名と属性セクションのフォーマット、サンプル、注記、エラーの診断方法、環境変数の説明などが丁寧に記載されています。

## 秘密鍵のパスフレーズ入力を自動化する
対話入力のパスフレーズをスクリプトで自動入力する方法を調べます。
`req`コマンドのドキュメントを読むと、出力用のパスフレーズを指定する`-passout`オプションが見つかりました。

> `-passout arg`
出力ファイルのパスワード入力。引数`arg`の詳細は、`openssl`コマンドの`PASS PHRASE ARGUMENTS`セクションを参照のこと。

指示どおりにopensslコマンドのページを参照すると、`PASS PHRASE ARGUMENTS`セクションで以下のように説明されています。
`openssl`コマンド manpage: https://www.openssl.org/docs/man1.0.2/man1/openssl.html

> いくつかのコマンドは、入出力パスワードの引数を`-passin`と`-passout`で、以下の入力元とフォーマットのどれか1つを受け取ることができる。

> **`pass:password`**
`password`で実際のパスワードを指定する。この形式は`ps`コマンドなどでパスワードが見られてしまうので、セキュリティが重要でない場合でのみ使用すること。

> **`env:var`**
環境変数からパスワードを取得する。`ps`コマンドなどで他のプロセスの環境変数が読み取られるので、この方法は注意して使用すること。

> **`file:pathname`**
`pathname`の1行目でパスワードを指定する。同じ`pathname`が`-passin`と`-passout`で指定された場合は、1行目が入力用、2行目が出力用として使用される。`pathname`は通常のファイル以外に、デバイスや名前付きパイプを参照することもできる。

> **`fd:number`**
ファイルディスクリプタからパスワードを読み取る。例えばパイプを介してデータを送るために使用できる。

> **`stdin`**
標準入力からパスワードを読み取る。

今回は`file:pathname`の方法を採用し、パスフレーズを1行目に記述したファイルを作成します。例として`/etc/pki/CA/.capass`を作成し、パスフレーズを'mycap@ss'とします。

```:/etc/pki/CA/.capass
mycap@ss
```
このファイルを秘密鍵の出力用パスフレーズとして`-passout`オプションで指定すると、`req`コマンドのオプションは以下のようになります。

```Bash:/etc/pki/tls/misc/newca.sh
# 秘密鍵の生成から署名要求の作成までを行う
openss req -new \
    -keyout   ${CATOP}/private/cakey.pem \
    -out      ${CATOP}/careq.pem \
    -passout  file:${CATOP}/.capass  # 出力用パスフレーズを記述したファイル
```

## 識別名の入力を自動化する
このままでは、識別名の各フィールド（国、都道府県、組織、コモンネームなど）が対話入力のままです。
またコモンネームを設定ファイルで指定してしまうと、証明書を発行するサーバやクライアントの数だけ設定ファイルが必要になってしまうという問題もあります。

これらの問題を解決するために、識別名をコマンドラインオプションで指定する方法を調べます。
`req`コマンド manpage: https://www.openssl.org/docs/man1.0.2/man1/openssl-req.html

> **`-subj arg`**
署名要求のサブジェクトのフィールドを指定データで置換して出力する。 `arg`は`/type0=value0/type1=value1/type2=...`の形式にしなければならない。`\`で文字をエスケープすることができ、スペースはスキップされない。

また同じページのサンプルで、対話形式でない場合の識別名の各フィールドが以下のように記載されています。
>  [ req_distinguished_name ]
 C                      = 2文字の国名
 ST                     = 都道府県
 L                      = 地域
 O                      = 組織名
 OU                     = 組織単位名
 CN                     = コモンネーム
 emailAddress           = test@email.address

コモンネーム以外は必須ではないので省略できるようですが、今回は一部を省略して以下のようにします。
`/C=JP/ST=Chiba/O=myorg/CN=ca1.mydomain`

また他の組織の証明書を取り扱うことはないので`/C=JP/ST=Chiba/O=myorg`までを固定にして、コモンネームだけを署名の対象ごとに変更できるようにします。

コモンネームを変数`common_name`で指定すると、`req`コマンドのオプションは以下のようになります。

```Bash:/etc/pki/tls/misc/newca.sh
# 秘密鍵の生成から署名要求の作成までを行う
openss req -new \
    -keyout   ${CATOP}/private/cakey.pem \
    -out      ${CATOP}/careq.pem \
    -passout  file:${CATOP}/.capass \
    -subj     "/C=JP/ST=Chiba/O=myorg/CN=${common_name}"  # 識別名を指定する
```

今回は複数のCAを連続して生成したいので、CAのシリアル番号とコモンネームの一覧を記述したファイル`/etc/pki/CA/ca-list`（任意）を作成します。

```:/etc/pki/CA/ca-list
1 ca1.mydomain
2 ca2.mydomain
```

このリストを読み込んでループの中で`req`コマンドと`ca`コマンドを実行するように変更します。またそれぞれのCA用に、秘密鍵と証明書を出力するためのサブディレクトリも作成します。

```Bash:/etc/pki/tls/misc/newca.sh
CATOP=/etc/pki/CA

while read serial_num common_name
do
  # CAごとに出力用のサブディレクトリを作成する
  mkdir -p ${CATOP}/${common_name}/private

  # 秘密鍵の生成から署名要求の作成までを行う
  openss req -new \
      -keyout   ${CATOP}/${common_name}/private/cakey.pem \
      -out      ${CATOP}/${common_name}/careq.pem \
      -passout  file:${CATOP}/.capass \
      -subj     "/C=JP/ST=Chiba/O=myorg/CN=${common_name}"  # 識別名を指定する

  # 上の署名要求を受け取って証明書を発行する
  openssl ca -create_serial -out ${CATOP}/${common_name}/cacert.pem -days 1095 -batch \
      -keyfile ${CATOP}/${common_name}/private/cakey.pem -selfsign \
      -extensions v3_ca \
      -infiles ${CATOP}/${common_name}/careq.pem
done <${CATOP}/ca-list    # CAのシリアル番号とコモンネームの一覧を読み込む
```

## 証明書の自己署名を同時に行う
今回はルートCAとして証明書を自己署名するので、そもそも署名要求の生成と署名の処理を分ける必要がありません。そのために`req`コマンドには、署名要求の代わりに自己署名済みの証明書を出力する機能があることが分かりました。
この機能を使えば、`req`コマンドに続く`ca`コマンドの処理をなくすことができそうです。
`req`コマンド manpage: https://www.openssl.org/docs/man1.0.2/man1/openssl-req.html

> **`-x509`**
このオプションは署名要求の代わりに自己署名された証明書を出力する。通常これはテストまたは自己署名のルートCAに使用される。設定ファイルで指定されていれば拡張属性が証明書に付与される。`set_serial`オプションが使用されなければ、大きな乱数がシリアル番号として使用される。
既存の署名要求が`-in`オプションで指定されたら自己署名証明書に変換され、なければ新規の署名要求が作成される。

> **`-days n`**
`-x509`オプションが使用される場合に、証明書が有効な日数を指定する。デフォルトは30日。

> **`-set_serial n`**
自己署名証明書を出力するときに使用するシリアル番号。 10進数または`0x`で始まる16進数を指定できる。マイナス値は使用できるが推奨されない。

以下のようにこれらのオプションを`req`コマンドに追加することで、`ca`コマンドが行っていた署名と証明書出力の処理を置き換えることができそうです。

```Bash:/etc/pki/tls/misc/newca.sh
CATOP=/etc/pki/CA

# CAのシリアル番号とコモンネームを1件ずつ読み込む
while read serial_num common_name
do
  # CAごとに出力用のサブディレクトリを作成する
  mkdir -p ${CATOP}/${common_name}/private

  # 秘密鍵を生成して自己署名証明書を発行する
  openss req -new \
      -keyout   ${CATOP}/${common_name}/private/cakey.pem \
      -out      ${CATOP}/${common_name}/cacert.pem \
      -passout  file:${CATOP}/.capass \
      -subj     "/C=JP/ST=Chiba/O=myorg/CN=${common_name}" \
      -x509 -days 1095 -set_serial ${serial_num}  # 自己署名を行い証明書を出力する

done <${CATOP}/ca-list
```

* `-out`オプションで指定するファイル名を、署名要求から証明書のファイル名に変更
* `-days`オプションで証明書の有効日数を指定する

## 拡張セクションを指定する
元の公式`'CA'`スクリプトの中で、`ca`コマンドのオプションとして`-extensions`で拡張セクションが指定されています。`req`コマンドでも同様に、以下のオプションを使うことで拡張セクションを指定できることができると書かれています。
`req`コマンド manpage: https://www.openssl.org/docs/man1.0.2/man1/openssl-req.html

> **`-extensions section`**
> **`-reqexts section`**
これらのオプションは、証明書の拡張設定（`-x509`が与えられた場合）または署名要求に含める代替のセクションを指定する。これによって、さまざまな目的に応じて、同じ設定ファイル内の異るセクションを使用することができる。

設定ファイルを用途別に作り分ける方法がよく紹介されていますが、その理由の多くは拡張セクションのオプションが用途によって異なるためです。

しかし上記オプションの説明に書かれているように、1つのファイル内に用途別の拡張セクションをいくつか用意して指定すれば、設定ファイルを1つにすることができそうです。

今回はデフォルトの設定ファイルにCA用として用意されている`[ v3_ca ]`セクションを`-extensions`オプションで指定します。この中で`basicConstraints = CA:true`の拡張設定が含まれているので、CA用として署名に使用できる証明書になるはずです。

```Bash:/etc/pki/tls/misc/newca.sh
CATOP=/etc/pki/CA

# CAのシリアル番号とコモンネームを1件ずつ読み込む
while read serial_num common_name
do
  # CAごとに出力用のサブディレクトリを作成する
  mkdir -p ${CATOP}/${common_name}/private

  # 秘密鍵を生成して自己署名証明書を発行する
  openss req -new \
      -keyout   ${CATOP}/${common_name}/private/cakey.pem \
      -out      ${CATOP}/${common_name}/cacert.pem \
      -passout  file:${CATOP}/.capass \
      -subj     "/C=JP/ST=Chiba/O=myorg/CN=${common_name}" \
      -x509 -days 1095 -set_serial ${serial_num} \
      -extensions v3_ca   # 拡張セクションを指定する

  # 秘密鍵のパーミッションを設定する
  chmod 0400 ${CATOP}/${common_name}/private/cakey.pem
done <${CATOP}/ca-list
```

ついでに秘密鍵のパーミッション設定処理も追加しました。

## 作成したスクリプトでCAを作成する
スクリプトを実行して、秘密鍵の生成から自己署名証明書の発行までを行います。

```Shell:
bash newca.sh

Generating a 2048 bit RSA private key
.............................+++
.................................................................+++
writing new private key to '/etc/pki/CA/ca1.mydomain/private/cakey.pem'
-----
```
スクリプトが正常に終了すると、以下のようにサブディレクトリごとに秘密鍵と証明書のファイルが出力されます。

```
/etc/pki/CA/
├── ca1.mydomain
│   ├── cacert.pem
│   └── private
│       └── cakey.pem
├── ca2.mydomain
│   ├── cacert.pem
│   └── private
│       └── cakey.pem
```

## 証明書の内容を確認する
以下のように`x509`コマンドを使って証明書の内容を確認します。

```Shell:
openssl x509 -in /etc/pki/CA/ca1.mydomain/cacert.pem -text
```
問題がなければ以下のように表示されます。

```Shell:/etc/pki/CA/ca1.mydomain/cacert.pem
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 1 (0x1)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=JP, ST=Chiba, O=myorg, CN=ca1.mydomain
        Validity
            Not Before: Aug  3 07:25:17 2019 GMT
            Not After : Aug  2 07:25:17 2022 GMT
        Subject: C=JP, ST=Chiba, O=myorg, CN=ca1.mydomain
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
（略）
        X509v3 extensions:
（略）
            X509v3 Basic Constraints: 
                CA:TRUE
（略）
```
以下の設定値が期待したとおりであることが確認できました。

* `Issuer:`と`Subject:`の両方が、`-subj`オプションで指定した識別名／コモンネームになっている
* `Not After :`（有効期限）が、`-days`オプションで指定した日数経過後の日付になっている
* `X509v3 Basic Constraints:`が、`-extensions`オプションで指定した設定ファイルのセクション内の`basicConstraints = CA:true`で指定されたとおりになっている
* `Signature Algorithm:`のハッシュアルゴリズムが、設定ファイルの`[ req ]`セクション内の`default_md = sha256`で指定されたとおりになっている
* `Public-Key:`の鍵長が、設定ファイルの`[ req ]`セクション内の`default_bits = 2048`で指定されたとおりになっている

## 秘密鍵の内容を確認する
次に、生成された秘密鍵の内容を`rsa`コマンドを使って確認します。

```Shell:
openssl rsa -in /etc/pki/CA/ca1.mydomain/private/cakey.pem -text
```
以下のようにパスフレーズの入力を求められたら、設定したパスフレーズを入力します。今回の例では、ファイル`/etc/pki/CA/.capass`で設定した`mycap@ss`を入力します。

```Shell:
Enter pass phrase for /etc/pki/CA/ca1.mydomain/private/cakey.pem:
```
これで秘密鍵の内容が表示されれば、パスフレーズが意図したとおりに設定されています。

```Shell:/etc/pki/CA/ca1.mydomain/private/cakey.pem
Private-Key: (2048 bit)
modulus:
（略）
publicExponent: 65537 (0x10001)
privateExponent:
（略）
prime1:
（略）
prime2:
（略）
exponent1:
（略）
exponent2:
（略）
coefficient:
（略）
writing RSA key
-----BEGIN RSA PRIVATE KEY-----
（略）
-----END RSA PRIVATE KEY-----
```
また`Private-Key: (2048 bit)`から、鍵長が設定ファイルの`[ req ]`セクション内の`default_bits = 2048`で指定されたとおりになっていることが確認できます。

# 今後の予定

* 設定ファイルの各オプションを精査する
    * 設定ファイル manpage（1.0.2）: https://www.openssl.org/docs/man1.0.2/man5/config.html
    * X509 V3 拡張設定 manpage（1.0.2）: https://www.openssl.org/docs/man1.0.2/man5/x509v3_config.html

* 作成したCAでサーバー／クライアント証明書を発行し、OpenVPNでの動作を確認する
    * OpenVPN コミュニティ Wiki: https://community.openvpn.net/openvpn

# 記事一覧
今度こそopensslコマンドを理解して使いたい (1) ルートCAをスクリプトで作成する
[今度こそopensslコマンドを理解して使いたい (2) 設定ファイル（openssl.cnf）を理解する](https://qiita.com/3244/items/8c7e6892efdb4e500be9)
[今度こそopensslコマンドを理解して使いたい (3) CA証明書の拡張設定を検証する](https://qiita.com/3244/items/2a2a2dc6cd1e2e35beb9)
[今度こそopensslコマンドを理解して使いたい (4) サーバー／クライアント証明書を一括生成する](https://qiita.com/3244/items/2618429ebe6dc16c074e)
[今度こそopensslコマンドを理解して使いたい (5) CRL（証明書失効リスト）を作成してOpenVPNに配布する](https://qiita.com/3244/items/21c2cb009de53db03f0e)
[今度こそopensslコマンドを理解して使いたい (補足1) サンプルスクリプトのまとめ](https://qiita.com/3244/items/65dd3926fab76b9dd4aa)