Ruby: SSL_CTX_load_verify_file: system lib (OpenSSL::SSL::SSLError)

OpenSSL まわりでちょっとハマったのでメモ。 こんな Ruby のコードを動かすと環境によって warning になったりエラーになったりする。

require 'faraday'
ssl_opts = {ca_file: OpenSSL::X509::DEFAULT_CERT_FILE}
Faraday::Connection.new('https://tmtms.net', ssl: ssl_opts).get
puts 'OK'

環境1:

warning になるが成功する。

% ruby -w hoge.rb
/usr/local/lib/ruby/3.1.0/net/http.rb:1081: warning: can't set verify locations
OK

環境2:

エラーになる。

% ruby -w hoge.rb
/usr/lib/ruby/3.0.0/net/http.rb:1030:in `initialize': SSL_CTX_load_verify_file: system lib (Faraday::SSLError)
    from /usr/lib/ruby/3.0.0/net/http.rb:1030:in `new'
    from /usr/lib/ruby/3.0.0/net/http.rb:1030:in `connect'
    from /usr/lib/ruby/3.0.0/net/http.rb:970:in `do_start'
    from /usr/lib/ruby/3.0.0/net/http.rb:959:in `start'
    from /var/lib/gems/3.0.0/gems/faraday-net_http-3.0.2/lib/faraday/adapter/net_http.rb:112:in `request_with_wrapped_block'
    from /var/lib/gems/3.0.0/gems/faraday-net_http-3.0.2/lib/faraday/adapter/net_http.rb:102:in `perform_request'
    from /var/lib/gems/3.0.0/gems/faraday-net_http-3.0.2/lib/faraday/adapter/net_http.rb:66:in `block in call'
    from /var/lib/gems/3.0.0/gems/faraday-2.7.2/lib/faraday/adapter.rb:45:in `connection'
    from /var/lib/gems/3.0.0/gems/faraday-net_http-3.0.2/lib/faraday/adapter/net_http.rb:65:in `call'
    from /var/lib/gems/3.0.0/gems/faraday-2.7.2/lib/faraday/request/url_encoded.rb:25:in `call'
    from /var/lib/gems/3.0.0/gems/faraday-2.7.2/lib/faraday/rack_builder.rb:153:in `build_response'
    from /var/lib/gems/3.0.0/gems/faraday-2.7.2/lib/faraday/connection.rb:445:in `run_request'
    from /var/lib/gems/3.0.0/gems/faraday-2.7.2/lib/faraday/connection.rb:200:in `get'
    from hoge.rb:3:in `<main>'
/usr/lib/ruby/3.0.0/net/http.rb:1030:in `initialize': SSL_CTX_load_verify_file: system lib (OpenSSL::SSL::SSLError)
    from /usr/lib/ruby/3.0.0/net/http.rb:1030:in `new'
    from /usr/lib/ruby/3.0.0/net/http.rb:1030:in `connect'
    from /usr/lib/ruby/3.0.0/net/http.rb:970:in `do_start'
    from /usr/lib/ruby/3.0.0/net/http.rb:959:in `start'
    from /var/lib/gems/3.0.0/gems/faraday-net_http-3.0.2/lib/faraday/adapter/net_http.rb:112:in `request_with_wrapped_block'
    from /var/lib/gems/3.0.0/gems/faraday-net_http-3.0.2/lib/faraday/adapter/net_http.rb:102:in `perform_request'
    from /var/lib/gems/3.0.0/gems/faraday-net_http-3.0.2/lib/faraday/adapter/net_http.rb:66:in `block in call'
    from /var/lib/gems/3.0.0/gems/faraday-2.7.2/lib/faraday/adapter.rb:45:in `connection'
    from /var/lib/gems/3.0.0/gems/faraday-net_http-3.0.2/lib/faraday/adapter/net_http.rb:65:in `call'
    from /var/lib/gems/3.0.0/gems/faraday-2.7.2/lib/faraday/request/url_encoded.rb:25:in `call'
    from /var/lib/gems/3.0.0/gems/faraday-2.7.2/lib/faraday/rack_builder.rb:153:in `build_response'
    from /var/lib/gems/3.0.0/gems/faraday-2.7.2/lib/faraday/connection.rb:445:in `run_request'
    from /var/lib/gems/3.0.0/gems/faraday-2.7.2/lib/faraday/connection.rb:200:in `get'
    from hoge.rb:3:in `<main>'

Faraday 使わずに OpenSSL だけで再現させるとこんな感じ。

require 'openssl'
sock = Socket.tcp('tmtms.net', 443)
store = OpenSSL::X509::Store.new
store.set_default_paths
ctx = OpenSSL::SSL::SSLContext.new
ctx.set_params(ca_file: OpenSSL::X509::DEFAULT_CERT_FILE, cert_store: store)
tls = OpenSSL::SSL::SSLSocket.new(sock, ctx)
tls.hostname = 'tmtms.net'
tls.sync_close = true
tls.connect
puts 'OK'

環境1:

% ruby -w hoge.rb
hoge.rb:7: warning: can't set verify locations
OK

環境2:

% ruby -w hoge.rb
hoge.rb:7:in `initialize': SSL_CTX_load_verify_file: system lib (OpenSSL::SSL::SSLError)
    from hoge.rb:7:in `new'
    from hoge.rb:7:in `<main>'

エラーになるのは、Ruby の openssl ライブラリが 3.0 で、かつ OS の OpenSSL ライブラリ(libssl)が 3.x で、OpenSSL::X509::DEFAULT_CERT_FILE のファイルが存在していない場合。たとえば、Ubuntu 22.04 とか。

Ubuntu では以前から OpenSSL::X509::DEFAULT_CERT_FILE が存在ないファイル(/usr/lib/ssl/cert.pem)になってるんだけど、Ubuntu 22.04 で libssl が 3.0 になって発生するようになった。

libssl の問題かと思ったんだけど、調べてみたら Ruby の openssl の方だった。

https://github.com/ruby/openssl/blob/c263cd40057fd4a7ea36ceefc7ad88054ed6ffea/ext/openssl/ossl_ssl.c#L882-L892

#ifdef HAVE_SSL_CTX_LOAD_VERIFY_FILE
    if (ca_file && !SSL_CTX_load_verify_file(ctx, ca_file))
        ossl_raise(eSSLError, "SSL_CTX_load_verify_file");
    if (ca_path && !SSL_CTX_load_verify_dir(ctx, ca_path))
        ossl_raise(eSSLError, "SSL_CTX_load_verify_dir");
#else
    if(ca_file || ca_path){
    if (!SSL_CTX_load_verify_locations(ctx, ca_file, ca_path))
        rb_warning("can't set verify locations");
    }
#endif

ca_file が読めないときに OpenSSL 3.x (HAVE_SSL_CTX_LOAD_VERIFY_FILE が真) だと SSLError 例外になるけど、OpenSSL 1.x だと warning になる。

ca_file に存在しないファイルを指定するのが悪いんだけど、Ubuntu では OpenSSL::X509::DEFAULT_CERT_FILE が存在してないファイルというところが罠。

デフォルトでいいなら ca_file は指定しない方がいいんだろうな。