123456789_123456789_123456789_123456789_123456789_

Class: Mongo::Socket::OcspVerifier Private

Do not use. This class is for internal use only.
Relationships & Source Files
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

Instance Attribute Details

#ca_cert (readonly)

Since:

  • 2.0.0

[ GitHub ]

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

attr_reader :ca_cert

#cert (readonly)

Since:

  • 2.0.0

[ GitHub ]

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

attr_reader :cert

#cert_store (readonly)

Since:

  • 2.0.0

[ GitHub ]

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

attr_reader :cert_store

#host_name (readonly)

Since:

  • 2.0.0

[ GitHub ]

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

attr_reader :host_name

#options (readonly)

Since:

  • 2.0.0

[ GitHub ]

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

attr_reader :options

Instance Method Details

#cert_id

Since:

  • 2.0.0

[ GitHub ]

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

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

#do_verify (private)

Since:

  • 2.0.0

[ GitHub ]

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

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 304

def handle_exceptions
  begin
    yield
  rescue Error::ServerCertificateRevoked
    raise
  rescue => exc
    Utils.warn_bg_exception(
      "Error performing OCSP verification for '#{host_name}'",
      exc,
      **options)
    false
  end
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 70

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 318

def raise_revoked_error(resp)
  if resp.uri == resp.original_uri
    redirect = ''
  else
    redirect = " (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 335

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 327

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 283

def return_ocsp_response(resp, errors = nil)
  if resp
    if resp.cert_status == OpenSSL::OCSP::V_CERTSTATUS_REVOKED
      raise_revoked_error(resp)
    end
    true
  else
    reasons = []
    errors.length.times do
      reasons << errors.shift
    end
    if reasons.empty?
      msg = "No responses from responders: #{ocsp_uris.join(', ')} within #{timeout} seconds"
    else
      msg = "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 65

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 122

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 175

def verify_one_responder(uri)
  original_uri = uri
  redirect_count = 0
  http_response = nil
  loop do
    http_response = begin
      uri = URI(uri)
      Net::HTTP.start(uri.hostname, uri.port) do |http|
        path = uri.path
        if path.empty?
          path = '/'
        end
        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

  resp = resp.find_response(cert_id)
  unless resp
    @resp_errors << "OCSP response from #{report_uri(original_uri, uri)} did not include information about the requested certificate"
    return false
  end
  # TODO make a new class instead of patching the stdlib one?
  resp.instance_variable_set('@uri', uri)
  resp.instance_variable_set('@original_uri', original_uri)
  class << resp
    attr_reader :uri, :original_uri
  end

  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 => exc
  Utils.warn_bg_exception("Error performing OCSP verification for '#{host_name}' via '#{uri}'", exc,
    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
    if @outstanding_requests == 0
      @resp_queue << nil
    end
  end
end

#verify_with_cache

Since:

  • 2.0.0

[ GitHub ]

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

def verify_with_cache
  handle_exceptions do
    return false if ocsp_uris.empty?

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

    resp, errors = do_verify

    if resp
      OcspCache.set(cert_id, resp)
    end

    return_ocsp_response(resp, errors)
  end
end