class Mongo::Socket::OcspVerifier
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 URI
(following up to 5 redirects) to verify the certificate status.
@see ruby-doc.org/stdlib/libdoc/openssl/rdoc/OpenSSL/OCSP.html
@api private
Attributes
Public Class Methods
@param [ String ] host_name
The host name being verified, for
diagnostic output.
@param [ OpenSSL::X509::Certificate ] cert The certificate presented by
the server at host_name.
@param [ OpenSSL::X509::Certificate ] ca_cert
The CA certificate
presented by the server or resolved locally from the server certificate.
@param [ OpenSSL::X509::Store ] cert_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.
# File lib/mongo/socket/ocsp_verifier.rb, line 48 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
Public Instance Methods
# File lib/mongo/socket/ocsp_verifier.rb, line 88 def cert_id @cert_id ||= OpenSSL::OCSP::CertificateId.new( cert, ca_cert, OpenSSL::Digest::SHA1.new, ) end
@return [ Array<String> ] OCSP URIs in the specified server certificate.
# File lib/mongo/socket/ocsp_verifier.rb, line 67 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
# File lib/mongo/socket/ocsp_verifier.rb, line 62 def timeout options[:timeout] || 5 end
@return [ true | false ] Whether the certificate was verified.
@raise [ Error::ServerCertificateRevoked
] If the certificate was
definitively revoked.
# File lib/mongo/socket/ocsp_verifier.rb, line 119 def verify handle_exceptions do return false if ocsp_uris.empty? resp, errors = do_verify return_ocsp_response(resp, errors) end end
# File lib/mongo/socket/ocsp_verifier.rb, line 96 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
Private Instance Methods
# File lib/mongo/socket/ocsp_verifier.rb, line 130 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
# File lib/mongo/socket/ocsp_verifier.rb, line 328 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
# File lib/mongo/socket/ocsp_verifier.rb, line 342 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
# File lib/mongo/socket/ocsp_verifier.rb, line 359 def report_response_body(body) if body ": #{body}" else '' end end
# File lib/mongo/socket/ocsp_verifier.rb, line 351 def report_uri(original_uri, uri) if URI(uri) == URI(original_uri) uri else "#{original_uri} (redirected to #{uri})" end end
# File lib/mongo/socket/ocsp_verifier.rb, line 307 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
# File lib/mongo/socket/ocsp_verifier.rb, line 172 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).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 if resp.respond_to?(:find_response) # Ruby 2.4+ resp = resp.find_response(cert_id) # 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 else # Ruby 2.3 found = nil resp.status.each do |_cert_id, cert_status, revocation_reason, revocation_time, this_update, next_update, extensions| if _cert_id.cmp(cert_id) found = OpenStruct.new( cert_status: cert_status, certid: _cert_id, next_update: next_update, this_update: this_update, revocation_reason: revocation_reason, revocation_time: revocation_time, extensions: extensions, uri: uri, original_uri: original_uri, ) class << found # Unlike the stdlib method, this one doesn't accept # any arguments. def check_validity now = Time.now this_update <= now && next_update >= now end end break end end resp = found end unless resp @resp_errors << "OCSP response from #{report_uri(original_uri, uri)} did not include information about the requested certificate" return false 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