エラー:OpenSSL::SSL::SSLError SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed

症状

ruby で任意のwebサーバーに対してHTTPS接続を行おうとするとエラーが発生した。
エラーの内容は次の通り。

OpenSSL::SSL::SSLError SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed

問題となった ruby のコードは以下のような感じになっていた。

  # ...
  https = Net::HTTP.new('example.com', 443)
  https.open_timeout = SYSTEM_TIMEOUT_SEC
  https.read_timeout = SYSTEM_TIMEOUT_SEC
  https.use_ssl = true
  https.verify_mode = OpenSSL::SSL::VERIFY_PEER
  https.verify_depth = 5
  https.start { |w|
  # ...

エラーの発生した実行環境は以下の様な感じ。

原因

OpenSSL::SSL::SSLError SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed

これは、Ruby(Net::HTTP?)が SSL証明書を見つけることができなくて、HTTPS 接続に失敗しているのが原因らしい。Net::HTTP が、SSL証明書を見つけられるようにしてあげれば良い。

解決策

この問題について情報がよくまとまっていたのは、下記のページだと思う。

ruby on rails - SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed - Stack Overflow

私の場合、上記で紹介されている方法ではうまくいかなかったので、
次のような方法を試してみたところ、問題が解決できた。
方法としては次の手順を踏んだ。

  1. 証明書をダウンロードし、実行したいRubyスクリプトと同じディレクトリに保存
  2. Rubyスクリプト内のNet::HTTP呼び出し部分で、ダウンロードした証明書を明示的に指定

証明書をダウンロードする

$ wget http://curl.haxx.se/ca/cacert.pem

Ruby スクリプトの Net::HTTP に対して、明示的に証明書の場所を教える

  #...
  https = Net::HTTP.new('example.com', 443)
  https.open_timeout = SYSTEM_TIMEOUT_SEC
  https.read_timeout = SYSTEM_TIMEOUT_SEC
  https.use_ssl = true
  https.verify_mode = OpenSSL::SSL::VERIFY_PEER
  https.verify_depth = 5
  https.ca_file = "./cacert.pem" # <= 追加
  https.start { |w|
  #...

【余談】問題が解決するまでの経緯

以下、問題解決に至るまでの経緯です。結構、迷ってしまいました。

まず問題が発生した時に「SSLv3 read server certificate B: certificate verify failed」あたりをキーにして、Google で検索していました。最初に出くわしたのは、Rubyスクリプト内で、下記のコードを追加するとよいよ。という情報でした。

OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE

この一行を追加すると、私の環境では確かにうまく動きました。しかしこれはHTTPS接続時にSSLチェックを行わないということになるので、些か危険な行為に思われます。ネットで調べても「一時的な用途で使ってね」とか「問題の切り分けのために使ってね」という意見が多かったので、引き続き情報を探すことにしました。

まずこの問題が発生する原因は証明書が見つからないからです。Rubyが証明書を正しく見に行くことができているのか?これをチェックする必要があります。これは下記のコードで調べることができます。irb を起動して次のコマンドを入力してみてください。

# irb
> require 'openssl'
> p OpenSSL::X509::DEFAULT_CERT_FILE

私の環境では「/etc/pki/tls/cert.pem」と表示されました。では「/etc/pki/tls/cert.pem」は本当に存在しているのか?次のコマンドでチェックしてみました。

# bash
$ cat /etc/pki/tls/cert.pem

私の環境では、ずらずらーっと証明書情報が出力されて、正しく保存されているのが確認できました。では何がいったい悪いのか?

主に2つの記事を見つめていたのですが、私の原因がいまいち掴めませんでした。何らかの原因で証明書を正しく参照できていないというのは分かっているのですが...。
そんな中ヒントになったのが、Stackoverflow で紹介されていた次の gem でした。

ソースをみてみると、Net::HTTP をこじ開けて、無理やり gem に含まれている証明書を参照するように書き換える gem です。確かし、強引ですがうまく行きそうな気がします。単純な Ruby スクリプトを実行したいだけなのに、そこまでするのも気が引ける...。

というわけで、自前で証明書をダウンロードして、Net::HTTP の証明書参照部分に直接渡したらいいんじゃないか?という結論に至りました。一応、今のところ、うまく動いているようです。