class DevPKI::CLI

Public Instance Methods

ocsp(*cer_files) click to toggle source
# File lib/devpki/cli/ocsp.rb, line 28
def ocsp(*cer_files)

  raise InvalidOption.new("Please specify at least one CA and subject certificate file.") if cer_files.empty?

  chain_cert_files = []
  if options[:"chain-certs"] != nil
    chain_cert_files=options[:"chain-certs"].split(",")
  end

  ca_subj_map = {}
  cer_files.each do |ca_subj_pair|

    raise InvalidOption.new("\"#{ca_subj_pair}\" is an invalid CA and subject pair. Please pass a pair with format similar to \"ca.cer:a.cer[,b.cer...]\"") if not ca_subj_pair.include?(":")

    ca_subjlist_split = ca_subj_pair.split(":")

    ca_file = ca_subjlist_split.first

    raise InvalidOption.new("No subject certificates specified for CA file \"#{ca_file}\". Please pass a pair with format similar to \"ca.cer:a.cer[,b.cer...]\"") if ca_subjlist_split.length != 2
    subj_files = ca_subjlist_split.last.split(",")

    ca_subj_map[ca_file] = subj_files
  end

  ca_subj_map.each_pair do |ca_file,subj_file_list|
    raise InvalidOption.new("CA certificate file \"#{ca_file}\" does not exist.") if not File.exist?(ca_file)
    subj_file_list.each do |subj_file|
      raise InvalidOption.new("Subject certificate file \"#{subj_file}\" does not exist.") if not File.exist?(subj_file)
    end
  end

  method = options[:method].upcase
  raise InvalidOption.new("GET method not implemented yet.") if method == "GET"

  ###### -------------- move this to DevPKI::OCSP

  cert_ids = []
  store = OpenSSL::X509::Store.new
  chain_cert_files.each do |chain_cert_file|
    puts "Adding #{chain_cert_file} to store"
    store.add_file(chain_cert_file)
  end
  #store.set_default_paths
  #store.add_file("root.crt")

  ca_subj_map.each_pair do |ca_file, subj_file_list|
    ca_cert = OpenSSL::X509::Certificate.new File.read(ca_file)
    store.add_cert(ca_cert)

    subj_file_list.each do |subj_file|
      subj_cert = OpenSSL::X509::Certificate.new File.read(subj_file)

      cert_ids << OpenSSL::OCSP::CertificateId.new(subj_cert, ca_cert)
    end
  end

  request = OpenSSL::OCSP::Request.new
  cert_ids.each do |cert_id|
    request.add_certid(cert_id)
  end

  request_uri = URI(options[:uri])
  request_uri.path = "/" if request_uri.path.to_s.empty?

  http_req = Net::HTTP::Post.new(request_uri.path)
  http_req.content_type = "application/ocsp-request"
  http_req.body = request.to_der

  http_response = Net::HTTP.new(request_uri.host, request_uri.port).start do |http|
    http.request(http_req)
  end

  ## ----

  if http_response.code != "200"
    raise StandardError, "Invalid response code from OCSP responder: #{http_response.code}"
  end

  response = OpenSSL::OCSP::Response.new(http_response.body)
  puts "Status: #{response.status}"
  puts "Status string: #{response.status_string}"

  # Statuses from http://tools.ietf.org/html/rfc2560 section 4.2.1
  #
  # 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
  if response.status != 0
    raise StandardError, "Not a successful status"
  end

  # response.basic.status will be populated, if response.status == 0
  cert_ids.each_with_index do |cert_id,ix|

    # SingleResponse structure from http://tools.ietf.org/html/rfc2560 section 4.2.1
    #
    # SingleResponse ::= SEQUENCE {
    # certID                       CertID,
    # certStatus                   CertStatus,
    # thisUpdate                   GeneralizedTime,
    # nextUpdate         [0]       EXPLICIT GeneralizedTime OPTIONAL,
    # singleExtensions   [1]       EXPLICIT Extensions OPTIONAL }
    # single_response = response.basic.status[ix]

    # Find the single response that matches current cert_id
    single_response = nil
    response.basic.status.each do |single_response_candidate|
      if single_response_candidate[0].serial.to_s == cert_id.serial.to_s
        single_response = single_response_candidate
        break
      end
    end

    raise StandardError.new("SingleResponse for certificate s/n ##{cert_id.serial.to_s} not found.") if single_response == nil

    # CertStatus from from http://tools.ietf.org/html/rfc2560 section 4.2.1
    #
    # CertStatus ::= CHOICE {
    # good        [0]     IMPLICIT NULL,
    # revoked     [1]     IMPLICIT RevokedInfo,
    # unknown     [2]     IMPLICIT UnknownInfo }
    if single_response[1] != 0
      raise StandardError, "CertStatus for cert s/n #{cert_id.serial.to_s} is #{single_response[1]}"
    end

    current_time = Time.now
    if single_response[4] > current_time or single_response[5] < current_time
      raise StandardError, "The response for cert_id s/n #{cert_id.serial.to_s} is not within its validity window"
    end
  end

  if response.basic.verify([],store) != true
    binding.pry
    raise StandardError, "Response not signed by a trusted certificate"
  end

end