2010/11/16

クライアント認証を有効にしたstunnelに接続するCouchDB用Rubyクライアントを 作ってみた

前回までで、stunnelを起動したものの接続テストがまだでした。 元々別件で使用した CouchDB Ruby用ガイドのCouchモジュールをSSLに対応させてみました。

クライアント認証は、サーバ(今回はstunnel)にクライアントの持っているCertificateを認めさせる事なので、クライアント用にCertificateを作成して、そのCA局情報をstunnelに登録しておきます。

ついでにRubyスクリプト側にcacertファイルを持たせて、サーバ認証もするようにしました。

SSLのサーバ認証とクライアント認証で必要なこと

今回の作業で stunnelのFAQやら RubyのNet::Httpsマニュアルやら、その他のドキュメントを参考にしました。

Rubyのマニュアルは素晴しい内容だと思いましたが、大抵のドキュメントはサーバ認証に必要なことと、クライアント認証に必要なことを、あまり分けて書いていないか、説明していない印象を受けました。

前回のStunnel用のPEMファイルを準備したのだって、ほとんどサーバ認証用に必要な作業です。

サーバ認証の流れ

今回はサーバ認証のためにcacert.pemをRubyスクリプトにも配置しますが、その作業を先にします。

  • サーバ(stunnel)側にnewcert.pemとnewkey.pemを埋め込む (既にstunnel.pemとして完了済み)
  • クライアント側にサーバのnewcert.pemに対応したcacert.pemファイルを配置する
  • クライアントが動作し、手元のcacert.pemと送られてきたnewcert.pemの内容を確認した上で、Common Nameが接続に使用したサーバ名と一致するか確認する

変更個所は2点で、sslを使うためにrequireで"net/https"を指定します。

Couchモジュールファイル先頭のrequire行の変更個所

require 'net/https'

次にCouchモジュールのrequestメソッドを書き換えました。 今回はBasic認証周りの変更は外してあります。

書き換えたCouchモジュールのrequestメソッド

    def request(req)
      client = Net::HTTP.new(@host, @port)
      if @options.kind_of?(Hash) and @options.has_key?('cacert')
        client.use_ssl = true
        client.ca_file = @options['cacert']
        client.verify_mode  = @options['ssl_verify_mode'] if @options.has_key?('ssl_verify_mode')
        client.verify_mode  = OpenSSL::SSL::VERIFY_PEER if not @options.has_key?('ssl_verify_mode')
        client.verify_depth = @options['ssl_verify_mode'] if @options.has_key?('ssl_verify_depth')
        client.verify_depth = 5 if not @options.has_key?('ssl_verify_depth')
      end

      res = client.start { |http| http.request(req) }
      unless res.kind_of?(Net::HTTPSuccess)
        handle_error(req, res)
      end
      res
    end

Rubyスクリプトから使う場合には、スクリプトと同じディレクトリに cacert.pemファイルを配置して、インスタンスメソッドの第3引数でHashオブジェクトを指定します。

変更したCouchモジュールを使うRubyスクリプトの抜粋

couch = Couch::Server.new("home.example.org","5984", {
  'cacert' => File.expand_path('cacert.pem', File.dirname($0))})

これでサーバ認証用の設定は終りです。cacert.pemをクライアント側のRubyスクリプトに読み込ませたのはサーバ認証のためでクライアント認証には反対にサーバ側にcacert.pemを配置します。

サーバ側に配置するcacert.pemファイルは、クライアント用に作成したnewcert.pemに対応するものであるのが後続作業のポイントになります。

クライアント認証用のPEMファイルの作成と配置

まずは作業内容の説明と準備

今回はサーバ用Certificate(newcert.pemとnewkey.pem)を作成する時に使用したのと、同じdemoCAを使います。

とはいえ、demoCAは同じでなくても構いません。 この先の作業に使うdemoCAは1つですから、クライアント用PEMファイルのdemoCAの事だと思って読んでください。

作業自体は「 Ubuntu 8.04 LTS上のApache2をSSL化」の「Apacheサーバ用のSSL鍵ファイルの準備」とほぼ同じです。

作業を"demoCA"のあるディレクトリで行なっていますが、これはUbuntuデフォルトのopenssl.cnfファイル(/etc/ssl/openssl.cnf)の中に、CA局のdirとして"./demoCA"が登録されていることに起因します。 これを変更すれば必ずしも"demoCA"のあるディレクトリで作業する必要はありません。

こう書いても常識的にはdemoCAディレクトリにcdしたくなるので、 まずは作業ディレクトリから$ ls -ld demoCAでdemoCAディレクトリがみえることを確認します。

$ ls -ld demoCA
drwxr-xr-x 6 yasu yasu 4096 2010-11-15 20:13 demoCA
実際の作業はここから

次に、まずは"newreq.pem"ファイルを作成します。 注意点は次の2点です。

  • 最初に入力する"PEM passphrase"は、後で作成する"newkey.pem(couchdb_stunnel_client_key.pem)"を使う時に必要になるので覚えておく
  • クライアント側のcertファイルに埋め込むCommon Nameは何でも良い (サーバはcacert.pemしかみないため)
/usr/lib/ssl/misc/CA.pl -newreq

いろいろ入力しますが、次のような内容になっています。 入力したところは強調表示にしています。

  PEM pass phrase:   ijsafuiowe890rwe
  ...
  Country Name (2 letter code) []:  JP
  State or Province Name (full name) []:  Fukushima
  Locality Name (eg, city) []:  AizuWakamatsu
  Organization Name (eg, company) []:  Yet Another Sundial Org.
  Organizational Unit Name (eg, section) []:  CouchDB Management Team
  Common Name (eg, YOUR name) []:  couchdb@example.org
  Email Address []:  admin@example.org
  
  Please enter the following 'extra' attributes
  to be sent with your certificate request
  A challenge password []:
  An optional company name []:
  Request is in newreq.pem, private key is in newkey.pem

続けて"newcert.pem"と"newkey.pem"を作成します。 ここでは"demoCA"のdemoCA/private/cakey.pemを開くために必要なパスフレーズを入力します。 あとは'y'を入力して作業を完了するだけです。

/usr/lib/ssl/misc/CA.pl -sign

カレントディレクトリにPEMファイルを放置しておくと、わけがわからなくなるのでディレクトリに移動しておきます。 一緒に作成した時に使ったdemoCAのcacert.pemもコピーしておきます。

$ mkdir demoCA.couchdb_stunnel_client_keys
$ cp demoCA/cacert.pem demoCA.couchdb_stunnel_client_keys/
$ mv newcert.pem newkey.pem demoCA.couchdb_stunnel_client_keys/
$ rm newreq.pem

デフォルトのnewcert.pemとnewkey.pemという名前も、配布する時には判りずらいので、それらしい名前にコピーしておきます。

$ cd demoCA.couchdb_stunnel_client_keys/
$ cp newcert.pem stunnel.client.cert.pem
$ cp newkey.pem stunnel.client.key.pem

さてnewreq.pemを作成した時のパスフレーズは覚えておかないと使えないので、あまりお勧めしませんが、passphraseなしにアクセスできる"stunnel.client.key.nopass.pem"ファイルを作成する事もできます。

openssl rsa < stunnel.client.key.pem > stunnel.client.key.nopass.pem

ここまでで次のファイルが作成できました。

  • cacert.pem
  • stunnel.client.cert.pem
  • stunnel.client.key..pem
  • stunnel.client.key.nopass.pem (これはなくてもいい)

作成したPEMファイルの配置

いよいよ作成したファイルを配置します。

stunnelの動いているサーバ側での作業

ここではstunnel.client.cert.pemと、cacert.pemファイルを配置します。c_rehashは必須です。

$ sudo cp stunnel.client.cert.pem cacert.pem /etc/couchdb/certs
$ sudo c_rehash /etc/couchdb/certs

クライアントのPEMファイルだけでは不十分で、そのPEMファイルが正規のものかを確認するために発行したCAのcacerts.pemファイルも必要なところがポイントです。

クライアント側でのファイル配置とスクリプトの変更

まずはRubyスクリプトのあるディレクトリに残りのPEMファイル(stunnel.client.*.pem)をコピーします。

まずはCouchDBモジュールのrequestメソッドを変更します。

書き換えたCouchモジュールのrequestメソッド v.2

    def request(req)
      client = Net::HTTP.new(@host, @port)
      if @options.kind_of?(Hash) and @options.has_key?('cacert')
        client.use_ssl = true
        client.ca_file = @options['cacert']
        client.verify_mode  = @options['ssl_verify_mode'] if @options.has_key?('ssl_verify_mode')
        client.verify_mode  = OpenSSL::SSL::VERIFY_PEER if not @options.has_key?('ssl_verify_mode')
        client.verify_depth = @options['ssl_verify_mode'] if @options.has_key?('ssl_verify_depth')
        client.verify_depth = 5 if not @options.has_key?('ssl_verify_depth')
        client.cert         = @options['ssl_client_cert'] if @options.has_key?('ssl_client_cert')
        client.key          = @options['ssl_client_key'] if @options.has_key?('ssl_client_key')
      end

      res = client.start { |http| http.request(req) }
      unless res.kind_of?(Net::HTTPSuccess)
        handle_error(req, res)
      end
      res
    end

次にRubyスクリプト本体を変更します。

変更したCouchモジュールを使うRubyスクリプトの抜粋 v.2


ssl_client_cert = OpenSSL::X509::Certificate.new(File.new('stunnel.client.cert.pem'))
ssl_client_key = OpenSSL::PKey::RSA.new(File.new('stunnel.client.key.pem'),'ijsafuiowe890rwe')
couch = Couch::Server.new("home.example.org","5984", {
  'cacert' => File.expand_path('cacert.pem', File.dirname($0)),
  'ssl_client_cert' => ssl_client_cert,
  'ssl_client_key'  => ssl_client_key
})

"stunnel.client.key.nopass.pem"を使う場合は、第2引数のパスフレーズは省略します。 これでStunnelを経由してCouchDBに接続する事ができるようになりました。

RubyのEOFError

cacert.pemファイルの読み込みがうまくいかないと、Rubyスクリプト内で例外を保続してprint $!をしてみてもなかなか不可解なエラーを発生します。

/usr/local/stow/ruby-1.9.2-p0/lib/ruby/1.9.1/net/protocol.rb:135:in `read_nonblock': Connection reset by peer (Errno::ECONNRESET)
	from /usr/local/stow/ruby-1.9.2-p0/lib/ruby/1.9.1/net/protocol.rb:135:in `rbuf_fill'
        ...

rbuf_fillとか、read_*メソッド関連のエラーは大抵はstunnelとかは関係なくて、クライアント側のコーディングとファイルの配置が間違っている可能性が高いです。

こういうエラーが出た場合には、curlやwgetでサーバが正しく動いているか確認するのがお勧めです。

$ cat stunnel.client.cert.pem stunnel.client.key.pem > stunnel.client.pem
$ curl -X GET -cacert cacert.pem --cert stunnel.client.pem:ijsafuiowe890rwe https://home.example.org:5984/example/

stunnelを起動する時に -D 7デバッグオプションをつけると、facilityがdaemonでsyslogに書き出されます。 Ubuntuであれば/var/log/daemon.logを確認すると解決策が分かるかもしれません。

まとめ

自分でもサーバ認証とクライアント認証はごっちゃになったり、わけもわからずに*cert.pemファイルを配置してc_rehashコマンドを打つ場合があります。

自分でアプリケーションを作れば知識も整理できるんでしょうけれど、stunnelをいきなり使おうとするとPEMファイルの作成が最初のハードルになります。

いろいろなスクリプトやガイドはありますが、opensslを実行するコマンドラインに -config ./openssl.cnfとか足されていると、それだけで、コマンドが実行できずに混乱するかもしれません。

楽をしようとスクリプトを使っても、クライアント認証まで持っていくには、そういう手間を省こうとした事で余計に混乱するかもしれません。

突き詰めればreq.pemを経由したcert.pemとkey.pemの2つのファイルしか扱わないんですけどね。cacert.pemが絡んで、意味もわからずコマンドを打っていくと、うまく動かないなんてことにもなるでしょう。

とりあえずは自分用のdemoCAを作成して繰り返し使うのがお勧めです。

企業内利用を想定したクライアント認証用PEMファイルを生成するスクリプトなどを公開している方はいて、中身を理解して使う分には効率を大きくあげてくれます。

CouchDBはちょうどモチベーションを保つのに良いセキュリティのなさ加減だったので、良い教材になりました。

0 件のコメント: