12c593315Sopenharmony_ci#!/usr/bin/env python
22c593315Sopenharmony_ci# -*- coding: utf-8 -*-
32c593315Sopenharmony_ci
42c593315Sopenharmony_ci# nghttp2 - HTTP/2 C Library
52c593315Sopenharmony_ci
62c593315Sopenharmony_ci# Copyright (c) 2015 Tatsuhiro Tsujikawa
72c593315Sopenharmony_ci
82c593315Sopenharmony_ci# Permission is hereby granted, free of charge, to any person obtaining
92c593315Sopenharmony_ci# a copy of this software and associated documentation files (the
102c593315Sopenharmony_ci# "Software"), to deal in the Software without restriction, including
112c593315Sopenharmony_ci# without limitation the rights to use, copy, modify, merge, publish,
122c593315Sopenharmony_ci# distribute, sublicense, and/or sell copies of the Software, and to
132c593315Sopenharmony_ci# permit persons to whom the Software is furnished to do so, subject to
142c593315Sopenharmony_ci# the following conditions:
152c593315Sopenharmony_ci
162c593315Sopenharmony_ci# The above copyright notice and this permission notice shall be
172c593315Sopenharmony_ci# included in all copies or substantial portions of the Software.
182c593315Sopenharmony_ci
192c593315Sopenharmony_ci# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
202c593315Sopenharmony_ci# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
212c593315Sopenharmony_ci# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
222c593315Sopenharmony_ci# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
232c593315Sopenharmony_ci# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
242c593315Sopenharmony_ci# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
252c593315Sopenharmony_ci# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
262c593315Sopenharmony_ci
272c593315Sopenharmony_ci# This program was translated from the program originally developed by
282c593315Sopenharmony_ci# h2o project (https://github.com/h2o/h2o), written in Perl.  It had
292c593315Sopenharmony_ci# the following copyright notice:
302c593315Sopenharmony_ci
312c593315Sopenharmony_ci# Copyright (c) 2015 DeNA Co., Ltd.
322c593315Sopenharmony_ci#
332c593315Sopenharmony_ci# Permission is hereby granted, free of charge, to any person obtaining a copy
342c593315Sopenharmony_ci# of this software and associated documentation files (the "Software"), to
352c593315Sopenharmony_ci# deal in the Software without restriction, including without limitation the
362c593315Sopenharmony_ci# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
372c593315Sopenharmony_ci# sell copies of the Software, and to permit persons to whom the Software is
382c593315Sopenharmony_ci# furnished to do so, subject to the following conditions:
392c593315Sopenharmony_ci#
402c593315Sopenharmony_ci# The above copyright notice and this permission notice shall be included in
412c593315Sopenharmony_ci# all copies or substantial portions of the Software.
422c593315Sopenharmony_ci#
432c593315Sopenharmony_ci# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
442c593315Sopenharmony_ci# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
452c593315Sopenharmony_ci# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
462c593315Sopenharmony_ci# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
472c593315Sopenharmony_ci# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
482c593315Sopenharmony_ci# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
492c593315Sopenharmony_ci# IN THE SOFTWARE.
502c593315Sopenharmony_ci
512c593315Sopenharmony_cifrom __future__ import unicode_literals
522c593315Sopenharmony_ciimport argparse
532c593315Sopenharmony_ciimport io
542c593315Sopenharmony_ciimport os
552c593315Sopenharmony_ciimport os.path
562c593315Sopenharmony_ciimport re
572c593315Sopenharmony_ciimport shutil
582c593315Sopenharmony_ciimport subprocess
592c593315Sopenharmony_ciimport sys
602c593315Sopenharmony_ciimport tempfile
612c593315Sopenharmony_ci
622c593315Sopenharmony_ci# make this program work for both Python 3 and Python 2.
632c593315Sopenharmony_citry:
642c593315Sopenharmony_ci    from urllib.parse import urlparse
652c593315Sopenharmony_ci    stdout_bwrite = sys.stdout.buffer.write
662c593315Sopenharmony_ciexcept ImportError:
672c593315Sopenharmony_ci    from urlparse import urlparse
682c593315Sopenharmony_ci    stdout_bwrite = sys.stdout.write
692c593315Sopenharmony_ci
702c593315Sopenharmony_ci
712c593315Sopenharmony_cidef die(msg):
722c593315Sopenharmony_ci    sys.stderr.write(msg)
732c593315Sopenharmony_ci    sys.stderr.write('\n')
742c593315Sopenharmony_ci    sys.exit(255)
752c593315Sopenharmony_ci
762c593315Sopenharmony_ci
772c593315Sopenharmony_cidef tempfail(msg):
782c593315Sopenharmony_ci    sys.stderr.write(msg)
792c593315Sopenharmony_ci    sys.stderr.write('\n')
802c593315Sopenharmony_ci    sys.exit(os.EX_TEMPFAIL)
812c593315Sopenharmony_ci
822c593315Sopenharmony_ci
832c593315Sopenharmony_cidef run_openssl(args, allow_tempfail=False):
842c593315Sopenharmony_ci    buf = io.BytesIO()
852c593315Sopenharmony_ci    try:
862c593315Sopenharmony_ci        p = subprocess.Popen(args, stdout=subprocess.PIPE)
872c593315Sopenharmony_ci    except Exception as e:
882c593315Sopenharmony_ci        die('failed to invoke {}:{}'.format(args, e))
892c593315Sopenharmony_ci    try:
902c593315Sopenharmony_ci        while True:
912c593315Sopenharmony_ci            data = p.stdout.read()
922c593315Sopenharmony_ci            if len(data) == 0:
932c593315Sopenharmony_ci                break
942c593315Sopenharmony_ci            buf.write(data)
952c593315Sopenharmony_ci        if p.wait() != 0:
962c593315Sopenharmony_ci            raise Exception('nonzero return code {}'.format(p.returncode))
972c593315Sopenharmony_ci        return buf.getvalue()
982c593315Sopenharmony_ci    except Exception as e:
992c593315Sopenharmony_ci        msg = 'OpenSSL exitted abnormally: {}:{}'.format(args, e)
1002c593315Sopenharmony_ci        tempfail(msg) if allow_tempfail else die(msg)
1012c593315Sopenharmony_ci
1022c593315Sopenharmony_ci
1032c593315Sopenharmony_cidef read_file(path):
1042c593315Sopenharmony_ci    with open(path, 'rb') as f:
1052c593315Sopenharmony_ci        return f.read()
1062c593315Sopenharmony_ci
1072c593315Sopenharmony_ci
1082c593315Sopenharmony_cidef write_file(path, data):
1092c593315Sopenharmony_ci    with open(path, 'wb') as f:
1102c593315Sopenharmony_ci        f.write(data)
1112c593315Sopenharmony_ci
1122c593315Sopenharmony_ci
1132c593315Sopenharmony_cidef detect_openssl_version(cmd):
1142c593315Sopenharmony_ci    return run_openssl([cmd, 'version']).decode('utf-8').strip()
1152c593315Sopenharmony_ci
1162c593315Sopenharmony_ci
1172c593315Sopenharmony_cidef extract_ocsp_uri(cmd, cert_fn):
1182c593315Sopenharmony_ci    # obtain ocsp uri
1192c593315Sopenharmony_ci    ocsp_uri = run_openssl(
1202c593315Sopenharmony_ci        [cmd, 'x509', '-in', cert_fn, '-noout',
1212c593315Sopenharmony_ci         '-ocsp_uri']).decode('utf-8').strip()
1222c593315Sopenharmony_ci
1232c593315Sopenharmony_ci    if not re.match(r'^https?://', ocsp_uri):
1242c593315Sopenharmony_ci        die('failed to extract ocsp URI from {}'.format(cert_fn))
1252c593315Sopenharmony_ci
1262c593315Sopenharmony_ci    return ocsp_uri
1272c593315Sopenharmony_ci
1282c593315Sopenharmony_ci
1292c593315Sopenharmony_cidef save_issuer_certificate(issuer_fn, cert_fn):
1302c593315Sopenharmony_ci    # save issuer certificate
1312c593315Sopenharmony_ci    chain = read_file(cert_fn).decode('utf-8')
1322c593315Sopenharmony_ci    m = re.match(
1332c593315Sopenharmony_ci        r'.*?-----END CERTIFICATE-----.*?(-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----)',
1342c593315Sopenharmony_ci        chain, re.DOTALL)
1352c593315Sopenharmony_ci    if not m:
1362c593315Sopenharmony_ci        die('--issuer option was not used, and failed to extract issuer certificate from the certificate')
1372c593315Sopenharmony_ci    write_file(issuer_fn, (m.group(1) + '\n').encode('utf-8'))
1382c593315Sopenharmony_ci
1392c593315Sopenharmony_ci
1402c593315Sopenharmony_cidef send_and_receive_ocsp(respder_fn, cmd, cert_fn, issuer_fn, ocsp_uri,
1412c593315Sopenharmony_ci                          ocsp_host, openssl_version):
1422c593315Sopenharmony_ci    # obtain response (without verification)
1432c593315Sopenharmony_ci    sys.stderr.write('sending OCSP request to {}\n'.format(ocsp_uri))
1442c593315Sopenharmony_ci    args = [
1452c593315Sopenharmony_ci        cmd, 'ocsp', '-issuer', issuer_fn, '-cert', cert_fn, '-url', ocsp_uri,
1462c593315Sopenharmony_ci        '-noverify', '-respout', respder_fn
1472c593315Sopenharmony_ci    ]
1482c593315Sopenharmony_ci    ver = openssl_version.lower()
1492c593315Sopenharmony_ci    if ver.startswith('openssl 1.0.') or ver.startswith('libressl '):
1502c593315Sopenharmony_ci        args.extend(['-header', 'Host', ocsp_host])
1512c593315Sopenharmony_ci    resp = run_openssl(args, allow_tempfail=True)
1522c593315Sopenharmony_ci    return resp.decode('utf-8')
1532c593315Sopenharmony_ci
1542c593315Sopenharmony_ci
1552c593315Sopenharmony_cidef verify_response(cmd, tempdir, issuer_fn, respder_fn):
1562c593315Sopenharmony_ci    # verify the response
1572c593315Sopenharmony_ci    sys.stderr.write('verifying the response signature\n')
1582c593315Sopenharmony_ci
1592c593315Sopenharmony_ci    verify_fn = os.path.join(tempdir, 'verify.out')
1602c593315Sopenharmony_ci
1612c593315Sopenharmony_ci    # try from exotic options
1622c593315Sopenharmony_ci    allextra = [
1632c593315Sopenharmony_ci        # for comodo
1642c593315Sopenharmony_ci        ['-VAfile', issuer_fn],
1652c593315Sopenharmony_ci        # these options are only available in OpenSSL >= 1.0.2
1662c593315Sopenharmony_ci        ['-partial_chain', '-trusted_first', '-CAfile', issuer_fn],
1672c593315Sopenharmony_ci        # for OpenSSL <= 1.0.1
1682c593315Sopenharmony_ci        ['-CAfile', issuer_fn],
1692c593315Sopenharmony_ci    ]
1702c593315Sopenharmony_ci
1712c593315Sopenharmony_ci    for extra in allextra:
1722c593315Sopenharmony_ci        with open(verify_fn, 'w+b') as f:
1732c593315Sopenharmony_ci            args = [cmd, 'ocsp', '-respin', respder_fn]
1742c593315Sopenharmony_ci            args.extend(extra)
1752c593315Sopenharmony_ci            p = subprocess.Popen(args, stdout=f, stderr=f)
1762c593315Sopenharmony_ci            if p.wait() == 0:
1772c593315Sopenharmony_ci                # OpenSSL <= 1.0.1, openssl ocsp still returns exit
1782c593315Sopenharmony_ci                # code 0 even if verification was failed.  So check
1792c593315Sopenharmony_ci                # the error message in stderr output.
1802c593315Sopenharmony_ci                f.seek(0)
1812c593315Sopenharmony_ci                if f.read().decode('utf-8').find(
1822c593315Sopenharmony_ci                        'Response Verify Failure') != -1:
1832c593315Sopenharmony_ci                    continue
1842c593315Sopenharmony_ci                sys.stderr.write('verify OK (used: {})\n'.format(extra))
1852c593315Sopenharmony_ci                return True
1862c593315Sopenharmony_ci
1872c593315Sopenharmony_ci    sys.stderr.write(read_file(verify_fn).decode('utf-8'))
1882c593315Sopenharmony_ci    return False
1892c593315Sopenharmony_ci
1902c593315Sopenharmony_ci
1912c593315Sopenharmony_cidef fetch_ocsp_response(cmd, cert_fn, tempdir, issuer_fn=None):
1922c593315Sopenharmony_ci    openssl_version = detect_openssl_version(cmd)
1932c593315Sopenharmony_ci
1942c593315Sopenharmony_ci    sys.stderr.write(
1952c593315Sopenharmony_ci        'fetch-ocsp-response (using {})\n'.format(openssl_version))
1962c593315Sopenharmony_ci
1972c593315Sopenharmony_ci    ocsp_uri = extract_ocsp_uri(cmd, cert_fn)
1982c593315Sopenharmony_ci    ocsp_host = urlparse(ocsp_uri).netloc
1992c593315Sopenharmony_ci
2002c593315Sopenharmony_ci    if not issuer_fn:
2012c593315Sopenharmony_ci        issuer_fn = os.path.join(tempdir, 'issuer.crt')
2022c593315Sopenharmony_ci        save_issuer_certificate(issuer_fn, cert_fn)
2032c593315Sopenharmony_ci
2042c593315Sopenharmony_ci    respder_fn = os.path.join(tempdir, 'resp.der')
2052c593315Sopenharmony_ci    resp = send_and_receive_ocsp(
2062c593315Sopenharmony_ci        respder_fn, cmd, cert_fn, issuer_fn, ocsp_uri, ocsp_host,
2072c593315Sopenharmony_ci        openssl_version)
2082c593315Sopenharmony_ci
2092c593315Sopenharmony_ci    sys.stderr.write('{}\n'.format(resp))
2102c593315Sopenharmony_ci
2112c593315Sopenharmony_ci    # OpenSSL 1.0.2 still returns exit code 0 even if ocsp responder
2122c593315Sopenharmony_ci    # returned error status (e.g., trylater(3))
2132c593315Sopenharmony_ci    if resp.find('Responder Error:') != -1:
2142c593315Sopenharmony_ci        raise Exception('responder returned error')
2152c593315Sopenharmony_ci
2162c593315Sopenharmony_ci    if not verify_response(cmd, tempdir, issuer_fn, respder_fn):
2172c593315Sopenharmony_ci        tempfail('failed to verify the response')
2182c593315Sopenharmony_ci
2192c593315Sopenharmony_ci    # success
2202c593315Sopenharmony_ci    res = read_file(respder_fn)
2212c593315Sopenharmony_ci    stdout_bwrite(res)
2222c593315Sopenharmony_ci
2232c593315Sopenharmony_ci
2242c593315Sopenharmony_ciif __name__ == '__main__':
2252c593315Sopenharmony_ci    parser = argparse.ArgumentParser(
2262c593315Sopenharmony_ci        description=
2272c593315Sopenharmony_ci        '''The command issues an OCSP request for given server certificate, verifies the response and prints the resulting DER.''',
2282c593315Sopenharmony_ci        epilog=
2292c593315Sopenharmony_ci        '''The command exits 0 if successful, or 75 (EX_TEMPFAIL) on temporary error.  Other exit codes may be returned in case of hard errors.''')
2302c593315Sopenharmony_ci    parser.add_argument(
2312c593315Sopenharmony_ci        '--issuer',
2322c593315Sopenharmony_ci        metavar='FILE',
2332c593315Sopenharmony_ci        help=
2342c593315Sopenharmony_ci        'issuer certificate (if omitted, is extracted from the certificate chain)')
2352c593315Sopenharmony_ci    parser.add_argument('--openssl',
2362c593315Sopenharmony_ci                        metavar='CMD',
2372c593315Sopenharmony_ci                        help='openssl command to use (default: "openssl")',
2382c593315Sopenharmony_ci                        default='openssl')
2392c593315Sopenharmony_ci    parser.add_argument('certificate',
2402c593315Sopenharmony_ci                        help='path to certificate file to validate')
2412c593315Sopenharmony_ci    args = parser.parse_args()
2422c593315Sopenharmony_ci
2432c593315Sopenharmony_ci    tempdir = None
2442c593315Sopenharmony_ci    try:
2452c593315Sopenharmony_ci        # Python3.2 has tempfile.TemporaryDirectory, which has nice
2462c593315Sopenharmony_ci        # feature to delete its tree by cleanup() function.  We have
2472c593315Sopenharmony_ci        # to support Python2.7, so we have to do this manually.
2482c593315Sopenharmony_ci        tempdir = tempfile.mkdtemp()
2492c593315Sopenharmony_ci        fetch_ocsp_response(args.openssl, args.certificate, tempdir,
2502c593315Sopenharmony_ci                            args.issuer)
2512c593315Sopenharmony_ci    finally:
2522c593315Sopenharmony_ci        if tempdir:
2532c593315Sopenharmony_ci            shutil.rmtree(tempdir)
254