123456789_123456789_123456789_123456789_123456789_

Class: Mongo::Socket::SSL Private

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, Socket
Instance Chain:
self, ::Mongo::Loggable, OpenSSL, Socket
Inherits: Socket
  • Object
Defined in: lib/mongo/socket/ssl.rb

Overview

Wrapper for TLS sockets.

Since:

  • 2.0.0

Constant Summary

::Mongo::Loggable - Included

PREFIX

Class Method Summary

Instance Attribute Summary

Instance Method Summary

::Mongo::Loggable - Included

#log_debug

Convenience method to log debug messages with the standard prefix.

#log_error

Convenience method to log error messages with the standard prefix.

#log_fatal

Convenience method to log fatal messages with the standard prefix.

#log_info

Convenience method to log info messages with the standard prefix.

#log_warn

Convenience method to log warn messages with the standard prefix.

#logger

Get the logger instance.

#_mongo_log_prefix, #format_message

Constructor Details

.new(host, port, host_name, timeout, family, options = {}) ⇒ SSL

Initializes a new TLS socket.

Examples:

Create the TLS socket.

SSL.new('::1', 27017, 30)

Parameters:

  • host (String)

    The hostname or IP address.

  • port (Integer)

    The port number.

  • timeout (Float)

    The socket timeout value.

  • family (Integer)

    The socket family.

  • options (Hash) (defaults to: {})

    The options.

Options Hash (options):

  • :connect_timeout (Float)

    Connect timeout.

  • :connection_address (Address)

    ::Mongo::Address of the connection that created this socket.

  • :connection_generation (Integer)

    Generation of the connection (for non-monitoring connections) that created this socket.

  • :monitor (true | false)

    Whether this socket was created by a monitoring connection.

  • :ssl_ca_cert (String)

    The file containing concatenated certificate authority certificates used to validate certs passed from the other end of the connection. Intermediate certificates should NOT be specified in files referenced by this option. One of :ssl_ca_cert, :ssl_ca_cert_string or :ssl_ca_cert_object (in order of priority) is required when using :ssl_verify.

  • :ssl_ca_cert_object (Array<OpenSSL::X509::Certificate>)

    An array of OpenSSL::X509::Certificate objects representing the certificate authority certificates used to validate certs passed from the other end of the connection. Intermediate certificates should NOT be specified in files referenced by this option. One of :ssl_ca_cert, :ssl_ca_cert_string or :ssl_ca_cert_object (in order of priority) is required when using :ssl_verify.

  • :ssl_ca_cert_string (String)

    A string containing certificate authority certificate used to validate certs passed from the other end of the connection. This option allows passing only one CA certificate to the driver. Intermediate certificates should NOT be specified in files referenced by this option. One of :ssl_ca_cert, :ssl_ca_cert_string or :ssl_ca_cert_object (in order of priority) is required when using :ssl_verify.

  • :ssl_cert (String)

    The certificate file used to identify the connection against MongoDB. A certificate chain may be passed by specifying the client certificate first followed by any intermediate certificates up to the CA certificate. The file may also contain the certificate's private key, which will be ignored. This option, if present, takes precedence over the values of :ssl_cert_string and :ssl_cert_object

  • :ssl_cert_object (OpenSSL::X509::Certificate)

    The OpenSSL::X509::Certificate used to identify the connection against MongoDB. Only one certificate may be passed through this option.

  • :ssl_cert_string (String)

    A string containing the PEM-encoded certificate used to identify the connection against MongoDB. A certificate chain may be passed by specifying the client certificate first followed by any intermediate certificates up to the CA certificate. The string may also contain the certificate's private key, which will be ignored, This option, if present, takes precedence over the value of :ssl_cert_object

  • :ssl_key (String)

    The private keyfile used to identify the connection against MongoDB. Note that even if the key is stored in the same file as the certificate, both need to be explicitly specified. This option, if present, takes precedence over the values of :ssl_key_string and :ssl_key_object

  • :ssl_key_object (OpenSSL::PKey)

    The private key used to identify the connection against MongoDB

  • :ssl_key_pass_phrase (String)

    A passphrase for the private key.

  • :ssl_key_string (String)

    A string containing the PEM-encoded private key used to identify the connection against MongoDB. This parameter, if present, takes precedence over the value of option :ssl_key_object

  • :ssl_verify (true, false)

    Whether to perform peer certificate validation and hostname verification. Note that the decision of whether to validate certificates will be overridden if :ssl_verify_certificate is set, and the decision of whether to validate hostnames will be overridden if :ssl_verify_hostname is set.

  • :ssl_verify_certificate (true, false)

    Whether to perform peer certificate validation. This setting overrides :ssl_verify with respect to whether certificate validation is performed.

  • :ssl_verify_hostname (true, false)

    Whether to perform peer hostname validation. This setting overrides :ssl_verify with respect to whether hostname validation is performed.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 102

def initialize(host, port, host_name, timeout, family, options = {})
  super(timeout, options)
  @host, @port, @host_name = host, port, host_name
  @context = create_context(options)
  @family = family
  @tcp_socket = ::Socket.new(family, SOCK_STREAM, 0)
  begin
    @tcp_socket.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
    set_socket_options(@tcp_socket)
    run_tls_context_hooks

    connect!
  rescue StandardError
    @tcp_socket.close
    raise
  end
end

Instance Attribute Details

#contextSSLContext (readonly)

Returns:

  • (SSLContext)

    context The TLS context.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 121

attr_reader :context

#hostString (readonly)

Returns:

  • (String)

    host The host to connect to.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 124

attr_reader :host

#host_nameString (readonly)

Returns:

  • (String)

    host_name The original host name.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 127

attr_reader :host_name

#portInteger (readonly)

Returns:

  • (Integer)

    port The port to connect to.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 130

attr_reader :port

#verify_certificate?Boolean (readonly, private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 280

def verify_certificate?
  # If ssl_verify_certificate is not present, disable only if
  # ssl_verify is explicitly set to false.
  if options[:ssl_verify_certificate].nil?
    options[:ssl_verify] != false
  # If ssl_verify_certificate is present, enable or disable based on its value.
  else
    !!options[:ssl_verify_certificate]
  end
end

#verify_hostname?Boolean (readonly, private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 291

def verify_hostname?
  # If ssl_verify_hostname is not present, disable only if ssl_verify is
  # explicitly set to false.
  if options[:ssl_verify_hostname].nil?
    options[:ssl_verify] != false
  # If ssl_verify_hostname is present, enable or disable based on its value.
  else
    !!options[:ssl_verify_hostname]
  end
end

#verify_ocsp_endpoint?Boolean (readonly, private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 302

def verify_ocsp_endpoint?
  if !options[:ssl_verify_ocsp_endpoint].nil?
    options[:ssl_verify_ocsp_endpoint] != false
  elsif !options[:ssl_verify_certificate].nil?
    options[:ssl_verify_certificate] != false
  else
    options[:ssl_verify] != false
  end
end

Instance Method Details

#connect!SSL (private)

Note:

This method mutates the object by setting the socket internally.

Establishes a socket connection.

Examples:

Connect the socket.

sock.connect!

Returns:

  • (SSL)

    The connected socket instance.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 143

def connect!
  sockaddr = ::Socket.pack_sockaddr_in(port, host)
  connect_timeout = options[:connect_timeout]
  map_exceptions do
    if connect_timeout && connect_timeout != 0
      deadline = Utils.monotonic_time + connect_timeout
      if BSON::Environment.jruby?
        # We encounter some strange problems with connect_nonblock for
        # ssl sockets on JRuby. Therefore, we use the old +Timeout.timeout+
        # solution, even though it is known to be not very reliable.
        raise Error::SocketTimeoutError, 'connect_timeout expired' if connect_timeout < 0

        Timeout.timeout(connect_timeout, Error::SocketTimeoutError,
                        "The socket took over #{options[:connect_timeout]} seconds to connect") do
          connect_without_timeout(sockaddr)
        end
      else
        connect_with_timeout(sockaddr, connect_timeout)
      end
      remaining_timeout = deadline - Utils.monotonic_time
      verify_certificate!(@socket)
      verify_ocsp_endpoint!(@socket, remaining_timeout)
    else
      connect_without_timeout(sockaddr)
      verify_certificate!(@socket)
      verify_ocsp_endpoint!(@socket)
    end
  end
  self
rescue StandardError
  @socket&.close
  @socket = nil
  raise
end

#connect_tcp_socket_with_timeout(sockaddr, deadline, connect_timeout) (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 221

def connect_tcp_socket_with_timeout(sockaddr, deadline, connect_timeout)
  if deadline <= Utils.monotonic_time
    raise Error::SocketTimeoutError, "The socket took over #{connect_timeout} seconds to connect"
  end

  begin
    @tcp_socket.connect_nonblock(sockaddr)
  rescue IO::WaitWritable
    with_select_timeout(deadline, connect_timeout) do |select_timeout|
      IO.select(nil, [ @tcp_socket ], nil, select_timeout)
    end
    retry
  rescue Errno::EISCONN
    # Socket is connected, nothing to do.
  end
end

#connect_with_timeout(sockaddr, connect_timeout) (private)

Connects the socket with the connect timeout. The timeout applies to connecting both ssl socket and the underlying tcp socket.

Parameters:

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 211

def connect_with_timeout(sockaddr, connect_timeout)
  if connect_timeout <= 0
    raise Error::SocketTimeoutError, "The socket took over #{connect_timeout} seconds to connect"
  end

  deadline = Utils.monotonic_time + connect_timeout
  connect_tcp_socket_with_timeout(sockaddr, deadline, connect_timeout)
  connnect_ssl_socket_with_timeout(deadline, connect_timeout)
end

#connect_without_timeout(sockaddr) (private)

Connects the socket without a timeout provided.

Parameters:

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 199

def connect_without_timeout(sockaddr)
  @tcp_socket.connect(sockaddr)
  @socket = OpenSSL::SSL::SSLSocket.new(@tcp_socket, context)
  @socket.hostname = @host_name
  @socket.sync_close = true
  @socket.connect
end

#connnect_ssl_socket_with_timeout(deadline, connect_timeout) (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 238

def connnect_ssl_socket_with_timeout(deadline, connect_timeout)
  if deadline <= Utils.monotonic_time
    raise Error::SocketTimeoutError, "The socket took over #{connect_timeout} seconds to connect"
  end

  @socket = OpenSSL::SSL::SSLSocket.new(@tcp_socket, context)
  @socket.hostname = @host_name
  @socket.sync_close = true

  # We still have time, connecting ssl socket.
  begin
    @socket.connect_nonblock
  rescue IO::WaitReadable, OpenSSL::SSL::SSLErrorWaitReadable
    with_select_timeout(deadline, connect_timeout) do |select_timeout|
      IO.select([ @socket ], nil, nil, select_timeout)
    end
    retry
  rescue IO::WaitWritable, OpenSSL::SSL::SSLErrorWaitWritable
    with_select_timeout(deadline, connect_timeout) do |select_timeout|
      IO.select(nil, [ @socket ], nil, select_timeout)
    end
    retry
  rescue Errno::EISCONN
    # Socket is connected, nothing to do
  end
end

#create_context(options) (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 312

def create_context(options)
  OpenSSL::SSL::SSLContext.new.tap do |context|
    if OpenSSL::SSL.const_defined?(:OP_NO_RENEGOTIATION)
      context.options = context.options | OpenSSL::SSL::OP_NO_RENEGOTIATION
    end

    if context.respond_to?(:renegotiation_cb=)
      # Disable renegotiation for older Ruby versions per the sample code at
      # https://rubydocs.org/d/ruby-2-6-0/classes/OpenSSL/SSL/SSLContext.html
      # In JRuby we must allow one call as this callback is invoked for
      # the initial connection also, not just for renegotiations -
      # https://github.com/jruby/jruby-openssl/issues/180
      allowed_calls = if BSON::Environment.jruby?
                        1
                      else
                        0
                      end
      context.renegotiation_cb = lambda do |_ssl|
        raise 'Client renegotiation disabled' if allowed_calls <= 0

        allowed_calls -= 1
      end
    end

    set_cert(context, options)
    set_key(context, options)

    if verify_certificate?
      context.verify_mode = OpenSSL::SSL::VERIFY_PEER
      set_cert_verification(context, options)
    else
      context.verify_mode = OpenSSL::SSL::VERIFY_NONE
    end

    if context.respond_to?(:verify_hostname=)
      # We manually check the hostname after the connection is established if necessary, so
      # we disable it here in order to give consistent errors across Ruby versions which
      # don't support hostname verification at the time of the handshake.
      context.verify_hostname = OpenSSL::SSL::VERIFY_NONE
    end
  end
end

#extract_certs(text) (private)

This was originally a scan + regex, but the regex was particularly inefficient and was flagged as a concern by static analysis.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 496

def extract_certs(text)
  [].tap do |list|
    pos = 0

    while (begin_idx = text.index(BEGIN_CERT, pos))
      end_idx = text.index(END_CERT, begin_idx)
      break unless end_idx

      end_idx += END_CERT.length
      list.push(text[begin_idx...end_idx])

      pos = end_idx
    end
  end
end

#find_issuer(cert, cert_chain) (private)

Find the issuer certificate in the chain.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 513

def find_issuer(cert, cert_chain)
  cert_chain.find { |c| c.subject == cert.issuer }
end

#human_address (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 481

def human_address
  "#{host}:#{port} (#{host_name}:#{port}, TLS)"
end

#load_private_key(text, passphrase) (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 414

def load_private_key(text, passphrase)
  args = if passphrase
           [ text, passphrase ]
         else
           [ text ]
         end
  # On JRuby, PKey.read does not grok cert+key bundles.
  # https://github.com/jruby/jruby-openssl/issues/176
  if BSON::Environment.jruby?
    [ OpenSSL::PKey::RSA, OpenSSL::PKey::DSA ].each do |cls|
      return cls.send(:new, *args)
    rescue OpenSSL::PKey::PKeyError
      # ignore
    end
    # Neither RSA nor DSA worked, fall through to trying PKey
  end
  OpenSSL::PKey.send(:read, *args)
end

#read_buffer_size (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 475

def read_buffer_size
  # Buffer size for TLS reads.
  # Capped at 16k due to https://linux.die.net/man/3/ssl_read
  16_384
end

#readbyteObject

Read a single byte from the socket.

Examples:

Read a single byte.

socket.readbyte

Returns:

  • (Object)

    The read byte.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 187

def readbyte
  map_exceptions do
    byte = socket.read(1).bytes.to_a[0]
    byte.nil? ? raise(EOFError) : byte
  end
end

#run_tls_context_hooks (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 485

def run_tls_context_hooks
  Mongo.tls_context_hooks.each do |hook|
    hook.call(@context)
  end
end

#set_cert(context, options) (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 355

def set_cert(context, options)
  # Since we clear cert_text during processing, we need to examine
  # ssl_cert_object here to avoid considering it if we have also
  # processed the text.
  if options[:ssl_cert]
    cert_text = File.read(options[:ssl_cert])
    cert_object = nil
  elsif cert_text = options[:ssl_cert_string]
    cert_object = nil
  else
    cert_object = options[:ssl_cert_object]
  end

  # The client certificate may be a single certificate or a bundle
  # (client certificate followed by intermediate certificates).
  # The text may also include private keys for the certificates.
  # OpenSSL supports passing the entire bundle as a certificate chain
  # to the context via SSL_CTX_use_certificate_chain_file, but the
  # Ruby openssl extension does not currently expose this functionality
  # per https://github.com/ruby/openssl/issues/254.
  # Therefore, extract the individual certificates from the certificate
  # text, and if there is more than one certificate provided, use
  # extra_chain_cert option to add the intermediate ones. This
  # implementation is modeled after
  # https://github.com/venuenext/ruby-kafka/commit/9495f5daf254b43bc88062acad9359c5f32cb8b5.
  # Note that the parsing here is not identical to what OpenSSL employs -
  # for instance, if there is no newline between two certificates
  # this code will extract them both but OpenSSL fails in this situation.
  if cert_text
    certs = extract_certs(cert_text)
    if certs.length > 1
      context.cert = OpenSSL::X509::Certificate.new(certs.shift)
      context.extra_chain_cert = certs.map do |cert|
        OpenSSL::X509::Certificate.new(cert)
      end
      # All certificates are already added to the context, skip adding
      # them again below.
      cert_text = nil
    end
  end

  if cert_text
    context.cert = OpenSSL::X509::Certificate.new(cert_text)
  elsif cert_object
    context.cert = cert_object
  end
end

#set_cert_verification(context, options) (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 433

def set_cert_verification(context, options)
  context.verify_mode = OpenSSL::SSL::VERIFY_PEER
  cert_store = OpenSSL::X509::Store.new
  if options[:ssl_ca_cert]
    cert_store.add_file(options[:ssl_ca_cert])
  elsif options[:ssl_ca_cert_string]
    cert_store.add_cert(OpenSSL::X509::Certificate.new(options[:ssl_ca_cert_string]))
  elsif options[:ssl_ca_cert_object]
    unless options[:ssl_ca_cert_object].is_a? Array
      raise TypeError('Option :ssl_ca_cert_object should be an array of OpenSSL::X509:Certificate objects')
    end

    options[:ssl_ca_cert_object].each { |cert| cert_store.add_cert(cert) }
  else
    cert_store.set_default_paths
  end
  context.cert_store = cert_store
end

#set_key(context, options) (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 403

def set_key(context, options)
  passphrase = options[:ssl_key_pass_phrase]
  if options[:ssl_key]
    context.key = load_private_key(File.read(options[:ssl_key]), passphrase)
  elsif options[:ssl_key_string]
    context.key = load_private_key(options[:ssl_key_string], passphrase)
  elsif options[:ssl_key_object]
    context.key = options[:ssl_key_object]
  end
end

#verify_certificate!(socket) (private)

Raises:

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 452

def verify_certificate!(socket)
  return unless verify_hostname?
  return if OpenSSL::SSL.verify_certificate_identity(socket.peer_cert, host_name)

  raise Error::SocketError, 'TLS handshake failed due to a hostname mismatch.'
end

#verify_ocsp_endpoint!(socket, timeout = nil) (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 459

def verify_ocsp_endpoint!(socket, timeout = nil)
  return unless verify_ocsp_endpoint?

  cert = socket.peer_cert
  ca_cert = find_issuer(cert, socket.peer_cert_chain)

  unless ca_cert
    log_warn("TLS certificate of '#{host_name}' could not be definitively verified via OCSP: issuer certificate not found in the chain.")
    return
  end

  verifier = OcspVerifier.new(@host_name, cert, ca_cert, context.cert_store,
                              **Utils.shallow_symbolize_keys(options), timeout: timeout)
  verifier.verify_with_cache
end

#with_select_timeout(deadline, connect_timeout) (private)

Raises ::Mongo::Error::SocketTimeoutError exception if deadline reached or the block returns nil. The block should call IO.select with the connect_timeout value. It returns nil if the connect_timeout expires.

Raises:

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ssl.rb', line 268

def with_select_timeout(deadline, connect_timeout)
  select_timeout = deadline - Utils.monotonic_time
  if select_timeout <= 0
    raise Error::SocketTimeoutError, "The socket took over #{connect_timeout} seconds to connect"
  end

  rv = yield(select_timeout)
  return unless rv.nil?

  raise Error::SocketTimeoutError, "The socket took over #{connect_timeout} seconds to connect"
end