Translate fetch-ocsp-response into Python
This commit is contained in:
parent
00efa86fb6
commit
b7ba1baf48
|
@ -41,3 +41,5 @@ clang-format:
|
||||||
$${CLANGFORMAT} -i lib/*.{c,h} lib/includes/nghttp2/*.h \
|
$${CLANGFORMAT} -i lib/*.{c,h} lib/includes/nghttp2/*.h \
|
||||||
src/*.{c,cc,h} src/includes/nghttp2/*.h examples/*.{c,cc} \
|
src/*.{c,cc,h} src/includes/nghttp2/*.h examples/*.{c,cc} \
|
||||||
tests/*.{c,h}
|
tests/*.{c,h}
|
||||||
|
|
||||||
|
dist_pkgdata_SCRIPTS = script/fetch-ocsp-response
|
||||||
|
|
|
@ -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.
|
|
@ -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)
|
|
@ -32,5 +32,3 @@ libhttp_parser_la_SOURCES = \
|
||||||
|
|
||||||
|
|
||||||
endif # ENABLE_THIRD_PARTY
|
endif # ENABLE_THIRD_PARTY
|
||||||
|
|
||||||
dist_pkgdata_SCRIPTS = h2o/fetch-ocsp-response
|
|
||||||
|
|
|
@ -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.
|
|
|
@ -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>] <certificate-file>
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--issuer <file> issuer certificate (if omitted, is extracted from the
|
|
||||||
certificate chain)
|
|
||||||
--openssl <cmd> 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;
|
|
||||||
}
|
|
Loading…
Reference in New Issue