OpenSSL で認証局 (CA) を構築してサーバ証明書を発行し、Node (+Express) をウェブサーバーにして HTTPS 通信を行う手順 (macOS)

ここでは OpenSSL を利用して公開鍵証明書認証局 (CA, Certificate Authority) を構築する手順について説明します。

そしてさらに、HTTPS 通信用にサーバ証明書を作成して、さらに Node.js + Express をウェブサーバーにして HTTPS 通信の動作確認を行います。

環境は macOS を使います。

Windows をお使いの場合は「OpenSSL で認証局 (CA) を構築する手順 (Windows)」をみてください。

ちなみに、私がこの記事を書いた経緯として、Windows の記事を先に書いたので、一般的な公開鍵の仕組みの説明などは、Windows 環境向けの記事の方に少し多めに書いてます。

1. OpenSSL の確認

macOS では openssl は標準でインストールされており、直ちに利用可能です。

私の環境 macOS 10.15.1 ではバージョンを確認すると次のようになりました。

% openssl version
LibreSSL 2.8.3

2. CA 用のディレクトリの作成

ここでは ~/ca というフォルダ以下に環境を構築するものとします。 もし同じ名前のフォルダが既にあって使われていたら、適当にパスを読み替えてください。

~/ca 内に、次の内容を setup という名前で作成してください。

mkdir -p root/certs
mkdir -p root/crl
mkdir -p root/newcerts
mkdir -p root/private
mkdir -p root/csr
touch root/index.txt
touch root/crlnumber
echo 1000 > root/serial
mkdir -p intermediate/certs
mkdir -p intermediate/crl
mkdir -p intermediate/newcerts
mkdir -p intermediate/private
mkdir -p intermediate/csr
touch intermediate/index.txt
touch intermediate/crlnumber
echo 1000 > intermediate/serial

コマンドプロンプトを開き ~/ca に移動して、setup を実行してください。

% cd ~/ca
% chmod u+x setup
% ./setup

3. ルート CA の作成

3-1. OpenSSL 設定ファイルの準備

~/ca/root 内に以下の内容を openssl.cfg という名前で作成してください。

先頭行の user1 の部分はあなたのユーザーアカウント名に書き換えてください。

USER = user1
HOME = /Users/$USER

[ ca ]
default_ca    = CA_default

[ CA_default ]
dir              = $HOME/ca/root
certs            = $dir/certs
crl_dir          = $dir/crl
database         = $dir/index.txt
new_certs_dir    = $dir/newcerts
serial           = $dir/serial
crlnumber        = $dir/crlnumber
crl              = $dir/crl.pem
certificate      = $dir/certs/ca.cert.pem
private_key      = $dir/private/ca.key.pem
name_opt         = ca_default
cert_opt         = ca_default
crl_extensions   = crl_ext
default_days     = 365
default_crl_days = 30
default_md       = sha256
preserve         = no
policy           = policy_match

[ policy_match ]
countryName             = match
stateOrProvinceName     = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ policy_anything ]
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ req ]
default_bits            = 2048
distinguished_name      = req_distinguished_name
x509_extensions         = v3_ca
string_mask             = utf8only
default_md              = sha256

[ req_distinguished_name ]
countryName                     = Country Name (2 letter code)
countryName_default             = US
countryName_min                 = 2
countryName_max                 = 2
stateOrProvinceName             = State or Province Name (full name)
stateOrProvinceName_default     = California
localityName                    = Locality Name (eg, city)
localityName_default            = Los Angeles
0.organizationName              = Organization Name (eg, company)
0.organizationName_default      = Ace Internet Inc.
organizationalUnitName          = Organizational Unit Name (eg, section)
organizationalUnitName_default  = http://ace.example.com/
commonName                      = Common Name (e.g. server FQDN or YOUR name)
commonName_default              = Ace Internet Root CA
commonName_max                  = 64
emailAddress                    = Email Address
emailAddress_default            = ace@example.com
emailAddress_max                = 64

[ server_cert ]
basicConstraints       = CA:FALSE
nsCertType             = server
nsComment              = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage               = critical, digitalSignature, keyEncipherment
extendedKeyUsage       = serverAuth

[ v3_ca ]
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints       = critical,CA:true
keyUsage               = critical, digitalSignature, cRLSign, keyCertSign

[ v3_intermediate_ca ]
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints       = critical,CA:true, pathlen:0
keyUsage               = critical, digitalSignature, cRLSign, keyCertSign

[ crl_ext ]
authorityKeyIdentifier = keyid:always

3-2. 秘密鍵とルートCA証明書の作成

コマンドプロンプトから、次のコマンドを実行します。

特に秘密鍵を生成するときなどはパスフレーズの入力を求められる時がありますが、この資料では全て password と入力することにします。 実際に利用する場合はあなたのセキュリティの要件に合わせて、適宜強固なパスフレーズを使ってください。ただし、秘密鍵にアクセスするたびに必要となるので、忘れないように注意してください。

% cd ~/ca
% openssl genrsa -aes256 -out root/private/ca.key.pem 4096
% openssl req -config root/openssl.cfg \
        -key root/private/ca.key.pem \
        -new -x509 -days 3000 -sha256 \
        -extensions v3_ca -out root/certs/ca.cert.pem

ここで作成した ca.key.pemca.key.pem がそれぞれ、 ルートCAの秘密鍵とルートCA証明書です。

認証局の名前などは上で作成した設定ファイルでデフォルト値を利用する場合は、 質問に単に Enter を押してください。

ルートCA証明書の内容は次のコマンドで表示できます。

% cd ~/ca
% openssl x509 -noout -text -in root/certs/ca.cert.pem

次に中間CAを作成します。

4. 中間CAの作成

4-1. OpenSSL 設定ファイルの準備

~/ca/intermediate 内に以下の内容を openssl.cfg という名前で作成してください。

先頭行の user1 の部分はあなたのユーザーアカウント名に書き換えてください。

USER = user1
HOME = /Users/$USER

[ ca ]
default_ca    = CA_default

[ CA_default ]
dir              = $HOME/ca/intermediate
certs            = $dir/certs
crl_dir          = $dir/crl
database         = $dir/index.txt
new_certs_dir    = $dir/newcerts
serial           = $dir/serial
crlnumber        = $dir/crlnumber
crl              = $dir/crl.pem
certificate      = $dir/certs/intermediate.cert.pem
private_key      = $dir/private/intermediate.key.pem
x509_extensions  = usr_cert
name_opt         = ca_default
cert_opt         = ca_default
crl_extensions   = crl_ext
default_days     = 365
default_crl_days = 30
default_md       = sha256
preserve         = no
policy           = policy_anything
copy_extensions  = copy

[ policy_anything ]
countryName            = optional
stateOrProvinceName    = optional
localityName           = optional
organizationName       = optional
organizationalUnitName = optional
commonName             = supplied
emailAddress           = optional

[ req ]
default_bits           = 2048
distinguished_name     = req_distinguished_name
x509_extensions        = v3_ca
string_mask            = utf8only
default_md             = sha256

[ req_distinguished_name ]
countryName                    = Country Name (2 letter code)
countryName_default            = US
countryName_min                = 2
countryName_max                = 2
stateOrProvinceName            = State or Province Name (full name)
stateOrProvinceName_default    = California
localityName                   = Locality Name (eg, city)
localityName_default           = Los Angeles
0.organizationName             = Organization Name (eg, company)
0.organizationName_default     = Ace Internet Inc.
organizationalUnitName         = Organizational Unit Name (eg, section)
organizationalUnitName_default = http://ace.example.com/
commonName                     = Common Name (e.g. server FQDN or YOUR name)
commonName_default             = Ace Internet Code Signing CA
commonName_max                 = 64
emailAddress                   = Email Address
emailAddress_default           = ace@example.com
emailAddress_max               = 64

[ server_cert ]
basicConstraints       = CA:FALSE
nsCertType             = server
nsComment              = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage               = critical, digitalSignature, keyEncipherment
extendedKeyUsage       = serverAuth

[ v3_ca ]
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints       = critical,CA:true
keyUsage               = critical, digitalSignature, cRLSign, keyCertSign

[ v3_intermediate_ca ]
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints       = critical,CA:true, pathlen:0
keyUsage               = critical, digitalSignature, cRLSign, keyCertSign

[ crl_ext ]
authorityKeyIdentifier = keyid:always

4-2. 中間CA証明書の発行

次のコマンドで中間CAの秘密鍵を生成し、CA証明書のCSRを作成して、ルートCAにて中間CAの証明書を発行します。

% cd ~/ca
% openssl genrsa -aes256 \
        -out intermediate/private/intermediate.key.pem 4096
% openssl req -config intermediate/openssl.cfg \
        -new -sha256 \
        -key intermediate/private/intermediate.key.pem \
        -out intermediate/csr/intermediate.csr.pem
% openssl ca -config root/openssl.cfg \
        -extensions v3_intermediate_ca \
        -days 1000 -notext -md sha256 \
        -in intermediate/csr/intermediate.csr.pem \
        -out intermediate/certs/intermediate.cert.pem

5. CA証明書をシステムに登録

5-1. CA 証明書のキーチェーンへの登録

ルートCA証明書は キーチェーンに登録することで、ブラウザが信頼されたCAであることを認識できるようになります。

Finder で上記で作成したルートCA証明書 ~/ca/root/certs/ca.cert.pem をダブルクリックしてください。 キーチェーンが起動して、証明書の登録場所を聞かれたら System を選択して、追加 (Add) します。

追加しただけではデフォルトでは信頼された状態になりません。

CA証明書をダブルクリックして、表示された証明書の設定画面で、常に信頼 (Always Trust) に設定します。

これでルートCA証明書が使用可能になります。

中間CA証明書 ~/ca/intermediate/intermediate.cert.pem もキーチェーンに追加してください。

尚、中間CA証明書は常に信頼するという設定をしなくても構いません。ルートCA証明書が信頼されていれば、中間CA証明書も信頼されます。

5-2. Windows 向けの PKCS #7 証明書の準備

CA 証明書はクライアント(ブラウザ側)のシステムに登録する必要があります。 Windows に登録するためには PEM 形式では登録できないので、 PKCS #7 (p7b) 形式の証明書を作成しておく必要があります。

念のため、次のコマンドで PKCS #7 形式の証明書を作成しておきましょう。

% cd ~/ca
% openssl crl2pkcs7 -nocrl \
        -certfile root/certs/ca.cert.pem \
        -out root/certs/ca.cert.p7b
% openssl crl2pkcs7 -nocrl \
        -certfile intermediate/certs/intermediate.cert.pem \
        -out intermediate/certs/intermediate.cert.p7b

Windows への CA 証明書の登録方法については 「OpenSSL で認証局 (CA) を構築する手順 (Windows)」をみてください。

以上でルートCAと中間CAを構築することができました。

6. サーバ証明書を作成

6-1. OpenSSL を用いた CSR の作成方法

さて、まずはウェブサーバー管理者として CSR (Certificate Signing Request) を作成しましょう。 次の内容を foo.example.com.cfg として作成して、~/ca に配置してください。

[ req ]
default_bits       = 2048
distinguished_name = req_distinguished_name
req_extensions     = req_ext

[ req_distinguished_name ]
countryName                 = Country Name (2 letter code)
countryName_default         = US
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = California
localityName                = Locality Name (eg, city)
localityName_default        = Torrance
organizationName            = Organization Name (eg, company)
organizationName_default    = Foo Company
commonName                  = Common Name (e.g. server FQDN or YOUR name)
commonName_default          = foo.example.com

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1   = foo.example.com
DNS.2   = bar.test.com
DNS.3   = localhost

ここで alt_names セクションの DNS 名 (DNS name) は、 あなたの環境に合わせて適当に書き換えてください。

この設定ファイルでは、foo.example.com, bar.test.com, localhost というウェブサーバーにアクセスする想定としています。

次のコマンドでウェブサーバーの秘密鍵と CSR を作成します。

% cd ~/ca
% openssl genrsa -aes256 \
        -out intermediate/private/foo.example.com.key.pem 2048
% openssl req -config foo.example.com.cfg \
        -key intermediate/private/foo.example.com.key.pem \
        -new -sha256 \
        -out intermediate/csr/foo.example.com.csr.pem

~/ca/intermediate/csr/foo.example.com.csr.pem が CSR です。

次のコマンドで CSR の内容を確認できます。

% cd ~/ca
% openssl req -noout -text \
        -in intermediate/csr/foo.example.com.csr.pem

出力内容の中で特に、 SAN (Subject Alternative Name) が正しく設定されているか確認してください。

...
 X509v3 Subject Alternative Name:
    DNS:foo.example.com, DNS:bar.test.com, DNS:localhost

6-2. OpenSSL を用いたサーバ証明書の発行

さて、上で作成した CSR を基に、次は認証局(CA)の作業として、サーバ証明書を作成します。

% cd ~/ca
% openssl ca -config intermediate/openssl.cfg \
        -extensions server_cert -days 365 -notext -md sha256 \
        -in intermediate/csr/foo.example.com.csr.pem \
        -out intermediate/certs/foo.example.com.cert.pem

この結果作成されたファイル ~/ca/intermediate/certs/foo.example.com.cert.pem がサーバ証明書になります。

キーファイル ~/ca/intermediate/private/foo.example.com.key.pem と 証明書ファイル ~/ca/intermediate/certs/foo.example.com.cert.pem を使うことで、 HTTPS通信を行うことができます。

以上で、サーバ証明書が発行できました。

7. Node + Express で HTTPS 通信を行う

7-1. Node.js + Express で HTTPS に応答するプログラムの作成

それでは簡単なプログラムを作成して、HTTPS 通信の確認をしましょう。

コマンドプロンプトを開き現在のディレクトリの下に、test というディレクトリを作成して、その中にコードを作成します。

% mkdir test
% cd test
% npm init
% npm install --save express

この test フォルダ内に証明書ファイルと秘密鍵ファイルの両方コピーしてください。

index.js を次の内容で作成します。

秘密鍵のパスフレーズはあなたが設定した値で書き換えてください。

const express = require('express');
const http = require('http');
const https = require('https');
const fs = require('fs');
const options = {
    cert: fs.readFileSync('foo.example.com.cert.pem'),
    key: fs.readFileSync('foo.example.com.key.pem'),
    passphrase: 'password',
};

const app = express();

app.get('/test1', (req, res) => {
    res.status(200).json({
        message: 'Hello'
    });
});

http.createServer(app).listen(3001, () => {
    console.log('HTTP listening on 3001...');
});
https.createServer(options, app).listen(3002, () => {
    console.log('HTTPS listening on 3002...');
});

このプログラムを実行します。

% node index.js

7-2. ブラウザからの動作確認

さて、これにブラウザ (あるいは Postman など) から HTTPS 要求を行います。

同じシステム内であれば https://localhost:3002/test1 で接続できるはずです。 成功すれば警告やエラーなしに { "message": "Hello" } という文字が返ります。

ブラウザで認識しているサーバ証明書を確認し、問題ないことを確認します。

証明書の詳細を見ると、ルートCA証明書、中間CA証明書、そしてサーバー証明書と順に信頼されていることが確認できます。

また、localhost というホスト名でのアクセスに対して、問題がないという点については、 証明書の Subject Alternative Name の DNS Name の一つとして localhost が登録されているためです。

7-3. トラブルシューティング

Chrome ブラウザから HTTPS 要求を送ったときに、セキュリティの警告が表示される場合があります。

NET::ERR_CERT_AUTHORITY_INVALID が発生したときには、CA 証明書が正しく登録されていないことが原因です。

キーチェーンに正しく登録してあり、またそれが信頼されていることを確認してください。

証明書が正しく登録されていても、NET::ERR_CERT_REVOKED エラーが発生する場合があります。

私が調べた限り、この原因はいくつかあります。ひとつはシステムの時計がずれているために、誤った時刻を認識しているために、 有効期限の確認に失敗している場合です。

もうひとつの原因は、macOS は証明書の有効期限として、あまりに長い有効期限を無効と見なすことが原因のようです。

私の場合、テスト証明書だし有効期限は長くて良いだろうと思って、9999 日にしていたら、 エラーが発生してしまいました。これを短く設定することで、問題が解消しました。

以上、ここでは macOS 向けに OpenSSL を用いてCAを構築して、HTTPS 通信向けのサーバー証明書を発行し、 HTTPS の動作確認を行いました。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Node.js 入門