#!/usr/bin/env python # # Copyright (C) 2015, Mikkel Krautz # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # - Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # - Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # - Neither the name of the Mumble Developers nor the names of its # contributors may be used to endorse or promote products derived from this # software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # # cipherinfo.py # Generate static TLS cipher information for Mumble. from __future__ import (unicode_literals, print_function, division) import json import re import subprocess try: from urllib2 import urlopen except: from urllib.request import urlopen from xml.dom import minidom IETF_TLS_PARAMETERS_WWW = "https://www.ietf.org/assignments/tls-parameters/tls-parameters.xml" def rfcNameLut(): lut = {} # Auto-generate from IETF_TLS_PARAMETERS_WWW u = urlopen(IETF_TLS_PARAMETERS_WWW) s = u.read() s = s.decode('utf-8') u.close() dom = minidom.parseString(s) registries = dom.getElementsByTagName("registry") for registry in registries: ident = registry.getAttribute("id") if ident == "tls-parameters-4": records = registry.getElementsByTagName("record") for record in records: value = record.getElementsByTagName("value")[0].childNodes[0].nodeValue.strip() description = record.getElementsByTagName("description")[0].childNodes[0].nodeValue.strip() # Skip free-form informational entries that use ranges (-) and * # in their value. if re.match("^[0xA-Z1-9,]*$", value) is None: continue lut[value] = description ########################################################################## # Obsolete SSLv2 cipher suites from RFC 4346, Appendix E: ########################################################################## # TLS_RC4_128_WITH_MD5 # 0x01, 0x00, 0x80 lut["0x01,0x00,0x80"] = "TLS_RC4_128_WITH_MD5" # TLS_RC4_128_EXPORT40_WITH_MD5 # 0x02, 0x00, 0x80 lut["0x02,0x00,0x80"] = "TLS_RC4_128_EXPORT40_WITH_MD5" # TLS_RC2_CBC_128_CBC_WITH_MD5 # 0x03, 0x00, 0x80 lut["0x03,0x00,0x80"] = "TLS_RC2_CBC_128_CBC_WITH_MD5" # TLS_RC2_CBC_128_CBC_EXPORT40_WITH_MD5 # 0x04, 0x00, 0x80 lut["0x04,0x00,0x80"] = "TLS_RC2_CBC_128_CBC_EXPORT40_WITH_MD5" # TLS_IDEA_128_CBC_WITH_MD5 # 0x05, 0x00, 0x80 lut["0x05,0x00,0x80"] = "TLS_IDEA_128_CBC_WITH_MD5" # TLS_DES_64_CBC_WITH_MD5 # 0x06, 0x00, 0x40 lut["0x06,0x00,0x40"] = "TLS_DES_64_CBC_WITH_MD5" # TLS_DES_192_EDE3_CBC_WITH_MD5 # 0x07, 0x00, 0xC0 lut["0x07,0x00,0xC0"] = "TLS_DES_192_EDE3_CBC_WITH_MD5" return lut def opensslCiphersOutput(): p = subprocess.Popen(['openssl', 'ciphers', '-V', 'ALL'], stdout=subprocess.PIPE) stdout, stderr = p.communicate() if stdout is not None: stdout = stdout.decode('utf-8') if stderr is not None: stderr = stderr.decode('utf-8') if p.returncode != 0: raise Exception('"openssl ciphers" failed: %s', stderr) return stdout def extract(splat): if len(splat) < 8: splat.extend(['']) return splat def Cstr(val): if val is None: return 'NULL' return '"{0}"'.format(val) def CPPbool(val): if val is True: return 'true' return 'false' def main(): added_ids = [] output = [] lut = rfcNameLut() ciphers = opensslCiphersOutput() for line in ciphers.split('\n'): if len(line) == 0: continue line = line.replace(' - ', ' ') line = line.replace('Kx=', '') line = line.replace('Au=', '') line = line.replace('Enc=', '') line = line.replace('Mac=', '') line = line.replace('(', '_') line = line.replace(')', '') line = line.replace('/', '_') tabline = re.sub('\ +', '', line, 1) tabline = re.sub('\ +', '\t', tabline) splat = tabline.split('\t') ident, osslname, minproto, kx, au, enc, mac, exp = extract(splat) # Look up the RFC name of this cipher suite. if ident in lut: rfcname = lut[ident] else: raise Exception('missing rfc_name in lut for %s' % ident) # Normalize kx, au, enc and mac. if 'None' in au: au = 'Anonymous' enc = enc.upper() if 'AESGCM' in enc: enc = enc.replace('GCM', '') enc = enc + '_GCM' if 'GCM' in rfcname and not 'GCM' in enc: enc = enc + '_GCM' elif 'EDE_CBC' in rfcname: enc = enc + '_EDE_CBC' elif 'CBC' in rfcname: enc = enc + '_CBC' elif 'CCM_8' in rfcname: enc = enc + '_CCM_8' elif 'CCM' in rfcname: enc = enc + '_CCM' if 'ECDHE' in osslname and not 'ECDHE' in kx: kx = kx.replace('ECDH', 'ECDHE') if 'ECDHE' in osslname and not 'ECDHE' in au: au = au.replace('ECDH', 'ECDHE') if 'DHE' in osslname and not 'DHE' in kx: kx = kx.replace('DH', 'DHE') if 'DHE' in osslname and not 'DHE' in au: au = au.replace('DH', 'DHE') if 'EDH' in osslname and not 'EDH' in kx: kx = kx.replace('DH', 'DHE') if 'EDH' in osslname and not 'EDH' in au: au = au.replace('DH', 'DHE') if mac != 'AEAD': mac = 'HMAC-' + mac # Validate macs valid_macs = ['AEAD', 'HMAC-MD5', 'HMAC-SHA1', 'HMAC-SHA256', 'HMAC-SHA384'] if mac not in valid_macs: raise Exception('invalid mac found: %s' % mac) # Use key exchange names from the RFCs, but also create # verbose key exchange names for export ciphers. match = re.match('^(TLS_|SSL_)(.*)_WITH.*$', rfcname) valid_rfc_kex = [ "ECDHE_RSA", "ECDHE_ECDSA", "SRP_SHA_DSS", "SRP_SHA_RSA", "SRP_SHA", "DHE_DSS", "DHE_RSA", "ECDH_anon", "DH_anon", "ECDH_RSA", "ECDH_ECDSA", "RSA", "PSK", ] valid_export_rfc_kex = [ "DHE_RSA_EXPORT", "DHE_DSS_EXPORT", "DHE_DSS_EXPORT", "DH_anon_EXPORT", "RSA_EXPORT", ] skip_rfc_kex = [ "IDEA_128_CBC", "RC2_CBC_128_CBC", "RC4_128", "DES_192_EDE3_CBC", "DES_64_CBC", "IDEA_128_CBC", "RC2_CBC_128_CBC_EXPORT40", "RC4_128_EXPORT40" ] if match is not None: rfc_kex = match.groups()[1] rfc_verbose_kex = rfc_kex if rfc_kex in skip_rfc_kex: rfc_kex = kx rfc_verbose_kex = kx elif rfc_kex in valid_rfc_kex: pass elif rfc_kex in valid_export_rfc_kex: if rfc_kex == 'DHE_RSA_EXPORT': rfc_verbose_kex = 'DHE_512_RSA_EXPORT' elif rfc_kex == 'DHE_DSS_EXPORT': rfc_verbose_kex = 'DHE_512_DSS_EXPORT' elif rfc_kex == 'DH_anon_EXPORT': rfc_verbose_kex = 'DH_anon_512_EXPORT' elif rfc_kex == 'RSA_EXPORT': rfc_verbose_kex = 'RSA_512_EXPORT' else: raise Exception('missing check for rfc_kex?') else: raise Exception('bad rfc_kex found: %s' % rfc_kex) pfs = False if rfc_verbose_kex == 'ECDHE_RSA': pfs = True elif rfc_verbose_kex == 'ECDHE_ECDSA': pfs = True elif rfc_verbose_kex == 'DHE_RSA': pfs = True elif rfc_verbose_kex == 'DHE_DSS': pfs = True elif rfc_verbose_kex == 'DHE_512_RSA_EXPORT': pfs = True elif rfc_verbose_kex == 'DHE_512_DSS_EXPORT': pfs = True # XXX: should SRP be marked as forward_secret? output.append({ 'identifier': ident, 'openssl_name': osslname, 'rfc_name': rfcname, 'minimum_protocol': minproto, 'key_exchange': rfc_kex, 'key_exchange_verbose': rfc_verbose_kex, 'openssl_key_exchange': kx, 'openssl_authentication': au, 'key_exchange': rfc_kex, 'encryption': enc, 'message_authentication': mac, 'export': exp == 'export', 'forward_secret': pfs }) added_ids.append(ident) # Add everything we missed from OpenSSL... include_extras = False if include_extras: for key in lut.keys(): if not key in added_ids: output.append({ 'identifier': key, 'openssl_name': None, 'rfc_name': lut[key], 'minimum_protocol': None, 'key_exchange': None, 'key_exchange_verbose': None, 'openssl_key_exchange': None, 'openssl_authentication': None, 'key_exchange': None, 'encryption': None, 'message_authentication': None, 'export': None, 'forward_secret': None }) output_c = True if output_c: print('// Automatically generated by "cipherinfo.py". DO NOT EDIT BY HAND.') print('//') print('// I also agree to have manually vetted this file for correctness.') print('//') print('// If I do not agree, I will not have removed the line above saying') print('// otherwise. Nor will I have removed the line below this one which') print('// will cause a preprocessor error. Oops!') print('#error Please verify this file is correct') print('static const SSLCipherInfo cipher_info_lookup_table[] = {') for entry in output: print('\t{') print('\t\t// openssl_name') print('\t\t{0},'.format(Cstr(entry["openssl_name"]))) print('\t\t// rfc_name') print('\t\t{0},'.format(Cstr(entry["rfc_name"]))) print('\t\t// encryption') print('\t\t{0},'.format(Cstr(entry["encryption"]))) print('\t\t// key_exchange_verbose. kx = {0}, au = {1}'.format(entry["openssl_key_exchange"], entry["openssl_authentication"])) print('\t\t{0},'.format(Cstr(entry["key_exchange_verbose"]))) print('\t\t// forward secret') print('\t\t{0},'.format(CPPbool(entry["forward_secret"]))) print('\t\t// message authentication') print('\t\t{0},'.format(Cstr(entry["message_authentication"]))) print('\t},') print('};') else: print(json.dumps(output, sort_keys=True, indent=4, separators=(',', ': '))) if __name__ == '__main__': main()