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 50 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 90 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 69 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 64 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 121 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 98 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 132 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 303 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 317 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 334 def report_response_body(body) if body ": #{body}" else '' end end
# 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
# File lib/mongo/socket/ocsp_verifier.rb, line 282 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 174 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