社内ネットワークや開発環境で複数のサーバー証明書を使う場合、サーバーごとに自己署名の証明書を作ると、クライアントに1つずつ信頼設定が必要になる。自分用の認証局(CA)を1つ作り、CAの証明書だけをクライアントに信頼させれば、CAが署名したサーバー証明書をまとめて信頼できる。

流れは次のとおり。

graph LR
  A[CAの秘密鍵と証明書を作る] --> B[サーバーの秘密鍵とCSRを作る]
  B --> C[CAの秘密鍵でCSRに署名する]
  C --> D[クライアントにCA証明書を信頼させる]

実行例はUbuntu 24.04のOpenSSL 3.0.13のもの。

CAの秘密鍵と証明書を作る

CAの証明書は、自己署名の証明書として作る。basicConstraintsをCA:TRUE、keyUsageをkeyCertSignにする。

$ openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc \
    -keyout ca.key -out ca.crt -days 3650 \
    -subj "/O=Example Inc./CN=Example Private CA" \
    -addext "basicConstraints=critical,CA:TRUE" \
    -addext "keyUsage=critical,keyCertSign,cRLSign"
$ openssl x509 -in ca.crt -noout -subject -issuer -ext basicConstraints,keyUsage
subject=O = Example Inc., CN = Example Private CA
issuer=O = Example Inc., CN = Example Private CA
X509v3 Basic Constraints: critical
    CA:TRUE
X509v3 Key Usage: critical
    Certificate Sign, CRL Sign

ca.keyはCAの秘密鍵である。漏れると、誰でもCAとして証明書を発行できる。パーミッションを600にして、CAの用途以外では使わない。

自己署名の証明書の作り方は別の記事で説明している。

参考: 【openssl】自己署名の証明書を作成する (req -x509)

サーバーの秘密鍵とCSRを作る

サーバー側では、秘密鍵とCSRを作る。SANはCSRに含めておく。

$ openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc \
    -keyout server.key -out server.csr \
    -subj "/CN=server.example.test" \
    -addext "subjectAltName=DNS:server.example.test,DNS:localhost,IP:127.0.0.1"

参考: 【openssl】CSRを作成する (req -new)

CSRに署名する

CAの証明書と秘密鍵を指定して、openssl x509 -reqでCSRに署名する。

$ openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
    -out server.crt -days 365 -extfile ext.cnf

ext.cnfは、証明書に付ける拡張を書いた設定ファイルである。

subjectAltName = DNS:server.example.test,DNS:localhost,IP:127.0.0.1
basicConstraints = CA:FALSE
keyUsage = digitalSignature
extendedKeyUsage = serverAuth

指定したオプションは次のとおり。

オプション説明
-in署名するCSR
-CACAの証明書
-CAkeyCAの秘密鍵
-CAcreateserialシリアル番号のファイル(ca.srl)を作る
-days発行する証明書の有効日数
-extfile証明書に付ける拡張を書いたファイル

作成した証明書の内容を確認する。発行者(issuer)がCAになっている。

$ openssl x509 -in server.crt -noout -subject -issuer -dates -ext subjectAltName,basicConstraints,extendedKeyUsage
subject=CN = server.example.test
issuer=O = Example Inc., CN = Example Private CA
notBefore=Sep 30 14:56:46 2026 GMT
notAfter=Sep 30 14:56:46 2027 GMT
X509v3 Subject Alternative Name:
    DNS:server.example.test, DNS:localhost, IP Address:127.0.0.1
X509v3 Basic Constraints:
    CA:FALSE
X509v3 Extended Key Usage:
    TLS Web Server Authentication

拡張を指定しないとSANが消える

-extfileを省略すると、CSRに含めたSANも含めて、拡張が1つも付かない証明書が作られる。

$ openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out s0.crt -days 365
$ openssl x509 -in s0.crt -noout -ext subjectAltName
No extensions in certificate

CSRのSANを引き継ぐには、2つの方法がある。

1つ目は、-extfileにSANを書き直す方法である。前述の手順と同じである。

2つ目は、-copy_extensions copyを付ける方法である。CSRの拡張が証明書にコピーされる。

$ openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
    -out s2.crt -days 365 -copy_extensions copy
$ openssl x509 -in s2.crt -noout -ext subjectAltName
X509v3 Subject Alternative Name:
    DNS:server.example.test, DNS:localhost, IP Address:127.0.0.1

-copy_extensions copyは、CSRに含まれる拡張をすべてコピーする。CSRの提出者がbasicConstraints=CA:TRUEなどを含めていた場合、意図しない権限を持つ証明書を発行してしまう。他人から受け取ったCSRに署名する場合は、-extfileで必要な拡張だけを指定する。

シリアル番号

証明書にはCAごとに一意のシリアル番号が必要である。-CAcreateserialを付けると、CAの証明書と同じ場所にca.srlが作られ、署名のたびに値が増える。

$ cat ca.srl
6E6E0D8E9E6290E61397AE971D88023484E934F3

2回目以降の署名では、-CAcreateserialの代わりに-CAserial ca.srlを指定してもよい。-CAcreateserialを付けたままでも、既存のca.srlを使って値が続く。

署名した証明書を検証する

CAの証明書を渡して、openssl verifyで証明書を検証する。

$ openssl verify -CAfile ca.crt server.crt
server.crt: OK

CAの証明書を渡さないと、検証に失敗する。

$ openssl verify server.crt
error 20 at 0 depth lookup: unable to get local issuer certificate
error server.crt: verification failed

用途やホスト名も合わせて検証できる。ホスト名がSANに含まれていない場合は失敗する。

$ openssl verify -CAfile ca.crt -purpose sslserver -verify_hostname server.example.test server.crt
server.crt: OK
$ openssl verify -CAfile ca.crt -verify_hostname wrong.example server.crt
error 62 at 0 depth lookup: hostname mismatch
error server.crt: verification failed

サーバーに設定して接続を確認する

サーバーには、サーバー証明書とサーバーの秘密鍵を設定する。CA証明書は、サーバーには設定せず、クライアントに信頼させる。

openssl s_serverで起動して、openssl s_clientで接続を確認できる。

$ openssl s_server -accept 4433 -cert server.crt -key server.key -www
$ echo | openssl s_client -connect localhost:4433 -CAfile ca.crt -verify_hostname localhost 2>&1 | grep -E 'Verif'
Verification: OK
Verify return code: 0 (ok)

CAの証明書を渡さない場合は、検証に失敗する。

$ echo | openssl s_client -connect localhost:4433 2>&1 | grep 'Verify return'
Verify return code: 21 (unable to verify the first certificate)

参考: 【openssl】指定したURLの証明書をコマンドで確認する

クライアントにCAを信頼させる

OSの信頼済みCAにca.crtを追加すると、そのOS上のほとんどのクライアントがCAを信頼する。Ubuntuでは、/usr/local/share/ca-certificates/に.crt拡張子で置いて、update-ca-certificatesを実行する。

$ sudo cp ca.crt /usr/local/share/ca-certificates/example-ca.crt
$ sudo update-ca-certificates
$ openssl verify server.crt
server.crt: OK

-CAfileを指定しなくても、検証に成功する。

OSへ追加せずに、個別のコマンドにだけ渡す方法もある。opensslは-CAfile、curlは--cacertで指定する。OSの信頼済みCAを変更する影響は大きいため、検証用途では個別に渡す方法を優先する。

サーバー証明書とCA証明書を1つのファイルにまとめる場合は、サーバー証明書、CA証明書の順にcatで連結する。

$ cat server.crt ca.crt > chain.pem

バージョンによる違い

OpenSSL 3.5でも、ここまでの例は同じように動く。-extfileを省略すると、拡張が付かない挙動も同じである。

macOS標準の/usr/bin/openssl(LibreSSL 3.3.6)では、以下が異なる。

  • -copy_extensionsに対応していない
  • -extfileを省略すると、拡張のないバージョン1の証明書が作られる

-extfileでSANを指定する方法は、LibreSSL 3.3.6でも動く。

参考

openssl-x509 - OpenSSL Documentation