1# -*- coding: utf-8 -*- 2 3# nghttp2 - HTTP/2 C Library 4 5# Copyright (c) 2015 Tatsuhiro Tsujikawa 6 7# Permission is hereby granted, free of charge, to any person obtaining 8# a copy of this software and associated documentation files (the 9# "Software"), to deal in the Software without restriction, including 10# without limitation the rights to use, copy, modify, merge, publish, 11# distribute, sublicense, and/or sell copies of the Software, and to 12# permit persons to whom the Software is furnished to do so, subject to 13# the following conditions: 14 15# The above copyright notice and this permission notice shall be 16# included in all copies or substantial portions of the Software. 17 18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 26# This program was translated from the program originally developed by 27# h2o project (https://github.com/h2o/h2o), written in Perl. It had 28# the following copyright notice: 29 30# Copyright (c) 2015 DeNA Co., Ltd. 31# 32# Permission is hereby granted, free of charge, to any person obtaining a copy 33# of this software and associated documentation files (the "Software"), to 34# deal in the Software without restriction, including without limitation the 35# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 36# sell copies of the Software, and to permit persons to whom the Software is 37# furnished to do so, subject to the following conditions: 38# 39# The above copyright notice and this permission notice shall be included in 40# all copies or substantial portions of the Software. 41# 42# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 43# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 44# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 45# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 46# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 47# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 48# IN THE SOFTWARE. 49 50from __future__ import unicode_literals 51import argparse 52import io 53import os 54import os.path 55import re 56import shutil 57import subprocess 58import sys 59import tempfile 60 61# make this program work for both Python 3 and Python 2. 62try: 63 from urllib.parse import urlparse 64 stdout_bwrite = sys.stdout.buffer.write 65except ImportError: 66 from urlparse import urlparse 67 stdout_bwrite = sys.stdout.write 68 69 70def die(msg): 71 sys.stderr.write(msg) 72 sys.stderr.write('\n') 73 sys.exit(255) 74 75 76def tempfail(msg): 77 sys.stderr.write(msg) 78 sys.stderr.write('\n') 79 sys.exit(os.EX_TEMPFAIL) 80 81 82def run_openssl(args, allow_tempfail=False): 83 buf = io.BytesIO() 84 try: 85 p = subprocess.Popen(args, stdout=subprocess.PIPE) 86 except Exception as e: 87 die('failed to invoke {}:{}'.format(args, e)) 88 try: 89 while True: 90 data = p.stdout.read() 91 if len(data) == 0: 92 break 93 buf.write(data) 94 if p.wait() != 0: 95 raise Exception('nonzero return code {}'.format(p.returncode)) 96 return buf.getvalue() 97 except Exception as e: 98 msg = 'OpenSSL exitted abnormally: {}:{}'.format(args, e) 99 tempfail(msg) if allow_tempfail else die(msg) 100 101 102def read_file(path): 103 with open(path, 'rb') as f: 104 return f.read() 105 106 107def write_file(path, data): 108 with open(path, 'wb') as f: 109 f.write(data) 110 111 112def detect_openssl_version(cmd): 113 return run_openssl([cmd, 'version']).decode('utf-8').strip() 114 115 116def extract_ocsp_uri(cmd, cert_fn): 117 # obtain ocsp uri 118 ocsp_uri = run_openssl( 119 [cmd, 'x509', '-in', cert_fn, '-noout', 120 '-ocsp_uri']).decode('utf-8').strip() 121 122 if not re.match(r'^https?://', ocsp_uri): 123 die('failed to extract ocsp URI from {}'.format(cert_fn)) 124 125 return ocsp_uri 126 127 128def save_issuer_certificate(issuer_fn, cert_fn): 129 # save issuer certificate 130 chain = read_file(cert_fn).decode('utf-8') 131 m = re.match( 132 r'.*?-----END CERTIFICATE-----.*?(-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----)', 133 chain, re.DOTALL) 134 if not m: 135 die('--issuer option was not used, and failed to extract issuer certificate from the certificate') 136 write_file(issuer_fn, (m.group(1) + '\n').encode('utf-8')) 137 138 139def send_and_receive_ocsp(respder_fn, cmd, cert_fn, issuer_fn, ocsp_uri, 140 ocsp_host, openssl_version): 141 # obtain response (without verification) 142 sys.stderr.write('sending OCSP request to {}\n'.format(ocsp_uri)) 143 args = [ 144 cmd, 'ocsp', '-issuer', issuer_fn, '-cert', cert_fn, '-url', ocsp_uri, 145 '-noverify', '-respout', respder_fn 146 ] 147 ver = openssl_version.lower() 148 if ver.startswith('openssl 1.0.') or ver.startswith('libressl '): 149 args.extend(['-header', 'Host', ocsp_host]) 150 resp = run_openssl(args, allow_tempfail=True) 151 return resp.decode('utf-8') 152 153 154def verify_response(cmd, tempdir, issuer_fn, respder_fn): 155 # verify the response 156 sys.stderr.write('verifying the response signature\n') 157 158 verify_fn = os.path.join(tempdir, 'verify.out') 159 160 # try from exotic options 161 allextra = [ 162 # for comodo 163 ['-VAfile', issuer_fn], 164 # these options are only available in OpenSSL >= 1.0.2 165 ['-partial_chain', '-trusted_first', '-CAfile', issuer_fn], 166 # for OpenSSL <= 1.0.1 167 ['-CAfile', issuer_fn], 168 ] 169 170 for extra in allextra: 171 with open(verify_fn, 'w+b') as f: 172 args = [cmd, 'ocsp', '-respin', respder_fn] 173 args.extend(extra) 174 p = subprocess.Popen(args, stdout=f, stderr=f) 175 if p.wait() == 0: 176 # OpenSSL <= 1.0.1, openssl ocsp still returns exit 177 # code 0 even if verification was failed. So check 178 # the error message in stderr output. 179 f.seek(0) 180 if f.read().decode('utf-8').find( 181 'Response Verify Failure') != -1: 182 continue 183 sys.stderr.write('verify OK (used: {})\n'.format(extra)) 184 return True 185 186 sys.stderr.write(read_file(verify_fn).decode('utf-8')) 187 return False 188 189 190def fetch_ocsp_response(cmd, cert_fn, tempdir, issuer_fn=None): 191 openssl_version = detect_openssl_version(cmd) 192 193 sys.stderr.write( 194 'fetch-ocsp-response (using {})\n'.format(openssl_version)) 195 196 ocsp_uri = extract_ocsp_uri(cmd, cert_fn) 197 ocsp_host = urlparse(ocsp_uri).netloc 198 199 if not issuer_fn: 200 issuer_fn = os.path.join(tempdir, 'issuer.crt') 201 save_issuer_certificate(issuer_fn, cert_fn) 202 203 respder_fn = os.path.join(tempdir, 'resp.der') 204 resp = send_and_receive_ocsp( 205 respder_fn, cmd, cert_fn, issuer_fn, ocsp_uri, ocsp_host, 206 openssl_version) 207 208 sys.stderr.write('{}\n'.format(resp)) 209 210 # OpenSSL 1.0.2 still returns exit code 0 even if ocsp responder 211 # returned error status (e.g., trylater(3)) 212 if resp.find('Responder Error:') != -1: 213 raise Exception('responder returned error') 214 215 if not verify_response(cmd, tempdir, issuer_fn, respder_fn): 216 tempfail('failed to verify the response') 217 218 # success 219 res = read_file(respder_fn) 220 stdout_bwrite(res) 221 222 223if __name__ == '__main__': 224 parser = argparse.ArgumentParser( 225 description= 226 '''The command issues an OCSP request for given server certificate, verifies the response and prints the resulting DER.''', 227 epilog= 228 '''The command exits 0 if successful, or 75 (EX_TEMPFAIL) on temporary error. Other exit codes may be returned in case of hard errors.''') 229 parser.add_argument( 230 '--issuer', 231 metavar='FILE', 232 help= 233 'issuer certificate (if omitted, is extracted from the certificate chain)') 234 parser.add_argument('--openssl', 235 metavar='CMD', 236 help='openssl command to use (default: "openssl")', 237 default='openssl') 238 parser.add_argument('certificate', 239 help='path to certificate file to validate') 240 args = parser.parse_args() 241 242 tempdir = None 243 try: 244 # Python3.2 has tempfile.TemporaryDirectory, which has nice 245 # feature to delete its tree by cleanup() function. We have 246 # to support Python2.7, so we have to do this manually. 247 tempdir = tempfile.mkdtemp() 248 fetch_ocsp_response(args.openssl, args.certificate, tempdir, 249 args.issuer) 250 finally: 251 if tempdir: 252 shutil.rmtree(tempdir) 253