Ruby の openssl ライブラリを使って、サーバ証明書やクライアント証明書を作成する


# はじめに
Ruby には OpenSSL を扱うためのライブラリ (openssl ライブラリ) が標準添付されていますが、[リファレンスマニュアル](http://docs.ruby-lang.org/ja/2.2.0/library/openssl.html)だけでは使い方が少しわかりづらいので、標準的な使い方をまとめてみました。

以下のような人には役に立つかと思います。

- シェルスクリプトよりも Ruby のスクリプトをよく使う人
- SSL サーバ証明書の作成をよく行うが、それを自動化したい人
- 証明書を操作する Ruby 製のアプリケーションを作成したい人
- Rails で作った Web サービスにクライアント証明書による認証機能を追加したい人


# プライベート CA (認証局) 構築
*CA の役割については、[こちら](https://www.google.co.jp/webhp?hl=ja#safe=off&hl=ja&q=認証局&spell=1)などをご覧ください*

- `./ca_private_key.pem` に CA の秘密鍵を、`./ca.pem` に CA 証明書を作成します
- CA の名前は "Example CA" にします
- CA の秘密鍵ファイルには "ca_passphrase" というパスフレーズを設定します
- CA 証明書の有効期限は 1 年間に設定します

```ruby:irb
require 'openssl'
OpenSSL::Random.seed File.read('/dev/random', 16)
digest = OpenSSL::Digest::SHA1.new

ca_passphrase = 'ca_passphrase' # CA の秘密鍵に設定するパスフレーズ (適切に設定してください)

# CA の情報を設定 (適切に設定してください)
issu = OpenSSL::X509::Name.new
issu.add_entry 'C' , 'JP'
issu.add_entry 'ST', 'Tokyo'
issu.add_entry 'DC', 'Minato-ku'
issu.add_entry 'O' , 'Example, Inc.'
issu.add_entry 'CN', 'Example CA'

# CA の秘密鍵/公開鍵を生成
issu_rsa = OpenSSL::PKey::RSA.generate 2048

# CA の秘密鍵を書き出し
File.open 'ca_private_key.pem', 'wb' do |f|
  f.write issu_rsa.export(OpenSSL::Cipher::Cipher.new('aes256'), ca_passphrase)
end

# CA 証明書を作成
issu_cer = OpenSSL::X509::Certificate.new
issu_cer.not_before = Time.now
issu_cer.not_after  = Time.now + 1*365*24*60*60 # 有効期限を 1 年後に設定
issu_cer.public_key = issu_rsa.public_key
issu_cer.serial  = 1
issu_cer.issuer  = issu
issu_cer.subject = issu
ex = OpenSSL::X509::Extension.new 'basicConstraints', OpenSSL::ASN1.Sequence([OpenSSL::ASN1::Boolean(true)])
issu_cer.add_extension ex
issu_cer.sign issu_rsa, digest
File.open 'ca.pem', 'wb' do |f|
  f.write issu_cer.to_pem
end
```


# サーバ証明書を作る
*サーバ証明書の役割については、[こちら](https://www.google.co.jp/webhp?hl=ja#safe=off&hl=ja&q=サーバ証明書&spell=1)などをご覧ください*

- `secure.example.com` 用のサーバ証明書を `./secure.example.com.crt` に作成します
- サーバの秘密鍵を `./secure.example.com.crt.key` に作成します
- 証明書の有効期限は 1 年間に設定します

```ruby:irb
require 'openssl'
OpenSSL::Random.seed File.read('/dev/random', 16)
digest = OpenSSL::Digest::SHA1.new

ca_passphrase     = 'ca_passphrase'
domain = 'secure.example.com'

# CA 秘密鍵の再読み込み
issu_rsa = OpenSSL::PKey::RSA.new File.read('ca_private_key.pem'), ca_passphrase

# CA 証明書の再読み込み
issu_cer = OpenSSL::X509::Certificate.new File.read('ca.pem')
issu     = issu_cer.issuer

# サーバの情報を設定 (適切に設定してください)
sub = OpenSSL::X509::Name.new
sub.add_entry 'C' , 'JP'
sub.add_entry 'ST', 'Tokyo'
sub.add_entry 'DC', 'Minato-ku'
sub.add_entry 'O' , 'Example, Inc.'
sub.add_entry 'CN', domain

# サーバの秘密鍵/公開鍵を生成
sub_rsa = OpenSSL::PKey::RSA.generate 2048

# サーバの秘密鍵を書き出し
File.open "#{domain}.key", 'wb' do |f|
  f.write sub_rsa.export
end

# サーバ証明書を作成
sub_cer = OpenSSL::X509::Certificate.new
sub_cer.not_before = Time.now
sub_cer.not_after  = Time.now + 1*365*24*60*60 # 有効期限を 1 年後に設定
sub_cer.public_key = sub_rsa.public_key
sub_cer.serial  = 2
sub_cer.issuer  = issu
sub_cer.subject = sub
sub_cer.sign issu_rsa, digest
ex = OpenSSL::X509::Extension.new 'basicConstraints', OpenSSL::ASN1.Sequence([OpenSSL::ASN1::Boolean(false)])
sub_cer.add_extension ex
ex = OpenSSL::X509::Extension.new 'nsCertType', 'server'
sub_cer.add_extension ex
sub_cer.sign issu_rsa, digest
File.open "#{domain}.crt", 'wb' do |f|
  f.write sub_cer.to_pem
end

```

# クライアント証明書を作る
*クライアント証明書の役割については、[こちら](https://www.google.co.jp/webhp?hl=ja#safe=off&hl=ja&q=クライアント証明書&spell=1)などをご覧ください*

- クライアント証明書を `./client.pfx` に作成します
- 証明書の有効期限は 1 年間に設定します

```ruby:irb
require 'openssl'
OpenSSL::Random.seed File.read('/dev/random', 16)
digest = OpenSSL::Digest::SHA1.new

ca_passphrase     = 'ca_passphrase'
client_passphrase = 'client_passphrase' # クライアント証明書に設定するパスワード (適切に設定してください)

# CA 秘密鍵の再読み込み
issu_rsa = OpenSSL::PKey::RSA.new File.read('ca_private_key.pem'), ca_passphrase

# CA 証明書の再読み込み
issu_cer = OpenSSL::X509::Certificate.new File.read('ca.pem')
issu     = issu_cer.issuer

# クライアントの情報を設定 (適切に設定してください)
sub = OpenSSL::X509::Name.new
sub.add_entry 'C' , 'JP'
sub.add_entry 'ST', 'Tokyo'
sub.add_entry 'DC', 'Minato-ku'
sub.add_entry 'O' , 'Example, Inc.'
sub.add_entry 'CN', 'Example Taro'

# クライアント用の秘密鍵/公開鍵を生成
sub_rsa = OpenSSL::PKey::RSA.generate 2048

# クライアント証明書を作成
sub_cer = OpenSSL::X509::Certificate.new
sub_cer.not_before = Time.now
sub_cer.not_after  = Time.now + 1*365*24*60*60 # 有効期限を 1 年後に設定
sub_cer.public_key = sub_rsa.public_key
sub_cer.serial  = 3
sub_cer.issuer  = issu
sub_cer.subject = sub
sub_cer.sign issu_rsa, digest
File.open 'client.pem', 'wb' do |f|
  f.write sub_cer.to_pem
end

# クライアント証明書を PKCS#12 形式で作成
sub_pkcs = OpenSSL::PKCS12.create client_passphrase, 'secure.example.com', sub_rsa, sub_cer, [issu_cer]
File.open 'client.pfx', 'wb' do |f|
  f.write sub_pkcs.to_der
end
```

# CRL (証明書失効リスト) を作る
*CRL の役割については、[こちら](https://www.google.co.jp/webhp?hl=ja#safe=off&hl=ja&q=失効リスト&spell=1)などをご覧ください*

- `./crl.pem` に CRL を作成します

```ruby:irb
require 'openssl'
digest = OpenSSL::Digest::SHA1.new

ca_passphrase = 'ca_passphrase'

# CA 秘密鍵の再読み込み
issu_rsa = OpenSSL::PKey::RSA.new File.read('ca_private_key.pem'), ca_passphrase

# CA 証明書の再読み込み
issu_cer = OpenSSL::X509::Certificate.new File.read('ca.pem')
issu     = issu_cer.issuer

# CRL を作成する
crl = OpenSSL::X509::CRL.new
crl.issuer      = issu
crl.last_update = Time.now
crl.next_update = Time.now + 1*24*60*60 # 次回アップデート予定日時を 1 日後に設定
crl.sign issu_rsa, digest
File.open 'crl.pem', 'wb' do |f|
  f.write crl.to_pem
end
```

## CRL に追加する (クライアント証明書を無効にする)
- 「クライアント証明書を作る」で作成したクライアント証明書を CRL に追加します

```ruby:irb
require 'openssl'
digest = OpenSSL::Digest::SHA1.new

ca_passphrase     = 'ca_passphrase'

# CA 秘密鍵の再読み込み
issu_rsa = OpenSSL::PKey::RSA.new File.read('ca_private_key.pem'), ca_passphrase

# CRL の再読み込み
crl = OpenSSL::X509::CRL.new File.read('crl.pem')

# クライアント証明書の再読み込み
sub_cer = OpenSSL::X509::Certificate.new File.read('client.pem')

# CRL の更新
revoked = OpenSSL::X509::Revoked.new
revoked.serial = sub_cer.serial
revoked.time = Time.now
crl.add_revoked revoked
crl.last_update = Time.now
crl.sign issu_rsa, digest
File.open 'crl.pem', 'wb' do |f|
  f.write crl.to_pem
end
```

## CRL から取り除く (クライアント証明書を有効にする)
- 「クライアント証明書を作る」で作成したクライアント証明書を CRL から取り除きます

```ruby:irb
require 'openssl'
digest = OpenSSL::Digest::SHA1.new

ca_passphrase     = 'ca_passphrase'

# CA 秘密鍵の再読み込み
issu_rsa = OpenSSL::PKey::RSA.new File.read('ca_private_key.pem'), ca_passphrase

# CRL の再読み込み
crl = OpenSSL::X509::CRL.new File.read('crl.pem')

# クライアント証明書の再読み込み
sub_cer = OpenSSL::X509::Certificate.new File.read('client.pem')

# CRL の更新
sub_cer_serial = sub_cer.serial.to_i
crl.revoked = crl.revoked.reject {|_revoked| _revoked.serial.to_i == sub_cer_serial}
crl.last_update = Time.now
crl.sign issu_rsa, digest
File.open 'crl.pem', 'wb' do |f|
  f.write crl.to_pem
end
```

# Apache に設定する
*CentOS の場合を例にしています。その他のディストリビューションをお使いの場合は、パスが異なる場合があるのでご注意ください。*

## ファイルを整理する

```bash:cmd
# mv ca.pem /etc/pki/CA/
# mv secure.example.com.crt /etc/pki/CA/certs/
# mv secure.example.com.key /etc/pki/CA/private/
# mv crl.pem /etc/pki/CA/
```

## 設定ファイルを編集する
設定ファイルに以下を追記してください

```:/etc/httpd/conf.d/ssl.conf
# サーバ証明書の設定
SSLCertificateFile /etc/pki/CA/certs/secure.example.com.crt # サーバ証明書
SSLCertificateKeyFile /etc/pki/CA/private/secure.example.com.key　# サーバ秘密鍵

# クライアント証明書の設定
SSLCACertificateFile /etc/pki/CA/cacert.pem　# CA 証明書
SSLVerifyClient require # クライアント証明書を要求する設定
SSLVerifyDepth  1

# CRL の設定
SSLCARevocationFile /etc/pki/CA/crl.pem
```

*注: プライベート CA により署名されたサーバ証明書なのでブラウザ側でプライベート CA の証明書を信頼する設定を行う必要があります。*

## Apache を再起動する

```bash:cmd
# service httpd restart
```
*注: CRL の更新をした場合も Apache を再起動する必要があります*

# クライアント証明書のインストール
*クライアント証明書のインストールはブラウザ毎に行う必要があります*

## Mac で Google Chrome を使う場合
1. 「クライアント証明書を作る」で作成した `client.pfx` をダブルクリックする
2. パスワードの入力を求められるので "client_passphrase" と入力し、キーチェーンアクセスに登録する
2. Web サーバにアクセスすると証明書の選択を求められるので、インストールした証明書を選択する

## その他の OS、ブラウザを使う場合
[サイボウズさんのサイト](https://manual.cybozu.co.jp/tech/webbrowser/certificate.html)によくまとまっているので、こちらを参考に設定を行ってください

# 終わりに
私自身は何かを自動処理させる場合、シェルスクリプトよりも Ruby のスクリプトで書くことが多いので、今回 Ruby の openssl のライブラリの使い方がわかって非常に楽になりました。"シェルスクリプトより Ruby！"という方は是非ご活用ください!!