Skip to content

Instantly share code, notes, and snippets.

@akadata
Forked from meineerde/LICENSE.txt
Created December 14, 2018 01:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save akadata/d0696dc0662299f98602845ff9cf7128 to your computer and use it in GitHub Desktop.
Save akadata/d0696dc0662299f98602845ff9cf7128 to your computer and use it in GitHub Desktop.
Update existing OCSP responses in HAProxy. Assuming you have all your SSL certificates in one directory, you can simply call `haproxy_update_ocsp /path/to/haproxy/certificates` with cron every couple of hours. For new certificates, call `haproxy_ocsp /path/to/haproxy/certificates/server.pem` once to fetch the initial OCSP response.
#!/usr/bin/env ruby
# Copyright 2015 Holger Just, Planio GmbH
#
# This software may be modified and distributed under the terms
# of the MIT license. See the LICENSE.txt file for details.
require 'net/http'
require 'openssl'
require 'uri'
def usage(stream=STDOUT)
stream.puts <<-EOF
Usage: #{$0} [--do-not-verify] haproxy_cert_file.pem
Queries the OCSP server specified in the leaf certificate and writes the OCSP
response into a file named the same as the one given with .ocsp appended, e.g.
haproxy_cert_file.pem.ocsp.
If --do-not-verify is given, we don't verify the OCSP response against the
certificate chain and do not ensure proper timestamps. This is the
responsibility of the checking client. We still check the response status in
any case.
Return codes:
0 - All OK, OCSP file was correctly generated / updated
1 - wrong command line usage or unknown error
2 - could not extract certificates or OCSP URL
3 - Error in OCSP response
EOF
end
def parts(pem_file)
data = File.read(pem_file).split(/^(?=-----BEGIN [^-]+-----$)/).map(&:strip)
parts = {}
parts[:crts], parts[:keys] = data.partition do |part|
part.start_with? '-----BEGIN CERTIFICATE-----'
end
parts
end
def ocsp_uri(crt)
crt = OpenSSL::X509::Certificate.new(crt) unless crt.is_a? OpenSSL::X509::Certificate
authority_info_access = crt.extensions.find do |extension|
extension.oid == 'authorityInfoAccess'
end
# we might now have a valid OCSP URL in the certificate
return unless authority_info_access
descriptions = authority_info_access.value.split "\n"
ocsp = descriptions.find do |description|
description.start_with? 'OCSP'
end
URI ocsp[/URI:(.*)/, 1]
end
def verify_ocsp_response(ocsp_request, ocsp_response, certificates, options={})
basic_response = ocsp_response.basic
# verify the OCSP response, trust the given certificates in the chain
store = OpenSSL::X509::Store.new
store.set_default_paths
certificates.each do |crt|
store.add_cert OpenSSL::X509::Certificate.new crt
end
status = {}
status[:response_certificate_id], status[:status], status[:reason],
status[:revocation_time], status[:this_update], status[:next_update],
status[:extensions] = basic_response.status.first
now = Time.now
# Check various properties of the OCSP response in order to ensure it is valid
unless status[:status] == OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL
return false, "Could not get OCSP response. Status was: #{ocsp_response.status_string}"
end
unless ocsp_request.certid.all?{|id| id.cmp(status[:response_certificate_id]) }
return false, "Certificate ID mismatch in OCSP response!"
end
unless options[:do_not_verify]
unless basic_response.verify [], store
return false, "OCSP response is not signed by a trusted certificate"
end
if status[:this_update] > (now + 60) # add 1 minute grace period
return false, "Update date in OCSP response is too much in the future!"
end
if now > status[:next_update]
return false, "Next update time in OCSP response has passed!"
end
end
true
end
if ARGV.any?{ |arg| arg == '--help' }
usage(STDOUT)
exit 0
end
options = {}
if ARGV[0] == '--do-not-verify' && ARGV.count == 2
options[:do_not_verify] = true
pem_file = ARGV[1]
elsif ARGV.count == 1
pem_file = ARGV[0]
else
usage(STDERR)
exit 1
end
unless File.readable?(pem_file)
STDERR.puts "[#{pem_file}] Could not read #{pem_file}"
exit 2
end
crts = parts(pem_file)
# Subject is the first certificate in the pem file
if crts[:crts].length >= 1
subject = OpenSSL::X509::Certificate.new crts[:crts][0]
else
STDERR.puts "[#{pem_file}] Could not extract certificate from #{pem_file}"
exit 2
end
# The issuer is either the second crt in the pem file or the one in
# haproxy_cert_file.pem.issuer
if crts[:crts].length >= 2
issuer = OpenSSL::X509::Certificate.new crts[:crts][1]
else
if File.readable?("#{pem_file}.issuer") && issuer_crt = parts("#{pem_file}.issuer")[:crts][0]
issuer = OpenSSL::X509::Certificate.new issuer_crt
else
STDERR.puts "[#{pem_file}] Could not extract issuer certificate from #{pem_file} or #{pem_file}.issuer"
exit 2
end
end
# generate the certificate ID for the subject crt
digest = OpenSSL::Digest::SHA1.new
certificate_id = OpenSSL::OCSP::CertificateId.new subject, issuer, digest
# create an OCSP request for this certificate ID
ocsp_request = OpenSSL::OCSP::Request.new
ocsp_request.add_certid certificate_id
uri = ocsp_uri(subject)
unless uri
STDERR.puts "[#{pem_file}] Could not extract OCSP query URI from the certificate."
exit 2
end
uri.path = '/' if uri.path.empty?
failed_http_requests = 0
begin
http_response = Net::HTTP.start uri.hostname, uri.port, :use_ssl => (uri.scheme == 'https') do |http|
http.post uri.path, ocsp_request.to_der, 'Content-Type' => 'application/ocsp-request'
end
http_response.value # raise exception if the response wasn't a success
ocsp_response = OpenSSL::OCSP::Response.new http_response.body
is_valid, error = verify_ocsp_response(ocsp_request, ocsp_response, crts[:crts], options)
unless is_valid
STDERR.puts "[#{pem_file}] #{error}"
exit 3
end
rescue => e
if failed_http_requests < 3
sleep 2 ** failed_http_requests
failed_http_requests += 1
retry
else
raise
end
end
# Finally, if all went well, we can write the OCSP response
File.open("#{pem_file}.ocsp", 'wb') do |f|
f.write ocsp_response.to_der
end
#!/bin/bash
# Copyright 2015 Holger Just, Planio GmbH
#
# This software may be modified and distributed under the terms
# of the MIT license. See the LICENSE.txt file for details.
set -eu -o pipefail
# Adapt the script and add the path to the haproxy stats socket here
# as configured in your haproxy config
HAPROXY_STATS_SOCKET="/var/lib/haproxy/stats.socket"
usage() {
echo "Usage: $0 [--do-not-verify] /path/to/haproxy/certificates" 1>&2
}
if [ "$1" = "--do-not-verify" ]; then
options="--do-not-verify"
shift
else
options=""
fi
[ $# -eq 1 ] || { usage; exit 1; }
update_ocsp() {
local ocsp_file="$1"
cert_file="${ocsp_file%.ocsp}"
haproxy_ocsp $options "${cert_file}"
}
update_haproxy() {
local ocsp_file="$1"
echo "set ssl ocsp-response $(base64 -w 0 ${ocsp_file})" | \
socat stdio "$HAPROXY_STATS_SOCKET"
}
RETURN=0
while read -d '' -r ocsp_file; do
echo "Updating ${ocsp_file}... "
update_ocsp "${ocsp_file}" && update_haproxy "${ocsp_file}" || {
RETURN=1
echo "ERROR updating ${ocsp_file}" 1>&2
}
done < <(find "$1" -name '*.ocsp' -print0)
exit $RETURN
The MIT License (MIT)
Copyright (c) 2015 Holger Just, Planio GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment