Dockerのコンテナでオレオレ認証局を立ててサーバー証明書に署名する

DockerコンテナでHTTPS通信をする必要があったので、オレオレ認証局コンテナを作成してオレオレルート証明書を発行し、サーバ証明書に署名しました。

オレオレ認証局についての記事はすでにたくさんありますがメモとして残しておきます。

Docker固有の操作は最初の方だけなので、OpenSSLの操作にのみ興味のある方は「オレオレルート証明書の作成」まで飛んでください。

署名の詳しい仕組みについては扱いません。

# 環境
・Windows 10 Home 20H2
・Docker version 20.10.2

# オレオレ認証局コンテナの作成
まずはオレオレ認証局コンテナを作成します。

[OpenSSL](https://www.openssl.org/)で署名鍵(秘密鍵)や証明書を扱うので[Alpine Linuxのイメージ](https://hub.docker.com/_/alpine)を元にオレオレ認証局のイメージを作っていきましょう。

```dockerfile:Dockerfile
FROM alpine

RUN apk add --update openssl && \
    rm -rf /var/cache/apk/*
```

PowerShellでDockerfileのあるディレクトリに移動した上でdocker buildコマンドを実行し、コンテナを作ります。

```powershell
> docker build -t openssl ./
```

イメージができたので、イメージからコンテナを生成するためにdocker-compose.ymlを書いていきます。

```yml:docker-compose.yml
version: '3'

services:
  rootCA:
    image: openssl
    hostname: rootCA
    volumes:
      - ./rootCA/etc/pki:/etc/pki
    tty: true
```

コンテナを停止しても作成した秘密鍵などが消えないように、適当なvolumeを作成しておく必要があります。


```powershell
> docker compose up
```

#オレオレルート証明書の作成
オレオレ認証局コンテナができたので、オレオレルート証明書(自己署名証明書)を発行していきます。

OpenSSLはデフォルトの設定ファイルとしてある場所のopenssl.cnfを参照します。
Alpine Linuxの場合はUbuntuなどと同じく/etc/ssl/openssl.cnfにあります。

```console
/etc/ssl # ls
cert.pem              ct_log_list.cnf       misc                  openssl.cnf.dist
certs                 ct_log_list.cnf.dist  openssl.cnf           private
```

CentOSなどの場合は/etc/pki/tls/openssl.cnfにあるようです。
設定ファイルはvolumeの下に置いておきたいのでコピーしておきます。

```console
/etc/ssl # cd /etc/pki
/etc/pki # cp /etc/ssl/openssl.cnf ./
```

コピーしたファイルを編集していきます。

```shell
default_md = sha256 #77行目 デフォルトのハッシュ関数を変更
default_bits = 4096 #108行目 デフォルトの公開鍵のビット数を変更
countryName_default = JP #131行目 デフォルトの国コード
stateOrProvinceName_default = Tokyo #136行目 デフォルトの都道府県名
localityName_default = Shinjuku #139行目 デフォルトの地域
0.organizationName_default = iamMe inc. #142行目 オレオレ認証局の組織
```
`default_bits`は元々は2048ビットで、これは現在のところ十分な強度のあるRSA鍵の長さとされていますが、せっかくなので4096ビットにしてみました。

それではいよいよオレオレルート証明書の作成の段階に入っていきます。
ワンライナーで一気にルート証明書を発行する方法もあるようですが今回は順を追ってコマンドを実行していきます。
まずはAES256で暗号化されたパスフレーズ付きの4096bitRSA署名鍵(privkey.pem)を作成します。

```console
/etc/pki # mkdir private
/etc/pki # openssl genrsa -aes256 -out ./private/privkey.pem 4096
```

この際パスフレーズを尋ねられます。
ここで入力したパスフレーズはprivkey.pemを署名に使用するたびに尋ねられます。

署名鍵の中身を確認するには次のコマンドを使用します。

```console
/etc/pki # openssl rsa -text -noout -in ./private/privkey.pem
```

中身は次の通りです。

```
RSA Private-Key: (4096 bit, 2 primes)
modulus:
    00:d4:a1:1a:5d:88:37:ba:62:c3:80:b2:e2:2b:6d:
    (中略)
    ee:48:41
publicExponent: 65537 (0x10001)
privateExponent:
    00:b2:c6:19:62:d6:aa:f4:5d:21:bf:4d:a7:f7:97:
    (中略)
    0a:a3:59
prime1:
    00:eb:27:a3:28:e3:cd:20:97:7a:d6:24:ff:2a:f9:
    (中略)
    99:83
prime2:
    00:e7:7a:4b:63:5b:7f:44:97:b3:35:90:5e:2e:a6:
    (中略)
    9f:eb
exponent1:
    3a:a8:9c:8e:aa:a4:94:a8:b4:bf:8e:63:08:79:38:
    (中略)
    b9
exponent2:
    56:c9:00:18:c9:46:26:f6:65:47:30:d0:4e:d7:26:
    (中略)
    6f
coefficient:
    00:d7:ec:19:72:d6:48:eb:ec:f5:f8:28:b5:fe:3c:
    (中略)
    b0:93
```

`prime1`と`prime2`は知られるとまずい2つの素数$p$と$q$、`privateExponent`は$(p-1)(q-1)$でやはり知られるとまずい値です。`exponent1`や`exponent2`、`coefficient`は署名や(RSA鍵を暗号として使う場合の)複号の計算を効率的に進めるための値らしいです。

次にさっき編集したopenssl.cnfを読み込んで、自己署名証明書署名要求(cacert.csr)を発行します。

``` console
/etc/pki # mkdir cert
/etc/pki # openssl req -new -key ./private/privkey.pem -config openssl.cnf -out ./cert/cacert.csr
```

いろいろ聞かれますが、openssl.cnfに書き込んだ設定がデフォルトの値に反映されているので入力する箇所はほとんどありません。

```console
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [JP]: <=入力せずEnter
State or Province Name (full name) [Tokyo]:  <=入力せずEnter
Locality Name (eg, city) [Shinjuku]:  <=入力せずEnter
Organization Name (eg, company) [iamMe inc.]:  <=入力せずEnter
Organizational Unit Name (eg, section) []:  <=入力せずEnter
Common Name (e.g. server FQDN or YOUR name) []:MY OWN CERTIFICATE AUTHORITY <=好きな名前を入力
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
```

署名要求の中身は次のコマンドで確認します。

```console
/etc/pki # openssl req -text -noout -in ./cert/cacert.csr
```

中身はこんな感じです。

```console
Certificate Request:
    Data:
        Version: 1 (0x0)
        Subject: C = JP, ST = Tokyo, L = Shinjuku, O = iamMe inc., CN = MY OWN CERTIFICATE AUTHORITY
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (4096 bit)
                Modulus:
                    00:d4:a1:1a:5d:88:37:ba:62:c3:80:b2:e2:2b:6d:
                    (中略)
                    ee:48:41
                Exponent: 65537 (0x10001)
        Attributes:
            a0:00
    Signature Algorithm: sha256WithRSAEncryption
         61:75:01:a6:03:eb:ee:4c:8c:9a:73:fe:32:30:c3:56:48:25:
         (中略)
         2b:62:2c:2b:f5:c4:74:72
```

RSA Public-Key: (4096 bit)やSignature Algorithm: sha256WithRSAEncryptionに先ほど編集したopenssl.cnfの設定が反映されていることがわかります。

最後に、この証明書署名要求に自分の署名鍵を使って署名してオレオレルート証明書(cacert.pem)を発行します。

```console
etc/pki # openssl x509 -req -in ./cert/cacert.csr -signkey ./private/privkey.pem -days 1000 -out ./cert/cacert.pem
```

`-days 1000`は証明書の有効日数の指定です。

証明書の中身は次のコマンドで確認します。

```console
/etc/pki # openssl x509 -text -noout -in ./cert/cacert.pem
```

中身はこんな感じです。

```
Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number:
            2c:b3:0f:be:54:af:3c:2f:1f:bd:a7:6e:6f:41:6f:44:5a:a2:e7:74
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = JP, ST = Tokyo, L = Shinjuku, O = iamMe inc., CN = MY OWN CERTIFICATE AUTHORITY
        Validity
            Not Before: Feb 22 14:50:38 2021 GMT
            Not After : Nov 19 14:50:38 2023 GMT
        Subject: C = JP, ST = Tokyo, L = Shinjuku, O = iamMe inc., CN = MY OWN CERTIFICATE AUTHORITY
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (4096 bit)
                Modulus:
                    00:d4:a1:1a:5d:88:37:ba:62:c3:80:b2:e2:2b:6d:
                    (中略)
                    ee:48:41
                Exponent: 65537 (0x10001)
    Signature Algorithm: sha256WithRSAEncryption
         d3:35:85:95:53:4e:39:fc:26:03:3d:5f:b7:ac:b6:87:31:79:
         (中略)
         be:36:e1:a2:6e:d0:1d:74
```
当然ですが、IssuerとSubjectが同一です。
公開鍵基盤の仕組みでは、ルート認証局が自分で署名したルート証明書を信頼するほかありません。

# オレオレルート認証局が署名したサーバー証明書の発行
## サーバー証明書を発行してほしいサーバーでの操作
次はサーバー証明書を発行します。
まずサーバーにOpenSSLをインストールします。
私の場合、証明書を発行してほしいのはnode:alpineコンテナなので、インストールもさっきと同じ操作です。

```console
/etc/ssl # cd /etc/pki
/etc/pki # cp /etc/ssl/openssl.cnf ./
```

openssl.cnfをvolumeの下にコピーして編集していきます。

```shell
default_md = sha256 #77行目 デフォルトのハッシュ関数を変更
default_bits = 4096 #108行目 デフォルトの公開鍵のビット数を変更
countryName_default = JP #131行目 デフォルトの国コード
stateOrProvinceName_default = Tokyo #136行目 デフォルトの都道府県名
localityName_default = Shibuya #139行目 デフォルトの地域
0.organizationName_default = YouareYou inc. #142行目 オレオレ認証局の組織
```

署名鍵の生成と証明書署名要求の作成まではさっきと同じコマンドです。
まず署名鍵(privkey.pem)を生成します。

```console
/etc/pki # mkdir private
/etc/pki # openssl genrsa -aes256 -out ./private/privkey.pem 4096
```

ここで新たにパスフレーズを決めます。
生成した署名鍵を用いて証明書署名要求(servercert.csr)を作成します。

``` console
/etc/pki # mkdir cert
/etc/pki # openssl req -new -key ./private/privkey.pem -config openssl.cnf -out ./cert/servercert.csr
```

質問されるので答えます。

```console
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [JP]: <=入力せずEnter
State or Province Name (full name) [Tokyo]:  <=入力せずEnter
Locality Name (eg, city) [Shibuya]:  <=入力せずEnter
Organization Name (eg, company) [YouareYou inc.]:  <=入力せずEnter
Organizational Unit Name (eg, section) []:  <=入力せずEnter
Common Name (e.g. server FQDN or YOUR name) []:web1.local.com <=サーバーのドメイン名(私の場合はDockerのネットワークでのコンテナ名のエイリアス)
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
```

作ったcacert.csrを何らかの方法でオレオレ認証局にコピーします。

## オレオレ認証局での操作

コピーしてきた証明書発行要求に、オレオレ認証局の署名鍵を使って署名します。
他のサーバーの証明書署名発行要求に署名するため、さっきとはコマンドのオプションが違います。

```console
# openssl x509 -req -in ./cert/servercert.csr -CA ./cert/servercert.pem -CAkey ./private/privkey.pem -C
```

中身を確認してみましょう。

```console
# openssl x509 -text -noout -in ./cert/web1.local.com.cert.pem
Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number:
            22:91:d6:82:1e:3d:6c:af:eb:06:2a:38:c4:c3:da:f8:05:e0:6c:51
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = JP, ST = Tokyo, L = Shinjuku, O = iamMe inc., CN = MY OWN CERTIFICATE AUTHORITY
        Validity
            Not Before: Feb 22 15:26:07 2021 GMT
            Not After : May 28 15:26:07 2023 GMT
        Subject: C = JP, ST = Tokyo, L = Shibuya, O = YouareYou inc., CN = web1.local.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (4096 bit)
                Modulus:
                    00:b9:05:f9:25:82:73:8f:c4:1f:43:45:c1:d3:59:
                    (中略)
                    0e:10:a1
                Exponent: 65537 (0x10001)
    Signature Algorithm: sha256WithRSAEncryption
         05:3a:eb:c5:10:7b:1b:6b:47:f1:ae:30:be:95:b7:1a:a0:38:
         (中略)
         d8:7a:d3:8a:2a:60:f0:99
```

今回はIssuerとSubjectが異なります。

検証者はこのサーバー証明書の検証鍵を使って署名を検証します。
検証に成功するような証明書を発行できるのは認証局の署名鍵を知っている者(認証局自身)だけなので、このサーバー証明書を発行したのは認証局だということを検証者は確認できます。
また、認証局は証明書を発行する前に証明対象(この場合YouareYou inc.やweb1.local.com)についてきちんと調べる(はず)ので、証明対象が本物であることも検証者は確認できます。