From 405b10458373cdea98fd883e39195f67e9bb56d0 Mon Sep 17 00:00:00 2001 From: Julien Vehent Date: Thu, 9 Oct 2014 09:35:59 -0400 Subject: [PATCH] improved configuration analysis --- analyze.py | 337 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 287 insertions(+), 50 deletions(-) mode change 100644 => 100755 analyze.py diff --git a/analyze.py b/analyze.py old mode 100644 new mode 100755 index e94c7ef..57179cc --- a/analyze.py +++ b/analyze.py @@ -5,59 +5,218 @@ # # Contributor: Julien Vehent jvehent@mozilla.com [:ulfr] -import fileinput -import sys -import json -import subprocess -from collections import defaultdict +import sys, os, json, subprocess, logging, argparse +from collections import namedtuple +# is_fubar assumes that a configuration is not completely messed up +# and looks for reasons to think otherwise. it will return True if +# it finds one of these reason def is_fubar(results): + fubar = False fubar_ciphers = set(all_ciphers) - set(old_ciphers) for conn in results['ciphersuite']: if conn['cipher'] in fubar_ciphers: - return True + logging.debug(conn['cipher'] + ' is in the list of fubar ciphers') + fubar = True if 'SSLv2' in conn['protocols']: - return True + logging.debug('SSLv2 is in the list of fubar protocols') + fubar = True if conn['pubkey'] < 2048: - return True - return False + logging.debug(conn['pubkey'] + ' is a fubar pubkey size') + fubar = True + if 'md5WithRSAEncryption' in conn['sigalg']: + logging.debug(conn['sigalg']+ ' is a fubar cert signature') + fubar = True + if conn['trusted'] == 'False': + logging.debug('The certificate is not trusted, which is quite fubar') + fubar = True + return fubar +# is_old assumes a configuration *is* old, and will return False if +# the parameters of an old configuration are not found. Those parameters +# are defined in https://wiki.mozilla.org/Security/Server_Side_TLS#Old_backward_compatibility def is_old(results): + lvl = 'old' + old = True + has_sslv3 = False + has_3des = False + has_sha1 = True + has_dhparam = True + has_ocsp = True + all_proto = [] for conn in results['ciphersuite']: + # flag unwanted ciphers if conn['cipher'] not in old_ciphers: - return False - if 'SSLv3' not in conn['protocols']: - return False + logging.debug(conn['cipher'] + ' is not in the list of old ciphers') + failures[lvl].append("remove cipher " + conn['cipher']) + old = False + # verify required 3des cipher is present + if conn['cipher'] == 'DES-CBC3-SHA': + has_3des = True + # verify required ssl3 protocol is present + if 'SSLv3' in conn['protocols']: + has_sslv3 = True + for proto in conn['protocols']: + if proto not in all_proto: + all_proto.append(proto) + # verify required sha1 signature is used if 'sha1WithRSAEncryption' not in conn['sigalg']: - return False - return True + logging.debug(conn['sigalg'][0] + ' is a not an old signature') + old = False + has_sha1 = False + # verify required pfs parameter is used + if conn['cipher'][0:2] == 'DHE': + if conn['pfs'] != 'DH,1024bits': + logging.debug(conn['pfs']+ ' is not a good DH parameters for the old configuration') + old = False + has_dhparam = False + if conn['ocsp_stapling'] == 'False': + has_ocsp = False + missing_ciphers = set(old_ciphers) - set(all_ciphers) + for cipher in missing_ciphers: + logging.debug("missing cipher " + cipher + " wanted in the " + lvl + " configuration") + failures[lvl].append('add cipher ' + cipher) + extra_proto = set(all_proto) - set(['SSLv3', 'TLSv1', 'TLSv1.1', 'TLSv1.2']) + for proto in extra_proto: + logging.debug("found protocol not wanted in the old configuration:" + proto) + failures[lvl].append('disable ' + proto) + modern = False + missing_proto = set(['SSLv3', 'TLSv1', 'TLSv1.1', 'TLSv1.2']) - set(all_proto) + for proto in missing_proto: + logging.debug("missing protocol wanted in the old configuration:" + proto) + failures[lvl].append('enable ' + proto) + if not has_sslv3: + logging.debug("SSLv3 is not supported and required by the old configuration") + old = False + if not has_3des: + logging.debug("DES-CBC3-SHA is not supported and required by the old configuration") + failures[lvl].append("add cipher DES-CBC3-SHA") + old = False + if not has_sha1: + failures[lvl].append("use a certificate with sha1WithRSAEncryption signature") + old = False + if not has_dhparam: + failures[lvl].append("use a DH parameter of 1024 bits") + old = False + if not has_ocsp: + failures[lvl].append("enable OCSP Stapling") + return old +# is_intermediate is similar to is_old but for intermediate configuration from +# https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28default.29 def is_intermediate(results): + lvl = 'intermediate' + inter = True + has_tls1 = False + has_aes = False + has_dhparam = True + has_sha256 = True + has_ocsp = True + all_proto = [] for conn in results['ciphersuite']: if conn['cipher'] not in intermediate_ciphers: - return False - if len(set(conn['protocols']) - set(['TLSv1', 'TLSv1.1', 'TLSv1.2'])) > 0: - return False - return True + logging.debug(conn['cipher'] + ' is not in the list of intermediate ciphers') + failures[lvl].append("remove cipher " + conn['cipher']) + inter = False + if conn['cipher'] == 'AES128-SHA': + has_aes = True + for proto in conn['protocols']: + if proto not in all_proto: + all_proto.append(proto) + if 'TLSv1' in conn['protocols']: + has_tls1 = True + if 'sha256WithRSAEncryption' not in conn['sigalg']: + logging.debug(conn['sigalg'][0] + ' is a not an intermediate signature') + inter = False + has_sha256 = False + if conn['cipher'][0:2] == 'DHE': + if conn['pfs'] != 'DH,2048bits': + logging.debug(conn['pfs']+ ' is not a good DH parameters for the old configuration') + inter = False + has_dhparam = False + if conn['ocsp_stapling'] == 'False': + has_ocsp = False + extra_proto = set(all_proto) - set(['TLSv1', 'TLSv1.1', 'TLSv1.2']) + for proto in extra_proto: + logging.debug("found protocol not wanted in the intermediate configuration:" + proto) + failures[lvl].append('disable ' + proto) + modern = False + missing_proto = set(['TLSv1', 'TLSv1.1', 'TLSv1.2']) - set(all_proto) + for proto in missing_proto: + logging.debug("missing protocol wanted in the intermediate configuration:" + proto) + failures[lvl].append('enable ' + proto) + if not has_tls1: + logging.debug("TLSv1 is not supported and required by the old configuration") + inter = False + if not has_aes: + logging.debug("AES128-SHA is not supported and required by the intermediate configuration") + failures[lvl].append("add cipher AES128-SHA") + inter = False + if not has_sha256: + failures[lvl].append("use a certificate with sha256WithRSAEncryption signature") + inter = False + if not has_dhparam: + failures[lvl].append("use a DH parameter of 2048 bits") + inter = False + if not has_ocsp: + failures[lvl].append("enable OCSP Stapling") + return inter +# is_modern is similar to is_old but for modern configuration from +# https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility def is_modern(results): + lvl = 'modern' + modern = True + has_dhparam = True + has_sha256 = True + has_ocsp = True + all_proto = [] for conn in results['ciphersuite']: if conn['cipher'] not in modern_ciphers: - errors["modern"]["ciphers"].append(conn['cipher']) - return False - if len(set(conn['protocols']) - set(['TLSv1.1', 'TLSv1.2'])) > 0: - # deprecated protocols are supported - return False - return True + logging.debug(conn['cipher'] + ' is not in the list of modern ciphers') + failures[lvl].append("remove cipher " + conn['cipher']) + modern = False + for proto in conn['protocols']: + if proto not in all_proto: + all_proto.append(proto) + if 'sha256WithRSAEncryption' not in conn['sigalg']: + logging.debug(conn['sigalg'][0] + ' is a not an intermediate signature') + inter = False + has_sha256 = False + if conn['cipher'][0:2] == 'DHE': + if conn['pfs'] != 'DH,2048bits': + logging.debug(conn['pfs']+ ' is not a good DH parameters for the old configuration') + inter = False + has_dhparam = False + if conn['ocsp_stapling'] == 'False': + has_ocsp = False + extra_proto = set(all_proto) - set(['TLSv1.1', 'TLSv1.2']) + for proto in extra_proto: + logging.debug("found protocol not wanted in the modern configuration:" + proto) + failures[lvl].append('disable ' + proto) + modern = False + missing_proto = set(['TLSv1.1', 'TLSv1.2']) - set(all_proto) + for proto in missing_proto: + logging.debug("missing protocol wanted in the modern configuration:" + proto) + failures[lvl].append('enable ' + proto) + if not has_sha256: + failures[lvl].append("use a certificate with sha256WithRSAEncryption signature") + modern = False + if not has_dhparam: + failures[lvl].append("use a DH parameter of 2048 bits") + modern = False + if not has_ocsp: + failures[lvl].append("enable OCSP Stapling") + return modern def is_ordered(results, ciphersuite): return True -def evaluate(results): +def evaluate_all(results): status = "obscure unknown ssl" if len(results['ciphersuite']) == 0: - status = "no ssl" + return "no ssl" if is_modern(results): if is_ordered(results, modern_ciphers): @@ -78,44 +237,122 @@ def evaluate(results): status = "old ssl with bad ordering" if is_fubar(results): - status = "fubar ssl" + return "fubar ssl" return status -def process_results(data): +def process_results(data, level=None): results = dict() + # initialize the failures struct + global failures + failures = dict() + failures['old'] = [] + failures['intermediate'] = [] + failures['modern'] = [] try: results = json.loads(data) except ValueError, e: print("invalid json data") try: if results: - print(results['target'] + " has " + evaluate(results)) + print(results['target'] + " has " + evaluate_all(results)) except TypeError, e: pass -def main(): - global all_ciphers, old_ciphers, intermediate_ciphers, modern_ciphers, errors - all_ciphers = subprocess.check_output(['./openssl', 'ciphers', all_ciphersuite]).rstrip().split(':') - old_ciphers = subprocess.check_output(['./openssl', 'ciphers', old_ciphersuite]).rstrip().split(':') - intermediate_ciphers = subprocess.check_output(['./openssl', 'ciphers', intermediate_ciphersuite]).rstrip().split(':') - modern_ciphers = subprocess.check_output(['./openssl', 'ciphers', modern_ciphersuite]).rstrip().split(':') - if len(sys.argv) > 1: - # evaluate target specified as argument - data = subprocess.check_output(['./cipherscan', '-j', sys.argv[1]]) - process_results(data) + # print failures + if level: + if len(failures[level]) > 0: + print("\nFailed to pass " + level + " level. The following items are failing:") + for failure in failures[level]: + print("* " + failure) else: - # take input from stdin - for data in fileinput.input(): - if data: - process_results(data) - print errors -# from https://wiki.mozilla.org/Security/Server_Side_TLS -all_ciphersuite = "ALL:COMPLEMENTOFALL:+aRSA" -old_ciphersuite = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128:AES256:AES:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA" -intermediate_ciphersuite = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128:AES256:AES:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA" -modern_ciphersuite = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK" -errors = defaultdict(str) + for lvl in ['old', 'intermediate', 'modern']: + if len(failures[lvl]) > 0: + print("\nFailed to pass " + lvl + " level. The following items are failing:") + for failure in failures[lvl]: + print("* " + failure) + +def build_ciphers_lists(): + global all_ciphers, old_ciphers, intermediate_ciphers, modern_ciphers, errors + # from https://wiki.mozilla.org/Security/Server_Side_TLS + allC = 'ALL:COMPLEMENTOFALL:+aRSA' + oldC = 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-S' \ + 'HA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM' \ + '-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-' \ + 'AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA' \ + '384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AE' \ + 'S128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-' \ + 'AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128:AES256:AES:DES-CBC3-SHA:HI' \ + 'GH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-R' \ + 'SA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' + intC = 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-S' \ + 'HA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM' \ + '-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-' \ + 'AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA' \ + '384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AE' \ + 'S128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-' \ + 'AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128:AES256:AES:CAMELLIA!aNULL:' \ + '!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC' \ + '3-SHA:!KRB5-DES-CBC3-SHA' + modernC = 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-S' \ + 'HA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM' \ + '-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-' \ + 'AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA' \ + '384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AE' \ + 'S128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-' \ + 'AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK' + logging.debug('Loading all ciphers: ' + allC) + all_ciphers = subprocess.check_output( + ['./openssl', 'ciphers', allC]).rstrip().split(':') + logging.debug('Loading old ciphers: ' + oldC) + old_ciphers = subprocess.check_output( + ['./openssl', 'ciphers', oldC]).rstrip().split(':') + logging.debug('Loading intermediate ciphers: ' + intC) + intermediate_ciphers = subprocess.check_output( + ['./openssl', 'ciphers', intC]).rstrip().split(':') + logging.debug('Loading modern ciphers: ' + modernC) + modern_ciphers = subprocess.check_output( + ['./openssl', 'ciphers', modernC]).rstrip().split(':') + +def main(): + parser = argparse.ArgumentParser( + description='Analyze cipherscan results and provides guidelines to improve configuration.', + usage='\n* Analyze a single target, invokes cipherscan: $ ./analyze.py -t [target]' \ + '\n* Evaluate json results passed through stdin: $ python analyze.py < target_results.json' \ + '\nexample: ./analyze.py mozilla.org', + epilog='Julien Vehent [:ulfr] - 2014') + parser.add_argument('-d', dest='debug', action='store_true', + help='debug output') + parser.add_argument('infile', nargs='?', type=argparse.FileType('r'), + default=sys.stdin, help='cipherscan json results') + parser.add_argument('outfile', nargs='?', type=argparse.FileType('w'), + default=sys.stdout, help='json formatted analysis') + parser.add_argument('-l', dest='level', + help='target configuration level [old, intermediate, modern]') + parser.add_argument('-t', dest='target', + help='analyze a , invokes cipherscan') + args = parser.parse_args() + + if args.debug: + logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) + else: + logging.basicConfig(stream=sys.stderr, level=logging.INFO) + + build_ciphers_lists() + + if args.target: + # evaluate target specified as argument + logging.debug('Invoking cipherscan with target: ' + args.target) + data = subprocess.check_output(['./cipherscan', '-j', args.target]) + process_results(data, args.level) + else: + if os.fstat(args.infile.fileno()).st_size < 2: + logging.error("invalid input file") + parser.print_help() + sys.exit(1) + data = args.infile.readline() + logging.debug('Evaluating results from stdin: ' + data) + process_results(data, args.level) if __name__ == "__main__": main()