Ruby script to craft OCSP queries from AIA field of SSL cert(s)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

110 lines
3.0 KiB

#!/usr/bin/env ruby
# Daniel Bowling <swaggboi@slackware.uk>
# Jul 2021
# https://ruby.github.io/openssl/OpenSSL/OCSP.html
require 'openssl'
require 'optparse'
require 'net/http'
require 'uri'
# Vars
cmd = File.basename($PROGRAM_NAME)
digest = OpenSSL::Digest.new('SHA1')
options = {}
root_pems = []
# Parse options
OptionParser.new do |opts|
opts.banner = "Usage: #{cmd} [OPTIONS] <ISSUER CERT> <SUBJECT CERT(S)>"
opts.on('-h', '--help', 'Show this help message') do
puts opts
exit
end
opts.on('-i', '--ignore-signature', "Don\'t validate signatures") do
options[:no_sign] = true
end
opts.on('-n', '--nonce', 'Use nonce (CA must support this)') do
options[:nonce] = true
end
opts.on('-r', '--root=FILE', 'Add root cert to trust chain') do |file|
root_pems.push(file)
end
end.parse!
# Set up the cert store for root CAs
store = OpenSSL::X509::Store.new
store.set_default_paths
# https://curl.se/ca/cacert.pem
root_pems.each { |root_pem| store.add_file(root_pem) } unless root_pems.empty?
# Grab the PEM file names
pem_files = ARGV
abort "#{cmd}: I need two PEM files, try -h or --help" unless pem_files[1]
# Load the issuer cert
issuer = OpenSSL::X509::Certificate.new File.read pem_files.shift
# Loop over all the leaf certs
pem_files.each do |pem_file|
# Load the leaf cert
leaf = OpenSSL::X509::Certificate.new File.read pem_file
abort "cert is expired: #{pem_file}" if Time.now >= leaf.not_after
# We need a certificate ID
certificate_id = OpenSSL::OCSP::CertificateId.new(leaf, issuer, digest)
# Create the request
request = OpenSSL::OCSP::Request.new
request.add_certid(certificate_id)
# Add nonce
request.add_nonce if options[:nonce]
# Extract OCSP URI from the leaf cert
ocsp_uri = URI(leaf.ocsp_uris[0])
ocsp_uri.path = '/' if ocsp_uri.path.empty? # Handle path
# Make the OCSP request
http_response = Net::HTTP.start(ocsp_uri.hostname, ocsp_uri.port) do |http|
http.post(
ocsp_uri.path,
request.to_der,
'content-type' => 'application/ocsp-request'
)
end
# Parse response
response = OpenSSL::OCSP::Response.new(http_response.body)
# Check the signature
abort 'response is not signed by a trusted certificate' \
unless options[:no_sign] || response.basic.verify([leaf, issuer], store)
# Check the nonce
abort 'nonce is less than 1' \
if options[:nonce] && request.check_nonce(response.basic).positive?
# Parse the status out
status_info = response.basic.find_response(certificate_id)
abort 'no status' unless status_info
abort 'invalid status' unless status_info.check_validity
# Grab the CN string and clean it up
cn = leaf.subject.to_s.split('/').last.sub(/CN ?= ?/, '')
# Return the status
case status_info.cert_status
when OpenSSL::OCSP::V_CERTSTATUS_GOOD
puts "#{pem_file} (#{cn}) is valid"
when OpenSSL::OCSP::V_CERTSTATUS_REVOKED
puts "#{pem_file} (#{cn}) is revoked"
when OpenSSL::OCSP::V_CERTSTATUS_UNKNOWN
puts "#{pem_file} (#{cn}) is unknown"
end
end