diff --git a/.gitignore b/.gitignore index 04ab27c..77184aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,10 @@ mozilla/* top1m/results/* +tlslite +.tlslite-ng +*/__pycache__ +*.pyc +.coverage +.python-ecdsa +ecdsa +tlslite diff --git a/cipherscan b/cipherscan index 5f79abd..8d5ec3f 100755 --- a/cipherscan +++ b/cipherscan @@ -980,6 +980,8 @@ display_results_in_terminal() { done | sort fi fi + + echo "$cscan_tests" } display_results_in_json() { @@ -1080,7 +1082,12 @@ display_results_in_json() { echo -n "}" ctr=$((ctr+1)) done - echo '}}' + echo -n '}' + if [[ -n $cscan_tests ]]; then + echo -n ',"intolerancies":' + echo -n "$cscan_tests" + fi + echo '}' } test_serverside_ordering() { @@ -1550,6 +1557,17 @@ test_tls_tolerance() { tls_tolerance['small-SSLv3']="True $current_protocol $current_cipher $current_trusted" fi fi + + # finally run the Python based test to perform more precise scan + if [[ "$OUTPUTFORMAT" == "json" ]]; then + cscan_tests="$($DIRNAMEPATH/cscan.sh -j $TARGET $sni_target)" + else + if [[ $VERBOSE != 0 ]]; then + cscan_tests="$($DIRNAMEPATH/cscan.sh -v --no-header $TARGET $sni_target)" + else + cscan_tests="$($DIRNAMEPATH/cscan.sh --no-header $TARGET $sni_target)" + fi + fi } test_kex_sigalgs() { diff --git a/cscan.py b/cscan.py new file mode 100644 index 0000000..f685ef0 --- /dev/null +++ b/cscan.py @@ -0,0 +1,467 @@ +# Copyright 2016(c) Hubert Kario +# This work is released under the Mozilla Public License Version 2.0 +"""tlslite-ng based server configuration (and bug) scanner.""" + +from __future__ import print_function +from tlslite.messages import ClientHello, ServerHello, ServerHelloDone, Alert +from tlslite.constants import CipherSuite, \ + ExtensionType, AlertLevel +from tlslite.extensions import TLSExtension +import sys +import json +import getopt +import itertools +import copy + +from cscan.scanner import Scanner +from cscan.config import Xmas_tree, IE_6, IE_8_Win_XP, \ + IE_11_Win_7, IE_11_Win_8_1, Firefox_46, Firefox_42, HugeCipherList, \ + VeryCompatible +from cscan.modifiers import no_sni, set_hello_version, set_record_version, \ + no_extensions, truncate_ciphers_to_size, append_ciphers_to_size, \ + extend_with_ext_to_size, add_empty_ext +from cscan.bisector import Bisect + + +def scan_with_config(host, port, conf, hostname, __sentry=None, __cache={}): + """Connect to server and return set of exchanged messages.""" + assert __sentry is None + key = (host, port, conf, hostname) + if key in __cache: + return __cache[key] + + scanner = Scanner(conf, host, port, hostname) + ret = scanner.scan() + __cache[key] = ret + if verbose and not json_out: + print(".", end='') + sys.stdout.flush() + return ret + + +class IE_6_ext_tls_1_0(IE_6): + def __init__(self): + super(IE_6_ext_tls_1_0, self).__init__() + self.modifications += ["TLSv1.0", "ext"] + self.version = (3, 1) + self.record_version = (3, 0) + + def __call__(self, hostname): + ret = super(IE_6_ext_tls_1_0, self).__call__(hostname) + ret.ssl2 = False + # filter out SSLv2 ciphersuites + ret.cipher_suites = [i for i in ret.cipher_suites if i <= 0xffff and + i != CipherSuite. + TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + ret.extensions = [TLSExtension(extType=ExtensionType. + renegotiation_info) + .create(bytearray(1))] + return ret + + +def simple_inspector(result): + """ + Perform simple check to see if connection was successful. + + Returns True is connection was successful, server replied with + ServerHello and ServerHelloDone messages, and the cipher selected + was present in ciphers advertised by client, False otherwise + """ + if any(isinstance(x, ServerHelloDone) for x in result): + ch = next((x for x in result if isinstance(x, ClientHello)), None) + sh = next((x for x in result if isinstance(x, ServerHello)), None) + if ch and sh: + if sh.cipher_suite not in ch.cipher_suites: + # FAILURE cipher suite mismatch + return False + return True + # incomplete response or error + return False + + +def verbose_inspector(desc, result): + """Describe the connection result in human-readable form.""" + ret = "{0}:".format(desc) + if any(isinstance(x, ServerHelloDone) for x in result): + ch = next((x for x in result if isinstance(x, ClientHello)), None) + sh = next((x for x in result if isinstance(x, ServerHello)), None) + if sh and ch: + if sh.cipher_suite not in ch.cipher_suites: + ret += " FAILURE cipher suite mismatch" + return ret + name = CipherSuite.ietfNames[sh.cipher_suite] \ + if sh.cipher_suite in CipherSuite.ietfNames \ + else hex(sh.cipher_suite) + ret += " ok: {0}, {1}".format(sh.server_version, + name) + return ret + ret += " FAILURE " + errors = [] + for msg in result: + if isinstance(msg, ClientHello): + continue + # check if returned message supports custom formatting + if msg.__class__.__format__ is not object.__format__: + errors += ["{:vxm}".format(msg)] + else: + errors += [repr(msg)] + # skip printing close errors after fatal alerts, they are expected + if isinstance(msg, Alert) and msg.level == AlertLevel.fatal: + break + ret += "\n".join(errors) + return ret + +configs = {} + + +def load_configs(): + """Load known client configurations for later use in scanning.""" + base_configs = [Xmas_tree, Firefox_42, IE_8_Win_XP, IE_11_Win_7, + VeryCompatible] + for conf in base_configs: + # only no extensions + gen = no_extensions(conf()) + configs[gen.name] = gen + + gen = no_sni(conf()) + configs[gen.name] = gen + + for version in ((3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (3, 254)): + if conf().version != version: + # just changed version + gen = set_hello_version(conf(), version) + if gen.record_version > version: + gen = set_record_version(gen, version) + configs[gen.name] = gen + + # changed version and no extensions + gen = set_hello_version(conf(), version) + if gen.record_version > version: + gen = set_record_version(gen, version) + gen = no_extensions(gen) + configs[gen.name] = gen + + # changed version and no sni + gen = set_hello_version(conf(), version) + if gen.record_version > version: + gen = set_record_version(gen, version) + gen = no_sni(gen) + configs[gen.name] = gen + + # Xmas tree configs + gen = Xmas_tree() + configs[gen.name] = gen + + gen = no_sni(Xmas_tree()) + configs[gen.name] = gen + + # Firefox 42 configs + gen = Firefox_42() + configs[gen.name] = gen + + # Firefox 46 configs + gen = Firefox_46() + configs[gen.name] = gen + + gen = set_hello_version(Firefox_46(), (3, 254)) + configs[gen.name] = gen + + gen = set_hello_version(Firefox_46(), (3, 5)) + configs[gen.name] = gen + + gen = no_extensions(set_hello_version(Firefox_46(), (3, 5))) + configs[gen.name] = gen + + gen = set_hello_version(Firefox_46(), (3, 1)) + configs[gen.name] = gen + + # IE 6 configs + gen = IE_6() + configs[gen.name] = gen + + gen = IE_6_ext_tls_1_0() + configs[gen.name] = gen + + # IE 8 configs + gen = IE_8_Win_XP() + configs[gen.name] = gen + + gen = extend_with_ext_to_size(IE_8_Win_XP(), 200) + configs[gen.name] = gen + + for ext_id in (0, 1, 2, 3, 4, 5): + gen = add_empty_ext(IE_8_Win_XP(), ext_id) + configs[gen.name] = gen + + # IE 11 on Win 7 configs + gen = IE_11_Win_7() + configs[gen.name] = gen + + gen = no_sni(IE_11_Win_7()) + configs[gen.name] = gen + + gen = set_hello_version(no_sni(IE_11_Win_7()), (3, 2)) + configs[gen.name] = gen + + # IE 11 on Win 8.1 configs + gen = IE_11_Win_8_1() + configs[gen.name] = gen + + gen = set_hello_version(IE_11_Win_8_1(), (3, 1)) + configs[gen.name] = gen + + gen = set_hello_version(IE_11_Win_8_1(), (3, 4)) + configs[gen.name] = gen + + # Huge Cipher List + gen = HugeCipherList() + configs[gen.name] = gen + + gen = truncate_ciphers_to_size(HugeCipherList(), 16388) + configs[gen.name] = gen + + # Very Compatible + gen = VeryCompatible() + configs[gen.name] = gen + + gen = append_ciphers_to_size(VeryCompatible(), 2**16) + configs[gen.name] = gen + + gen = extend_with_ext_to_size(VeryCompatible(), 2**16) + configs[gen.name] = gen + + gen = extend_with_ext_to_size(VeryCompatible(), 16388) + configs[gen.name] = gen + +def scan_TLS_intolerancies(host, port, hostname): + """Look for intolerancies (version, extensions, ...) in a TLS server.""" + results = {} + + def result_iterator(predicate): + """ + Selecting iterator over cached results. + + Looks for matching result from already performed scans + """ + return (not simple_inspector(results[name]) for name in results + if predicate(configs[name])) + + def result_cache(name, conf): + """Perform scan if config is not in results, caches result.""" + return results[name] if name in results \ + else results.setdefault(name, scan_with_config(host, port, conf, + hostname)) + + def conf_iterator(predicate): + """ + Caching, selecting iterator over configs. + + Returns an iterator that will go over configs that match the provided + predicate (a function that returns true or false depending if given + config is ok for test at hand) while saving the results to the + cache/verbose `results` log/dictionary + """ + scan_iter = (not simple_inspector(result_cache(name, conf)) + for name, conf in configs.items() + if predicate(conf)) + return itertools.chain(result_iterator(predicate), scan_iter) + + host_up = not all(conf_iterator(lambda conf: True)) + + intolerancies = {} + if not host_up: + if json_out: + print(json.dumps(intolerancies)) + else: + print("Host does not seem to support SSL or TLS protocol") + return + + intolerancies["SSL 3.254"] = all(conf_iterator(lambda conf: + conf.version == (3, 254))) + intolerancies["TLS 1.4"] = all(conf_iterator(lambda conf: + conf.version == (3, 5))) + intolerancies["TLS 1.3"] = all(conf_iterator(lambda conf: + conf.version == (3, 4))) + intolerancies["TLS 1.2"] = all(conf_iterator(lambda conf: + conf.version == (3, 3))) + intolerancies["TLS 1.1"] = all(conf_iterator(lambda conf: + conf.version == (3, 2))) + intolerancies["TLS 1.0"] = all(conf_iterator(lambda conf: + conf.version == (3, 1))) + intolerancies["extensions"] = all(conf_iterator(lambda conf: + conf.extensions and not + conf.ssl2)) + + #for name in ["Xmas tree", "Huge Cipher List", + # "Huge Cipher List (trunc c/16388)"]: + # intolerancies[name] = all(conf_iterator(lambda conf: + # conf.name == name)) + + def test_cb(client_hello): + ret = scan_with_config(host, port, lambda _:client_hello, hostname) + return simple_inspector(ret) + + # most size intolerancies lie between 16385 and 16389 so short-circuit to + # them if possible + good_conf = next((configs[name] for name, result in results.items() + if simple_inspector(result)), None) + + if good_conf: + size_c_16382 = simple_inspector(scan_with_config(host, port, + append_ciphers_to_size(copy.deepcopy(good_conf), 16382), hostname)) + size_c_16392 = simple_inspector(scan_with_config(host, port, + append_ciphers_to_size(copy.deepcopy(good_conf), 16392), hostname)) + + if size_c_16382 and not size_c_16392: + good = append_ciphers_to_size(copy.deepcopy(good_conf), 16382) + bad = append_ciphers_to_size(copy.deepcopy(good_conf), 16392) + elif not size_c_16382: + good = good_conf + bad = append_ciphers_to_size(copy.deepcopy(good_conf), 16382) + else: + bad = append_ciphers_to_size(copy.deepcopy(good_conf), 65536) + size_c_65536 = simple_inspector(scan_with_config(host, port, + bad, hostname)) + if not size_c_65536: + good = None + intolerancies["size c/65536"] = False + else: + good = append_ciphers_to_size(copy.deepcopy(good_conf), 16392) + + if good: + bisect = Bisect(good, bad, hostname, test_cb) + good_h, bad_h = bisect.run() + intolerancies["size c/{0}".format(len(bad_h.write()))] = True + intolerancies["size c/{0}".format(len(good_h.write()))] = False + + # test extension size intolerance, again, most lie between 16385 + # and 16389 so short-circuit if possible + good_conf = next((configs[name] for name, result in results.items() + if configs[name].extensions and + simple_inspector(result)), None) + + if good_conf: + size_e_16382 = simple_inspector(scan_with_config(host, port, + extend_with_ext_to_size(copy.deepcopy(good_conf), 16382), hostname)) + size_e_16392 = simple_inspector(scan_with_config(host, port, + extend_with_ext_to_size(copy.deepcopy(good_conf), 16392), hostname)) + + if size_e_16382 and not size_e_16392: + good = extend_with_ext_to_size(copy.deepcopy(good_conf), 16382) + bad = extend_with_ext_to_size(copy.deepcopy(good_conf), 16392) + elif not size_e_16382: + good = good_conf + bad = extend_with_ext_to_size(copy.deepcopy(good_conf), 16382) + else: + bad = extend_with_ext_to_size(copy.deepcopy(good_conf), 65536) + size_e_65536 = simple_inspector(scan_with_config(host, port, + bad, hostname)) + if not size_e_65536: + good = None + intolerancies["size e/65536"] = False + else: + good = extend_with_ext_to_size(copy.deepcopy(good_conf), 16392) + + if good: + bisect = Bisect(good, bad, hostname, test_cb) + good_h, bad_h = bisect.run() + intolerancies["size e/{0}".format(len(bad_h.write()))] = True + intolerancies["size e/{0}".format(len(good_h.write()))] = False + + if json_out: + print(json.dumps(intolerancies)) + else: + if not no_header: + if verbose: + print() + print("Host {0}:{1} scan complete".format(host, port)) + if hostname: + print("SNI hostname used: {0}".format(hostname)) + if verbose: + print() + print("Individual probe results:") + for desc, ret in sorted(results.items()): + print(verbose_inspector(desc, ret)) + + print() + print("Intolerance to:") + for intolerance, value in sorted(intolerancies.items()): + print(" {0:20}: {1}".format(intolerance, + "PRESENT" if value else "absent")) + + +def single_probe(name): + """Run a single probe against a server, print result.""" + print(verbose_inspector(name, scan_with_config(host, port, + configs[name], hostname))) + + +def usage(): + """Print usage information.""" + print("./cscan.py [ARGUMENTS] host[:port] [SNI-HOST-NAME]") + print() + print("-l, --list List probe names") + print("-p name, --probe Run just a single probe") + print("-j, --json Output in JSON format") + print("-v, --verbose Use verbose output") + +if __name__ == "__main__": + try: + opts, args = getopt.getopt(sys.argv[1:], + "jvhlp:", + ["json", "verbose", "help", "list", + "probe=", "no-header"]) + except getopt.GetoptError as err: + print(err) + usage() + sys.exit(2) + + json_out = False + verbose = False + list_probes = False + run_probe = None + no_header = False + + for opt, arg in opts: + if opt in ('-j', '--json'): + json_out = True + elif opt in ('-v', '--verbose'): + verbose = True + elif opt in ('-h', '--help'): + usage() + sys.exit(0) + elif opt in ('-l', '--list'): + list_probes = True + elif opt in ('-p', '--probe'): + run_probe = arg + elif opt in ('--no-header', ): + no_header = True + else: + raise AssertionError("Unknown option {0}".format(opt)) + + if len(args) > 2: + print("Too many arguments") + usage() + sys.exit(2) + + load_configs() + + if list_probes: + for desc, ret in sorted(configs.items()): + print("{0}: {1}".format(desc, ret.__doc__)) + sys.exit(0) + + hostname = None + if len(args) == 2: + hostname = args[1] + hostaddr = args[0].split(":") + if len(hostaddr) > 1: + host, port = hostaddr + else: + host = hostaddr[0] + port = 443 + + if run_probe: + single_probe(run_probe) + else: + scan_TLS_intolerancies(host, port, hostname) diff --git a/cscan.sh b/cscan.sh new file mode 100755 index 0000000..e0a3b01 --- /dev/null +++ b/cscan.sh @@ -0,0 +1,26 @@ +#!/bin/bash +pushd "$(dirname ${BASH_SOURCE[0]})" > /dev/null +if [ ! -d ./tlslite ]; then + git clone --depth=1 https://github.com/tomato42/tlslite-ng.git .tlslite-ng + ln -s .tlslite-ng/tlslite tlslite +fi +if [ ! -d ./ecdsa ]; then + git clone --depth=1 https://github.com/warner/python-ecdsa.git .python-ecdsa + ln -s .python-ecdsa/ecdsa ecdsa +fi + +# update the code if it is running in interactive terminal +#if [[ -t 1 ]]; then +if [[ $UPDATE ]]; then + pushd .tlslite-ng >/dev/null + git pull origin master --quiet + popd >/dev/null + pushd .python-ecdsa >/dev/null + git pull origin master --quiet + popd >/dev/null +fi + +PYTHONPATH=. python cscan.py "$@" +ret=$? +popd > /dev/null +exit $ret diff --git a/cscan/__init__.py b/cscan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cscan/bisector.py b/cscan/bisector.py new file mode 100644 index 0000000..bf35e7f --- /dev/null +++ b/cscan/bisector.py @@ -0,0 +1,172 @@ +# Copyright (c) 2015 Hubert Kario +# Released under Mozilla Public License Version 2.0 + +"""Find an itolerance through bisecting Client Hello""" + +import copy +from tlslite.extensions import PaddingExtension + +def list_union(first, second): + """Return an union between two lists, preserving order""" + first_i = iter(first) + second_i = iter(second) + first_s = set(first) + second_s = set(second) + + ret = [] + first_el = next(first_i, None) + second_el = next(second_i, None) + while first_el is not None and second_el is not None: + if first_el != second_el: + if first_el in second_s and second_el in first_s: + # the second list is longer, so take from it + ret.append(second_el) + # no discard as we would have duplicates + second_el = next(second_i, None) + continue + if first_el not in second_s: + ret.append(first_el) + first_s.discard(first_el) + first_el = next(first_i, None) + if second_el not in first_s: + ret.append(second_el) + second_s.discard(second_el) + second_el = next(second_i, None) + else: + ret.append(first_el) + first_s.discard(first_el) + second_s.discard(first_el) + first_el = next(first_i, None) + second_el = next(second_i, None) + while first_el: + if first_el not in second_s: + ret.append(first_el) + first_el = next(first_i, None) + while second_el: + if second_el not in first_s: + ret.append(second_el) + second_el = next(second_i, None) + return ret + + +def bisect_lists(first, second): + """Return a list that is in the "middle" between the given ones""" + # handle None special cases + if first is None and second is None: + return None + if first is not None and second is None: + first, second = second, first + if first is None and second is not None: + if len(second) == 0: + return None + elif len(second) == 1: + return [] + else: + first = [] + # make the second lists always the longer one + if len(first) > len(second): + second, first = first, second + first_s = set(first) + second_s = set(second) + union = list_union(first, second) + symmetric_diff = first_s.symmetric_difference(second_s) + # preserve order for the difference + symmetric_diff = [x for x in union if x in symmetric_diff] + half_diff = set(symmetric_diff[:len(symmetric_diff)//2]) + intersection = first_s & second_s + + return [x for x in union if x in half_diff or x in intersection] + + +def bisect_padding_extension(first, second): + if first is None and second is None: + return None + if first is not None and second is None: + first, second = second, first + if first is None and second is not None: + if len(second.paddingData) == 0: + return None + elif len(second.paddingData) == 1: + return PaddingExtension() + else: + first = PaddingExtension() + return PaddingExtension().create((len(first.paddingData) + + len(second.paddingData)) // 2) + + +def bisect_extensions(first, second): + # handle padding extension + if first is None and second is None: + return None + if first is not None and second is None: + first, second = second, first + if first is None and second is not None: + if len(second) == 0: + return None + if len(second) == 1: + return [] + first = [] + f_ext = next((x for x in first if isinstance(x, PaddingExtension)), None) + s_ext = next((x for x in second if isinstance(x, PaddingExtension)), None) + + ext = bisect_padding_extension(f_ext, s_ext) + if ext is None: + # remove the extension + return [x for x in first if not isinstance(x, PaddingExtension)] + else: + if f_ext is None: + return first + [ext] + # replace extension + return [ext if isinstance(x, PaddingExtension) else x for x in first] + + +def bisect_hellos(first, second): + """Return a client hello that is in the "middle" of two other""" + ret = copy.copy(first) + + ret.client_version = ((first.client_version[0] + second.client_version[0]) + // 2, + (first.client_version[1] + second.client_version[1]) + // 2) + ret.cipher_suites = bisect_lists(first.cipher_suites, second.cipher_suites) + ret.extensions = bisect_lists(first.extensions, second.extensions) + ret.compression_methods = bisect_lists(first.compression_methods, + second.compression_methods) + if first.extensions == ret.extensions \ + or second.extensions == ret.extensions: + ret.extensions = bisect_extensions(first.extensions, + second.extensions) + return ret + +class Bisect(object): + """ + Perform a bisection between two Client Hello's to find intolerance + + Tries to find a cause for intolerance by using a bisection-like + algorithm + """ + + def __init__(self, good, bad, hostname, callback): + """Set the generators for good and bad hello's and callback to test""" + self.good = good + self.bad = bad + if hostname is not None: + self.hostname = bytearray(hostname, 'utf-8') + else: + self.hostname = None + self.callback = callback + + def run(self): + good_hello = self.good(self.hostname) + bad_hello = self.bad(self.hostname) + middle = bisect_hellos(good_hello, bad_hello) + + while good_hello != middle and \ + middle != bad_hello: + if self.callback(middle): + good_hello = middle + else: + bad_hello = middle + middle = bisect_hellos(good_hello, bad_hello) + + return (good_hello, bad_hello) diff --git a/cscan/config.py b/cscan/config.py new file mode 100644 index 0000000..2079f1b --- /dev/null +++ b/cscan/config.py @@ -0,0 +1,580 @@ +# Copyright (c) 2016 Hubert Kario +# Released under Mozilla Public License Version 2.0 + +"""Typical Client Hello messages sent by different clients.""" + +import random +from tlslite.messages import ClientHello +from tlslite.constants import \ + ECPointFormat, HashAlgorithm, SignatureAlgorithm +from tlslite.mathtls import goodGroupParameters +from tlslite.extensions import SNIExtension, SupportedGroupsExtension, \ + TLSExtension, SignatureAlgorithmsExtension, NPNExtension, \ + ECPointFormatsExtension, PaddingExtension +from tlslite.utils.cryptomath import numberToByteArray +from tlslite.utils.ecc import getCurveByName, encodeX962Point +from tlslite.utils.cryptomath import powMod +from .constants import CipherSuite, ExtensionType, GroupName +from .extensions import KeyShareExtension + +class HelloConfig(object): + """Base object for all Client Hello configurations.""" + + def __init__(self): + """Initialize object with default settings.""" + self._name = None + self.modifications = [] + self.callbacks = [] + self.version = (3, 3) + self.record_version = (3, 0) + self.ciphers = [] + self.extensions = None + self.random = None + self.session_id = bytearray(0) + self.compression_methods = [0] + self.ssl2 = False + + @property + def name(self): + """Return the name of config with all the modifications applied.""" + if self.modifications: + return "{0} ({1})".format(self._name, + ", ".join(self.modifications)) + else: + return self._name + + @name.setter + def name(self, value): + """Set the base name of the configuration.""" + self._name = value + + def __call__(self, hostname): + """Generate a client hello object, use hostname in SNI extension.""" + # SNI is special in that we don't want to send it if it is empty + if self.extensions: + sni = next((x for x in self.extensions + if isinstance(x, SNIExtension)), + None) + if sni: + if hostname is not None: + if sni.serverNames is None: + sni.serverNames = [] + sni.hostNames = [hostname] + else: + # but if we were not provided with a host name, we want + # to remove empty extension + if sni.serverNames is None: + self.extensions = [x for x in self.extensions + if not isinstance(x, SNIExtension)] + + if self.random: + rand = self.random + else: + # we're not doing any crypto with it, just need "something" + # TODO: place unix time at the beginning + rand = numberToByteArray(random.getrandbits(256), 32) + + ch = ClientHello(self.ssl2).create(self.version, rand, self.session_id, + self.ciphers, + extensions=self.extensions) + ch.compression_methods = self.compression_methods + for cb in self.callbacks: + ch = cb(ch) + return ch + + +class Firefox_42(HelloConfig): + """Create Client Hello like Firefox 42.""" + + def __init__(self): + """Set the configuration to Firefox 42.""" + super(Firefox_42, self).__init__() + self._name = "Firefox 42" + self.version = (3, 3) + self.record_version = (3, 1) + self.ciphers = [CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA] + ext = self.extensions = [] + ext.append(SNIExtension()) + ext.append(TLSExtension(extType=ExtensionType.renegotiation_info) + .create(bytearray(1))) + ext.append(SupportedGroupsExtension().create([GroupName.secp256r1, + GroupName.secp384r1, + GroupName.secp521r1])) + ext.append(ECPointFormatsExtension() + .create([ECPointFormat.uncompressed])) + ext.append(TLSExtension(extType=ExtensionType.session_ticket)) + ext.append(NPNExtension()) + ext.append(TLSExtension(extType=ExtensionType.alpn) + .create(bytearray(b'\x00\x15' + + b'\x02' + b'h2' + + b'\x08' + b'spdy/3.1' + + b'\x08' + b'http/1.1'))) + ext.append(TLSExtension(extType=ExtensionType.status_request) + .create(bytearray(b'\x01' + + b'\x00\x00' + + b'\x00\x00'))) + sig_algs = [] + for alg in ['sha256', 'sha384', 'sha512', 'sha1']: + sig_algs.append((getattr(HashAlgorithm, alg), + SignatureAlgorithm.rsa)) + for alg in ['sha256', 'sha384', 'sha512', 'sha1']: + sig_algs.append((getattr(HashAlgorithm, alg), + SignatureAlgorithm.ecdsa)) + for alg in ['sha256', 'sha1']: + sig_algs.append((getattr(HashAlgorithm, alg), + SignatureAlgorithm.dsa)) + ext.append(SignatureAlgorithmsExtension() + .create(sig_algs)) + + +class Firefox_46(HelloConfig): + """Create ClientHello like Firefox 46""" + # verified by packet capture + def __init__(self): + super(Firefox_46, self).__init__() + self._name = "Firefox 46" + self.version = (3, 3) + self.record_version = (3, 1) + self.ciphers = [CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA] + + ext = self.extensions = [] + ext.append(SNIExtension()) + ext.append(TLSExtension(extType=ExtensionType.extended_master_secret)) + ext.append(TLSExtension(extType=ExtensionType.renegotiation_info) + .create(bytearray(1))) + ext.append(SupportedGroupsExtension().create([GroupName.secp256r1, + GroupName.secp384r1, + GroupName.secp521r1])) + ext.append(ECPointFormatsExtension().create([ECPointFormat.uncompressed])) + ext.append(TLSExtension(extType=ExtensionType.session_ticket)) + ext.append(NPNExtension()) + ext.append(TLSExtension(extType=ExtensionType.alpn) + .create(bytearray(b'\x00\x15' + + b'\x02' + b'h2' + + b'\x08' + b'spdy/3.1' + + b'\x08' + b'http/1.1'))) + ext.append(TLSExtension(extType=ExtensionType.status_request) + .create(bytearray(b'\x01' + + b'\x00\x00' + + b'\x00\x00'))) + sig_algs = [] + for alg in ['sha256', 'sha384', 'sha512', 'sha1']: + sig_algs.append((getattr(HashAlgorithm, alg), + SignatureAlgorithm.rsa)) + for alg in ['sha256', 'sha384', 'sha512', 'sha1']: + sig_algs.append((getattr(HashAlgorithm, alg), + SignatureAlgorithm.ecdsa)) + for alg in ['sha256', 'sha1']: + sig_algs.append((getattr(HashAlgorithm, alg), + SignatureAlgorithm.dsa)) + ext.append(SignatureAlgorithmsExtension() + .create(sig_algs)) + +_ec_precomputed = { + 'secp256r1' : [ + bytearray(b'\x04\x85\x96\xe5\xa1\x92\x99\xf8\xb1\xb1}\xcd\xdc\x1c^\xfd05\xa1\xe1\xd3\xd0\x87s\xb1\xd8>\x00TX\xac\x86\xe1\xa3\xb0c~\x85\x8d&\xaf"X\xb5\x9cf\x8d\xf4\xf5d\xd7u\'&]\xbe\xe3\x83]ul\xff\x86e\x89'), + bytearray(b'\x04\xa89\xfc\xb2r\xf3\x03p6\xca\xb6\xe2\xb1t2\t\x8b\xb2\x89\xce\x8f\x84\r\x8b\x1b\r\\\r/\xcd\xabn\xb6WE\xb7\x9a\xba\r\xdb\x03\xc3\x9c\xd9N\x19\x03[\xe0\nK\xf3\xfb\x00\xbe]d\x96e\xf2\xde\x94\td'), + bytearray(b'\x04\xaa\xf1Q\xfb\xf1\x95\x03\xaa\x8d\x8c\xefB\x8c;\x04u\xdaG\xee\xe5\xd9`J\xe3\x90\xc3T\x02\xe7\x80\x9b\x0f,bi4m2\n\xffD\x9b\xb68\x10\xa3\xbdd]\x05\xa3\x81\xfe\x97J\x9aY\x0e\xe3\x1b\xad\xe4\x84\xbd'), + bytearray(b'\x04\x17\xf9K\x1b\x95\xd3\xf7\xfc`y\xe7g*A\xeb4!\xbca\xd1)\xa0\x0e\xa88\xbd\xb0\xd6\x9a\x97\x95\xa1R2\xcc]$[\xc1\x90T\x83Lu\xbb%I\x81\x8a\x11\x0c\x01\x07)"\x80\xbb\xbaS\xa5`r\x91\xe6'), + bytearray(b"\x04xGX\x06\xb2N\xe2\x9f\xfc\xdc}\xd2\xdbU\x9a$\xb6\xbc`\xb6\xec\xaa\xad(\xa4\x7fwU\xcfK/T\xea\xa5\xc2Y\xf7<\xfe\xa9\xaf\xa2f7\xa2\'/\xfd\x18\xae\x1e\xc0,\xa5cd\x8d2\x98\xeeA#\x13\x9c"), + bytearray(b'\x04\x14\xf0\xf5_\xa1\xf6\xa5\x1c\xf4%\x17\xc4\x86uE|\xc3#\x11\x9b\xf8\xf2\xcc\xc8F/\xcb\xc46,\x81+@\xbeS\x93\xa59\xae\xc8\xe9 {\r\xdfE j!\xa7\xef\x85\x04\xa0\x9bI\xbbiC\x9c#II\xb2')], + 'secp384r1' : [ + bytearray(b'\x04*\x9c\x14\xfe!_\xea\xa7\xef\x8aR\xd7\x9d\xab\xc3fU6)\x8b\xce\x935q\x92l\xbc\xb84\x96$\xf0\x86\x05\x85\xaaEq\xca0\x99\xf6c\xf2+=\xc9!Ms%z\xf7\x98\x1auCOb\xcc\xdc\x89\x1d\x97\xa7y\xe0\x8e\xea/\x80brj\xd7\x1b\x17\x89<\xe5\xba\xa3*\x0c\xb5\r\xde}\xa7{\x94\x9f\x81\xb3:\x92'), + bytearray(b"\x04\x9b]\xfap\xf5R\xefCv\x95\xc4c\xea\xee\xe1i\xf80\x97\x08\xdb\xe7\x83a\x8f~`\xd7`>A\x8aS6\x82\'\xd6711\x9b,+\xc9\xbb\xa7\xa3E\xe3\xfb\x1a\xa2\x83\xac\xf8\x0eV\x93y\xb2w\xe5\xf5hQ3H\xa0\xa9\x8d\xfdg\xe9\xac\x12\xcd%(bY\xc5+\x98o\xb5D1\xcbu\x7f\t\x8f\xfe\xfa\x16q"), + bytearray(b'\x04@\'b\'{\x15H\xb0\x9c?\xe5dg\xbf\xe4\'\xe1D\xf7FIs\xb9\x153\x87x\xcb\xb7\xf6b\x9b\xe5\x10_\xfc%Z\x87\xa5\xae(\x13\xb1*]_g\xd4\xeaeH\x83\xed\xa0N\x82Mqz;\xbba\x0f\xf6\x9c\x08"\xca\x00\x8fq\xc5\xc7\t{\xf1l_h\x05\xec\x9a\x19N\x98+%=?\x14\xbf0\x94\x13"'), + bytearray(b"\x04pM\xfe\xf0+h\xe3\x9d\xda\xe1x1\x02}\xa4\xa2\x83\'\xfem\x1a;\xbf\x10\xd7\xb2\xd9I\x06-\x9fY\xa3k9\xc4\xc8\xda\x1e>G\xaf\xcb|\x01\xd6j\xff|\xe0>\x81I\x8b\x03\xd3\xf8\xc9|\xab&w\xf8\x1b\x9a\x98\xb1CXAU\t\x03\xd6\xcf\xa0L\xd1B \x13Mr\xbe\x8b\xe6}\xf2\xb2oc\x98A\x94\x9eX"), + bytearray(b'\x04\x1a[X\x13\x17"^\x02\x19d\xc4\xec\x14\xea<\x8c\xd2f\x95\xb4b\xdc\xbbG;pq\x9a^\xf8\xd0\xe7\x99Ofk\x1e#\x8dZ\xcb\xbf\x1e=p\xaa\xe5\x92\xda^\xb2\x86\xdf\xf8(\xfcIc\xde\xcempP\x82\xcd\x88\xe3\x87\xd1\x8b\xac\xe5\x19\xd4)\ndPX\\)\xf7\x1c\xdef\x0e` \x13\xa1\xa8YSun4'), + bytearray(b"\x04\xce\xbe\xb9{$#\x03c\xdemi\xf0\xec\x07]Q8^\x8b\xdd,)~\x972]cI\xbe\xc9\x8b\xa0\xd7\xc7\x96H\xa4\xea\xb2\x86%\xee\xb2\xc8\x08\x1d\xa4\xb9\xc5Bc \xac-;J{\x10\xafh\xceU*+h\xda&7\'\x15\xb7d5\xfd\x94\x86\xd3\xe8=\xcc\xbf\x8d\x07\x17\xc9(\x17U@gH\x8d]\xc5\xe2\xe2;.S\xf9\xa1\x00\xc2\xb5\xb7\'\x05f\xe6\xf3\xf7Za\xa1\xa4\x9209\xf3I\xe3\xdd%\xd1\x8a\x82\n4Z\xa1ol?l(\x939_+(\x1c\xca\xa4\xc3\xdb\x01\x19\xc5\xb4\xfc\xb7\xa8\xa4\xd3\x14x\x0c\x1f\x91\xd1!5\x1a\xb0\x0b\xa7\xe9"), + bytearray(b'\x04\x01\xban\x80\x85\xe8\xc7\xd0r\x17\x84\xea\x16\xc0\xfdM\xde\x98\xfe\x82\xd9\x06{\t\x1f*\xa2(t\xd4\x95y\xb7\x98\xbbP\xf1\xd9_9\xe3\xd3\xf8\xf1\xa9\xec\x1c\xce\xf6\\\xb1\xb6`|\xb9`]Wy\xaf\x99\xfc\xc2\xa5$\xd9\x01\xb5Y\x85\x85.\xc8\x8a\x94\xde\xbc\xe6m/\x04C 6|m\x92\x00\xfan*\x1c\xb7z\xbc\xc4|\xea,/t\xc7,"\xe2\x8b\xe7R\xb6\xc1IC\x12\x1f\x1f7Z\xdf\x1e\x04\xeb-@\xa2\x1e\xb4E\x83\xe8\xfe\xba\xbb3la\xa6\x8e\xf7\x8e\xd7\xa7Q\x81\x9f|(\xe7\xf6\xc0\x96\xdf\x01\x945\xb0\xdd=\x99\xde\xa4\xf1}(4\xe8\xf1L\xb5\x88\xb2\x9a\xde\x12\xcfn\'\xce\xb8g\x89\x0eY\x9c\x8a6-i\xafO\x0f\xbfY\x8b\xb3T\x0f"g3\xdf%r\xbakOV\xcd\xc5\x1f&\xa7\x1e$\x8c`IV\xc3\x1af\xbar\xfb2\xe1\xfe{k,\xb0\xe8{\x17\x9b\xd8\xccrM\x17&4\xd1\xad*fM\x9b\xfetW\xafU\x1bDYCW\xb25O`/p\xb7\xf4\xcb\xedI\xf9\x17~\xa7\x13\x18\xb9Vv\xb4\xa3\xe4\xde\xe8\x16\x8a\x8d%\xf3Cc\x977\x17\x947\xd7%\xac-0\xbeX@\xf2o\xad\xf1w\xa3N\xf9}\x13\xeb\xc7\x97T3\x008\xa5\xc1\x1c\xc4w\x91F\xd3L\x9eY\xd1\x89\xbdc\n\'\x80\x80\x8f\x18\xee#\xdc[\xac\xae\xe50\xdc\xa2\xec\x9c\xf1\rO\x95\xe4\x06\x8b5\xb1\xb3s\'4M\x11u\xfd\x87\xc0\x19\xa0\x0b\xc6=\xe5\xb4t\xe7\x19\x19h\xd9*\xef\x9e"UK\xadb-\xe7\xca)\xda\xaf \xf3\n\xd9\xc9\xccN\x06\xe4\x1aQb\xd6\xccw\x0e\x1d\xfc\x05\x12x\xe1\xc9hxpa\x13\xbf\xca\xc31\x15r$;\xf1\xaf\xa4\xd2u7\xdc\xc0\x9e\xb8\x8f\xd1My\x92r\xf5\xd1\\#\xfb_\x9b(\xab\xe3\x95\x9cEdk\xc1\xac\xe7\xfb+\xbdZ\xc5d\x85\xc0(V\xf6$\xd8w4[\xd8z\xae\tl\xf2\xa7\x07\x07o\t\xd0\x93\x95\xa6B\x99\r\xf4\xfcm\x1e\tgw\xf8\x90\xdd~\x12\x9c\xff\xe09T\xe5\ti\x15\xa9N\xf3\x19\xa2\xbf\xb4v\xc5~\xa9\xdct\xb6\xce\xf1\xdd6\xf4\x86\xffu\x0cR@\xbb\xa6\x17\xbcN\xb0\xd6)\xeba\x7f+\xdc\x9f\x0cU\x7f2\xa9\x07\xd0qo\xea\xc1I\x8b\x90\xbd\x13\x04\xa7\x13]"\xf7\x15\xae\xcf\xf0\xe3G\xdd\\O\x0cJ{\xf1G\xffK\xd9\xfdbc)\xa98\xa2\x88\x13MT\xaf\xda\xed\xc4\xa7\x19A\x9b\xe4L\x06\xfb\x05{\xa8\xa5l3\x8e;\x97\x1d\x18P\x13\x9cBn_@\xe3X\x18!\xb6\xc4\x99WQ\x9d\xd6)\x88\xb3\xcff%{\x83\xf2NK\xca\xf0\x02\x06\xb77\xdb\xd3\xbe\x7f\x98\x03\xbf<\xe3\x0b\x05\x8b7j6\x8e\x87\x86{\x83\x16\xb8\xceD\xdf3\x1d\x9d\xb7xG\xe5y\xa4\x82r\x81\xa0\xc8\xd6CP)\xbdV\xf6\x08\xd3l\xc1\x80q\xac\xd2\x92\x86p1cK[\xdd:\n`\xe2@\xfc\x13N\x80\x11\x98\xc9\xfb\xd8\xaaE\xeeeo\xca\xc5\x1a\x07\xa4}u9X\xef\xdb\xea\xb0/\x98!\xe0#N\xdc\xf8!\x8fu\x1a\xe9'), + bytearray(b'7\x1aE,\xd6ZA`l\xee\x05>j"x\xa3};&\x98\xda\x06O\xe5\xb0Z\xc2\x947*b\x89\xb6\xe0\x17\xc0\x96\x17\x8b\xd0\xbeb\xe42;Nh\x82\x83i\xb7\xc5\x88^\x14I\x10\x8a\xa9ez\xb2\xa6\xf8\x03\xc0%\xf3+x\xb6<\x02\xeb\x90\x8fkm\x9c\xd6\x99\xa9\xc0o\x94\xc1\x1eT\xe9\x84V\x89\xccMEkH\x05\xb3\x00G\x82\xdb\xd1>B\x07\xe7/B\xa0\t[\xb3\xdeC\xba\x94\xfb\x87=*IU\x11\xa9\x03\x1a\x04\x8e@\x8dN\x81\x06\xed\x02\x8f\x9b\x89?!\xa3[H\x072W_C?\x01\xae+\x92g\xb0\xc2DE:\x82D\x97\xf6\xb9\xd7H\xcc\xf5\xfe\naZ\xa4\xf8\x16CubL\n\x83\xe4<52\xd5\xa1I\tV\xdf\xf8\x07\xd2\xf6\xfe\\\x9d\xe0\xa1y\xda\xbb\x97\x9aE\xd2\xb7\x97\xe3\xa02\xd0kW\xd5=\xc8\xbd\x8a\xb5\xf9RA\xfb\xef\xd4\x19\xf3\x14\xcb\xdc&1\x82Kzc\x0e\x91\x86\xe7?\x1c\x91\x03\t8\x8d\xd4\x9b\x06\x14q\xa9l\x16\xa99\xf3\xe0\xc7\x9f\xd8\xa2\xfb\xd4+\xd3\x92\xcb\x10\xbd\x8b\x08\xb2.\xfd\xef6`\xba\x1eu\x80\xcd\xd4\x90\x9e\xcd=\x8b\x03\xa5;\x1c\xcd\xf5\x873m\xeeZ\x9f\xffk*\xc9\r\x86&?]4@7\xc2@\x04\x8e\xba\t\xbd\xe7eDC\xad~\xf8V1~\xee;\r\xa8\x05TSWOF\xec\xd2\x08\x129\x7f\xccR\x06\xd2\xb0\x11\xb1\t\x03\x84}I\x1b\xe8\xc2!\xc34e\xa0\xab\x88T\xcf\x14\x84\xa7J4x\x01\x97n\xeal\xfcL~V\xd6+\xd0\xc9U\xaf\x8a\xdb\x9e\x8e\x84\x9a\x98\xf2\xd1\x07vw0\xf2\xb5R\x94\xbfU\x87r!\xab\xce):!a{\x96#eMD\xb2\x0em\xc38\x0e\x8a\x9b\x89\x7f\xf1>\x01&\x10\xaa\x03\xdd\x91\x90\x04]\xd0\xdfES\xed%E\xb8\xfc\xed\xfc\x1e\xaa\x88\xd3\x10\xce\x93\t\xe0\xc1\x18%\xcc\x89]\x04\xc2~8I\xd2:\x03\x83\xe2\xed\xb9\x8c\xd1\xc0*\x882\xbb\xb0\xa1uP4\xcbR\x8f\x03\xac\x05\x9c\xbcl\x17\x9b\x9a\xbc\xa5\x0c\xbby\xb1\xba\x91\xa3\x86\xb3|_\xae\x10\xdf\xafyF}\x8d\xe1j\x9adi,K\x1d\xcby\x87\xcf\xc2\xcdp\xa4\xcbk\x8d(\'b\xcd^\xb9\xb1J6\x87\xf2\xf7j\xc4\xc6\'K\x1e\x0e\x91<@\x88\xec3\xf9\xeb\xc9\n\n\x13\x96\x8a\x16\xc7\xc4\xea(~\xb8K\xfe\x1awsvru\x94r\x97\xd9<\xca=dFT\xa9\x08\xbe\xf7\x1c\xc5\xe6]\xc1(R\x86\x1e\xf12<\x9aw\x99\x16\xab\xf2R\x1fD\xea\x95=\xbd;T\xb1\xa4\xa1\x02\xb1\xe9\xdb\xa2\xfc\xd4\xdd\xa7[\xac\xfc\xc4\x8b\xe4\xd3\x18\xdf\xae\xb4\x98eo.0\xa7M\xd0\xe9\xf8\x87D\x99\xcbJ\xfb\x03\x9fFiY\x16"\xde\xb3$\xd2\xbdJ\x98G\x8b\xb1_\xc9\xd7\x81\xca\xbbKK\xf7~R\xe4@\x99RY6'), + bytearray(b'0\x83\xcd\xbce\x92\xefu\x01?p\xff~\x10W\xa9\xfa\x16\x89\xd1\x9d\xbe\xf6:\xd2\x9c!B\xff$z\xb9\x97]\xdf\x02\xd5\xd2K\xd5\x0e\xc1}\x97,\x0f\xdb\xa5k\xb1\xc8 J@\xa2Y\x0e\xf1\xc1g\xbc}\x9f\x9b\t\x93\xb6\xab\xf7\xd2\xfaK\xe9(D\x8cq\xedf\x03\x0c\x07\x96\xa4D\xa2\xf8,\x8d\x87B\x96\x1fqY+\xd18\x8b\x94\x03<\x1d\x80\xbc#\xcc\x8b\xef\xfdGo\xac(h,\x11\xa3\xa0\x91Y\x91!\xdf\xd2\xfe9\xf3\xa0x\xd5\xec5Qf\xd1\xdabK\xf5\xbe\x9244\xb0\xa5>\x06(@\xc6\x11Z\xe7n%,\xff\xb4\xe9\x97 \x894&\x08n~\x17\x87\xb2\x8c\x15\xcc\xa4\x8e\x86\xfb\x0f\x8c\xa4\xa6 \x9d\x92\x95Zr<\t\xdeC6\xf9\x0c\xa9mq\xcd CB\x9c\xa6g\xb7\xad \x8dY\xcb\xb5\xb7\xd4&\x9cq\x85\r\xc1\xdd\x1f\x89 \xf2\x9e\xee{\xa3h.\xba\x0e2\xb3\x84\x93\xacFY\xf5{ZG\xa4\x9a\x7fT\x81\x04\xcd_\xd9\xc75\xfbOR4\x0br\xd3\x11\xce\x90\x97-]\x87^Ug1\x14\x02\x86IS \xbd2\x128\xf1\xd0=\xa8s\x16/8:.O{\xa8\xf4ZW^\xda\x89\xa7\xa2\x11\xf8/\xa9\xd0\xf9\x19\xf67_\x9bkA\xdf\xbc\xe1v\xf6\x9e\xe6z\xe0\xd4\xb4\xef\xffuz\x14\x0c\xcd\xcb~8;Y^\xee\x06\xec\'c\xe3#\xa3\x10\xb9\xa9P\xd8\x08\xb15\xdc\xe4\xfb\xed0n\xfd~3\xd5Sh\xf3\x94\x97\x8bb]\xcb\xa3f\x0b6\xc3\x1a{\xda\xc9\xb1M\xa1\xf7\xa1\x96\xec\x11\x07\xab\x81\xd9-\xc9zz\xec\xf3W\x95^APs\x94q\t;\xb4\xea\xf4D\xeaA$\xf8+\xf9.\xbf8.^u\xfa\xbb\x17\xc7\xeb_u\x00\xa4{\xe4$\xc21XJ5\xf1\xbe"bd\x87J\x8d\xf9\x8e\x00\xa3\xb4\xdb3\x99L\x95\x1f?G\x86\xa9HL\xb8\x039+\xdfe\x0c\xeb\xa6u\xecU\xa6\x99\xb0A\x1a^\xf4Od\x9fw\x02h\xc28\xbbwp\\hi\x00\xd7\xeb\x8cKv\xe4\x9c\x1a:\xb1n\x84:\x0f\x81[\x9a\xed\xdc\xac^\xf1\xba.\xc3=3\x1c\xfaM\xdbr\xf50\xe9\xb9e\xf0-\x99+\xbe\xaa|yb\xb5\xf1S\xb8\xb5\x08\xd8\xa9\xfc\x84\xad\xbc\xd3\x15e\xb3R\x91cA\xef\xddO\xf8\xc2\xc4\xb8\xbce\xdb\xb9m\xe8\xdb\x15\xf2\xf6\x9d\x16\x1c|\xa62K\x9b\xa1\xf3\xba\x908\xfc,,\xa5\x9e\xba\x0b;C\xe7\xe9s:\xd1\x0f\xe6Y\xa9\x8b\\\xf1\xdd\x83\x03\x04\x9e$\x8a;\xa5*4\xab\xaaGV\x13R\x8f\xfa\x19\x18\xcf\xd6Ibe\x19\xa8\xcc\xf0\xbcv\x16\x93\xff\\\x13\x1d\x89\xff7\xeb\xf4\x81\xc2\xd3\x9aN8pCp\xa1\xd5%\x8c\x19\x94\xee\xa7\xc1\xf5\xf5\xff\xb5O\xfb\x04\xfd\x02A\xe8\xfaz|\xbd\x9e\xdb\xd5\xe0bO\x8eW\xfa\xe8\xc1\xac+N\x95\xc0\xa5\xcaN\x1cw\xa2bgfu\x0e\xa6\xac-2N\x9c\x08&\xc6\xdf1\xec<\x9c\xabm\xe5\xac\xa2z\xfa|\x8a#e\'{]\x92v"\xf9\x0f\xe6S\x95\x19\xbc\xacAC\xbd\xf7\xcb!(\xf9\xc2~\x1c\x95\r\xd6\x8a\xf8\xede\x1ev:\xda\xbf\x80\x1fSi\x81,sr:6\xb3K8\xd8\x86\xf6\xc2yl\xa0K$.\x0f\xca#\xc9vv9D%\x8c\xe3qJ\x94|\x01\x88\xf6\x1f1\x0e\xd3\xd0\xb1?l\x14uy\xa8\xa4OH\xcb,\x04x\xf4\xef-FU\xa9i\xc2\x8d)\xed\xe0\xa7\xdaX%\xb2\xb2\xe6^\xe3\xa6\xcd\xaahX\xef|.^\x15>\xd1\x0b\x1c\x85\x18\xb4\x1c\xdaAy\xccP\xc6\x17\x85\x19A\xaf\x11N\xbf\xb9\xbe%]\xe0@kP.\xb6i,Z\xb6\x9f\x8cH2\x8a\xd8\xa8FQPl\x14m$t\xc1\xe5u\xb3\xf9\xab\xcbsa\xd7\xf2\x82&\x16\xfa\x13Wu\xd3+\x87\xe8|n>+\xb81\x03^\xc7\t\xa4[\x90O\xebT%\x82\xd5\x05O\x9f\x87\xddF\x82t\xbf\x04\x8b}s}\xda\xc8|\xae\xb1\x9a\xad\xbc\xce\xef\xa2t\xd2\x87\x8a\xaa\r\xe9yoL\xaa,\nj\x04n\t*\x93A"8)g\xcf\n\xba\xf2Y'), + bytearray(b'\x8f\xafe\xf6YA\xe2\r\xfdB\xf8b\x8a\xdf\xb6\xd2\xbet}=u\x13\xe3f\xdb(\xd3r\xd9K\x88d\xdcS\xfe\xf6d\xa2\x95\xee\xad0\xcd\x08\xba\xfc\xf9\x10\x11jB\x1d\xfb\xbd\xe9C\xa7\x82\x86\x16[\x81\xac\xff\x1a\xe4>\x84\xe1}\xba\xa8_Q\xb4\\\x0cJ\xdc\xda6`\xa9\x86)7\xd8\xfa\xd4\x890\\\x07\xf6s:\xf5c\xb2\xd1\xcfW`\xa6\x96=\xe8P\x18\xb8\xa3^\xa5\xb0L\xea\xb1\'i\x12%;\xbb\xc8A\xca\x9c&\xfc\x08\xab\xd5\x89\xfe\xbb\x05\xfa\x00\xebkU\xb6g\xf8qt8\x10.\xd0\xbe;\xe6\xb2L\x95M,\x9c\xb0\x06\x98\xb4\xae\xa1>\x058|;\xd3\xc3\x93\xc1\x80YH\xe3i\xd4\xd0\xd35h\xc0)4\x0fv\xc5\xd1\x96 R\xe3\xbc\x9e&(\xd9l\xf2\xdb\xfe\x12\xfa\xbb\x8a\x0es\xc2IK\xa1\xca\x94`\xe3\x14X\xfb\x84K\xb9\xc2Ch\x86#\x95\xbf\xd0\xa7\x10n\x94\xcd\x07\xc7\xdc\xd0\xc0\xcdL\xb4AL\x8d\xae\xb3\x7f\x95{X\x929\x91N\xf6\xe3\xa5\x1d;\x8d\xfd\xd8\xee\x07\x90\xd2\x13\xafG\xc9\xc3\xfe\xb3\x99\xd6(\xbax\x94\x11\xe5\xa6\xc9\x04\xa6~7XcqZ\xe8{\xad0\xc7\'\x9f\x1bZ\x95\xbf}\x1es\xa6\x1c\x1d\xff\xce\x05\xaa\x95G\t\xf7\xd0\xb1\xe4\x05 \x1b\xac\xac\x0c\xfa\xc1\'\x16/\x94\x85\xc0\x82\x88\xba$1\xcd\x88\x9cu\x88\x961\xa0\xdd\xa3\xe0v0\xdbD\xf4{\xbd\x13\x03\xb3\x7f\x03\xd7?\xc2\x00K\x07i\xff\x12\xfe\xdc\x86\x1eF\x80\\2\x84kT\x7f%\x13\x07:\xf8\xcd>(`y\x9aI\xa2\xd2\x1f\x8e\xcfy\xaa\xa7"\xd7\xef\xfap*{Yn^\xd9u\x03\x08\x85\x9e\x99\x85\xb5\xfa\xd3"\x9cOUH\t\x02\xd5\x1d\\;\xc7\xb9\xd4\x86V\xfa\xab\xb9\xbbU\x89\xf1\xf6Z\xa1*-\x10&\xcb|rqy$\xe6P8\x9e\xe2G"&\xa2\xfd\x18\xf4\xd0J&\xb6\xa0\xf0}Q\x96\xe4\xb1\xc8"\xb0x\xeb\xd98\xb4&\xf6\x05;\x92]\xa7\\\xe5j\x04D\xf6c\xbe\xd6\x8br3\xae\xb4V\xfd\x9d\x0f\xc0\xa2G\x1e\x0c{\xe7!O,\xcc\x8d-U\xd5:F\xca\xe2\xccyA\xf1\x0b\xd6\xae\x8ar\x9c,\xc59\x02\x8e\x90\x8f\xddI\x9c\xe0H\x9f\xd3>\xed\x9d\x82lU\x81\xd6\xf7^7(\xd4"k\xc4\x1cO\xa8e\xa7\xce\x1d0\xa0\xf0\xb0\x02\xb3i\xbb\xe1\xe1Z\x8e$(\xf9mM\xb6$\x0e\x01+\'\x10F^\x90\xab\xb3:\x84\xfa"\xed\xc1A?D<\x17c\xba\xba`\xfd\xaa\xae\x9d\xc7:j\xb3#\xa0\xa6"R(P\\\xdc\xc9s\x00&2\x10\xe7*jvJ\xa0\rU\x80R\xe1\x8e\xe9\x97\xe6\x1a\x05W\xc1{\xe3y\xbf.\xbb\xde\x11j\xf4\xdfCc\xaa\x15@2\xe6V+w:\xebD&\x9a\x8cAC\xfa7\xd6\xb2\xd1\x8f\xf4\xb4d\xe4\xbb~5\xbaU\xa5\x8f\xa09\xea\xc1C\xb5\x9fR\x05E%\xe0\x0bp\x1b\x88\xbb\xf1lI\xd5\xe0{\xa4\x0e\x12)o\x8a\xefr\x11\xe2\xde\xca\x14\xb0\x8c\xf1\x97\x1b\xb2o\xe8g\xc5M\xcd\xb3I\xa7C\xd7\xd7\x0b\xa5c%\xf9s\xf2I\xba\x82\x8et(W\xf8\xf1\x8a\xd6\xcc\xbc\x13\x08O\x8a\xe6\xfa`?\xae\xc2H\x1aAN)\xe8\xb5\xc3\n\xd6l~\x8d\xd73\x18r&\x98\xfb7\x84\xbc\xff\xd8\xdd\xac%u,\xfb\xbf\x04\xcc\x9d\xa4a\x17\x90\x83\xd3\xe3UV\xcd\x9f\n"6_8R\xaf`NH\xf2\xf93,_E\xa99\x99\xbbL"\x9b\xd1qv91\x844\xea\x9f\xe7\x8a&\xd0v\x0c\x8c\xd8\xfd\xb8\x97\xff\x98O\xc8\xc9:\xdf\x89\xc2\x82\xac\xcaw\xa4\xf8h\xa8i\xa5A\x07s\xbb\xed\xcdLV{\x8b\xf9\x81\xcb2\xad\xb7>\x9a\x97\xcb\xe8|x\xa3p\x93\'\xff\xce\x91\x85\xb8\xdaq\xac\xcd\x90n\x1b\xffr\xd2\xdd\xf2<\xc1\xe5\x94\xc6\xbe0\xe61\xbe\xa4\xe4\xb2\x93\xa9\xaaoI\x11\x04\xc8\'3\xe0FJ\x9f^N\xed\x8f\xb4\xcb\x128\xa3x\xeb\xc8\xdaI\xeaBBb\x1d\xc2\xe3\xdc\x7f%\xc6J\xa3\x1fF\x12\xa3s\xc8C\xe2\xde\xc5\xe0\xe7\xeb\\\xf8\x99\xf7\xd6\xa4vPg\x8f,\xe0\xcf\xef8d\x9cZ\x80\xc1\xac\xba\x14s\x8d\x05\x8a.\xd5D\xe3\xcd\x1e\xf7\xd99\xb8\xb4\x81\xf5\xa1\x1a\x11m6\x80\'y\xef\xc3r~E3h\n7Q\x14V#\xde\x81\xcc\xe0V\xd7\xb3\x84\xd5\x9c\x83\x91\xaf\xf2\x832\x10\xf1\\\x05\x14L\xdf`\xf3\xf5\x0e\xd98N6%[.2\xc3\xd4L<\xc9(\x96\x88\x84\xca\xfb\xf7e\xe4\xdd\x9e\x91DK!\x8a\\\x85\xc1U\x91\xc8APvy\xf6z\x1b\xfc\xa1\x02^;\xb8\xa5\xe0\xab\xb8\xee.\xac\t\xb9\x16T\xb7g\x11/\x0c\x81\x93\xdeR\x07\xaf\x85\xd1\xbc\xb5\xe0\xc9\xcdL\x9c\x9d\xfc\xbf\xc3K+\x87\x07u\xe6\x02\xd4c\xf4\x857=\x14\xc3{\n\xc7\x08\xcd\xee2\xa9\x9f|\x04tu\x1d\n\xd4\xdbx>A\xc0D\xf8\xadOF@\xad\xa1\xb1i\x12:1\xd9\x05N}\x13\xd6C8\xc0\x83z\xb5J\xee9\xcf\xa3\xc5\xd2\xcb\xd2\x1f\x96\x8b7\x0fw\x9b"\xab\xb9\xa2z\x81\xa8M_\xe3\xa2z\xb8\xc2\x10Q\x05,\x1c\x81\x03,\xc7di\xba\x85\x07z8!F\x989\xcd\x17$\'\xc0^\t\xd6\xe8g\xabg&\xdc\x8f\xa1V\xd1\xb0\xbc\xcf\xe5\x82_\xdf\xe5\x8d\xa8\xe4\x91\x9a\x8eC\x92\xe0/\x93E\x7f\x00\xfb\x88\xaf\x87q\xb9\x7f\xf0\x17M\xb4R\x86\x19<\xd3\x9e;\xca\xa0|\xec\x1f\xbf\x92\xb9\x83[\x19eZ\xc1\x1f\x05\x01\xc2\xbe\xca!\xc3\xaa\x92\xcfE+,\x03\x1c\x1fAA\x04v\xc9\xbc\xe6!\xf9\xfe\x82Y\xef\xb55\x99v\x82\x16\x15\xd1\x9d\xf6t\xee\xd5\xc1\xa5\x8f.\xa9\x01cl\x87\x95<&bW\xbf\x92\x88\x1e0s[')] + } + +def _ff_key_share(name): + if name in _ff_precomputed: + return random.choice(_ff_precomputed[name]) + ff_map = {'ffdhe8192' : goodGroupParameters[6], + 'ffdhe6144' : goodGroupParameters[5], + 'ffdhe4096' : goodGroupParameters[4], + 'ffdhe3072' : goodGroupParameters[3], + 'ffdhe2048' : goodGroupParameters[2]} + + generator = ff_map[name] + + secret = random.randint(2, generator[1]) + + ret = numberToByteArray(powMod(generator[0], secret, generator[1])) + return ret + +class Xmas_tree(HelloConfig): + """ + Create a Xmas tree (all options enabled) Client Hello message + + Creates a ClientHello message with maximum number of options enabled, + currently for TLS 1.3 protocol. + """ + + def __init__(self): + super(Xmas_tree, self).__init__() + self._name = "Xmas tree" + self.version = (3, 4) + self.record_version = (3, 4) + self.ciphers = [] + self.ciphers.extend(CipherSuite.ecdheEcdsaSuites) + self.ciphers.extend(CipherSuite.ecdheCertSuites) + self.ciphers.extend(CipherSuite.dheCertSuites) + self.ciphers.extend(range(0x0100, 0x0200)) + self.ciphers.extend(CipherSuite.dheDssSuites) + self.ciphers.extend(CipherSuite.certSuites) + + ext = self.extensions = [] + ext.append(SNIExtension()) + ext.append(TLSExtension(extType=ExtensionType.renegotiation_info) + .create(bytearray(1))) + groups =[GroupName.secp256r1, + GroupName.secp384r1, + GroupName.secp521r1, + GroupName.ecdh_x25519, + GroupName.ecdh_x448] + groups.extend(GroupName.allFF) + ext.append(SupportedGroupsExtension().create(groups)) + formats = [ECPointFormat.uncompressed, + ECPointFormat.ansiX962_compressed_prime, + ECPointFormat.ansiX962_compressed_char2] + ext.append(ECPointFormatsExtension().create(formats)) + ext.append(TLSExtension(extType=ExtensionType.session_ticket)) + ext.append(TLSExtension(extType=ExtensionType.max_fragment_legth) + .create(bytearray(b'\x04'))) + ext.append(NPNExtension()) + ext.append(TLSExtension(extType=ExtensionType.alpn) + .create(bytearray(b'\x00\x15' + + b'\x02' + b'h2' + + b'\x08' + b'spdy/3.1' + + b'\x08' + b'http/1.1'))) + ext.append(TLSExtension(extType=ExtensionType.status_request) + .create(bytearray(b'\x01' + + b'\x00\x00' + + b'\x00\x00'))) + ext.append(TLSExtension(extType=ExtensionType.status_request_v2) + .create(bytearray(b'\x00\x07' + # overall length + b'\x02' + # status type + b'\x00\x04' + # request field length + b'\x00\x00' + # responder id list + b'\x00\x00'))) # request extensions + sig_algs = [] + # some not yet standardised algorithms: + for s_alg in [4, 5, 6, 7]: + for alg in ['sha256', 'sha384', 'sha512']: + sig_algs.append((getattr(HashAlgorithm, alg), + s_alg)) + # some not yet standardised hashes: + for s_alg in [SignatureAlgorithm.rsa, SignatureAlgorithm.ecdsa]: + for alg in [7, 8, 9, 10, 11]: + sig_algs.append((alg, s_alg)) + for alg in ['sha256', 'sha384', 'sha512', 'sha224', 'sha1', 'md5']: + sig_algs.append((getattr(HashAlgorithm, alg), + SignatureAlgorithm.rsa)) + for alg in ['sha256', 'sha384', 'sha512', 'sha224', 'sha1']: + sig_algs.append((getattr(HashAlgorithm, alg), + SignatureAlgorithm.ecdsa)) + for alg in ['sha256', 'sha384', 'sha512', 'sha224', 'sha1', 'md5']: + sig_algs.append((getattr(HashAlgorithm, alg), + SignatureAlgorithm.dsa)) + ext.append(SignatureAlgorithmsExtension() + .create(sig_algs)) + ext.append(KeyShareExtension() + .create([(GroupName.secp384r1, _ec_key_share('secp384r1')), + (GroupName.secp256r1, _ec_key_share('secp256r1')), + (GroupName.secp521r1, _ec_key_share('secp521r1')), + (GroupName.ffdhe8192, _ff_key_share('ffdhe8192'))])) + ext.append(TLSExtension(extType=ExtensionType.heartbeat) + .create(bytearray(b'\x01'))) # peer allowed to send + ext.append(PaddingExtension().create(512)) + ext.append(TLSExtension(extType=ExtensionType.encrypt_then_mac)) + # place an empty extension to trigger intolerancies in specific servers + ext.append(TLSExtension(extType=ExtensionType.extended_master_secret)) + + # interesting ones are 0, 1 + self.compression_methods = list(range(0, 80)) + + +class HugeCipherList(HelloConfig): + """Client Hello with list of ciphers that doesn't fit a single record""" + + def __init__(self): + super(HugeCipherList, self).__init__() + self._name = "Huge Cipher List" + self.record_version = (3, 1) + self.version = (3, 3) + self.ciphers = [] + self.ciphers.extend(CipherSuite.ecdheEcdsaSuites) + self.ciphers.extend(CipherSuite.ecdheCertSuites) + self.ciphers.extend(CipherSuite.dheCertSuites) + self.ciphers.extend(CipherSuite.dheDssSuites) + self.ciphers.extend(CipherSuite.certSuites) + self.ciphers.extend(range(0x2000, 0x2000+8192)) + + +class VeryCompatible(HelloConfig): + """ + Cipher compatible client hello with minimal intolerancies + + Create a Client Hello that can connect to as many servers as possible + without triggering intolerancies (with the exception of TLS extension + intolerance) + """ + def __init__(self): + super(VeryCompatible, self).__init__() + self._name = "Very Compatible" + self.version = (3, 3) + self.record_version = (3, 1) + self.ciphers = [CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA256, + CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + CipherSuite.TLS_RSA_WITH_RC4_128_SHA, + CipherSuite.TLS_RSA_WITH_RC4_128_MD5, + CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + + ext = self.extensions = [] + ext.append(SNIExtension()) + ext.append(SupportedGroupsExtension().create([GroupName.secp256r1, + GroupName.secp384r1, + GroupName.secp521r1])) + ext.append(ECPointFormatsExtension().create([ECPointFormat.uncompressed])) + ext.append(TLSExtension(extType=ExtensionType.session_ticket)) + ext.append(NPNExtension()) + ext.append(TLSExtension(extType=ExtensionType.alpn) + .create(bytearray(b'\x00\x15' + + b'\x02' + b'h2' + + b'\x08' + b'spdy/3.1' + + b'\x08' + b'http/1.1'))) + ext.append(TLSExtension(extType=ExtensionType.status_request) + .create(bytearray(b'\x01' + + b'\x00\x00' + + b'\x00\x00'))) + sig_algs = [] + for alg in ['sha256', 'sha384', 'sha512', 'sha1']: + sig_algs.append((getattr(HashAlgorithm, alg), + SignatureAlgorithm.rsa)) + for alg in ['sha256', 'sha384', 'sha512', 'sha1']: + sig_algs.append((getattr(HashAlgorithm, alg), + SignatureAlgorithm.ecdsa)) + for alg in ['sha256', 'sha1']: + sig_algs.append((getattr(HashAlgorithm, alg), + SignatureAlgorithm.dsa)) + ext.append(SignatureAlgorithmsExtension() + .create(sig_algs)) + + +class IE_6(HelloConfig): + """Create a Internet Explorer 6-like Client Hello message""" + + def __init__(self): + super(IE_6, self).__init__() + self._name = "IE 6" + self.version = (3, 0) + self.record_version = (0, 2) + self.ciphers = [] + self.ciphers.extend([CipherSuite.TLS_RSA_WITH_RC4_128_MD5, + CipherSuite.TLS_RSA_WITH_RC4_128_SHA, + CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + CipherSuite.SSL_CK_RC4_128_WITH_MD5, + CipherSuite.SSL_CK_DES_192_EDE3_CBC_WITH_MD5, + CipherSuite.SSL_CK_RC2_128_CBC_WITH_MD5, + CipherSuite.TLS_RSA_WITH_DES_CBC_SHA, + CipherSuite.SSL_CK_DES_64_CBC_WITH_MD5, + CipherSuite.TLS_RSA_EXPORT1024_WITH_RC4_56_SHA, + CipherSuite.TLS_RSA_EXPORT1024_WITH_DES_CBC_SHA, + CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5, + CipherSuite.TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5, + CipherSuite.SSL_CK_RC4_128_EXPORT40_WITH_MD5, + CipherSuite.SSL_CK_RC2_128_CBC_EXPORT40_WITH_MD5, + CipherSuite.TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA, + CipherSuite.TLS_DHE_DSS_WITH_DES_CBC_SHA, + CipherSuite.TLS_DHE_DSS_EXPORT1024_WITH_DES_CBC_SHA]) + self.ssl2=True + + +class IE_8_Win_XP(HelloConfig): + """Create a Internet Explorer 8 on WinXP-like Client Hello message""" + + def __init__(self): + super(IE_8_Win_XP, self).__init__() + self._name = "IE 8 on Win XP" + self.version = (3, 1) + self.record_version = (3, 0) + self.ciphers = [] + self.ciphers.extend([CipherSuite.TLS_RSA_WITH_RC4_128_MD5, + CipherSuite.TLS_RSA_WITH_RC4_128_SHA, + CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + CipherSuite.TLS_RSA_WITH_DES_CBC_SHA, + CipherSuite.TLS_RSA_EXPORT1024_WITH_RC4_56_SHA, + CipherSuite.TLS_RSA_EXPORT1024_WITH_DES_CBC_SHA, + CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5, + CipherSuite.TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5, + CipherSuite.TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA, + CipherSuite.TLS_DHE_DSS_WITH_DES_CBC_SHA, + CipherSuite.TLS_DHE_DSS_EXPORT1024_WITH_DES_CBC_SHA]) + + +class IE_11_Win_7(HelloConfig): + """Create an Internet Explorer 11 on Win7-like Client Hello message""" + + def __init__(self): + super(IE_11_Win_7, self).__init__() + self._name = "IE 11 on Win 7" + self.version = (3, 3) + self.record_version = (3, 1) + self.ciphers = [] + self.ciphers.extend([CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA256, + CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_RSA_WITH_RC4_128_SHA, + CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_DHE_DSS_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_DHE_DSS_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_DHE_DSS_WITH_AES_256_CBC_SHA256, + CipherSuite.TLS_DHE_DSS_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA, + CipherSuite.TLS_RSA_WITH_RC4_128_MD5]) + + ext = self.extensions = [] + ext.append(SNIExtension()) + ext.append(TLSExtension(extType=ExtensionType.renegotiation_info) + .create(bytearray(1))) + groups = [GroupName.secp256r1, + GroupName.secp384r1] + ext.append(SupportedGroupsExtension().create(groups)) + ext.append(TLSExtension(extType=ExtensionType.status_request) + .create(bytearray(b'\x01' + + b'\x00\x00' + + b'\x00\x00'))) + sig_algs = [] + for s_alg in ['rsa', 'ecdsa']: + for h_alg in ['sha256', 'sha384', 'sha1']: + sig_algs.append((getattr(HashAlgorithm, h_alg), + getattr(SignatureAlgorithm, s_alg))) + sig_algs.append((HashAlgorithm.sha1, SignatureAlgorithm.dsa)) + ext.append(SignatureAlgorithmsExtension().create(sig_algs)) + + +class IE_11_Win_8_1(HelloConfig): + """Create an Internet Explorer 11 on Win8.1-like Client Hello message""" + + def __init__(self): + super(IE_11_Win_8_1, self).__init__() + self._name = "IE 11 on Win 8.1" + self.version = (3, 3) + self.record_version = (3, 1) + self.ciphers = [CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA256, + CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_DHE_DSS_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_DHE_DSS_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_DHE_DSS_WITH_AES_256_CBC_SHA256, + CipherSuite.TLS_DHE_DSS_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA] + + ext = self.extensions = [] + ext.append(SNIExtension()) + ext.append(TLSExtension(extType=ExtensionType.renegotiation_info) + .create(bytearray(1))) + groups = [GroupName.secp256r1, + GroupName.secp384r1] + ext.append(SupportedGroupsExtension().create(groups)) + ext.append(TLSExtension(extType=ExtensionType.session_ticket)) + ext.append(TLSExtension(extType=ExtensionType.status_request) + .create(bytearray(b'\x01' + + b'\x00\x00' + + b'\x00\x00'))) + sig_algs = [] + for s_alg in ['rsa', 'ecdsa']: + for h_alg in ['sha256', 'sha384', 'sha1']: + sig_algs.append((getattr(HashAlgorithm, h_alg), + getattr(SignatureAlgorithm, s_alg))) + sig_algs.append((HashAlgorithm.sha1, SignatureAlgorithm.dsa)) + ext.append(SignatureAlgorithmsExtension().create(sig_algs)) + ext.append(NPNExtension()) + ext.append(TLSExtension(extType=ExtensionType.alpn) + .create(bytearray(b'\x00\x10' + + b'\x06' + b'spdy/3' + + b'\x08' + b'http/1.1'))) diff --git a/cscan/constants.py b/cscan/constants.py new file mode 100644 index 0000000..267fcae --- /dev/null +++ b/cscan/constants.py @@ -0,0 +1,165 @@ +# Copyright 2016(c) Hubert Kario +# This work is released under the Mozilla Public License Version 2.0 +"""Extend the tlslite-ng constants with values it does not support.""" + +import tlslite.constants + +from tlslite.constants import CipherSuite + +CipherSuite.ecdheEcdsaSuites = [] + +# RFC 5289 +CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 = 0xC02C +CipherSuite.ietfNames[0xC02C] = 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384' +CipherSuite.ecdheEcdsaSuites.append(CipherSuite. + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384) + +CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 = 0xC02B +CipherSuite.ietfNames[0xC02B] = 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256' +CipherSuite.ecdheEcdsaSuites.append(CipherSuite. + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) + +CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 = 0xC024 +CipherSuite.ietfNames[0xC024] = 'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384' +CipherSuite.ecdheEcdsaSuites.append(CipherSuite. + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384) + +CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 = 0xC023 +CipherSuite.ietfNames[0xC023] = 'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256' +CipherSuite.ecdheEcdsaSuites.append(CipherSuite. + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256) + +# RFC 4492 +CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA = 0xC00A +CipherSuite.ietfNames[0xC00A] = 'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA' +CipherSuite.ecdheEcdsaSuites.append(CipherSuite. + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA) + +CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA = 0xC009 +CipherSuite.ietfNames[0xC009] = 'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA' +CipherSuite.ecdheEcdsaSuites.append(CipherSuite. + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA) + +# RFC 7251 +CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CCM = 0xC0Ad +CipherSuite.ietfNames[0xC0AD] = 'TLS_ECDHE_ECDSA_WITH_AES_256_CCM' +CipherSuite.ecdheEcdsaSuites.append(CipherSuite. + TLS_ECDHE_ECDSA_WITH_AES_256_CCM) + +CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM = 0xC0AC +CipherSuite.ietfNames[0xC0AC] = 'TLS_ECDHE_ECDSA_WITH_AES_128_CCM' +CipherSuite.ecdheEcdsaSuites.append(CipherSuite. + TLS_ECDHE_ECDSA_WITH_AES_128_CCM) + +CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8 = 0xC0AF +CipherSuite.ietfNames[0xC0AF] = 'TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8' +CipherSuite.ecdheEcdsaSuites.append(CipherSuite. + TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8) + +CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 = 0xC0AE +CipherSuite.ietfNames[0xC0AE] = 'TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8' +CipherSuite.ecdheEcdsaSuites.append(CipherSuite. + TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8) + +CipherSuite.ecdhAllSuites.extend(CipherSuite.ecdheEcdsaSuites) +CipherSuite.certAllSuites.extend(CipherSuite.ecdheEcdsaSuites) + +# obsolete stuff +CipherSuite.TLS_RSA_WITH_DES_CBC_SHA = 0x0009 +CipherSuite.ietfNames[0x0009] = 'TLS_RSA_WITH_DES_CBC_SHA' + +CipherSuite.TLS_RSA_EXPORT1024_WITH_RC4_56_SHA = 0x0064 +CipherSuite.ietfNames[0x0064] = 'TLS_RSA_EXPORT1024_WITH_RC4_56_SHA' +CipherSuite.TLS_RSA_EXPORT1024_WITH_DES_CBC_SHA = 0x0062 +CipherSuite.ietfNames[0x0062] = 'TLS_RSA_EXPORT1024_WITH_DES_CBC_SHA' +CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5 = 0x0003 +CipherSuite.ietfNames[0x0003] = 'TLS_RSA_EXPORT_WITH_RC4_40_MD5' +CipherSuite.TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5 = 0x0006 +CipherSuite.ietfNames[0x0006] = 'TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5' + +# DSS +CipherSuite.dheDssSuites = [] + +CipherSuite.TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA = 0x0013 +CipherSuite.ietfNames[0x0013] = 'TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA' +CipherSuite.dheDssSuites.append(CipherSuite. + TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA) + +CipherSuite.TLS_DHE_DSS_WITH_DES_CBC_SHA = 0x0012 +CipherSuite.ietfNames[0x0012] = 'TLS_DHE_DSS_WITH_DES_CBC_SHA' +CipherSuite.dheDssSuites.append(CipherSuite. + TLS_DHE_DSS_WITH_DES_CBC_SHA) + +CipherSuite.TLS_DHE_DSS_EXPORT1024_WITH_DES_CBC_SHA = 0x0063 +CipherSuite.ietfNames[0x0063] = 'TLS_DHE_DSS_EXPORT1024_WITH_DES_CBC_SHA' +CipherSuite.dheDssSuites.append(CipherSuite. + TLS_DHE_DSS_EXPORT1024_WITH_DES_CBC_SHA) + +CipherSuite.TLS_DHE_DSS_WITH_AES_128_CBC_SHA = 0x0032 +CipherSuite.ietfNames[0x0032] = 'TLS_DHE_DSS_WITH_AES_128_CBC_SHA' +CipherSuite.dheDssSuites.append(CipherSuite. + TLS_DHE_DSS_WITH_AES_128_CBC_SHA) + +CipherSuite.TLS_DHE_DSS_WITH_AES_256_CBC_SHA = 0x0038 +CipherSuite.ietfNames[0x0038] = 'TLS_DHE_DSS_WITH_AES_256_CBC_SHA' +CipherSuite.dheDssSuites.append(CipherSuite. + TLS_DHE_DSS_WITH_AES_256_CBC_SHA) + +CipherSuite.TLS_DHE_DSS_WITH_AES_128_CBC_SHA256 = 0x0040 +CipherSuite.ietfNames[0x0040] = 'TLS_DHE_DSS_WITH_AES_128_CBC_SHA256' +CipherSuite.dheDssSuites.append(CipherSuite. + TLS_DHE_DSS_WITH_AES_128_CBC_SHA256) + +CipherSuite.TLS_DHE_DSS_WITH_AES_256_CBC_SHA256 = 0x006a +CipherSuite.ietfNames[0x006a] = 'TLS_DHE_DSS_WITH_AES_256_CBC_SHA256' +CipherSuite.dheDssSuites.append(CipherSuite. + TLS_DHE_DSS_WITH_AES_256_CBC_SHA256) + + +class ExtensionType(tlslite.constants.ExtensionType): + """Definitions of TLS extension IDs.""" + + status_request = 5 + alpn = 16 + session_ticket = 35 + + heartbeat = 15 # RFC 6520 + status_request_v2 = 17 # RFC 6961 + padding = 21 # RFC 7685 + max_fragment_legth = 1 # RFC 6066 + + # From: Eric Rescorla + # Date: Mon, 7 Dec 2015 05:36:22 -0800 + # [TLS] TLS 1.3 ServerConfiguration + early_data = 40 + pre_shared_key = 41 + key_share = 42 + cookie = 43 + + +class GroupName(tlslite.constants.GroupName): + """ECDH and FFDH key exchange group names.""" + + allEC = list(tlslite.constants.GroupName.allEC) + allFF = list(tlslite.constants.GroupName.allFF) + + ecdh_x25519 = 29 + allEC.append(ecdh_x25519) + + ecdh_x448 = 30 + allEC.append(ecdh_x448) + + eddsa_ed25519 = 31 + allEC.append(eddsa_ed25519) + + eddsa_ed448 = 32 + allEC.append(eddsa_ed448) + + all = allEC + allFF + + +class HandshakeType(tlslite.constants.HandshakeType): + """Type of messages in Handshake protocol.""" + + certificate_status = 22 + session_ticket = 4 diff --git a/cscan/extensions.py b/cscan/extensions.py new file mode 100644 index 0000000..6987048 --- /dev/null +++ b/cscan/extensions.py @@ -0,0 +1,200 @@ +# Copyright 2016(c) Hubert Kario +# This work is released under the Mozilla Public License Version 2.0 + +"""Extra TLS extensions.""" + +import binascii + +import tlslite.extensions +from tlslite.utils.codec import Writer +from tlslite.utils.compat import b2a_hex +from .constants import ExtensionType, GroupName +from . import messages + +# make TLSExtensions hashable (__eq__ is already defined in base class) +tlslite.extensions.TLSExtension.__hash__ = lambda self: hash(self.extType) ^ \ + hash(bytes(self.extData)) + + +class RenegotiationExtension(tlslite.extensions.TLSExtension): + """Secure Renegotiation extension RFC 5746.""" + + def __init__(self): + """Initialize secure renegotiation extension.""" + super(RenegotiationExtension, self).__init__( + extType=ExtensionType.renegotiation_info) + self.renegotiated_connection = None + + def create(self, data): + """Set the value of the Finished message.""" + self.renegotiated_connection = data + + @property + def extData(self): + """Serialise the extension.""" + if self.renegotiated_connection is None: + return bytearray(0) + + writer = Writer() + writer.addVarSeq(self.renegotiated_connection, 1, 1) + return writer.bytes + + def parse(self, parser): + """Deserialise the extension from binary data.""" + if parser.getRemainingLength() == 0: + self.renegotiated_connection = None + return + + self.renegotiated_connection = parser.getVarBytes(1) + return self + + def __repr__(self): + """Human readable representation of extension.""" + return "RenegotiationExtension(renegotiated_connection={0!r})"\ + .format(self.renegotiated_connection) + + def __format__(self, formatstr): + """Formatted representation of extension.""" + data = messages.format_bytearray(self.renegotiated_connection, + formatstr) + return "RenegotiationExtension(renegotiated_connection={0})"\ + .format(data) + +tlslite.extensions.TLSExtension._universalExtensions[ + ExtensionType.renegotiation_info] = RenegotiationExtension + + +class SessionTicketExtension(tlslite.extensions.TLSExtension): + """Session Ticket extension (a.k.a. OCSP staple).""" + + def __init__(self): + """Create Session Ticket extension.""" + super(SessionTicketExtension, self).__init__( + extType=ExtensionType.session_ticket) + self.data = bytearray(0) + + def parse(self, parser): + """Deserialise the extension from binary data.""" + self.data = parser.bytes + return self + + def __format__(self, formatstr): + """Print extension data in human-readable form.""" + data = messages.format_bytearray(self.data, formatstr) + return "SessionTicketExtension(data={0})".format(data) + +tlslite.extensions.TLSExtension._universalExtensions[ + ExtensionType.session_ticket] = SessionTicketExtension + + +class ServerStatusRequestExtension(tlslite.extensions.TLSExtension): + """Server Status Request extension.""" + + def __init__(self): + """Create server status request extension.""" + super(ServerStatusRequestExtension, self).__init__( + extType=ExtensionType.status_request) + + def parse(self, parser): + """Deserialise the extension from binary data.""" + if parser.getRemainingLength() != 0: + raise SyntaxError() # FIXME + return self + + def __repr__(self): + """Human readable representation of the object.""" + return "ServerStatusRequestExtension()" + +tlslite.extensions.TLSExtension._serverExtensions[ + ExtensionType.status_request] = ServerStatusRequestExtension + + +class KeyShareExtension(tlslite.extensions.TLSExtension): + """TLS1.3 extension for handling key negotiation.""" + + def __init__(self): + """Create key share extension object.""" + super(KeyShareExtension, self).__init__( + extType=ExtensionType.key_share) + self.client_shares = None + + def create(self, shares): + """ + Set the list of key shares to send. + + @type shares: list of tuples + @param shares: a list of tuples where the first element is a NamedGroup + ID while the second element in a tuple is an opaque bytearray encoding + of the key share. + """ + self.client_shares = shares + return self + + @property + def extData(self): + """Serialise the extension.""" + if self.client_shares is None: + return bytearray(0) + + writer = Writer() + for group_id, share in self.client_shares: + writer.add(group_id, 2) + if group_id in GroupName.allFF: + share_length_length = 2 + else: + share_length_length = 1 + writer.addVarSeq(share, 1, share_length_length) + ext_writer = Writer() + ext_writer.add(len(writer.bytes), 2) + ext_writer.bytes += writer.bytes + return ext_writer.bytes + + def parse(self, parser): + """Deserialise the extension.""" + if parser.getRemainingLength() == 0: + self.client_shares = None + return + + self.client_shares = [] + + parser.startLengthCheck(2) + while not parser.atLengthCheck(): + group_id = parser.get(2) + if group_id in GroupName.allFF: + share_length_length = 2 + else: + share_length_length = 1 + share = parser.getVarBytes(share_length_length) + self.client_shares.append((group_id, share)) + + return self + + def __repr__(self): + """Human readble representation of extension.""" + return "KeyShareExtension({0!r})".format(self.client_shares) + + def __format__(self, formatstr): + """Formattable representation of extension.""" + if self.client_shares is None: + return "KeyShareExtension(None)" + + verbose = "" + hexlify = False + if 'v' in formatstr: + verbose = "GroupName." + if 'h' in formatstr: + hexlify = True + + shares = [] + for group_id, share in self.client_shares: + if hexlify: + share = b2a_hex(share) + else: + share = repr(share) + shares += ["({0}{1}, {2})".format(verbose, + GroupName.toStr(group_id), + share)] + return "KeyShareExtension([" + ",".join(shares) + "])" + +tlslite.extensions.TLSExtension._universalExtensions[ + ExtensionType.key_share] = KeyShareExtension diff --git a/cscan/messages.py b/cscan/messages.py new file mode 100644 index 0000000..7c84bc2 --- /dev/null +++ b/cscan/messages.py @@ -0,0 +1,256 @@ +# Copyright (c) 2016 Hubert Kario +# Released under Mozilla Public License 2.0 + +"""Extensions and modification of the tlslite-ng messages classes.""" + +import tlslite.messages as messages +from tlslite.utils.compat import b2a_hex +from tlslite.constants import ContentType, CertificateType, ECCurveType, \ + HashAlgorithm, SignatureAlgorithm +from tlslite.x509certchain import X509CertChain +from tlslite.utils.cryptomath import secureHash +from .constants import HandshakeType, CipherSuite, GroupName + +# gotta go fast +# comparing client hello's using ClientHello.write() is painfully slow +# monkey patch in faster compare methods + + +def __CH_eq_fun(self, other): + """ + Check if the other is equal to the object. + + always returns false if other is not a ClientHello object + """ + if not isinstance(other, messages.ClientHello): + return False + + return self.ssl2 == other.ssl2 and \ + self.client_version == other.client_version and \ + self.random == other.random and \ + self.session_id == other.session_id and \ + self.cipher_suites == other.cipher_suites and \ + self.compression_methods == other.compression_methods and \ + self.extensions == other.extensions + +messages.ClientHello.__eq__ = __CH_eq_fun + + +def __CH_ne_fun(self, other): + """ + Check if the other is not equal to the object. + + always returns true if other is not a ClientHello object + """ + return not self.__eq__(other) + +messages.ClientHello.__ne__ = __CH_ne_fun + + +def format_bytearray(byte_array, formatstr): + """Format method for bytearrays.""" + if 'x' in formatstr: + return b2a_hex(byte_array) + else: + return repr(byte_array) + + +def format_array(array, formatstr): + """Return string representation of array while formatting elements.""" + if array is None: + return "None" + else: + str_array = [] + for elem in array: + if elem.__class__.__format__ is not object.__format__: + str_array += ["{0:{1}}".format(elem, formatstr)] + else: + str_array += [repr(elem)] + return "[" + ", ".join(str_array) + "]" + + +class ServerHello(messages.ServerHello): + """Class with enhanced human-readable serialisation.""" + + def __format__(self, formatstr): + """Return human readable representation of the object.""" + extensions = format_array(self.extensions, formatstr) + random = format_bytearray(self.random, formatstr) + session_id = format_bytearray(self.session_id, formatstr) + cipher_suite = CipherSuite.ietfNames.get(self.cipher_suite, + self.cipher_suite) + + if 'v' in formatstr: + cipher_suite = "CipherSuite.{0}".format(cipher_suite) + + # TODO cipher_suites (including verbose) + # TODO compression_method (including verbose) + return ("ServerHello(server_version=({0[0]}, {0[1]}), random={1}, " + "session_id={2!r}, cipher_suite={3}, compression_method={4}, " + "_tack_ext={5}, extensions={6})").format( + self.server_version, random, session_id, + cipher_suite, self.compression_method, self._tack_ext, + extensions) + + +class Certificate(messages.Certificate): + """Class with more robust certificate parsing and serialisation.""" + + def parse(self, parser): + """Deserialise the object from binary data.""" + index = parser.index + try: + return super(Certificate, self).parse(parser) + except (AssertionError, SyntaxError): + pass + parser.index = index + parser.startLengthCheck(3) + if self.certificateType == CertificateType.x509: + chainLength = parser.get(3) + index = 0 + certificate_list = [] + while index != chainLength: + certBytes = parser.getVarBytes(3) + certificate_list.append(certBytes) + index += len(certBytes)+3 + if certificate_list: + self.certChain = certificate_list + else: + raise AssertionError() + + parser.stopLengthCheck() + return self + + def __format__(self, formatstr): + """Advanced formatting for messages.""" + hexify = False + verbose = False + digest = False + if 'h' in formatstr: + hexify = True + if 'v' in formatstr: + verbose = True + if 'm' in formatstr: + digest = True + + if self.certChain is None: + cert_list = None + else: + if isinstance(self.certChain, X509CertChain): + cert_list = [cert.bytes for cert in self.certChain.x509List] + else: + cert_list = self.certChain + + if digest: + cert_list = "[" + ", ".join(b2a_hex(secureHash(cert, 'sha256')) + for cert in cert_list) + "]" + else: + cert_list = [repr(cert) for cert in cert_list] + + return "Certificate({0})".format(cert_list) + + +class NewSessionTicket(messages.HandshakeMsg): + """Class for handling the Session Tickets from RFC 5077.""" + + def __init__(self): + """Initilize new sesion ticket message object.""" + super(NewSessionTicket, self).__init__(HandshakeType.session_ticket) + self.ticket_lifetime_hintt = 0 + self.ticket = None + + def parse(self, parser): + """Parse the object from on-the-wire data.""" + self.ticket_lifetime_hint = parser.get(4) + self.ticket = parser.getVarBytes(2) + return self + + def __format__(self, formatstr): + """Return human-readable representation of the object.""" + ticket = format_bytearray(self.ticket, formatstr) + return "NewSessionTicket(ticket_lifetime_hint={0}, ticket={1})"\ + .format(self.ticket_lifetime_hintt, ticket) + + +class CertificateStatus(messages.HandshakeMsg): + """Class for handling the CertificateStatus OCSP staples from RFC 4366.""" + + def __init__(self): + """Create a certificate status message handling object.""" + super(CertificateStatus, self).__init__( + HandshakeType.certificate_status) + self.status_type = 0 + self.response = None + + def parse(self, parser): + """Deserialise certificate status message from binary data.""" + parser.startLengthCheck(3) + self.status_type = parser.get(1) + if self.status_type == 1: # FIXME, create registry + self.response = parser.getVarBytes(3) + else: + raise SyntaxError() # FIXME, use sane-er type + parser.stopLengthCheck() + return self + + def __format__(self, formatstr): + """Return human-readable representation of certificate status.""" + response = format_bytearray(self.response, formatstr) + return "CertificateStatus(status_type={0}, response={1})"\ + .format(self.status_type, response) + + +class Message(messages.Message): + """Message class with more robust formatting capability.""" + + def __format__(self, formatstr): + """Advanced formatting for messages.""" + hexify = False + verbose = "" + if 'h' in formatstr: + hexify = True + if 'v' in formatstr: + verbose = "ContentType." + + if hexify: + data = b2a_hex(self.data) + else: + data = repr(self.data) + + return "Message(contentType={0}{1}, data={2})"\ + .format(verbose, ContentType.toStr(self.contentType), data) + + +class ServerKeyExchange(messages.ServerKeyExchange): + """ServerKeyExchange class with more robust formatting capability.""" + + def __format__(self, formatstr): + """Return human-readable representation of the object.""" + if 'v' in formatstr: + verbose = "CipherSuite." + else: + verbose = "" + + ret = "ServerKeyExchange(cipherSuite={0}{1}, version={2}"\ + .format(verbose, CipherSuite.ietfNames[self.cipherSuite], + self.version) + if self.srp_N: + ret += ", srp_N={0}, srp_g={1}, srp_s={2}, srp_B={3}"\ + .format(self.srp_N, self.srp_g, self.srp_s, self.srp_B) + if self.dh_p: + ret += ", dh_p={0}, dh_g={1}, dh_Ys={2}"\ + .format(self.dh_p, self.dh_g, self.dh_Ys) + if self.ecdh_Ys: + ecdh_Ys = format_bytearray(self.ecdh_Ys, formatstr) + ret += ", curve_type={0}, named_curve={1}, ecdh_Ys={2}"\ + .format(ECCurveType.toStr(self.curve_type), + GroupName.toStr(self.named_curve), ecdh_Ys) + if self.signAlg: + ret += ", hashAlg={0}, signAlg={1}"\ + .format(HashAlgorithm.toStr(self.hashAlg), + SignatureAlgorithm.toStr(self.signAlg)) + if self.signature: + ret += ", signature={0}"\ + .format(format_bytearray(self.signature, formatstr)) + + return ret + ")" diff --git a/cscan/modifiers.py b/cscan/modifiers.py new file mode 100644 index 0000000..d3e5fcb --- /dev/null +++ b/cscan/modifiers.py @@ -0,0 +1,147 @@ +# Copyright (c) 2016 Hubert Kario +# Released under Mozilla Public License 2.0 +"""Methods for modifying the scan configurations on the fly.""" + +from __future__ import print_function +from tlslite.constants import CipherSuite +from tlslite.extensions import SNIExtension, PaddingExtension, TLSExtension +import itertools + + +def no_sni(generator): + if not generator.extensions: + return generator + generator.extensions[:] = (x for x in generator.extensions + if not isinstance(x, SNIExtension)) + generator.modifications.append("no SNI") + return generator + + +proto_versions = {(3, 0): "SSLv3", + (3, 1): "TLSv1.0", + (3, 2): "TLSv1.1", + (3, 3): "TLSv1.2", + (3, 4): "TLSv1.3", + (3, 5): "TLSv1.4", + (3, 6): "TLSv1.5"} + + +def version_to_str(version): + """Convert a version tuple to human-readable string.""" + version_name = proto_versions.get(version, None) + if version_name is None: + version_name = "{0[0]}.{0[1]}".format(version) + return version_name + + +def set_hello_version(generator, version): + """Set client hello version.""" + generator.version = version + generator.modifications += [version_to_str(version)] + return generator + + +def set_record_version(generator, version): + """Set record version, un-SSLv2-ify""" + + generator.record_version = version + generator.ciphers[:] = (i for i in generator.ciphers if i <= 0xffff) + generator.ssl2 = False + generator.modifications += ["r/{0}".format(version_to_str(version))] + return generator + + +def no_extensions(generator): + """Remove extensions""" + + generator.extensions = None + generator.modifications += ["no ext"] + return generator + + +def divceil(divident, divisor): + quot, r = divmod(divident, divisor) + return quot + int(bool(r)) + + +def truncate_ciphers_to_size(generator, size): + """Truncate list of ciphers until client hello is no bigger than size""" + + def cb_fun(client_hello, size=size): + hello_len = len(client_hello.write()) + bytes_to_remove = hello_len - size + if bytes_to_remove > 0: + ciphers_to_remove = divceil(bytes_to_remove, 2) + client_hello.cipher_suites[:] = \ + client_hello.cipher_suites[:-ciphers_to_remove] + return client_hello + + generator.callbacks.append(cb_fun) + generator.modifications += ["trunc c/{0}".format(size)] + return generator + + +def append_ciphers_to_size(generator, size): + """ + Add ciphers from the 0x2000-0xa000 range until size is reached + + Increases the size of the Client Hello message until it is at least + `size` bytes long. Uses cipher ID's from the 0x2000-0xc000 range to do + it (0x5600, a.k.a TLS_FALLBACK_SCSV, excluded) + """ + + def cb_fun(client_hello, size=size): + ciphers_iter = iter(range(0x2000, 0xc000)) + ciphers_present = set(client_hello.cipher_suites) + # we don't want to add a cipher id with special meaning + # and the set is used only internally + ciphers_present.add(CipherSuite.TLS_FALLBACK_SCSV) + + bytes_to_add = size - len(client_hello.write()) + if bytes_to_add > 0: + ciphers_to_add = divceil(bytes_to_add, 2) + ciphers_gen = (x for x in ciphers_iter + if x not in ciphers_present) + client_hello.cipher_suites.extend(itertools.islice(ciphers_gen, + ciphers_to_add)) + return client_hello + generator.callbacks.append(cb_fun) + generator.modifications += ["append c/{0}".format(size)] + return generator + + +def extend_with_ext_to_size(generator, size): + """ + Add the padding extension so that the Hello is at least `size` bytes + + Either adds a padding extension or extends an existing one so that + the specified size is reached + """ + + def cb_fun(client_hello, size=size): + if len(client_hello.write()) > size: + return client_hello + if not client_hello.extensions: + client_hello.extensions = [] + ext = next((x for x in client_hello.extensions + if isinstance(x, PaddingExtension)), None) + if not ext: + ext = PaddingExtension() + client_hello.extensions.append(ext) + # check if just adding the extension, with no payload, haven't pushed + # us over the limit + bytes_to_add = size - len(client_hello.write()) + if bytes_to_add > 0: + ext.paddingData += bytearray(bytes_to_add) + return client_hello + generator.callbacks.append(cb_fun) + generator.modifications += ["append e/{0}".format(size)] + return generator + +def add_empty_ext(generator, ext_type): + if generator.extensions is None: + generator.extensions = [] + generator.extensions += [TLSExtension(extType=ext_type) + .create(bytearray(0))] + generator.modifications += ["add ext {0}".format(ext_type)] + return generator diff --git a/cscan/scanner.py b/cscan/scanner.py new file mode 100644 index 0000000..e77b942 --- /dev/null +++ b/cscan/scanner.py @@ -0,0 +1,157 @@ +# Copyright (c) 2016 Hubert Kario +# Released under the Mozilla Public License 2.0 + +"""Classes used for scanning servers and getting their responses.""" + +import socket + +from .constants import CipherSuite, HandshakeType +from .messages import Certificate, ServerHello, Message, NewSessionTicket, \ + CertificateStatus, ServerKeyExchange +from tlslite.constants import CertificateType, ContentType +from tlslite.messages import \ + CertificateRequest, NextProtocol, ServerHelloDone, Alert +from tlslite.defragmenter import Defragmenter +from tlslite.messagesocket import MessageSocket +from tlslite.errors import TLSAbruptCloseError, TLSIllegalParameterException + + +class HandshakeParser(object): + """Inteligent parser for handshake messages.""" + + def __init__(self, version=(3, 1), + cipher_suite=CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA, + certificate_type=CertificateType.x509): + """Initialize parser object.""" + self.version = version + self.cipher_suite = cipher_suite + self.certificate_type = certificate_type + + def parse(self, parser): + """Parse a handshake message.""" + hs_type = parser.get(1) + if hs_type == HandshakeType.server_hello: + msg = ServerHello().parse(parser) + self.version = msg.server_version + self.cipher_suite = msg.cipher_suite + self.certificate_type = msg.certificate_type + return msg + elif hs_type == HandshakeType.certificate: + msg = Certificate(self.certificate_type) + elif hs_type == HandshakeType.server_key_exchange: + msg = ServerKeyExchange(self.cipher_suite, self.version) + elif hs_type == HandshakeType.certificate_request: + msg = CertificateRequest(self.version) + elif hs_type == HandshakeType.next_protocol: + msg = NextProtocol().parse(parser) + elif hs_type == HandshakeType.server_hello_done: + msg = ServerHelloDone() + elif hs_type == HandshakeType.session_ticket: + msg = NewSessionTicket() + elif hs_type == HandshakeType.certificate_status: + msg = CertificateStatus() + else: + raise ValueError("Unknown handshake type: {0}".format(hs_type)) + + # don't abort when we can't parse a message, save it as unparsed + try: + msg.parse(parser) + except SyntaxError: + msg = Message(ContentType.handshake, parser.bytes) + return msg + + +class Scanner(object): + """Helper class for scanning a host and returning serialised responses.""" + + def __init__(self, hello_gen, host, port=443, hostname=None): + """Initialize scanner.""" + self.host = host + self.hello_gen = hello_gen + self.port = port + self.hostname = hostname + + def scan(self): + """Perform a scan on server.""" + defragger = Defragmenter() + defragger.addStaticSize(ContentType.change_cipher_spec, 1) + defragger.addStaticSize(ContentType.alert, 2) + defragger.addDynamicSize(ContentType.handshake, 1, 3) + + try: + raw_sock = socket.create_connection((self.host, self.port), 5) + except socket.error as e: + return [e] + + sock = MessageSocket(raw_sock, defragger) + + if self.hostname is not None: + client_hello = self.hello_gen(bytearray(self.hostname, + 'utf-8')) + else: + client_hello = self.hello_gen(None) + + # record layer version - TLSv1.x + # use the version from configuration, if present, or default to the + # RFC recommended (3, 1) for TLS and (3, 0) for SSLv3 + if hasattr(client_hello, 'record_version'): + sock.version = client_hello.record_version + elif hasattr(self.hello_gen, 'record_version'): + sock.version = self.hello_gen.record_version + elif client_hello.client_version > (3, 1): # TLS1.0 + sock.version = (3, 1) + else: + sock.version = client_hello.client_version + + # we don't want to send invalid messages (SSLv2 hello in SSL record + # layer), so set the record layer version to SSLv2 if the hello is + # of SSLv2 format + if client_hello.ssl2: + sock.version = (0, 2) + + # save the record version used in the end for later analysis + client_hello.record_version = sock.version + + messages = [client_hello] + + handshake_parser = HandshakeParser() + + try: + sock.sendMessageBlocking(client_hello) + except socket.error as e: + messages.append(e) + return messages + except TLSAbruptCloseError as e: + sock.sock.close() + messages.append(e) + return messages + + # get all the server messages that affect connection, abort as soon + # as they've been read + try: + while True: + header, parser = sock.recvMessageBlocking() + + if header.type == ContentType.alert: + alert = Alert() + alert.parse(parser) + alert.record_version = header.version + messages += [alert] + elif header.type == ContentType.handshake: + msg = handshake_parser.parse(parser) + msg.record_version = header.version + messages += [msg] + if isinstance(msg, ServerHelloDone): + return messages + else: + raise TypeError("Unknown content type: {0}" + .format(header.type)) + except (TLSAbruptCloseError, TLSIllegalParameterException, + ValueError, TypeError, socket.error, SyntaxError) as e: + messages += [e] + return messages + finally: + try: + sock.sock.close() + except (socket.error, OSError): + pass diff --git a/cscan_tests/__init__.py b/cscan_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cscan_tests/test_bisector.py b/cscan_tests/test_bisector.py new file mode 100644 index 0000000..71d0242 --- /dev/null +++ b/cscan_tests/test_bisector.py @@ -0,0 +1,258 @@ +# Copyright (c) 2015 Hubert Kario +# Released under Mozilla Public License Version 2.0 + +try: + import unittest2 as unittest +except ImportError: + import unittest + +from tlslite.extensions import SignatureAlgorithmsExtension, SNIExtension, \ + SupportedGroupsExtension, ECPointFormatsExtension, TLSExtension +from cscan.config import HugeCipherList, VeryCompatible, IE_8_Win_XP +from cscan.bisector import bisect_lists, list_union, Bisect +from cscan.modifiers import extend_with_ext_to_size, append_ciphers_to_size + +class TestListUnion(unittest.TestCase): + def test_identical(self): + a = [1, 2, 3, 4] + b = [1, 2, 3, 4] + c = list_union(a, b) + self.assertEqual(c, [1, 2, 3, 4]) + + def test_extended(self): + a = [1, 2, 3, 4] + b = [1, 2, 3, 4, 5, 6, 7, 8] + c = list_union(a, b) + self.assertEqual(c, [1, 2, 3, 4, 5, 6, 7, 8]) + + def test_extended_reversed(self): + a = [1, 2, 3, 4, 5, 6, 7, 8] + b = [1, 2, 3, 4] + c = list_union(a, b) + self.assertEqual(c, [1, 2, 3, 4, 5, 6, 7, 8]) + + def test_prepended(self): + a = [5, 6, 7, 8] + b = [1, 2, 3, 4, 5, 6, 7, 8] + c = list_union(a, b) + self.assertEqual(c, [1, 2, 3, 4, 5, 6, 7, 8]) + + def test_mixed(self): + a = [1, 2, 3, 4] + b = [5, 1, 2, 6, 4] + c = list_union(a, b) + self.assertEqual(c, [5, 1, 2, 3, 6, 4]) + + def test_mixed_reversed(self): + a = [5, 1, 2, 6, 4] + b = [1, 2, 3, 4] + c = list_union(a, b) + self.assertEqual(c, [5, 1, 2, 6, 3, 4]) + + def test_different_order(self): + a = [1, 2, 3, 4] + b = [2, 3, 1, 4] + c = list_union(a, b) + self.assertEqual(c, [2, 3, 1, 4]) + + def test_different_order2(self): + a = [1, 2, 3, 4, 5, 6] + b = [3, 1, 4, 2, 5, 6] + c = list_union(a, b) + self.assertEqual(c, [3, 1, 4, 2, 5, 6]) + + def test_different_order_superset(self): + a = [1, 2, 3, 4] + b = [4, 3, 1, 2, 5, 6] + c = list_union(a, b) + self.assertEqual(c, [4, 3, 1, 2, 5, 6]) + + def test_completely_disjoint(self): + a = [1, 2, 3, 4] + b = [5, 6, 7, 8] + c = list_union(a, b) + self.assertEqual(c, [1, 5, 2, 6, 3, 7, 4, 8]) + + def test_different_suffix(self): + a = [1, 2, 3, 4] + b = [1, 2, 5, 6] + c = list_union(a, b) + self.assertEqual(c, [1, 2, 3, 5, 4, 6]) + + def test_different_prefix(self): + a = [1, 2, 3, 4] + b = [5, 6, 3, 4] + c = list_union(a, b) + self.assertEqual(c, [1, 5, 2, 6, 3, 4]) + + def test_one_empty(self): + a = [1, 2, 3, 4] + b = [] + c = list_union(a, b) + self.assertEqual(c, [1, 2, 3, 4]) + + def test_both_empty(self): + a = [] + b = [] + c = list_union(a, b) + self.assertEqual(c, []) + +class TestBisectLists(unittest.TestCase): + def test_sorted(self): + a = [1, 5, 7, 9] + b = [3, 5, 6, 8] + c = bisect_lists(a, b) + self.assertEqual(c, [1, 3, 5, 7]) + + d = bisect_lists(c, b) + self.assertEqual(d, [1, 3, 5, 7]) + + e = bisect_lists(a, c) + self.assertEqual(e, [1, 3, 5, 7]) + + def test_extended(self): + a = [1, 2, 3, 4] + b = [1, 2, 3, 4, 5, 6, 7, 8] + c = bisect_lists(a, b) + self.assertEqual(a, [1, 2, 3, 4]) + self.assertEqual(b, [1, 2, 3, 4, 5, 6, 7, 8]) + self.assertEqual(c, [1, 2, 3, 4, 5, 6]) + + d = bisect_lists(c, b) + self.assertEqual(d, [1, 2, 3, 4, 5, 6, 7]) + + def test_extended_reversed(self): + a = [1, 2, 3, 4, 5, 6, 7, 8] + b = [1, 2, 3, 4] + c = bisect_lists(a, b) + self.assertEqual(a, [1, 2, 3, 4, 5, 6, 7, 8]) + self.assertEqual(b, [1, 2, 3, 4]) + self.assertEqual(c, [1, 2, 3, 4, 5, 6]) + + def test_prepended(self): + a = [5, 6, 7, 8] + b = [1, 2, 3, 4, 5, 6, 7, 8] + c = bisect_lists(a, b) + self.assertEqual(c, [1, 2, 5, 6, 7, 8]) + + def test_both_different(self): + a = [1, 2, 3, 4] + b = [1, 2, 5, 6] + c = bisect_lists(a, b) + self.assertEqual(c, [1, 2, 3, 5]) + + def test_small_difference(self): + a = [1, 2, 3, 4] + b = [1, 2, 3, 5] + c = bisect_lists(a, b) + self.assertEqual(c, [1, 2, 3, 4]) + + def test_small_difference_with_different_order(self): + a = [2, 3, 1, 4] + b = [1, 2, 3, 5] + c = bisect_lists(a, b) + self.assertEqual(c, [1, 2, 3, 5]) + + def test_one_empty(self): + a = [] + b = [1, 2, 3, 4] + c = bisect_lists(a, b) + self.assertEqual(c, [1, 2]) + + def test_both_empty(self): + a = [] + b = [] + c = bisect_lists(a, b) + self.assertEqual(c, []) + + def test_one_None(self): + a = None + b = [1, 2, 3, 4] + c = bisect_lists(a, b) + self.assertEqual(c, [1, 2]) + + def test_short_and_None(self): + a = None + b = [1] + c = bisect_lists(a, b) + self.assertEqual(c, []) + + def test_empty_and_None(self): + a = None + b = [] + c = bisect_lists(a, b) + self.assertEqual(c, None) + +class TestBisect(unittest.TestCase): + def test___init__(self): + b = Bisect(None, None, None, None) + self.assertIsNotNone(b) + + def test_run(self): + def test_cb(hello): + return len(hello.write()) <= 2**14 + + bad = HugeCipherList() + good = VeryCompatible() + self.assertGreater(len(bad(b'').write()), 2**14) + self.assertLess(len(good(b'').write()), 2**14) + + bi = Bisect(good, bad, "localhost", test_cb) + + a, b = bi.run() + + self.assertEqual(len(a.write()), 2**14-1) + self.assertEqual(len(b.write()), 2**14+1) + + def test_run_with_extensions(self): + def test_cb(hello): + if not hello.extensions: + return True + a = next((x for x in hello.extensions + if isinstance(x, SignatureAlgorithmsExtension)), None) + return a is None + good = IE_8_Win_XP() + bad = VeryCompatible() + + self.assertTrue(test_cb(good(b'localhost'))) + self.assertFalse(test_cb(bad(b'localhost'))) + + bi = Bisect(good, bad, "localhost", test_cb) + + a, b = bi.run() + + ext = next((x for x in a.extensions + if isinstance(x, SignatureAlgorithmsExtension)), None) + self.assertIsNone(ext) + ext = next((x for x in b.extensions + if isinstance(x, SignatureAlgorithmsExtension)), None) + self.assertIsNotNone(ext) + + def test_run_with_extension_size(self): + def test_cb(hello): + return len(hello.write()) <= 2**14 + + bad = extend_with_ext_to_size(VeryCompatible(), 2**16) + good = VeryCompatible() + + bi = Bisect(good, bad, "localhost", test_cb) + + a, b = bi.run() + + self.assertEqual(len(a.write()), 2**14) + self.assertEqual(len(b.write()), 2**14+1) + + def test_run_with_ext_and_ciphers(self): + def test_cb(hello): + return len(hello.write()) <= 2**14 + + bad = extend_with_ext_to_size(VeryCompatible(), 2**15) + bad = append_ciphers_to_size(bad, 2**16) + good = VeryCompatible() + + bi = Bisect(good, bad, "localhost", test_cb) + + a, b = bi.run() + + self.assertEqual(len(a.write()), 2**14) + self.assertEqual(len(b.write()), 2**14+1) diff --git a/cscan_tests/test_config.py b/cscan_tests/test_config.py new file mode 100644 index 0000000..9b8c14e --- /dev/null +++ b/cscan_tests/test_config.py @@ -0,0 +1,98 @@ +# Copyright (c) 2015 Hubert Kario +# Released under Mozilla Public License Version 2.0 + +try: + import unittest2 as unittest +except ImportError: + import unittest + +from tlslite.messages import ClientHello +from tlslite.extensions import SNIExtension, SupportedGroupsExtension, \ + ECPointFormatsExtension, NPNExtension, SignatureAlgorithmsExtension +from tlslite.utils.codec import Parser +from cscan.config import Firefox_42, Xmas_tree, Firefox_46 +from cscan.extensions import RenegotiationExtension +from cscan.constants import ExtensionType + +class TestFirefox(unittest.TestCase): + def test_firefox_42(self): + gen = Firefox_42() + ch = gen(bytearray(b'example.com')) + + self.assertIsNotNone(ch) + self.assertIsInstance(ch, ClientHello) + self.assertEqual(len(ch.write()), 176) + self.assertEqual(ch.client_version, (3, 3)) + self.assertEqual(gen.record_version, (3, 1)) + self.assertEqual(len(ch.cipher_suites), 11) + self.assertIsInstance(ch.extensions[0], SNIExtension) + self.assertEqual(ch.extensions[1].extType, + ExtensionType.renegotiation_info) + self.assertIsInstance(ch.extensions[2], + SupportedGroupsExtension) + self.assertIsInstance(ch.extensions[3], + ECPointFormatsExtension) + self.assertEqual(ch.extensions[4].extType, + ExtensionType.session_ticket) + # bug in tlslite-ng, removes NPN extensions from provided extensions + #self.assertIsInstance(ch.extensions[5], + # NPNExtension) + self.assertEqual(ch.extensions[5].extType, + ExtensionType.alpn) + self.assertEqual(ch.extensions[6].extType, + ExtensionType.status_request) + self.assertIsInstance(ch.extensions[7], + SignatureAlgorithmsExtension) + self.assertEqual(ch.compression_methods, [0]) + + def test_firefox_46(self): + gen = Firefox_46() + ch = gen(bytearray(b'example.com')) + + self.assertIsNotNone(ch) + self.assertIsInstance(ch, ClientHello) + self.assertEqual(len(ch.write()), 180) + self.assertEqual(ch.client_version, (3, 3)) + self.assertEqual(gen.record_version, (3, 1)) + self.assertEqual(len(ch.cipher_suites), 11) + self.assertIsInstance(ch.extensions[0], SNIExtension) + self.assertEqual(ch.extensions[1].extType, + ExtensionType.extended_master_secret) + self.assertEqual(ch.extensions[2].extType, + ExtensionType.renegotiation_info) + self.assertIsInstance(ch.extensions[3], + SupportedGroupsExtension) + self.assertIsInstance(ch.extensions[4], + ECPointFormatsExtension) + self.assertEqual(ch.extensions[5].extType, + ExtensionType.session_ticket) + # bug in tlslite-ng, removes NPN extensions from provided extensions + #self.assertIsInstance(ch.extensions[6], + # NPNExtension) + self.assertEqual(ch.extensions[6].extType, + ExtensionType.alpn) + self.assertEqual(ch.extensions[7].extType, + ExtensionType.status_request) + self.assertIsInstance(ch.extensions[8], + SignatureAlgorithmsExtension) + self.assertEqual(ch.compression_methods, [0]) + +class TestXmasTree(unittest.TestCase): + def test_xmas_tree_tls_1_3(self): + ch = Xmas_tree()(bytearray(b'example.com')) + + self.assertIsNotNone(ch) + self.assertIsInstance(ch, ClientHello) + self.assertEqual(len(ch.write()), 2792) + + def test_xmas_tree_tls_1_3_parse(self): + ch = Xmas_tree()(bytearray(b'example.com')) + + parser = Parser(ch.write()[1:]) + + client_hello = ClientHello() + client_hello.parse(parser) + + +if __name__ == "__main__": + unittest.main() diff --git a/cscan_tests/test_extensions.py b/cscan_tests/test_extensions.py new file mode 100644 index 0000000..4a3c045 --- /dev/null +++ b/cscan_tests/test_extensions.py @@ -0,0 +1,76 @@ +# Copyright (c) 2015 Hubert Kario +# Released under Mozilla Public License Version 2.0 + +try: + import unittest2 as unittest +except ImportError: + import unittest + +from tlslite.utils.codec import Parser +from cscan.extensions import KeyShareExtension +from cscan.constants import GroupName + +class TestKeyShareExtension(unittest.TestCase): + def test___init__(self): + ext = KeyShareExtension() + + self.assertIsNotNone(ext) + + def test_create(self): + ext = KeyShareExtension() + + ext.create([(1, bytearray(b'\x12')), + (2, bytearray(b'\x33'))]) + + self.assertEqual(ext.client_shares, [(1, bytearray(b'\x12')), + (2, bytearray(b'\x33'))]) + + def test_write(self): + ext = KeyShareExtension() + + ext.create([(GroupName.secp256r1, bytearray(b'\xff\xfa')), + (GroupName.ffdhe2048, bytearray(b'\xaf\xaa'))]) + + data = ext.write() + + self.assertEqual(data, bytearray( + b'\x00\x2a\x00\x0d' + b'\x00\x0b' + b'\x00\x17\x02\xff\xfa' + b'\x01\x00\x00\x02\xaf\xaa')) + + def test_write_with_no_data(self): + ext = KeyShareExtension() + + data = ext.write() + + self.assertEqual(data, bytearray(b'\x00\x2a\x00\x00')) + + def test_parse(self): + parser = Parser(bytearray( + #b'\x00\x2a\x00\x0d' + b'\x00\x0b' + b'\x00\x17\x02\xff\xfa' + b'\x01\x00\x00\x02\xaf\xaa')) + + ext = KeyShareExtension() + ext.parse(parser) + + self.assertEqual(ext.client_shares, + [(GroupName.secp256r1, bytearray(b'\xff\xfa')), + (GroupName.ffdhe2048, bytearray(b'\xaf\xaa'))]) + + def test_parse_with_no_data(self): + parser = Parser(bytearray()) + + ext = KeyShareExtension() + ext.parse(parser) + + self.assertIsNone(ext.client_shares) + + def test___repr__(self): + ext = KeyShareExtension() + ext.create([(1, bytearray(b'\xff'))]) + + self.assertEqual("KeyShareExtension([(1, bytearray(b\'\\xff\'))])", + repr(ext)) diff --git a/cscan_tests/test_modifiers.py b/cscan_tests/test_modifiers.py new file mode 100644 index 0000000..0d3a893 --- /dev/null +++ b/cscan_tests/test_modifiers.py @@ -0,0 +1,45 @@ +# Copyright (c) 2015 Hubert Kario +# Released under Mozilla Public License Version 2.0 + +try: + import unittest2 as unittest +except ImportError: + import unittest + +from cscan.config import HugeCipherList, Firefox_42 +from cscan.modifiers import truncate_ciphers_to_size, append_ciphers_to_size, \ + extend_with_ext_to_size + +class TestTruncateCiphersToSize(unittest.TestCase): + def test_with_big_hello(self): + gen = HugeCipherList() + + self.assertGreater(len(gen(b'localhost').write()), 2**14) + self.assertEqual(gen(b'localhost').cipher_suites[0], 49196) + + gen = truncate_ciphers_to_size(gen, 2**12) + + self.assertEqual(len(gen(b'localhost').write()), 2**12-1) + self.assertEqual(gen(b'localhost').cipher_suites[0], 49196) + +class TestAppendCiphersToSize(unittest.TestCase): + def test_with_small_hello(self): + gen = Firefox_42() + + self.assertLess(len(gen(b'localhost').write()), 2**10) + self.assertEqual(gen(b'localhost').cipher_suites[0], 49195) + + gen = append_ciphers_to_size(gen, 2**12) + + self.assertEqual(len(gen(b'localhost').write()), 2**12) + self.assertEqual(gen(b'localhost').cipher_suites[0], 49195) + +class TestExtendWithExtToSize(unittest.TestCase): + def test_with_small_hello(self): + gen = Firefox_42() + + self.assertLess(len(gen(b'localhost').write()), 2**10) + + gen = extend_with_ext_to_size(gen, 2**12) + + self.assertEqual(len(gen(b'localhost').write()), 2**12) diff --git a/top1m/parse_results.py b/top1m/parse_results.py index b48cb19..d23f301 100644 --- a/top1m/parse_results.py +++ b/top1m/parse_results.py @@ -114,6 +114,7 @@ ecccurve = defaultdict(int) npn = defaultdict(int) ocspstaple = defaultdict(int) fallbacks = defaultdict(int) +intolerancies = defaultdict(int) # array with indexes of fallback names for the matrix report fallback_ids = defaultdict(int) i=0 @@ -177,6 +178,7 @@ for r,d,flist in os.walk(path): tempecccurve = {} tempnpn = {} tempfallbacks = {} + tempintolerancies = {} """ supported ciphers by the server under scan """ tempcipherstats = {} temppfssigalgordering = {} @@ -351,6 +353,21 @@ for r,d,flist in os.walk(path): except KeyError: pass + if 'intolerancies' in results: + intoler = results['intolerancies'] + for name, val in intoler.items(): + if val is True: + tempintolerancies[name] = 1 + size_intol = [x.replace('size ', '') + for x in tempintolerancies.keys() + if x.startswith('size ')] + if size_intol: + size_intol.sort(reverse=True) + tempintolerancies['size {0}' + .format(" ".join(size_intol))] = 1 + else: + tempintolerancies['x:missing information'] = 1 + """ get some extra data about server """ if 'renegotiation' in results: temprenegotiation[results['renegotiation']] = 1 @@ -582,6 +599,9 @@ for r,d,flist in os.walk(path): for s in tempfallbacks: fallbacks[s] += 1 + for s in tempintolerancies: + intolerancies[s] += 1 + for s in tempsigstats: sigalg[s] += 1 @@ -920,3 +940,9 @@ print("------------------------") fallback_ids_sorted=sorted(fallback_ids.items(), key=operator.itemgetter(1)) for touple in fallback_ids_sorted: print(str(touple[1]+1).rjust(3) + " " + str(touple[0])) + +print("\nClient Hello intolerance Count Percent") +print("----------------------------------------+---------+-------") +for stat in sorted(intolerancies): + percent = round(intolerancies[stat] / total * 100, 4) + sys.stdout.write(stat.ljust(40) + " " + str(intolerancies[stat]).ljust(10) + str(percent).ljust(4) + "\n")