lib/r509/ocsp/signer.rb in r509-ocsp-responder-0.3.1 vs lib/r509/ocsp/signer.rb in r509-ocsp-responder-0.3.2

- old
+ new

@@ -2,243 +2,243 @@ require 'r509/exceptions' require 'r509/config' require 'dependo' # OCSP related classes (signing, response, request) -module R509::Ocsp - # A class for signing OCSP responses - class Signer - attr_reader :validity_checker,:request_checker +module R509::OCSP + # A class for signing OCSP responses + class Signer + attr_reader :validity_checker,:request_checker - # @option options [Boolean] :copy_nonce copy nonce from request to response? - # @option options [R509::Config::CaConfigPool] :configs CaConfigPool object - # possible OCSP issuance roots that we want to issue OCSP responses for - def initialize(options) - if options.has_key?(:validity_checker) - @validity_checker = options[:validity_checker] - else - @validity_checker = R509::Validity::DefaultChecker.new - end - @request_checker = Helper::RequestChecker.new(options[:configs], @validity_checker) - @response_signer = Helper::ResponseSigner.new(options) - end + # @option options [Boolean] :copy_nonce copy nonce from request to response? + # @option options [R509::Config::CAConfigPool] :configs CAConfigPool object + # possible OCSP issuance roots that we want to issue OCSP responses for + def initialize(options) + if options.has_key?(:validity_checker) + @validity_checker = options[:validity_checker] + else + @validity_checker = R509::Validity::DefaultChecker.new + end + @request_checker = Helper::RequestChecker.new(options[:configs], @validity_checker) + @response_signer = Helper::ResponseSigner.new(options) + end - # @param request [String,OpenSSL::OCSP::Request] OCSP request (string or parsed object) - # @return [Hash] - # * :request [OpenSSL::OCSP::Request] parsed request object - # * :response [OpenSSL::OCSP::Response] full response object - def handle_request(request) - begin - parsed_request = OpenSSL::OCSP::Request.new request - rescue - return {:response => @response_signer.create_response(OpenSSL::OCSP::RESPONSE_STATUS_MALFORMEDREQUEST), :request => nil} - end + # @param request [String,OpenSSL::OCSP::Request] OCSP request (string or parsed object) + # @return [Hash] + # * :request [OpenSSL::OCSP::Request] parsed request object + # * :response [OpenSSL::OCSP::Response] full response object + def handle_request(request) + begin + parsed_request = OpenSSL::OCSP::Request.new request + rescue + return {:response => @response_signer.create_response(OpenSSL::OCSP::RESPONSE_STATUS_MALFORMEDREQUEST), :request => nil} + end - statuses = @request_checker.check_statuses(parsed_request) - if not @request_checker.validate_statuses(statuses) - return {:response => @response_signer.create_response(OpenSSL::OCSP::RESPONSE_STATUS_UNAUTHORIZED), :request => nil} - end + statuses = @request_checker.check_statuses(parsed_request) + if not @request_checker.validate_statuses(statuses) + return {:response => @response_signer.create_response(OpenSSL::OCSP::RESPONSE_STATUS_UNAUTHORIZED), :request => nil} + end - basic_response = @response_signer.create_basic_response(parsed_request,statuses) + basic_response = @response_signer.create_basic_response(parsed_request,statuses) - {:response => @response_signer.create_response( - OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, - basic_response - ), :request => parsed_request} - end - + {:response => @response_signer.create_response( + OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, + basic_response + ), :request => parsed_request} end + + end end #Helper module for OCSP handling -module R509::Ocsp::Helper - # checks requests for validity against a set of configs - class RequestChecker - include Dependo::Mixin - attr_reader :configs,:configs_hash +module R509::OCSP::Helper + # checks requests for validity against a set of configs + class RequestChecker + include Dependo::Mixin + attr_reader :configs,:configs_hash - # @param [R509::Config::CaConfigPool] configs CaConfigPool object - # @param [R509::Validity::Checker] validity_checker an implementation of the R509::Validity::Checker class - def initialize(configs, validity_checker) - unless configs.kind_of?(R509::Config::CaConfigPool) - raise R509::R509Error, "Must pass R509::Config::CaConfigPool object" - end - if configs.all.empty? - raise R509::R509Error, "Must be at least one R509::Config object" - end - @configs = configs.all - test_cid = OpenSSL::OCSP::CertificateId.new(OpenSSL::X509::Certificate.new,OpenSSL::X509::Certificate.new) - if test_cid.respond_to?(:issuer_key_hash) - @configs_hash = {} - @configs.each do |config| - ee_cert = OpenSSL::X509::Certificate.new - ee_cert.issuer = config.ca_cert.cert.subject - # per RFC 5019 - # Clients MUST use SHA1 as the hashing algorithm for the - # CertID.issuerNameHash and the CertID.issuerKeyHash values. - # so we can safely assume that our inbound hashes will be SHA1 - issuer_certid = OpenSSL::OCSP::CertificateId.new(ee_cert,config.ca_cert.cert,OpenSSL::Digest::SHA1.new) - @configs_hash[issuer_certid.issuer_key_hash] = config - end - end - @validity_checker = validity_checker - if @validity_checker.nil? - raise R509::R509Error, "Must supply a R509::Validity::Checker" - end - if not @validity_checker.respond_to?(:check) - raise R509::R509Error, "The validity checker must have a check method" - end + # @param [R509::Config::CAConfigPool] configs CAConfigPool object + # @param [R509::Validity::Checker] validity_checker an implementation of the R509::Validity::Checker class + def initialize(configs, validity_checker) + unless configs.kind_of?(R509::Config::CAConfigPool) + raise R509::R509Error, "Must pass R509::Config::CAConfigPool object" + end + if configs.all.empty? + raise R509::R509Error, "Must be at least one R509::Config object" + end + @configs = configs.all + test_cid = OpenSSL::OCSP::CertificateId.new(OpenSSL::X509::Certificate.new,OpenSSL::X509::Certificate.new) + if test_cid.respond_to?(:issuer_key_hash) + @configs_hash = {} + @configs.each do |config| + ee_cert = OpenSSL::X509::Certificate.new + ee_cert.issuer = config.ca_cert.cert.subject.name + # per RFC 5019 + # Clients MUST use SHA1 as the hashing algorithm for the + # CertID.issuerNameHash and the CertID.issuerKeyHash values. + # so we can safely assume that our inbound hashes will be SHA1 + issuer_certid = OpenSSL::OCSP::CertificateId.new(ee_cert,config.ca_cert.cert,OpenSSL::Digest::SHA1.new) + @configs_hash[issuer_certid.issuer_key_hash] = config end + end + @validity_checker = validity_checker + if @validity_checker.nil? + raise R509::R509Error, "Must supply a R509::Validity::Checker" + end + if not @validity_checker.respond_to?(:check) + raise R509::R509Error, "The validity checker must have a check method" + end + end - # Loads and checks a raw OCSP request - # - # @param request [OpenSSL::OCSP::Request] OpenSSL OCSP Request object - # @return [Hash] hash from the check_status method - def check_statuses(request) - request.certid.map { |certid| - if certid.respond_to?(:issuer_key_hash) - validated_config = @configs_hash[certid.issuer_key_hash] - else - validated_config = @configs.find do |config| - #we need to create an OCSP::CertificateId object that has the right - #issuer so we can pass it to #cmp_issuer. This is annoying because - #CertificateId wants a cert and its issuer, but we don't want to - #force users to provide an end entity cert just to make this comparison - #work. So, we create a fake new cert and pass it in. - ee_cert = OpenSSL::X509::Certificate.new - ee_cert.issuer = config.ca_cert.cert.subject - issuer_certid = OpenSSL::OCSP::CertificateId.new(ee_cert,config.ca_cert.cert) - certid.cmp_issuer(issuer_certid) - end - end - - log.info "#{validated_config.ca_cert.subject.to_s} found for issuer" if validated_config - check_status(certid, validated_config) - } + # Loads and checks a raw OCSP request + # + # @param request [OpenSSL::OCSP::Request] OpenSSL OCSP Request object + # @return [Hash] hash from the check_status method + def check_statuses(request) + request.certid.map { |certid| + if certid.respond_to?(:issuer_key_hash) + validated_config = @configs_hash[certid.issuer_key_hash] + else + validated_config = @configs.find do |config| + #we need to create an OCSP::CertificateId object that has the right + #issuer so we can pass it to #cmp_issuer. This is annoying because + #CertificateId wants a cert and its issuer, but we don't want to + #force users to provide an end entity cert just to make this comparison + #work. So, we create a fake new cert and pass it in. + ee_cert = OpenSSL::X509::Certificate.new + ee_cert.issuer = config.ca_cert.cert.subject + issuer_certid = OpenSSL::OCSP::CertificateId.new(ee_cert,config.ca_cert.cert) + certid.cmp_issuer(issuer_certid) + end end - # Determines whether the statuses constitute a request that is compliant. - # No config means we don't know the CA, different configs means there are - # requests from two different CAs in there. Both are invalid. - # - # @param statuses [Array<Hash>] array of hashes from check_statuses - # @return [Boolean] - def validate_statuses(statuses) - validity = true - config = nil + log.info "#{validated_config.ca_cert.subject.to_s} found for issuer" if validated_config + check_status(certid, validated_config) + } + end - statuses.each do |status| - if status[:config].nil? - validity = false - end - if config.nil? - config = status[:config] - end - if config != status[:config] - validity = false - end - end + # Determines whether the statuses constitute a request that is compliant. + # No config means we don't know the CA, different configs means there are + # requests from two different CAs in there. Both are invalid. + # + # @param statuses [Array<Hash>] array of hashes from check_statuses + # @return [Boolean] + def validate_statuses(statuses) + validity = true + config = nil - validity + statuses.each do |status| + if status[:config].nil? + validity = false end - - private - - # Checks the status of a certificate with the corresponding CA - # @param certid [OpenSSL::OCSP::CertificateId] The certId object from check_statuses - # @param validated_config [R509::Config] - def check_status(certid, validated_config) - if(validated_config == nil) then - return { - :certid => certid, - :config => nil - } - else - validity_status = @validity_checker.check(validated_config.ca_cert.subject.to_s,certid.serial) - return { - :certid => certid, - :status => validity_status.ocsp_status, - :revocation_reason => validity_status.revocation_reason, - :revocation_time => validity_status.revocation_time, - :config => validated_config - } - end + if config.nil? + config = status[:config] end + if config != status[:config] + validity = false + end + end + + validity end - #signs OCSP responses - class ResponseSigner - # @option options [Boolean] :copy_nonce - def initialize(options) - if options.has_key?(:copy_nonce) - @copy_nonce = options[:copy_nonce] - else - @copy_nonce = false - end - end + private - # It is UNWISE to call this method directly because it assumes that the request is - # validated. You probably want to take a look at R509::Ocsp::Signer#handle_request - # - # @param request [OpenSSL::OCSP::Request] - # @param statuses [Hash] hash from R509::Ocsp::Helper::RequestChecker#check_statuses - # @return [OpenSSL::OCSP::BasicResponse] - def create_basic_response(request,statuses) - basic_response = OpenSSL::OCSP::BasicResponse.new + # Checks the status of a certificate with the corresponding CA + # @param certid [OpenSSL::OCSP::CertificateId] The certId object from check_statuses + # @param validated_config [R509::Config] + def check_status(certid, validated_config) + if(validated_config == nil) then + return { + :certid => certid, + :config => nil + } + else + validity_status = @validity_checker.check(validated_config.ca_cert.subject.to_s,certid.serial) + return { + :certid => certid, + :status => validity_status.ocsp_status, + :revocation_reason => validity_status.revocation_reason, + :revocation_time => validity_status.revocation_time, + :config => validated_config + } + end + end + end - basic_response.copy_nonce(request) if @copy_nonce + #signs OCSP responses + class ResponseSigner + # @option options [Boolean] :copy_nonce + def initialize(options) + if options.has_key?(:copy_nonce) + @copy_nonce = options[:copy_nonce] + else + @copy_nonce = false + end + end - statuses.each do |status| - #revocation time is retarded and is relative to now, so - #let's figure out what that is. - if status[:status] == OpenSSL::OCSP::V_CERTSTATUS_REVOKED - revocation_time = status[:revocation_time].to_i - Time.now.to_i - end - basic_response.add_status(status[:certid], - status[:status], - status[:revocation_reason], - revocation_time, - -1*status[:config].ocsp_start_skew_seconds, - status[:config].ocsp_validity_hours*3600, - [] #array of OpenSSL::X509::Extensions - ) - end + # It is UNWISE to call this method directly because it assumes that the request is + # validated. You probably want to take a look at R509::OCSP::Signer#handle_request + # + # @param request [OpenSSL::OCSP::Request] + # @param statuses [Hash] hash from R509::OCSP::Helper::RequestChecker#check_statuses + # @return [OpenSSL::OCSP::BasicResponse] + def create_basic_response(request,statuses) + basic_response = OpenSSL::OCSP::BasicResponse.new - #this method assumes the request data is validated by validate_request so all configs will be the same and - #we can choose to use the first one safely - config = statuses[0][:config] + basic_response.copy_nonce(request) if @copy_nonce - #confusing, but R509::Cert contains R509::PrivateKey under #key. PrivateKey#key gives the OpenSSL object - #turns out BasicResponse#sign can take up to 4 params - #cert, key, array of OpenSSL::X509::Certificates, flags (not sure what the enumeration of those are) - basic_response.sign(config.ocsp_cert.cert,config.ocsp_cert.key.key,config.ocsp_chain) + statuses.each do |status| + #revocation time is retarded and is relative to now, so + #let's figure out what that is. + if status[:status] == OpenSSL::OCSP::V_CERTSTATUS_REVOKED + revocation_time = status[:revocation_time].to_i - Time.now.to_i end + basic_response.add_status(status[:certid], + status[:status], + status[:revocation_reason], + revocation_time, + -1*status[:config].ocsp_start_skew_seconds, + status[:config].ocsp_validity_hours*3600, + [] #array of OpenSSL::X509::Extensions + ) + end - # Builds final response. - # - # @param response_status [OpenSSL::OCSP::RESPONSE_STATUS_*] the primary response status - # @param basic_response [OpenSSL::OCSP::BasicResponse] an optional basic response object - # generated by create_basic_response - # @return [OpenSSL::OCSP::OCSPResponse] - def create_response(response_status,basic_response=nil) + #this method assumes the request data is validated by validate_request so all configs will be the same and + #we can choose to use the first one safely + config = statuses[0][:config] - # first arg is the response status code, comes from this list - # these can also be enumerated via OpenSSL::OCSP::RESPONSE_STATUS_* - #OCSPResponseStatus ::= ENUMERATED { - # successful (0), --Response has valid confirmations - # malformedRequest (1), --Illegal confirmation request - # internalError (2), --Internal error in issuer - # tryLater (3), --Try again later - # --(4) is not used - # sigRequired (5), --Must sign the request - # unauthorized (6) --Request unauthorized - #} - # - R509::Ocsp::Response.new( - OpenSSL::OCSP::Response.create( - response_status, basic_response - ) - ) - end + #confusing, but R509::Cert contains R509::PrivateKey under #key. PrivateKey#key gives the OpenSSL object + #turns out BasicResponse#sign can take up to 4 params + #cert, key, array of OpenSSL::X509::Certificates, flags (not sure what the enumeration of those are) + basic_response.sign(config.ocsp_cert.cert,config.ocsp_cert.key.key,config.ocsp_chain) end + + # Builds final response. + # + # @param response_status [OpenSSL::OCSP::RESPONSE_STATUS_*] the primary response status + # @param basic_response [OpenSSL::OCSP::BasicResponse] an optional basic response object + # generated by create_basic_response + # @return [OpenSSL::OCSP::OCSPResponse] + def create_response(response_status,basic_response=nil) + + # first arg is the response status code, comes from this list + # these can also be enumerated via OpenSSL::OCSP::RESPONSE_STATUS_* + #OCSPResponseStatus ::= ENUMERATED { + # successful (0), --Response has valid confirmations + # malformedRequest (1), --Illegal confirmation request + # internalError (2), --Internal error in issuer + # tryLater (3), --Try again later + # --(4) is not used + # sigRequired (5), --Must sign the request + # unauthorized (6) --Request unauthorized + #} + # + R509::OCSP::Response.new( + OpenSSL::OCSP::Response.create( + response_status, basic_response + ) + ) + end + end end