Ruby でバーチャルホスト対応の HTTPS サーバーを書こうとしていたのですが、Ruby から TLS 拡張の SNI を利用する方法が分からなかったので簡単に調べました。
SNI とは?
SNI (Server Name Indication) とは、RFC 6066 で定義されている TLS の拡張で、Client hello の中にサーバーのホスト名を埋めこむことができる拡張です。
これを利用することにより、例えばサーバーは要求されたホスト名によって使用するサーバー証明書を変えることができます。
例えば nginx の設定ファイルでは
nginx.conf
http {
server {
listen server_a:443 ssl;
ssl_certificate server_a.crt;
}
server {
listen server_b:443 ssl;
ssl_certificate server_b.crt;
}
}
と書くと、ブラウザから https://server_a/ でアクセスした場合には server_a.crt が使用され、https://server_b/ でアクセスした場合には server_b.crt が使用されますね。
Ruby では?
Ruby の openssl 標準添付ライブラリでは、サーバー側は OpenSSL::SSL::SSLContext#servername_cb=
、クライアント側では OpenSSL::SSL::SSLSocket#hostname=
を使用することで実現できます。
サーバー
server.rb
require "openssl"
require "socket"
ctx = OpenSSL::SSL::SSLContext.new
ctx.servername_cb = proc {|sslsock, hostname|
puts hostname
new_ctx = OpenSSL::SSL::SSLContext.new
case hostname
when "0.example.com"
new_ctx.cert = OpenSSL::X509::Certificate.new(File.read("0.example.com.crt"))
new_ctx.key = OpenSSL::PKey::RSA.new(File.read("0.example.com.key"))
else
new_ctx.cert = OpenSSL::X509::Certificate.new(File.read("1.example.com.crt"))
new_ctx.key = OpenSSL::PKey::RSA.new(File.read("1.example.com.key"))
end
new_ctx
}
tcp_server = TCPServer.new("127.0.0.1", 40443)
ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, ctx)
loop do
sock = ssl_server.accept
sock.close
end
クライアント
client.rb
require "openssl"
require "socket"
tcp_socket = TCPSocket.new("127.0.0.1", 40443)
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket)
ssl_socket.hostname = ARGV[0]
ssl_socket.connect
puts ssl_socket.peer_cert.subject
実行結果
サーバー
% ruby server.rb
0.example.com
example.org
クライアント
% ruby client.rb 0.example.com
/C=JP/CN=0.example.com/emailAddress=postmaster@0.example.com
% ruby client.rb example.org
/C=JP/CN=1.example.com/emailAddress=postmaster@1.example.com