123456789_123456789_123456789_123456789_123456789_

Class: Mongo::Socket::OcspVerifier Private

Relationships & Source Files
Namespace Children
Classes:
Super Chains via Extension / Inclusion / Inheritance
Instance Chain:
Inherits: Object
Defined in: lib/mongo/socket/ocsp_verifier.rb

Overview

OCSP endpoint verifier.

After a TLS connection is established, this verifier inspects the certificate presented by the server, and if the certificate contains an OCSP URI, performs the OCSP status request to the specified ::Mongo::URI (following up to 5 redirects) to verify the certificate status.

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_name, cert, ca_cert, cert_store, **opts) ⇒ OcspVerifier

Parameters:

  • host_name (String)

    The host name being verified, for diagnostic output.

  • cert (OpenSSL::X509::Certificate)

    The certificate presented by the server at host_name.

  • ca_cert (OpenSSL::X509::Certificate)

    The CA certificate presented by the server or resolved locally from the server certificate.

  • cert_store (OpenSSL::X509::Store)

    The certificate store to use for verifying OCSP response. This should be the same store as used in SSLContext used with the SSLSocket that we are verifying the certificate for. This must NOT be the CA certificate provided by the server (i.e. anything taken out of peer_cert) - otherwise the server would dictate which CA authorities the client trusts.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ocsp_verifier.rb', line 64

def initialize(host_name, cert, ca_cert, cert_store, **opts)
  @host_name = host_name
  @cert = cert
  @ca_cert = ca_cert
  @cert_store = cert_store
  @options = opts
end

Instance Attribute Details

#ca_cert (readonly)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ocsp_verifier.rb', line 72

attr_reader :host_name, :cert, :ca_cert, :cert_store, :options

#cert (readonly)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ocsp_verifier.rb', line 72

attr_reader :host_name, :cert, :ca_cert, :cert_store, :options

#cert_store (readonly)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ocsp_verifier.rb', line 72

attr_reader :host_name, :cert, :ca_cert, :cert_store, :options

#host_name (readonly)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ocsp_verifier.rb', line 72

attr_reader :host_name, :cert, :ca_cert, :cert_store, :options

#options (readonly)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ocsp_verifier.rb', line 72

attr_reader :host_name, :cert, :ca_cert, :cert_store, :options

Instance Method Details

#cert_id

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ocsp_verifier.rb', line 100

def cert_id
  @cert_id ||= OpenSSL::OCSP::CertificateId.new(
    cert,
    ca_cert,
    OpenSSL::Digest.new('SHA1')
  )
end

#do_verify (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ocsp_verifier.rb', line 138

def do_verify
  # This synchronized array contains definitive pass/fail responses
  # obtained from the responders. We'll take the first one but due to
  # concurrency multiple responses may be produced and queued.
  @resp_queue = Queue.new

  # This synchronized array contains strings, one per responder, that
  # explain why each responder hasn't produced a definitive response.
  # These are concatenated and logged if none of the responders produced
  # a definitive respnose, or if the main thread times out waiting for
  # a definitive response (in which case some of the worker threads'
  # diagnostics may be logged and some may not).
  @resp_errors = Queue.new

  @req = OpenSSL::OCSP::Request.new
  @req.add_certid(cert_id)
  @req.add_nonce
  @serialized_req = @req.to_der

  @outstanding_requests = ocsp_uris.count
  @outstanding_requests_lock = Mutex.new

  threads = ocsp_uris.map do |uri|
    Thread.new do
      verify_one_responder(uri)
    end
  end

  resp = begin
    ::Timeout.timeout(timeout) do
      @resp_queue.shift
    end
  rescue ::Timeout::Error
    nil
  end

  threads.map(&:kill)
  threads.map(&:join)

  [ resp, @resp_errors ]
end

#handle_exceptions (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ocsp_verifier.rb', line 303

def handle_exceptions
  yield
rescue Error::ServerCertificateRevoked
  raise
rescue StandardError => e
  Utils.warn_bg_exception(
    "Error performing OCSP verification for '#{host_name}'",
    e,
    **options
  )
  false
end

#ocsp_urisArray<String>

Returns:

  • (Array<String>)

    OCSP URIs in the specified server certificate.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ocsp_verifier.rb', line 79

def ocsp_uris
  @ocsp_uris ||= begin
    # https://tools.ietf.org/html/rfc3546#section-2.3
    # prohibits multiple extensions with the same oid.
    ext = cert.extensions.detect do |ext|
      ext.oid == 'authorityInfoAccess'
    end

    if ext
      # Our test certificates have multiple OCSP URIs.
      ext.value.split("\n").select do |line|
        line.start_with?('OCSP - URI:')
      end.map do |line|
        line.split(':', 2).last
      end
    else
      []
    end
  end
end

#raise_revoked_error(resp) (private)

Raises:

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ocsp_verifier.rb', line 316

def raise_revoked_error(resp)
  redirect = if resp.uri == resp.original_uri
               ''
             else
               " (redirected from #{resp.original_uri})"
             end
  raise Error::ServerCertificateRevoked,
        "TLS certificate of '#{host_name}' has been revoked according to '#{resp.uri}'#{redirect} for reason '#{resp.revocation_reason}' at '#{resp.revocation_time}'"
end

#report_response_body(body) (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ocsp_verifier.rb', line 334

def report_response_body(body)
  if body
    ": #{body}"
  else
    ''
  end
end

#report_uri(original_uri, uri) (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ocsp_verifier.rb', line 326

def report_uri(original_uri, uri)
  if URI(uri) == URI(original_uri)
    uri
  else
    "#{original_uri} (redirected to #{uri})"
  end
end

#return_ocsp_response(resp, errors = nil) (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ocsp_verifier.rb', line 284

def return_ocsp_response(resp, errors = nil)
  if resp
    raise_revoked_error(resp) if resp.cert_status == OpenSSL::OCSP::V_CERTSTATUS_REVOKED
    true
  else
    reasons = []
    errors.length.times do
      reasons << errors.shift
    end
    msg = if reasons.empty?
            "No responses from responders: #{ocsp_uris.join(', ')} within #{timeout} seconds"
          else
            "For responders #{ocsp_uris.join(', ')} with a timeout of #{timeout} seconds: #{reasons.join(', ')}"
          end
    log_warn("TLS certificate of '#{host_name}' could not be definitively verified via OCSP: #{msg}")
    false
  end
end

#timeout

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ocsp_verifier.rb', line 74

def timeout
  options[:timeout] || 5
end

#verifytrue | false

Returns:

  • (true | false)

    Whether the certificate was verified.

Raises:

Since:

  • 2.0.0

[ GitHub ]

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

def verify
  handle_exceptions do
    return false if ocsp_uris.empty?

    resp, errors = do_verify
    return_ocsp_response(resp, errors)
  end
end

#verify_one_responder(uri) (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ocsp_verifier.rb', line 180

def verify_one_responder(uri)
  original_uri = uri
  redirect_count = 0
  http_response = nil
  loop do
    begin
      uri = URI(uri)
      http_response = Net::HTTP.start(uri.hostname, uri.port) do |http|
        path = uri.path
        path = '/' if path.empty?
        http.post(path, @serialized_req,
                  'content-type' => 'application/ocsp-request')
      end
    rescue IOError, SystemCallError => e
      @resp_errors << "OCSP request to #{report_uri(original_uri, uri)} failed: #{e.class}: #{e}"
      return false
    end

    code = http_response.code.to_i
    if (300..399).include?(code)
      redirected_uri = http_response.header['location']
      uri = ::URI.join(uri, redirected_uri)
      redirect_count += 1
      if redirect_count > 5
        @resp_errors << "OCSP request to #{report_uri(original_uri, uri)} failed: too many redirects (6)"
        return false
      end
      next
    end

    if code >= 400
      @resp_errors << ("OCSP request to #{report_uri(original_uri,
                                                     uri)} failed with HTTP status code #{http_response.code}" + report_response_body(http_response.body))
      return false
    end

    if code != 200
      # There must be a body provided with the response, if one isn't
      # provided the response cannot be verified.
      @resp_errors << ("OCSP request to #{report_uri(original_uri,
                                                     uri)} failed with unexpected HTTP status code #{http_response.code}" + report_response_body(http_response.body))
      return false
    end

    break
  end

  resp = OpenSSL::OCSP::Response.new(http_response.body)
  unless resp.basic
    @resp_errors << "OCSP response from #{report_uri(original_uri, uri)} is #{resp.status}: #{resp.status_string}"
    return false
  end
  resp = resp.basic
  unless resp.verify([ ca_cert ], cert_store)
    # Ruby's OpenSSL binding discards error information - see
    # https://github.com/ruby/openssl/issues/395
    @resp_errors << "OCSP response from #{report_uri(original_uri,
                                                     uri)} failed signature verification; set `OpenSSL.debug = true` to see why"
    return false
  end

  if @req.check_nonce(resp) == 0
    @resp_errors << "OCSP response from #{report_uri(original_uri, uri)} included invalid nonce"
    return false
  end

  single_response = resp.find_response(cert_id)
  unless single_response
    @resp_errors << "OCSP response from #{report_uri(original_uri,
                                                     uri)} did not include information about the requested certificate"
    return false
  end
  resp = Response.new(single_response, uri, original_uri)

  unless resp.check_validity
    @resp_errors << "OCSP response from #{report_uri(original_uri,
                                                     uri)} was invalid: this_update was in the future or next_update time has passed"
    return false
  end

  unless [
    OpenSSL::OCSP::V_CERTSTATUS_GOOD,
    OpenSSL::OCSP::V_CERTSTATUS_REVOKED,
  ].include?(resp.cert_status)
    @resp_errors << "OCSP response from #{report_uri(original_uri,
                                                     uri)} had a non-definitive status: #{resp.cert_status}"
    return false
  end

  # Note this returns the redirected URI
  @resp_queue << resp
rescue StandardError => e
  Utils.warn_bg_exception("Error performing OCSP verification for '#{host_name}' via '#{uri}'", e,
                          logger: options[:logger],
                          log_prefix: options[:log_prefix],
                          bg_error_backtrace: options[:bg_error_backtrace])
  false
ensure
  @outstanding_requests_lock.synchronize do
    @outstanding_requests -= 1
    @resp_queue << nil if @outstanding_requests == 0
  end
end

#verify_with_cache

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/socket/ocsp_verifier.rb', line 108

def verify_with_cache
  handle_exceptions do
    return false if ocsp_uris.empty?

    resp = OcspCache.get(cert_id)
    return return_ocsp_response(resp) if resp

    resp, errors = do_verify

    OcspCache.set(cert_id, resp) if resp

    return_ocsp_response(resp, errors)
  end
end