RubyでHTTPS

なんてことはない、ただRubyでHTTPSなWebサーバにアクセスしてGETでデータをとってくるというだけの話なんだけど...

問題発生

まず思いついたのがこのコード。

require 'net/https'
Net::HTTP.get(URI.parse("https://mcrn.jp/ret.cgi"))

しかし、次のようなエラーが返ってきてしまう。

OpenSSL::SSL::SSLError: SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed
from C:/Ruby21x/lib/ruby/2.1.0/net/http.rb:923:in `connect'

原因究明

もしかしてSNIに対応していない?と思ったけど違った(4年以上前に対応してる)。

さらに調べて、以下の記事をヒントに証明書の場所を調べてみてびっくり。

エラー:OpenSSL::SSL::SSLError SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed - komiyakの通り道

require 'openssl'
p OpenSSL::X509::DEFAULT_CERT_FILE
"C:/Users/Justin/Projects/knap-build/var/knapsack/software/x64-windows/openssl/1.0.1l/ssl/cert.pem"
=> "C:/Users/Justin/Projects/knap-build/var/knapsack/software/x64-windows/openssl/1.0.1l/ssl/cert.pem"

Justin!?誰???(※1

RubyのインストールフォルダをサクラエディタでGrepしてみたところ、どうもlibeay32.dllに埋め込まれてるっぽい。

解決方法

上記記事によると「証明書(cacert.pem)」をダウンロードして、明示的に参照すればいいみたい。

https = Net::HTTP.new('mcrn.jp', 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 = "C:\\cacert.pem" #証明書を明示的に参照
https.start do
  response = https.get('/ret.cgi')
  puts response.body
end

別の方法としては、同じく上記記事で紹介されてるcertified gemを導入してもOK。

やってることといえばNet::HTTPOpenSSL::X509::Storeにモンキーパッチを当てて

  • 証明書の指定(※2
  • 証明書検証の必須

を自動化してるだけ。

今回は自サイトをフルHTTPS化してしまってるし、いちいち証明書指定するのが面倒なのでcertified gemを入れる方法を採用。

あとは冒頭のコードのrequireを

require 'certified'

に置き換えるだけ(※3)で無事HTTPSアクセスに成功。


ところで、さくらのレンタルサーバ内から自分自身(さくらのレンタルサーバでホスティングしてるSNI SSLサイト)にRubyでHTTPSアクセスしようとすると、何故かX-Sakura-Forwarded-Forヘッダ(※4)が設定されずにHTTPアクセスしようとして、.htaccessのリダイレクト設定にひっかかって301が返ってきてしまう。

強制HTTPSにしてなければサーバ内部の通信なんだから別にHTTPでもいいんだけど、.htaccessの判定条件を変えたくないし、強制HTTPSも止めたくない。

そこでちょっとしたトリック。

require 'net/https'
Net::HTTP.new("mcrn.jp",443).tap{|h|h.use_ssl=true}.get("/ret.cgi",{'X-Sakura-Forwarded-For' => '0.0.0.0'}).body

要するに「ヘッダを偽装して(SSLだとウソをついて)HTTP通信を強制」してるというわけ。

そして、実はこれを応用すると、telnetからX-Sakura-Forwarded-For(値はなんでもいい)を手打ちしてやれば、強制HTTPSを解除できてしまう。

脅威としては、マルウェアに感染して、そのマルウェアが必ずX-Sakura-Forwarded-Forを埋め込んで、さらにブラウザのHTTPS動作を上書きして「HTTPでアクセスできるホストはHTTPにしてしまう」あたりが考えられるかもしれないけど、そこまでされたらもう証明書検証をすっとばされたり、自己認証局証明書(オレオレ証明書)入れられたりしてるだろうし、たぶん手遅れ。

マルウェアなしの脅威だと、初回にHTTPでアクセスしたときに通信を改竄されてX-Sakura-Forwarded-Forを埋め込まれるぐらいだけど、一度でもHTTPSでアクセスすればHSTSで以降はHTTPS強制だし、現在preloadリストに申請中なので、無事通過すれば仮にアドレスバーに「http://mcrn.jp/」って打たれても初回からHTTPSになるから実質的には問題ない...かな?

さすがに根本的な対策をしようとすると、さくらのレンタルサーバ+SNI SSLの組み合わせ自体をやめないといけないので厳しい感じ。VPSにでも移行しちゃう?

  1. ※1:予想通り、RubyInstallerの開発メンバーだった
  2. ※2:証明書はgemディレクトリ内にあって、certified-updateコマンドで更新
  3. ※3:certified内でnet/httpsをrequireしてくれる
  4. ※4:.htaccessに指定する条件としては、全部大文字で%{HTTP:X-SAKURA-FORWARDED-FOR}になる