• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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