From b7ba1baf4836307408e7ae19a70111ed50401f2c Mon Sep 17 00:00:00 2001 From: Tatsuhiro Tsujikawa Date: Sat, 6 Jun 2015 22:53:33 +0900 Subject: [PATCH] Translate fetch-ocsp-response into Python --- Makefile.am | 2 + script/README.rst | 10 ++ script/fetch-ocsp-response | 241 ++++++++++++++++++++++++++++ third-party/Makefile.am | 2 - third-party/h2o/README.rst | 10 -- third-party/h2o/fetch-ocsp-response | 150 ----------------- 6 files changed, 253 insertions(+), 162 deletions(-) create mode 100644 script/README.rst create mode 100755 script/fetch-ocsp-response delete mode 100644 third-party/h2o/README.rst delete mode 100755 third-party/h2o/fetch-ocsp-response diff --git a/Makefile.am b/Makefile.am index 1058e02b..1196473e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -41,3 +41,5 @@ clang-format: $${CLANGFORMAT} -i lib/*.{c,h} lib/includes/nghttp2/*.h \ src/*.{c,cc,h} src/includes/nghttp2/*.h examples/*.{c,cc} \ tests/*.{c,h} + +dist_pkgdata_SCRIPTS = script/fetch-ocsp-response diff --git a/script/README.rst b/script/README.rst new file mode 100644 index 00000000..a9f77377 --- /dev/null +++ b/script/README.rst @@ -0,0 +1,10 @@ +fetch-ocsp-response is a Python script which performs OCSP query and +get response. It uses openssl command under the hood. nghttpx uses +it to enable OCSP stapling feature. + +fetch-ocsp-response is a translation from original fetch-ocsp-response +written in Perl and which has been developed as part of h2o project +(https://github.com/h2o/h2o). + +fetch-ocsp-response is usually installed under $(pkgdatadir), which is +$(prefix)/share/nghttp2. diff --git a/script/fetch-ocsp-response b/script/fetch-ocsp-response new file mode 100755 index 00000000..ddeef068 --- /dev/null +++ b/script/fetch-ocsp-response @@ -0,0 +1,241 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# nghttp2 - HTTP/2 C Library + +# Copyright (c) 2015 Tatsuhiro Tsujikawa + +# 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. + +# This program was translated from the program originally developed by +# h2o project (https://github.com/h2o/h2o), written in Perl. It had +# the following copyright notice: + +# Copyright (c) 2015 DeNA Co., Ltd. +# +# 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. + +from __future__ import unicode_literals +import argparse +import io +import os +import os.path +import re +import shutil +import subprocess +import sys +import tempfile + +# make this program work for both Python 3 and Python 2. +try: + from urllib.parse import urlparse + stdout_bwrite = sys.stdout.buffer.write +except ImportError: + from urlparse import urlparse + stdout_bwrite = sys.stdout.write + + +def die(msg): + sys.stderr.write(msg) + sys.stderr.write('\n') + sys.exit(255) + + +def tempfail(msg): + sys.stderr.write(msg) + sys.stderr.write('\n') + sys.exit(os.EX_TEMPFAIL) + + +def run_openssl(args, allow_tempfail=False): + buf = io.BytesIO() + try: + p = subprocess.Popen(args, stdout=subprocess.PIPE) + except Exception as e: + die('failed to invoke {}:{}'.format(args, e)) + try: + while True: + data = p.stdout.read() + if len(data) == 0: + break + buf.write(data) + if p.wait() != 0: + raise Exception('nonzero return code {}'.format(p.returncode)) + return buf.getvalue() + except Exception as e: + msg = 'OpenSSL exitted abnormally: {}:{}'.format(args, e) + tempfail(msg) if allow_tempfail else die(msg) + + +def read_file(path): + with open(path, 'rb') as f: + return f.read() + + +def write_file(path, data): + with open(path, 'wb') as f: + f.write(data) + + +def detect_openssl_version(cmd): + return run_openssl([cmd, 'version']).decode('utf-8').strip() + + +def extract_ocsp_uri(cmd, cert_fn): + # obtain ocsp uri + ocsp_uri = run_openssl( + [cmd, 'x509', '-in', cert_fn, '-noout', + '-ocsp_uri']).decode('utf-8').strip() + + if not re.match(r'^https?://', ocsp_uri): + die('failed to extract ocsp URI from {}'.format(cert_fn)) + + return ocsp_uri + + +def save_issuer_certificate(issuer_fn, cert_fn): + # save issuer certificate + chain = read_file(cert_fn).decode('utf-8') + m = re.match( + r'.*?-----END CERTIFICATE-----.*?(-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----)', + chain, re.DOTALL) + if not m: + die('--issuer option was not used, and failed to extract issuer certificate from the certificate') + write_file(issuer_fn, (m.group(1) + '\n').encode('utf-8')) + + +def send_and_receive_ocsp(respder_fn, cmd, cert_fn, issuer_fn, ocsp_uri, + ocsp_host, openssl_version): + # obtain response (without verification) + sys.stderr.write('sending OCSP request to {}\n'.format(ocsp_uri)) + args = [ + cmd, 'ocsp', '-issuer', issuer_fn, '-cert', cert_fn, '-url', ocsp_uri + ] + if openssl_version.lower().startswith('openssl 1.'): + args.extend(['-header', 'Host', ocsp_host]) + args.extend(['-noverify', '-respout', respder_fn]) + resp = run_openssl(args, allow_tempfail=True) + + return resp.decode('utf-8') + + +def verify_response(cmd, tempdir, issuer_fn, respder_fn): + # verify the response + sys.stderr.write('verifying the response signature\n') + + verify_fn = os.path.join(tempdir, 'verify.out') + + # try from exotic options + allextra = [ + # for comodo + ['-VAfile', issuer_fn], + # these options are only available in OpenSSL >= 1.0.2 + ['-partial_chain', '-trusted_first', '-CAfile', issuer_fn], + # for OpenSSL <= 1.0.1 + ['-CAfile', issuer_fn], + ] + + for extra in allextra: + with open(verify_fn, 'wb') as f: + args = [cmd, 'ocsp', '-respin', respder_fn] + args.extend(extra) + p = subprocess.Popen(args, stdout=f, stderr=f) + if p.wait() == 0: + sys.stderr.write('verify OK (used: {})\n'.format(extra)) + return True + + sys.stderr.write(read_file(verify_fn).decode('utf-8')) + return False + + +def fetch_ocsp_response(cmd, cert_fn, tempdir, issuer_fn=None): + openssl_version = detect_openssl_version(cmd) + + sys.stderr.write( + 'fetch-ocsp-response (using {})\n'.format(openssl_version)) + + ocsp_uri = extract_ocsp_uri(cmd, cert_fn) + ocsp_host = urlparse(ocsp_uri).hostname + + if not issuer_fn: + issuer_fn = os.path.join(tempdir, 'issuer.crt') + save_issuer_certificate(issuer_fn, cert_fn) + + respder_fn = os.path.join(tempdir, 'resp.der') + resp = send_and_receive_ocsp( + respder_fn, cmd, cert_fn, issuer_fn, ocsp_uri, ocsp_host, + openssl_version) + + sys.stderr.write('{}\n'.format(resp)) + + if not verify_response(cmd, tempdir, issuer_fn, respder_fn): + tempfail('failed to verify the response') + + # success + res = read_file(respder_fn) + stdout_bwrite(res) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description= + '''The command issues an OCSP request for given server certificate, verifies the response and prints the resulting DER.''', + epilog= + '''The command exits 0 if successful, or 75 (EX_TEMPFAIL) on temporary error. Other exit codes may be returned in case of hard errors.''') + parser.add_argument( + '--issuer', + metavar='FILE', + help= + 'issuer certificate (if omitted, is extracted from the certificate chain)') + parser.add_argument('--openssl', + metavar='CMD', + help='openssl command to use (default: "openssl")', + default='openssl') + parser.add_argument('certificate', + help='path to certificate file to validate') + args = parser.parse_args() + + tempdir = None + try: + # Python3.2 has tempfile.TemporaryDirectory, which has nice + # feature to delete its tree by cleanup() function. We have + # to support Python2.7, so we have to do this manually. + tempdir = tempfile.mkdtemp() + fetch_ocsp_response(args.openssl, args.certificate, tempdir, + args.issuer) + finally: + if tempdir: + shutil.rmtree(tempdir) diff --git a/third-party/Makefile.am b/third-party/Makefile.am index 0dc41621..8098e32e 100644 --- a/third-party/Makefile.am +++ b/third-party/Makefile.am @@ -32,5 +32,3 @@ libhttp_parser_la_SOURCES = \ endif # ENABLE_THIRD_PARTY - -dist_pkgdata_SCRIPTS = h2o/fetch-ocsp-response diff --git a/third-party/h2o/README.rst b/third-party/h2o/README.rst deleted file mode 100644 index 627d741f..00000000 --- a/third-party/h2o/README.rst +++ /dev/null @@ -1,10 +0,0 @@ -fetch-ocsp-response is a Perl script to perform OCSP query and get -response. It uses openssl command under the hood. nghttpx uses it to -enable OCSP stapling feature. - -fetch-ocsp-response has been developed as part of h2o project -(https://github.com/h2o/h2o). The script file with the same name in -this directory was copied from their github repository. - -fetch-ocsp-response is usually installed under $(pkgdatadir), which is -$(prefix)/share/nghttp2. diff --git a/third-party/h2o/fetch-ocsp-response b/third-party/h2o/fetch-ocsp-response deleted file mode 100755 index 9272dae2..00000000 --- a/third-party/h2o/fetch-ocsp-response +++ /dev/null @@ -1,150 +0,0 @@ -#! /bin/sh -exec perl -x $0 "$@" -#! perl - -# Copyright (c) 2015 DeNA Co., Ltd. -# -# 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. - -use strict; -use warnings; -use File::Temp qw(tempdir); -use Getopt::Long; - -# from sysexits.h -use constant EX_TEMPFAIL => 75; - -my ($issuer_fn, $opt_help); -my $openssl_cmd = 'openssl'; - -GetOptions( - "issuer=s" => \$issuer_fn, - "openssl=s", => \$openssl_cmd, - help => \$opt_help, -) or exit(1); -if ($opt_help) { - print << "EOT"; -Usage: $0 [] - -Options: - --issuer issuer certificate (if omitted, is extracted from the - certificate chain) - --openssl openssl command to use (default: "openssl") - --help prints this help - -The command issues an OCSP request for given server certificate, verifies the -response and prints the resulting DER. - -The command exits 0 if successful, or 75 (EX_TEMPFAIL) on temporary error. -Other exit codes may be returned in case of hard errors. - -EOT - exit(0); -} - -die "no certificate file\n" - if @ARGV == 0; -my $cert_fn = shift @ARGV; - -my $tempdir = tempdir(CLEANUP => 1); - -my $openssl_version = run_openssl("version"); -chomp $openssl_version; -print STDERR "fetch-ocsp-response (using $openssl_version)\n"; - -# obtain ocsp uri -my $ocsp_uri = run_openssl("x509 -in $cert_fn -noout -ocsp_uri"); -chomp $ocsp_uri; -die "failed to extract ocsp URI from $cert_fn\n" - if $ocsp_uri !~ m{^https?://}; -my($ocsp_host) = $ocsp_uri =~ m{^https?://([^/:]+)}; - -# save issuer certificate -if (! defined $issuer_fn) { - my $chain = read_file($cert_fn); - $chain =~ m{-----END CERTIFICATE-----.*?(-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----)}s - or die "--issuer option was not used, and failed to extract issuer certificate from the certificate\n"; - $issuer_fn = "$tempdir/issuer.crt"; - write_file($issuer_fn, "$1\n"); -} - -# obtain response (without verification) -print STDERR "sending OCSP request to $ocsp_uri\n"; -my $resp = run_openssl( - "ocsp -issuer $issuer_fn -cert $cert_fn -url $ocsp_uri" - . ($openssl_version =~ /^OpenSSL 1\./is ? " -header Host $ocsp_host" : "") - . " -noverify -respout $tempdir/resp.der " . join(' ', @ARGV), - 1, -); -print STDERR $resp; - -# verify the response -print STDERR "verifying the response signature\n"; -my $success; -for my $args ( - # try from exotic options - "-VAfile $issuer_fn", # for comodo - "-partial_chain -trusted_first -CAfile $issuer_fn", # these options are only available in OpenSSL >= 1.0.2 - "-CAfile $issuer_fn", # for OpenSSL <= 1.0.1 -) { - if (system("$openssl_cmd ocsp -respin $tempdir/resp.der $args > $tempdir/verify.out 2>&1") == 0) { - print STDERR "verify OK (used: $args)\n"; - $success = 1; - last; - } -} -if (! $success) { - print STDERR read_file("$tempdir/verify.out"); - tempfail("failed to verify the response\n"); -} - -# success -print read_file("$tempdir/resp.der"); -exit 0; - -sub run_openssl { - my ($args, $tempfail) = @_; - open my $fh, "-|", "$openssl_cmd $args" - or die "failed to invoke $openssl_cmd:$!"; - my $resp = do { local $/; <$fh> }; - close $fh - or ($tempfail ? \&tempfail : \&die)->("OpenSSL exitted abnormally: $openssl_cmd $args:$!"); - $resp; -} - -sub read_file { - my $fn = shift; - open my $fh, "<", $fn - or die "failed to open file:$fn:$!"; - local $/; - <$fh>; -} - -sub write_file { - my ($fn, $data) = @_; - open my $fh, ">", $fn - or die "failed to open file:$fn:$!"; - print $fh $data; - close $fh; -} - -sub tempfail { - print STDERR @_; - exit EX_TEMPFAIL; -}