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