ActionMailer SMTP “certificate verify failed”

2020-03-31 08:50发布

问题:

I want to send emails from my Rails web application, and I do not want to disable TLS certificate verification. However for some reason, it always fails with "SSLv3 read server certificate B: certificate verify failed", even though the server certificate is valid.

I doubled checked with openssl s_client (using /etc/ssl/certs/ca-certificates.crt), and running the following in the rails console also works, delivering successfully.

smtp = Net::SMTP.new(host, port)
smtp.enable_tls
smtp.start("localhost", username, password, :login) do |smtp|
  smtp.send_message msgstr, from, to
end

The server has Rails 4.2.6 and Ruby 2.3.0

config.action_mailer.smtp_setting = {
    address: 
    port: 465,
    user_name: 
    password: 
    authentication: :login,
    openssl_verify_mode: OpenSSL::SSL::VERIFY_PEER,
    enable_starttls_auto: false,
    ssl: true
}

回答1:

From the described behavior I am quite sure that peer verification has not been done in the console and that you need to explicitly set the certificate store for verifying peer certificates in your Rails configuration.

Why it "works" in the console and how to actually verify peers there:

The observation that it works from the console but does not from Rails code is caused by the fact that smtp.enable_tls in your console code does not force peer verification whereas your Rails configuration apparently does. Indeed, when you write the command to the console, you get the SSLContext printed out:

smtp.enable_tls
# => #<OpenSSL::SSL::SSLContext:0x000000064043d0 @cert=nil, @key=nil,
       @client_ca=nil, @ca_file=nil, @ca_path=nil, @timeout=nil,
       @verify_mode=nil, @verify_depth=nil, @renegotiation_cb=nil,
       @verify_callback=nil, @cert_store=nil, @extra_chain_cert=nil, 
       @client_cert_cb=nil, @session_id_context=nil, @tmp_dh_callback=nil, 
       @session_get_cb=nil, @session_new_cb=nil, @session_remove_cb=nil, 
       @tmp_ecdh_callback=nil, @servername_cb=nil, @npn_protocols=nil, 
       @alpn_protocols=nil, @alpn_select_cb=nil, @npn_select_cb=nil>

Note that @verify_mode is nil so there is no peer verification enabled by default on the SSLContext.

To force peer verification in console, so that you can play with the SSL settings manually, you need to use a custom SSLContext and pass it to enable_tls:

ssl_context = Net::SMTP.default_ssl_context
ssl_context.set_params
smtp.enable_tls(ssl_context)
# => #<OpenSSL::SSL::SSLContext:0x000000063c27c8 @cert=nil, @key=nil, 
       @client_ca=nil, @ca_file=nil, @ca_path=nil, @timeout=nil, 
       @verify_mode=1, @verify_depth=nil, @renegotiation_cb=nil, 
       @verify_callback=nil, @cert_store=#<OpenSSL::X509::Store:0x00000002894408 @verify_callback=nil, @error=nil, @error_string=nil, @chain=nil, @time=nil>, @extra_chain_cert=nil, 
       @client_cert_cb=nil, @session_id_context=nil, @tmp_dh_callback=nil, 
       @session_get_cb=nil, @session_new_cb=nil, @session_remove_cb=nil, 
       @tmp_ecdh_callback=nil, @servername_cb=nil, @npn_protocols=nil, 
       @alpn_protocols=nil, @alpn_select_cb=nil, @npn_select_cb=nil>

Watch closely the differences: the SSLContext now has verify_mode set to 1 and has a certificate store for the verifications defined. This is (among other things) what the set_params method in SSLContext does.

How to configure the certificate store in Rails

Now, Rails does not call the set_params methods when constructing the SSLContext for SMTP connection. Instead, it sets the individual attributes on it according to the options (see here and here in the source code). You have properly configured Rails that you want to verify peer certificates but you have not configured a certificate store to verify peers against.

This can be done using the ca_file or ca_path options, so the following Rails configuration should work for you:

config.action_mailer.smtp_setting = {
    ...
    ssl: true
    enable_starttls_auto: false,
    openssl_verify_mode: OpenSSL::SSL::VERIFY_PEER,
    ca_file: "/etc/ssl/certs/ca-certificates.crt",
    ...
} 

I have no idea why this is not properly documented in the Rails Guides...



回答2:

This Rails configuration works for me (using Ruby 2.2.2 and Rails 5):

ActionMailer::Base.smtp_setting = {
  ...
  enable_starttls_auto: true,
  openssl_verify_mode: OpenSSL::SSL::VERIFY_PEER,
  openssl_verify_depth: 3,  # if your CA is a sub signer
  ca_file: "/etc/ssl/certs/ca-certificates.crt",
  ...
}