🔐

localhost を https 化する ~独自CA証明書を作って~

2021/07/25に公開

ローカルなマシンでサーバーアプリを立ちあげて、同じマシンからアクセスしてテストしたりする際に、自己署名サーバー証明書、いわゆるオレオレ証明書を使うことがありますが、ブラウザとかgitとかcurlとかとかが、証明書検証をオフにしても、ワーニングを出したり、最悪の場合エラーを出して動作しなかったりします。昨今、サイバーセキュリティ対策の必要性が大きくなっていく中で、そういうケースが増えてきているようにも感じます。
セキュリティの観点から望ましいことではないのは承知の上で、あくまで個人が自分のためだけに使う前提で、そういうワーニングやエラーが出ないようにする方法を書きます。
繰り返しますが、この方法は、他人と共用するサーバーには使わないでください。

雑な要約

自己署名のサーバー証明書を作るのではなく、独自のCA証明書を作って、そのCA証明書でサーバー証明書を署名します。

手順の概観

ざっくりとした手順を、雑な図を交えて書きます。
(図は mermaid で描いたのですが配置を思うようにコントロールできなくて若干見づらいですすみません。)

  1. CA用の秘密鍵を生成し、その秘密鍵で公開鍵を自己署名して、CA証明書を作成する
  1. サーバー用の秘密鍵を生成し、1. で作ったCA証明書で公開鍵を署名して、サーバー証明書を作成する
  1. サーバーの秘密鍵と証明書をサーバーに、CA証明書をクライアントにインストールする
  1. クライアントからサーバーにアクセスすると、サーバーからクライアントへ証明書が提示され、クライアントではCA証明書を使ってサーバー証明書が検証される

手順の詳細

1. CA用の秘密鍵を生成し、公開鍵を秘密鍵で自己署名してCA証明書を作成する

  1. CA用の秘密鍵を生成
$ openssl genrsa -out localCA.key 2048
Generating RSA private key, 2048 bit long modulus (2 primes)
...............+++++
..............................+++++
e is 65537 (0x010001)
  1. 秘密鍵と識別情報とから CSR (certificate signing request 証明書署名要求) を生成
    識別情報は以下の : の右側のように適切な値を対話式に入力します。
$ openssl req -out localCA.csr -key localCA.key -new
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) [AU]:jp
State or Province Name (full name) [Some-State]:Tokyo  
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:John Doe
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:John Doe
Email Address []:john-doe@example.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

できあがったCSRの中身はこんなんです。
識別情報 (Subject) と秘密鍵から生成された公開鍵が入っています。

$ openssl req -text -noout -in localCA.csr
Certificate Request:
    Data:
        Version: 1 (0x0)
        Subject: C = jp, ST = Tokyo, O = John Doe, CN = John Doe, emailAddress = john-doe@example.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:b1:ea:90:a6:e9:64:d6:d2:c0:67:57:9b:bd:0c:
		    :snip
                    55:63:d9:c0:d3:60:21:44:dc:21:ef:6b:59:91:41:
                    4e:d1
                Exponent: 65537 (0x10001)
        Attributes:
            a0:00
    Signature Algorithm: sha256WithRSAEncryption
         9c:e5:6a:02:b7:6b:df:23:ed:64:06:15:18:d2:57:d7:99:d3:
	 :snip
         e8:99:69:f4:49:32:ae:a7:1c:b3:a8:b3:df:2b:72:28:bd:04:
         56:eb:41:81
  1. 公開鍵が入ったCSRを秘密鍵で自己署名してCA証明書を作成
    days は証明書の有効期間で日数で指定します。
$ openssl x509 -req -days 3650 -signkey localCA.key -in localCA.csr -out localCA.crt
Signature ok
subject=C = jp, ST = Tokyo, O = John Doe, CN = John Doe, emailAddress = john-doe@example.com
Getting Private key

できあがったCA証明書の中身はこんなんです。
Validity (有効期間) と Issuer が入っています。Issuer と Subject とが同じなので自己署名なのがわかります。

$ openssl x509 -text -noout -in localCA.crt
Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number:
            2d:e8:64:7a:50:23:62:58:90:9a:25:dc:97:05:9e:1a:1f:46:4f:76
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = jp, ST = Tokyo, O = John Doe, CN = John Doe, emailAddress = john-doe@example.com
        Validity
            Not Before: Jul 23 04:17:48 2021 GMT
            Not After : Jul 21 04:17:48 2031 GMT
        Subject: C = jp, ST = Tokyo, O = John Doe, CN = John Doe, emailAddress = john-doe@example.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:b1:ea:90:a6:e9:64:d6:d2:c0:67:57:9b:bd:0c:
		    :snip
                    55:63:d9:c0:d3:60:21:44:dc:21:ef:6b:59:91:41:
                    4e:d1
                Exponent: 65537 (0x10001)
    Signature Algorithm: sha256WithRSAEncryption
         85:5b:a7:b0:2d:4f:47:13:db:9f:6d:9d:40:b1:6b:fa:e9:76:
	 :snip
         01:ce:05:2d:f0:45:d6:7b:17:ba:b3:77:96:ac:56:1e:6b:f7:
         c8:e9:ee:48

2. サーバー用の秘密鍵を生成し、公開鍵を 1. で作ったCA証明書で署名してサーバー証明書を作成する

  1. サーバー用の秘密鍵を生成
$ openssl genrsa -out localhost.key 2048
Generating RSA private key, 2048 bit long modulus (2 primes)
...+++++
..................................................................................+++++
e is 65537 (0x010001)
  1. 秘密鍵と識別情報とから CSR (certificate signing request 証明書署名要求) を生成
    Common Name にはサーバーの FQDN (ここでは localhost) を入力します。
$ openssl req -out localhost.csr -key localhost.key -new
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) [AU]:jp
State or Province Name (full name) [Some-State]:Tokyo
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:John Doe
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:localhost
Email Address []:john-doe@example.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

できあがったCSRの中身はこんなんです。
CN (Common Name) がさっき入力した FQDN (ここでは localhost) になっています。

$ openssl req -text -noout -in localhost.csr
Certificate Request:
    Data:
        Version: 1 (0x0)
        Subject: C = jp, ST = Tokyo, O = John Doe, CN = localhost, emailAddress = john-doe@example.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:a0:e5:5f:04:a6:a2:fc:c1:de:37:ec:9b:76:b1:
		    :snip
                    2d:28:95:da:3e:af:ee:af:6a:f2:99:72:2d:a4:af:
                    d8:39
                Exponent: 65537 (0x10001)
        Attributes:
            a0:00
    Signature Algorithm: sha256WithRSAEncryption
         11:4d:90:6e:b4:89:46:f0:52:b9:3c:46:89:f2:0c:d9:0b:ad:
         :snip 
         c5:62:13:87:70:5e:68:b5:ef:3f:15:d4:0b:07:b8:06:64:6e:
         c2:e9:c7:64
  1. 証明書に付加する SAN (Subject Alternative Name サブジェクト代替名) を入れたテキストファイルを作る
    CN 以外のアドレスでも、SAN に指定してあれば証明書が有効になります。IPアドレスの他、コンテナで実行する場合は compose の services: 名なども指定しておくと便利です。
    CN と同じ localhost まで SAN に指定しているのは以下が理由です。
    Chromeがコモンネームの設定を非推奨化
$ echo 'subjectAltName = DNS:localhost, DNS:localhost.localdomain, IP:127.0.0.1, DNS:app, DNS:app.localdomain' > localhost.csx
  1. CSRにSAN情報も付加し、CA秘密鍵とCA証明書とで署名してサーバー証明書を作成
    さっきの自己署名とはオプションがだいぶ違います。
$ openssl x509 -req -days 1825 -CA localCA.crt -CAkey localCA.key -CAcreateserial -in localhost.csr -extfile localhost.csx -out localhost.crt
Signature ok
subject=C = jp, ST = Tokyo, O = John Doe, CN = localhost, emailAddress = john-doe@example.com
Getting CA Private Key

できあがったサーバー証明書の中身はこんなんです。
Issuer には CA証明書の Subject が入っています。
Subject Alternative Name も入っています。

$ openssl x509 -text -noout -in localhost.crt
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            5f:90:f8:bf:5f:cb:e2:98:d6:f1:9a:24:af:de:15:02:aa:cc:56:9f
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = jp, ST = Tokyo, O = John Doe, CN = John Doe, emailAddress = john-doe@example.com
        Validity
            Not Before: Jul 23 05:29:48 2021 GMT
            Not After : Jul 22 05:29:48 2026 GMT
        Subject: C = jp, ST = Tokyo, O = John Doe, CN = localhost, emailAddress = john-doe@example.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:a0:e5:5f:04:a6:a2:fc:c1:de:37:ec:9b:76:b1:
		    :snip
                    2d:28:95:da:3e:af:ee:af:6a:f2:99:72:2d:a4:af:
                    d8:39
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Alternative Name:
                DNS:localhost, DNS:localhost.localdomain, IP Address:127.0.0.1, DNS:app, DNS:app.localdomain
    Signature Algorithm: sha256WithRSAEncryption
         9d:e0:c0:12:33:0e:f6:58:ae:84:45:c6:64:0b:09:a0:4e:72:
	 :snip
         ac:e0:d3:a5:f0:34:cb:29:f2:8a:00:04:80:82:bd:53:c7:5b:
         88:bc:22:3b

3. サーバーの秘密鍵と証明書をサーバーに、CA証明書をクライアントにインストールする

環境によってまちまちなので、ものすごく雑に書きます。

  1. サーバーに秘密鍵とサーバー証明書とをインストール
    例えば Nginx だとこんな感じですね。
server {
    listen              443 ssl;
    server_name         localhost;

    ssl_certificate     /etc/nginx/ssl/localhost.crt;
    ssl_certificate_key /etc/nginx/ssl/localhost.key;
    ...
}
  1. CA証明書をクライアントにインストールする
    例えば、curl なら --cacert コマンドラインオプションで、git ならば git config http.sslCAInfo で、指定すればよいです。
    システムに入れてしまうなら、
    • Ubuntu の場合: /usr/share/ca-certificates/ の下に適当なディレクトリを掘って[1] /usr/local/share/ca-certificates/ に入れて、sudo update-ca-certificates を実行。
    • CentOS の場合: /usr/share/pki/ca-trust-source/anchors/ に入れて sudo update-ca-trust を実行。
    • Windows の場合: certlm.msc を起動して「信頼されたルート証明機関」へインポート。

4. クライアントからサーバーにアクセスする

openssl s_client で試してみましょう。
(CA証明書をシステムに入れていない場合は -CAfile /path/to/localCA.crt も付けて。)
こんな感じに返ってきたら成功です。

$ openssl s_client -connect localhost:443 --servername localhost
CONNECTED(00000003)
depth=1 C = jp, ST = Tokyo, O = John Doe, CN = John Doe, emailAddress = john-doe@example.com
verify return:1
depth=0 C = jp, ST = Tokyo, O = John Doe, CN = localhost, emailAddress = john-doe@example.com
verify return:1
---
Certificate chain
 0 s:C = jp, ST = Tokyo, O = John Doe, CN = localhost, emailAddress = john-doe@example.com
   i:C = jp, ST = Tokyo, O = John Doe, CN = John Doe, emailAddress = john-doe@example.com
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIDqjCCApKgAwIBAgIUX5D4v1/L4pjW8Zokr94VAqrMVp8wDQYJKoZIhvcNAQEL
:snip
Pi8g4Z6g3jR8175SfzCJpWudTmOMHuBJgb1O0vwOrpdQO0oaq3IcPBnMVUmcIpxI
zXKynuOuDsis4NOl8DTLKfKKAASAgr1Tx1uIvCI7
-----END CERTIFICATE-----
subject=C = jp, ST = Tokyo, O = John Doe, CN = localhost, emailAddress = john-doe@example.com

issuer=C = jp, ST = Tokyo, O = John Doe, CN = John Doe, emailAddress = john-doe@example.com
:snip

雑なまとめ

作るたびに毎回忘れてて調べ直してたんですが、こうやって書くと覚えますねー。(なんだこのまとめは。)

脚注
  1. 20.04あたりで変わった?っぽくて反映されなかった。要確認 ↩︎

Discussion