Skip to content

Commit 68999a9

Browse files
committed
🔒 Add ssl_ctx and ssl_ctx_params attr readers
`Net::IMAP#ssl_ctx` will return the `OpenSSL::SSL::SSLContext` used by the `OpenSSL::SSL::SSLSocket` when TLS is attempted, even when the TLS handshake is unsuccessful. The context object will be frozen. It will return `nil` for a plaintext connection. `Net::IMAP#ssl_ctx_params` will return the parameters that were sent to `OpenSSL::SSL::SSLContext#set_params` when the connection tries to use TLS (even when unsuccessful). It will return `false` for a plaintext connection. Additionally, error handling for the arguments to `#starttls` was moved *before* sending the command to the server. This way a simple argument error will still raise an exception, but won't result in dropping the connection. Because `ssl_ctx` is also created ahead of time, this will also catch invalid SSLContext params.
1 parent 805111e commit 68999a9

File tree

2 files changed

+89
-36
lines changed

2 files changed

+89
-36
lines changed

lib/net/imap.rb

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,21 @@ class << self
722722
# The port this client connected to
723723
attr_reader :port
724724

725+
# Returns the
726+
# {SSLContext}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html]
727+
# used by the SSLSocket when TLS is attempted, even when the TLS handshake
728+
# is unsuccessful. The context object will be frozen.
729+
#
730+
# Returns +nil+ for a plaintext connection.
731+
attr_reader :ssl_ctx
732+
733+
# Returns the parameters that were sent to #ssl_ctx
734+
# {set_params}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html#method-i-set_params]
735+
# when the connection tries to use TLS (even when unsuccessful).
736+
#
737+
# Returns +false+ for a plaintext connection.
738+
attr_reader :ssl_ctx_params
739+
725740
# Creates a new Net::IMAP object and connects it to the specified
726741
# +host+.
727742
#
@@ -804,7 +819,7 @@ def initialize(host, options = {}, *deprecated)
804819
@port = options[:port] || (options[:ssl] ? SSL_PORT : PORT)
805820
@open_timeout = options[:open_timeout] || 30
806821
@idle_response_timeout = options[:idle_response_timeout] || 5
807-
ssl_ctx_params = options[:ssl]
822+
@ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options[:ssl])
808823

809824
# Basic Client State
810825
@utf8_strings = false
@@ -835,7 +850,8 @@ def initialize(host, options = {}, *deprecated)
835850
# Connection
836851
@tls_verified = false
837852
@sock = tcp_socket(@host, @port)
838-
start_imap_connection(ssl_ctx_params)
853+
start_tls_session if ssl_ctx
854+
start_imap_connection
839855

840856
# DEPRECATED: to remove in next version
841857
@client_thread = Thread.current
@@ -1083,17 +1099,18 @@ def logout
10831099
# Cached #capabilities will be cleared when this method completes.
10841100
#
10851101
def starttls(options = {}, verify = true)
1102+
begin
1103+
# for backward compatibility
1104+
certs = options.to_str
1105+
options = create_ssl_params(certs, verify)
1106+
rescue NoMethodError
1107+
end
1108+
@ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options || {})
10861109
send_command("STARTTLS") do |resp|
10871110
if resp.kind_of?(TaggedResponse) && resp.name == "OK"
1088-
begin
1089-
# for backward compatibility
1090-
certs = options.to_str
1091-
options = create_ssl_params(certs, verify)
1092-
rescue NoMethodError
1093-
end
10941111
clear_cached_capabilities
10951112
clear_responses
1096-
start_tls_session(options)
1113+
start_tls_session
10971114
end
10981115
end
10991116
end
@@ -2351,8 +2368,7 @@ def convert_deprecated_options(
23512368
options
23522369
end
23532370

2354-
def start_imap_connection(ssl_ctx_params)
2355-
start_tls_session(ssl_ctx_params) if ssl_ctx_params
2371+
def start_imap_connection
23562372
@greeting = get_server_greeting
23572373
@capabilities = capabilities_from_resp_code @greeting
23582374
@receiver_thread = start_receiver_thread
@@ -2664,6 +2680,21 @@ def normalize_searching_criteria(keys)
26642680
end
26652681
end
26662682

2683+
def build_ssl_ctx(ssl)
2684+
if ssl
2685+
params = (Hash.try_convert(ssl) || {}).freeze
2686+
context = SSLContext.new
2687+
context.set_params(params)
2688+
if defined?(VerifyCallbackProc)
2689+
context.verify_callback = VerifyCallbackProc
2690+
end
2691+
context.freeze
2692+
[params, context]
2693+
else
2694+
false
2695+
end
2696+
end
2697+
26672698
def create_ssl_params(certs = nil, verify = true)
26682699
params = {}
26692700
if certs
@@ -2681,28 +2712,15 @@ def create_ssl_params(certs = nil, verify = true)
26812712
return params
26822713
end
26832714

2684-
def start_tls_session(params = {})
2685-
unless defined?(OpenSSL::SSL)
2686-
raise "SSL extension not installed"
2687-
end
2688-
if @sock.kind_of?(OpenSSL::SSL::SSLSocket)
2689-
raise RuntimeError, "already using SSL"
2690-
end
2691-
begin
2692-
params = params.to_hash
2693-
rescue NoMethodError
2694-
params = {}
2695-
end
2696-
context = SSLContext.new
2697-
context.set_params(params)
2698-
if defined?(VerifyCallbackProc)
2699-
context.verify_callback = VerifyCallbackProc
2700-
end
2701-
@sock = SSLSocket.new(@sock, context)
2715+
def start_tls_session
2716+
raise "SSL extension not installed" unless defined?(OpenSSL::SSL)
2717+
raise "already using SSL" if @sock.kind_of?(OpenSSL::SSL::SSLSocket)
2718+
raise "cannot start TLS without SSLContext" unless ssl_ctx
2719+
@sock = SSLSocket.new(@sock, ssl_ctx)
27022720
@sock.sync_close = true
27032721
@sock.hostname = @host if @sock.respond_to? :hostname=
27042722
ssl_socket_connect(@sock, @open_timeout)
2705-
if context.verify_mode != VERIFY_NONE
2723+
if ssl_ctx.verify_mode != VERIFY_NONE
27062724
@sock.post_connection_check(@host)
27072725
@tls_verified = true
27082726
end

test/net/imap/test_imap.rb

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ def test_imaps_with_ca_file
5757
end
5858
assert_equal true, verified
5959
assert_equal true, imap.tls_verified?
60+
assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params)
61+
assert_equal(CA_FILE, imap.ssl_ctx.ca_file)
62+
assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode)
63+
assert imap.ssl_ctx.verify_hostname
6064
end
6165

6266
def test_imaps_verify_none
@@ -76,6 +80,10 @@ def test_imaps_verify_none
7680
end
7781
assert_equal false, verified
7882
assert_equal false, imap.tls_verified?
83+
assert_equal({verify_mode: OpenSSL::SSL::VERIFY_NONE},
84+
imap.ssl_ctx_params)
85+
assert_equal(nil, imap.ssl_ctx.ca_file)
86+
assert_equal(OpenSSL::SSL::VERIFY_NONE, imap.ssl_ctx.verify_mode)
7987
end
8088

8189
def test_imaps_post_connection_check
@@ -92,16 +100,41 @@ def test_imaps_post_connection_check
92100
end
93101

94102
if defined?(OpenSSL::SSL)
103+
def test_starttls_unknown_ca
104+
imap = nil
105+
assert_raise(OpenSSL::SSL::SSLError) do
106+
ex = nil
107+
starttls_test do |port|
108+
imap = Net::IMAP.new("localhost", port: port)
109+
imap.starttls
110+
imap
111+
rescue => ex
112+
imap
113+
end
114+
raise ex if ex
115+
end
116+
assert_equal false, imap.tls_verified?
117+
assert_equal({}, imap.ssl_ctx_params)
118+
assert_equal(nil, imap.ssl_ctx.ca_file)
119+
assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode)
120+
end
121+
95122
def test_starttls
96-
verified, imap = :unknown, nil
123+
initial_verified, initial_ctx, initial_params = :unknown, :unknown, :unknown
124+
imap = nil
97125
starttls_test do |port|
98126
imap = Net::IMAP.new("localhost", :port => port)
127+
initial_verified = imap.tls_verified?
128+
initial_params = imap.ssl_ctx_params
129+
initial_ctx = imap.ssl_ctx
99130
imap.starttls(:ca_file => CA_FILE)
100-
verified = imap.tls_verified?
101131
imap
102132
end
103-
assert_equal true, verified
133+
assert_equal false, initial_verified
134+
assert_equal false, initial_params
135+
assert_equal nil, initial_ctx
104136
assert_equal true, imap.tls_verified?
137+
assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params)
105138
rescue SystemCallError
106139
skip $!
107140
ensure
@@ -111,17 +144,18 @@ def test_starttls
111144
end
112145

113146
def test_starttls_stripping
114-
verified, imap = :unknown, nil
147+
imap = nil
115148
starttls_stripping_test do |port|
116149
imap = Net::IMAP.new("localhost", :port => port)
117150
assert_raise(Net::IMAP::UnknownResponseError) do
118151
imap.starttls(:ca_file => CA_FILE)
119152
end
120-
verified = imap.tls_verified?
121153
imap
122154
end
123-
assert_equal false, verified
124155
assert_equal false, imap.tls_verified?
156+
assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params)
157+
assert_equal(CA_FILE, imap.ssl_ctx.ca_file)
158+
assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode)
125159
end
126160
end
127161

@@ -1123,6 +1157,7 @@ def starttls_test
11231157
sock.gets
11241158
sock.print("* BYE terminating connection\r\n")
11251159
sock.print("RUBY0002 OK LOGOUT completed\r\n")
1160+
rescue OpenSSL::SSL::SSLError
11261161
ensure
11271162
sock.close
11281163
server.close

0 commit comments

Comments
 (0)