mumble/scripts/generate-cipherinfo.py
Mikkel Krautz 16dc758690 scripts/generate-cipherinfo.py: ensure output is consistently sorted.
This ensures that the output of the script is consistently sorted.
This commit uses the integer value of the literal cipher suite value.

This will make future diffs from output of the script much easier to read.
2017-04-01 09:10:30 +02:00

337 lines
9.7 KiB
Python
Executable File

#!/usr/bin/env python
#
# Copyright 2005-2017 The Mumble Developers. All rights reserved.
# Use of this source code is governed by a BSD-style license
# that can be found in the LICENSE file at the root of the
# Mumble source tree or at <https://www.mumble.info/LICENSE>.
#
#
# 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 integerFromIdent(ident):
'''
Parses a TLS cipher suite identifier from the IETF TLS cipher suite registry.
The cipher suite identifier is represented as a string of hex-encoded byte values
separated by commas.
This function consumes the byte sequence and converts it into an unsigned integer.
'''
hexVals = ident.split(',')
nbytes = len(hexVals)
if nbytes > 3:
raise Exception("unexpected amount of bytes")
outVal = 0
for idx, val in enumerate(hexVals):
intVal = int(val, base=16)
outVal |= ((intVal & 0xff) << (((nbytes - 1) - idx) * 8))
return outVal
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",
"DH_DSS",
"DH_RSA",
"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({
'sort_order': integerFromIdent(ident),
'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({
'sort_order': integerFromIdent(key),
'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
})
sortedOutput = sorted(output, key=lambda meta: meta['sort_order'])
output_c = True
if output_c:
print('// Automatically generated by "generate-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 sortedOutput:
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(sortedOutput, sort_keys=True, indent=4, separators=(',', ': ')))
if __name__ == '__main__':
main()