tls-table.py (217 lines of code) (raw):

#!/usr/bin/env python from __future__ import print_function from bs4 import BeautifulSoup as bs from collections import OrderedDict import json import requests import subprocess import sys GNUTLS_URL = 'https://gitlab.com/gnutls/gnutls/raw/master/lib/algorithms/ciphersuites.c' IANA_URL = 'http://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml' MOZILLA_SERVER_SIDE_TLS_URL = 'https://raw.githubusercontent.com/mozilla/server-side-tls/gh-pages/Server_Side_TLS.mediawiki' NSS_URL = 'https://hg.mozilla.org/projects/nss/raw-file/tip/lib/ssl/sslproto.h' OPENSSL_URL = 'https://raw.githubusercontent.com/openssl/openssl/master/include/openssl/tls1.h' LIBRARY_ORDER = ['IANA', 'GnuTLS', 'NSS', 'OpenSSL'] COMPAT_ORDER = ['Old', 'Intermediate', 'Modern'] # Styles for the table COMPAT_STYLE_NO_MATCH = 'background-color: white;' COMPAT_STYLE = { 'Modern': 'background-color: #9EDB58; font-weight: bold;', 'Intermediate': 'background-color: #DBC158; font-weight: bold;', 'Old': 'background-color: #CCCCCC; font-weight: bold;' } __colorize_lists = { 'Modern': [], 'Intermediate': [], 'Old': [] } def usage(): print("""Generate a table of cipher names from all the major library makers. Usage: {0} <output-format> [--colorize] Valid output formats are: json, csv, mediawiki""".format(sys.argv[0])) sys.exit(1) # Parse the command line def parse_args(): colorize = False if len(sys.argv) < 2 or len(sys.argv) > 3: usage() if 'json' not in sys.argv[1] and 'csv' not in sys.argv[1] and 'mediawiki' not in sys.argv[1]: usage() if '--colorize' in sys.argv: if sys.argv[1] == 'mediawiki': colorize = True return [sys.argv[1], colorize] def get_colorize_chart(): # XXX: should prefer to use code points from IANA_URL name_code_points = {} try: output = subprocess.check_output(['openssl', 'ciphers', '-V'], universal_newlines=True) for line in output.split('\n'): if '0x' in line: fields = line.split() name_code_points[fields[2]] = fields[0] except: print('Unable to run openssl ciphers', file=sys.stderr) sys.exit() print('Retrieving cipher suites from Mozilla Server Side TLS page', file=sys.stderr) # Grab the cipher suites from the Mozilla page r = requests.get(MOZILLA_SERVER_SIDE_TLS_URL) # Try to grab the ciphersuites out the ugly mess that is a wiki page # XXX: should prefer to use structured data from guidelines instead of from wiki page recommendations = []; for line in r.text.split('\n'): if "* Cipher suites (" in line: if "'''" in line: recommendations.append(line.split("'''")[1]) else: recommendations.append('') __colorize_lists.update({ 'Modern': get_colorize_chart_openssl_ciphers(name_code_points, recommendations[0] + ':' + recommendations[1]), 'Intermediate': get_colorize_chart_openssl_ciphers(name_code_points, recommendations[2] + ':' + recommendations[3]), 'Old': get_colorize_chart_openssl_ciphers(name_code_points, recommendations[4] + ':' + recommendations[5]) }) def get_colorize_chart_openssl_ciphers(name_code_points, ciphersuites): code_points = [] for cipher in ciphersuites.split(':'): if cipher in name_code_points: code_points.append(name_code_points[cipher]) # XXX: explifies why input should be taken from IANA and guidelines instead of from wiki pages elif cipher == 'ECDHE-ECDSA-AES256-SHA384': code_points.append('0xC0,0x24') elif cipher == 'ECDHE-RSA-AES256-SHA384': code_points.append('0xC0,0x28') elif cipher == 'DES-CBC3-SHA': code_points.append('0x00,0x0A') elif cipher != '': print("Warning: not found: {0}".format(cipher)) return code_points def get_hex_values(): # Grab the list from the IANA print('Retrieving IANA cipher List', file=sys.stderr) try: r = requests.get(IANA_URL) soup = bs(r.text, 'html.parser')\ .select('table[id="table-tls-parameters-4"]')[0]\ .find_all('tbody')[0] # Store all the ciphers away cipher_hex_values = OrderedDict() for row in soup.find_all('tr'): columns = [ x.string for x in row.find_all('td') ] # For now, we can ignore any IANA entries with '-' or '*' in them if '-' not in columns[0] and '*' not in columns[0] and columns[1] != 'Unassigned' and columns[1] != 'Reserved': cipher_hex_values[ columns[0] ] = { 'GnuTLS': '', 'IANA': columns[1], 'NSS': '', 'OpenSSL': '' } except: print('Unable to retrieve or parse IANA cipher list', file=sys.stderr) # Grab the list from NSS (Mozilla) print('Retrieving NSS cipher list', file=sys.stderr) try: r = requests.get(NSS_URL) for line in r.text.split('\n'): # A typical line would look like: #define TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 0xC02F if '#define TLS' in line and '0x' in line: cipher = line.split()[1] hex = line.split()[2].upper() code_point = '0x' + hex[2:4] + ',0x' + hex[4:6] if code_point in cipher_hex_values: cipher_hex_values[code_point]['NSS'] = cipher # 0x00,0x60-66 Reserved to avoid conflicts with widely deployed implementations elif not code_point.startswith('0x00,0x6'): print(' Warning: code point {code_point} ({cipher}) not in IANA registry'.format( code_point=code_point, cipher=cipher ), file=sys.stderr) except: print('Unable to retrieve or parse NSS cipher list', file=sys.stderr) # Grab the list from OpenSSL print('Retrieving OpenSSL cipher list', file=sys.stderr) try: # OpenSSL splits up their code points and their text names for them openssl_hex_values = {} openssl_txt_values = {} r = requests.get(OPENSSL_URL) for line in r.text.split('\n'): if line.startswith('# define TLS1_CK'): cipher = line.split()[2].split('TLS1_CK_')[-1] hex = line.split()[3] code_point = '0x' + hex[6:8] + ',0x' + hex[8:10] # e.g., ECDHE_RSA_WITH_AES_128_GCM_SHA256 -> 0x0C,0x2F openssl_hex_values[cipher] = code_point elif line.startswith('# define TLS1_3_CK'): cipher = line.split()[2].split('TLS1_3_CK_')[-1] hex = line.split()[3] code_point = '0x' + hex[6:8] + ',0x' + hex[8:10] # e.g., TLS1_3_CK_AES_128_GCM_SHA256 -> 0x13,0x01 openssl_hex_values[cipher] = code_point elif line.startswith('# define TLS1_TXT'): cipher = line.split()[2].split('TLS1_TXT_')[-1] text = line.split()[3][1:-1] # e.g., ECDHE_RSA_WITH_AES_128_GCM_SHA256 -> ECDHE-RSA-AES128-GCM-SHA256 openssl_txt_values[cipher] = text elif line.startswith('# define TLS1_RFC'): cipher = line.split()[2].split('TLS1_RFC_')[-1] text = line.split()[3][1:-1] # e.g., TLS1_RFC_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 -> TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 openssl_txt_values[cipher] = text elif line.startswith('# define TLS1_3_RFC'): cipher = line.split()[2].split('TLS1_3_RFC_')[-1] text = line.split()[3][1:-1] # e.g., TLS1_3_RFC_AES_128_GCM_SHA256 -> TLS_AES_128_GCM_SHA256 openssl_txt_values[cipher] = text for key, value in openssl_hex_values.items(): if value in cipher_hex_values: cipher_hex_values[value]['OpenSSL'] = openssl_txt_values[key] else: print(' Warning: code point {code_point} ({cipher}) not in IANA registry'.format( code_point=value, cipher=key ), file=sys.stderr) except: print('Unable to retrieve or parse OpenSSL cipher list', file=sys.stderr) # Grab the list from GnuTLS print('Retrieving GnuTLS cipher list', file=sys.stderr) try: r = requests.get(GNUTLS_URL) # Some lines look like: #define GNUTLS_DH_ANON_3DES_EDE_CBC_SHA1 { 0x00, 0x1B } # Other look like: #define GNUTLS_ECDHE_ECDSA_CAMELLIA_128_CBC_SHA256 { 0xC0,0x72 } for line in r.text.split('\n'): if line.startswith('#define GNUTLS_') and '{' in line: cipher = line.split()[1][3:] code_point = line.split('{')[-1].replace(' ', '').replace('}', '').upper().replace('X', 'x') if code_point in cipher_hex_values: cipher_hex_values[code_point]['GnuTLS'] = cipher # 0x00,0x60-66 Reserved to avoid conflicts with widely deployed implementations elif not code_point.startswith('0x00,0x6'): print(' Warning: code point {code_point} ({cipher}) not in IANA registry'.format( code_point=code_point, cipher=cipher ), file=sys.stderr) except: print('Unable to retrieve or parse GnuTLS cipher list', file=sys.stderr) print('\n', file=sys.stderr) return cipher_hex_values def __print_wiki_entry(code_point, ciphers): print('|-') # Determine the style to use; use COMPAT_STYLE_NO_MATCH by default style = '| style="{style}" '.format(style=COMPAT_STYLE_NO_MATCH) # Now print all the columns for the various libraries for order in COMPAT_ORDER: if code_point in __colorize_lists[order]: style = '| style="{style}" '.format(style=COMPAT_STYLE[order]) centered_style = '| style="{style} text-align: center;" '.format(style=COMPAT_STYLE[order]) # Print the Hex column print('! scope=row | {code_point}'.format(code_point=code_point)) # Determine the priority if code_point in __colorize_lists.get('Old'): priority = __colorize_lists.get('Old').index(code_point) + 1 else: priority = None # Print the columns by priority if priority: print('{style}| {priority}'.format(style=centered_style, priority=priority)) else: print('{style}data-sort-value="1000" | '.format(style=style)) for library in LIBRARY_ORDER: print('{style}| {cipher}'.format(style=style, cipher=ciphers.get(library, ''))) def print_csv(data): values = [] header = ["hex",] + LIBRARY_ORDER print(','.join(header)) for code_point, cypher in data.items(): r = [code_point.replace(',0x','')] r.extend(list(cypher.values())) line = ",".join(r) print(line) # Print everything out def print_output(cipher_hex_values, output_format): # JSON output is super easy if output_format == 'json': print(json.dumps(cipher_hex_values, indent=2)) elif output_format == 'csv': print_csv(cipher_hex_values) elif output_format == 'mediawiki': # Table header print('{| class="wikitable sortable"') print('|-') print('\n'.join( ['! scope="col" | ' + x for x in ['Hex', 'Priority'] + LIBRARY_ORDER] )) # Determine the order to go by: first let's go by priority for code_point in __colorize_lists.get('Old'): __print_wiki_entry(code_point, cipher_hex_values[code_point]) del(cipher_hex_values[code_point]) # If they don't have a priority, then go by hex value for code_point, ciphers in reversed(cipher_hex_values.items()): __print_wiki_entry(code_point, ciphers) # Table footer print('|}') if __name__ == '__main__': output_format = parse_args() output = get_hex_values() if output_format[1]: get_colorize_chart() print_output(output, output_format[0])