From 0161e732df80ed203382b9b3d950b7578b482d0e Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Wed, 2 Aug 2023 14:58:08 +0200 Subject: [PATCH 1/6] sessionID-resumption: check if too long are rejected --- scripts/test-sessionID-resumption.py | 20 ++++++++++++++++++-- tests/tlslite-ng-random-subset.json | 4 +--- tests/tlslite-ng.json | 4 +--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/scripts/test-sessionID-resumption.py b/scripts/test-sessionID-resumption.py index 663f41a95..917c1ec0e 100644 --- a/scripts/test-sessionID-resumption.py +++ b/scripts/test-sessionID-resumption.py @@ -21,7 +21,7 @@ from tlsfuzzer.utils.lists import natural_sort_keys -version = 4 +version = 5 def help_msg(): @@ -183,7 +183,23 @@ def main(): conversations["Client Hello with garbage session ID"] = conversation - # run the conversation + # too long session ID + conversation = Connect(host, port) + node = conversation + ciphers = [CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA] + # session_id (and legacy_session_id in TLS 1.3) are specified as + # opaque SessionID<0..32>; + # which means that 33 byte long, and longer, are malformed + node = node.add_child(ClientHelloGenerator( + ciphers, + session_id=bytearray(33), + extensions={ExtensionType.renegotiation_info:None})) + node = node.add_child(ExpectAlert(AlertLevel.fatal, + AlertDescription.decode_error)) + node.add_child(ExpectClose()) + + conversations["Client Hello too long session ID"] = conversation + # run the conversation good = 0 bad = 0 diff --git a/tests/tlslite-ng-random-subset.json b/tests/tlslite-ng-random-subset.json index bda46be6c..06d8f7e83 100644 --- a/tests/tlslite-ng-random-subset.json +++ b/tests/tlslite-ng-random-subset.json @@ -174,9 +174,7 @@ {"name" : "test-invalid-server-name-extension-resumption.py", "comment" : "test requires support for multiple virtual hosts on server side", "exp_pass" : false}, - {"name" : "test-invalid-session-id.py", - "comment" : "session id is not verified correctly by tlslite-ng", - "exp_pass" : false}, + {"name" : "test-invalid-session-id.py"}, {"name" : "test-invalid-version.py"}, {"name" : "test-large-hello.py"}, {"name" : "test-large-number-of-extensions.py"}, diff --git a/tests/tlslite-ng.json b/tests/tlslite-ng.json index da8015091..7b6009b82 100644 --- a/tests/tlslite-ng.json +++ b/tests/tlslite-ng.json @@ -175,9 +175,7 @@ {"name" : "test-invalid-server-name-extension-resumption.py", "comment" : "test requires support for multiple virtual hosts on server side", "exp_pass" : false}, - {"name" : "test-invalid-session-id.py", - "comment" : "session id is not verified correctly by tlslite-ng", - "exp_pass" : false}, + {"name" : "test-invalid-session-id.py"}, {"name" : "test-invalid-version.py"}, {"name" : "test-large-hello.py"}, {"name" : "test-large-number-of-extensions.py"}, From 6967da629f2c42af239f1d61bcc1c5e240ae83ee Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Wed, 2 Aug 2023 15:25:01 +0200 Subject: [PATCH 2/6] sessionID-resumption: add (EC)DHE and EMS support --- scripts/test-sessionID-resumption.py | 91 ++++++++++++++++++++++------ tests/tlslite-ng-random-subset.json | 2 + tests/tlslite-ng.json | 2 + 3 files changed, 78 insertions(+), 17 deletions(-) diff --git a/scripts/test-sessionID-resumption.py b/scripts/test-sessionID-resumption.py index 917c1ec0e..93cf9a9d1 100644 --- a/scripts/test-sessionID-resumption.py +++ b/scripts/test-sessionID-resumption.py @@ -9,7 +9,7 @@ from random import sample from tlslite.constants import CipherSuite, AlertLevel, AlertDescription, \ - ExtensionType + ExtensionType, GroupName from tlsfuzzer.runner import Runner from tlsfuzzer.messages import Connect, ClientHelloGenerator, \ ClientKeyExchangeGenerator, ChangeCipherSpecGenerator, \ @@ -17,11 +17,15 @@ ResetHandshakeHashes, Close, ResetRenegotiationInfo from tlsfuzzer.expect import ExpectServerHello, ExpectCertificate, \ ExpectServerHelloDone, ExpectChangeCipherSpec, ExpectFinished, \ - ExpectAlert, ExpectClose, ExpectApplicationData + ExpectAlert, ExpectClose, ExpectApplicationData, \ + ExpectServerKeyExchange from tlsfuzzer.utils.lists import natural_sort_keys +from tlsfuzzer.helpers import AutoEmptyExtension, SIG_ALL +from tlslite.extensions import SupportedGroupsExtension, \ + SignatureAlgorithmsExtension, SignatureAlgorithmsCertExtension -version = 5 +version = 6 def help_msg(): @@ -41,6 +45,9 @@ def help_msg(): print(" usage: [-x probe-name] [-X exception], order is compulsory!") print(" -n num run 'num' or all(if 0) tests instead of default(all)") print(" (excluding \"sanity\" tests)") + print(" -d negotiate (EC)DHE instead of RSA key exchange, send") + print(" additional extensions, usually used for (EC)DHE ciphers") + print(" -M | --ems Advertise support for Extended Master Secret") print(" --help this message") @@ -52,9 +59,11 @@ def main(): run_exclude = set() expected_failures = {} last_exp_tmp = None + dhe = False + ems = False argv = sys.argv[1:] - opts, args = getopt.getopt(argv, "h:p:e:x:X:n:", ["help"]) + opts, args = getopt.getopt(argv, "h:p:e:x:X:n:dM", ["help", "ems"]) for opt, arg in opts: if opt == '-h': host = arg @@ -71,6 +80,10 @@ def main(): expected_failures[last_exp_tmp] = str(arg) elif opt == '-n': num_limit = int(arg) + elif opt == '-d': + dhe = True + elif opt == '-M' or opt == '--ems': + ems = True elif opt == '--help': help_msg() sys.exit(0) @@ -86,11 +99,33 @@ def main(): conversation = Connect(host, port) node = conversation - ciphers = [CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, - CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] - node = node.add_child(ClientHelloGenerator(ciphers)) + + ext = {} + if ems: + ext[ExtensionType.extended_master_secret] = AutoEmptyExtension() + if dhe: + ciphers = [CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + groups = [GroupName.secp256r1, + GroupName.ffdhe2048] + ext[ExtensionType.supported_groups] = SupportedGroupsExtension()\ + .create(groups) + ext[ExtensionType.signature_algorithms] = \ + SignatureAlgorithmsExtension().create(SIG_ALL) + ext[ExtensionType.signature_algorithms_cert] = \ + SignatureAlgorithmsCertExtension().create(SIG_ALL) + else: + ciphers = [CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + if not ext: + ext = None + node = node.add_child(ClientHelloGenerator(ciphers, extensions=ext)) node = node.add_child(ExpectServerHello()) node = node.add_child(ExpectCertificate()) + if dhe: + node = node.add_child(ExpectServerKeyExchange()) node = node.add_child(ExpectServerHelloDone()) node = node.add_child(ClientKeyExchangeGenerator()) node = node.add_child(ChangeCipherSpecGenerator()) @@ -108,13 +143,35 @@ def main(): conversation = Connect(host, port) node = conversation - ciphers = [CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA] + ext = {} + if ems: + ext[ExtensionType.extended_master_secret] = AutoEmptyExtension() + if dhe: + ciphers = [CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA] + groups = [GroupName.secp256r1, + GroupName.ffdhe2048] + ext[ExtensionType.supported_groups] = SupportedGroupsExtension()\ + .create(groups) + ext[ExtensionType.signature_algorithms] = \ + SignatureAlgorithmsExtension().create(SIG_ALL) + ext[ExtensionType.signature_algorithms_cert] = \ + SignatureAlgorithmsCertExtension().create(SIG_ALL) + else: + ciphers = [CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA] + ext[ExtensionType.renegotiation_info] = None node = node.add_child(ClientHelloGenerator( ciphers, - extensions={ExtensionType.renegotiation_info:None})) + extensions=ext)) + srv_ext = {ExtensionType.renegotiation_info:None} + if ems: + srv_ext[ExtensionType.extended_master_secret] = None node = node.add_child(ExpectServerHello( - extensions={ExtensionType.renegotiation_info:None})) + extensions=srv_ext)) node = node.add_child(ExpectCertificate()) + if dhe: + node = node.add_child(ExpectServerKeyExchange()) node = node.add_child(ExpectServerHelloDone()) node = node.add_child(ClientKeyExchangeGenerator()) node = node.add_child(ChangeCipherSpecGenerator()) @@ -135,9 +192,9 @@ def main(): node = node.add_child(ResetRenegotiationInfo()) node = node.add_child(ClientHelloGenerator( ciphers, - extensions={ExtensionType.renegotiation_info:None})) + extensions=ext)) node = node.add_child(ExpectServerHello( - extensions={ExtensionType.renegotiation_info:None}, + extensions=srv_ext, resume=True)) node = node.add_child(ExpectChangeCipherSpec()) node = node.add_child(ExpectFinished()) @@ -156,14 +213,15 @@ def main(): conversation = Connect(host, port) node = conversation - ciphers = [CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA] node = node.add_child(ClientHelloGenerator( ciphers, session_id=bytearray(32), - extensions={ExtensionType.renegotiation_info:None})) + extensions=ext)) node = node.add_child(ExpectServerHello( - extensions={ExtensionType.renegotiation_info:None})) + extensions=srv_ext)) node = node.add_child(ExpectCertificate()) + if dhe: + node = node.add_child(ExpectServerKeyExchange()) node = node.add_child(ExpectServerHelloDone()) node = node.add_child(ClientKeyExchangeGenerator()) node = node.add_child(ChangeCipherSpecGenerator()) @@ -186,14 +244,13 @@ def main(): # too long session ID conversation = Connect(host, port) node = conversation - ciphers = [CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA] # session_id (and legacy_session_id in TLS 1.3) are specified as # opaque SessionID<0..32>; # which means that 33 byte long, and longer, are malformed node = node.add_child(ClientHelloGenerator( ciphers, session_id=bytearray(33), - extensions={ExtensionType.renegotiation_info:None})) + extensions=ext)) node = node.add_child(ExpectAlert(AlertLevel.fatal, AlertDescription.decode_error)) node.add_child(ExpectClose()) diff --git a/tests/tlslite-ng-random-subset.json b/tests/tlslite-ng-random-subset.json index 06d8f7e83..a0a918172 100644 --- a/tests/tlslite-ng-random-subset.json +++ b/tests/tlslite-ng-random-subset.json @@ -242,6 +242,8 @@ "-X", "protocol_version"], "comment" : "tlslite-ng does not support SSLv3 by default"}, {"name" : "test-sessionID-resumption.py"}, + {"name" : "test-sessionID-resumption.py", + "arguments" : ["-d"]}, {"name" : "test-sig-algs.py", "comment" : "server has just one certificate configured", "arguments" : ["-x", "rsa_pss_pss_sha256 only", diff --git a/tests/tlslite-ng.json b/tests/tlslite-ng.json index 7b6009b82..8e9ec6e5f 100644 --- a/tests/tlslite-ng.json +++ b/tests/tlslite-ng.json @@ -243,6 +243,8 @@ "-X", "protocol_version"], "comment" : "tlslite-ng does not support SSLv3 by default"}, {"name" : "test-sessionID-resumption.py"}, + {"name" : "test-sessionID-resumption.py", + "arguments" : ["-d"]}, {"name" : "test-sig-algs.py", "comment" : "server has just one certificate configured", "arguments" : ["-x", "rsa_pss_pss_sha256 only", From 8c0f7e3dec718eca9a2ee94d12ffd3cdb8c6e8ef Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Tue, 27 Apr 2021 20:55:56 +0200 Subject: [PATCH 3/6] add support for session_ticket extension --- scripts/test-session-ticket-resumption.py | 530 ++++++++++++++++++++++ tests/test_tlsfuzzer_expect.py | 16 +- tests/tlslite-ng-random-subset.json | 5 + tests/tlslite-ng.json | 5 + tlsfuzzer/expect.py | 66 ++- tlsfuzzer/helpers.py | 24 +- tlsfuzzer/messages.py | 2 +- 7 files changed, 636 insertions(+), 12 deletions(-) create mode 100644 scripts/test-session-ticket-resumption.py diff --git a/scripts/test-session-ticket-resumption.py b/scripts/test-session-ticket-resumption.py new file mode 100644 index 000000000..01b1d1918 --- /dev/null +++ b/scripts/test-session-ticket-resumption.py @@ -0,0 +1,530 @@ +# Author: Hubert Kario, copyright 2015-2020 +# Released under Gnu GPL v2.0, see LICENSE file for details + +from __future__ import print_function +import traceback +import sys +import getopt +from itertools import chain +from random import sample + +from tlsfuzzer.runner import Runner +from tlsfuzzer.messages import Connect, ClientHelloGenerator, \ + ClientKeyExchangeGenerator, ChangeCipherSpecGenerator, \ + FinishedGenerator, ApplicationDataGenerator, AlertGenerator, Close, \ + ResetHandshakeHashes, ResetRenegotiationInfo +from tlsfuzzer.expect import ExpectServerHello, ExpectCertificate, \ + ExpectServerHelloDone, ExpectChangeCipherSpec, ExpectFinished, \ + ExpectAlert, ExpectApplicationData, ExpectClose, \ + ExpectServerKeyExchange, ExpectNewSessionTicket + + +from tlslite.constants import CipherSuite, AlertLevel, AlertDescription, \ + GroupName, ExtensionType +from tlslite.extensions import SupportedGroupsExtension, \ + SignatureAlgorithmsExtension, SignatureAlgorithmsCertExtension +from tlslite.utils.cryptomath import getRandomBytes +from tlsfuzzer.utils.lists import natural_sort_keys +from tlsfuzzer.utils.ordered_dict import OrderedDict +from tlsfuzzer.helpers import SIG_ALL, AutoEmptyExtension, \ + session_ticket_ext_gen + + +version = 1 + + +def help_msg(): + print("Usage: [-h hostname] [-p port] [[probe-name] ...]") + print(" -h hostname name of the host to run the test against") + print(" localhost by default") + print(" -p port port number to use for connection, 4433 by default") + print(" probe-name if present, will run only the probes with given") + print(" names and not all of them, e.g \"sanity\"") + print(" -e probe-name exclude the probe from the list of the ones run") + print(" may be specified multiple times") + print(" -x probe-name expect the probe to fail. When such probe passes despite being marked like this") + print(" it will be reported in the test summary and the whole script will fail.") + print(" May be specified multiple times.") + print(" -X message expect the `message` substring in exception raised during") + print(" execution of preceding expected failure probe") + print(" usage: [-x probe-name] [-X exception], order is compulsory!") + print(" -n num run 'num' or all(if 0) tests instead of default(all)") + print(" (\"sanity\" tests are always executed)") + print(" -d negotiate (EC)DHE instead of RSA key exchange") + print(" --no-new-ticket-on-resumption Don't expect the server to issue a new ticket") + print(" when resuming a session") + print(" --help this message") + + +def main(): + host = "localhost" + port = 4433 + num_limit = None + run_exclude = set() + expected_failures = {} + last_exp_tmp = None + dhe = False + ticket_on_resumption = True + + argv = sys.argv[1:] + opts, args = getopt.getopt(argv, "h:p:e:x:X:n:d", + ["help", "no-new-ticket-on-resumption"]) + for opt, arg in opts: + if opt == '-h': + host = arg + elif opt == '-p': + port = int(arg) + elif opt == '-e': + run_exclude.add(arg) + elif opt == '-x': + expected_failures[arg] = None + last_exp_tmp = str(arg) + elif opt == '-X': + if not last_exp_tmp: + raise ValueError("-x has to be specified before -X") + expected_failures[last_exp_tmp] = str(arg) + elif opt == '-n': + num_limit = int(arg) + elif opt == '-d': + dhe = True + elif opt == '--help': + help_msg() + sys.exit(0) + elif opt == '--no-new-ticket-on-resumption': + ticket_on_resumption = False + else: + raise ValueError("Unknown option: {0}".format(opt)) + + if args: + run_only = set(args) + else: + run_only = None + + conversations = {} + + conversation = Connect(host, port) + node = conversation + ext = {} + ext[ExtensionType.session_ticket] = AutoEmptyExtension() + if dhe: + groups = [GroupName.secp256r1, + GroupName.ffdhe2048] + ext[ExtensionType.supported_groups] = SupportedGroupsExtension()\ + .create(groups) + ext[ExtensionType.signature_algorithms] = \ + SignatureAlgorithmsExtension().create(SIG_ALL) + ext[ExtensionType.signature_algorithms_cert] = \ + SignatureAlgorithmsCertExtension().create(SIG_ALL) + ciphers = [CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + else: + ciphers = [CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + node = node.add_child(ClientHelloGenerator(ciphers, extensions=ext)) + ext = {} + ext[ExtensionType.session_ticket] = None + ext[ExtensionType.renegotiation_info] = None + node = node.add_child(ExpectServerHello(extensions=ext)) + node = node.add_child(ExpectCertificate()) + if dhe: + node = node.add_child(ExpectServerKeyExchange()) + node = node.add_child(ExpectServerHelloDone()) + node = node.add_child(ClientKeyExchangeGenerator()) + node = node.add_child(ChangeCipherSpecGenerator()) + node = node.add_child(FinishedGenerator()) + node = node.add_child(ExpectNewSessionTicket()) + node = node.add_child(ExpectChangeCipherSpec()) + node = node.add_child(ExpectFinished()) + node = node.add_child(ApplicationDataGenerator( + bytearray(b"GET / HTTP/1.0\r\n\r\n"))) + node = node.add_child(ExpectApplicationData()) + node = node.add_child(AlertGenerator(AlertLevel.warning, + AlertDescription.close_notify)) + node = node.add_child(ExpectAlert()) + node.next_sibling = ExpectClose() + conversations["sanity"] = conversation + + # try simple resumption + conversation = Connect(host, port) + node = conversation + ext = {} + ext[ExtensionType.session_ticket] = AutoEmptyExtension() + if dhe: + groups = [GroupName.secp256r1, + GroupName.ffdhe2048] + ext[ExtensionType.supported_groups] = SupportedGroupsExtension()\ + .create(groups) + ext[ExtensionType.signature_algorithms] = \ + SignatureAlgorithmsExtension().create(SIG_ALL) + ext[ExtensionType.signature_algorithms_cert] = \ + SignatureAlgorithmsCertExtension().create(SIG_ALL) + ciphers = [CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + else: + ciphers = [CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + node = node.add_child(ClientHelloGenerator(ciphers, extensions=ext)) + ext_srv = {} + ext_srv[ExtensionType.session_ticket] = None + ext_srv[ExtensionType.renegotiation_info] = None + node = node.add_child(ExpectServerHello(extensions=ext_srv, + description="first")) + node = node.add_child(ExpectCertificate()) + if dhe: + node = node.add_child(ExpectServerKeyExchange()) + node = node.add_child(ExpectServerHelloDone()) + node = node.add_child(ClientKeyExchangeGenerator()) + node = node.add_child(ChangeCipherSpecGenerator()) + node = node.add_child(FinishedGenerator()) + node = node.add_child(ExpectNewSessionTicket()) + node = node.add_child(ExpectChangeCipherSpec()) + node = node.add_child(ExpectFinished()) + node = node.add_child(AlertGenerator(AlertLevel.warning, + AlertDescription.close_notify)) + node = node.add_child(ExpectAlert()) + close = ExpectClose() + node.next_sibling = close + node = node.add_child(ExpectClose()) + node = node.add_child(Close()) + node = node.add_child(Connect(host, port)) + close.add_child(node) + + node = node.add_child(ResetHandshakeHashes()) + node = node.add_child(ResetRenegotiationInfo()) + + ext = dict(ext) + ext[ExtensionType.session_ticket] = session_ticket_ext_gen() + node = node.add_child(ClientHelloGenerator(ciphers, + extensions=ext)) + ext_srv = {} + if ticket_on_resumption: + ext_srv[ExtensionType.session_ticket] = None + ext_srv[ExtensionType.renegotiation_info] = None + node = node.add_child(ExpectServerHello(extensions=ext_srv, + force_resume=True, + description="second")) + if ticket_on_resumption: + node = node.add_child(ExpectNewSessionTicket()) + node = node.add_child(ExpectChangeCipherSpec()) + node = node.add_child(ExpectFinished()) + node = node.add_child(ChangeCipherSpecGenerator()) + node = node.add_child(FinishedGenerator()) + node = node.add_child(ApplicationDataGenerator( + bytearray(b"GET / HTTP/1.0\r\n\r\n"))) + node = node.add_child(ExpectApplicationData()) + node = node.add_child(AlertGenerator(AlertLevel.warning, + AlertDescription.close_notify)) + node = node.add_child(ExpectAlert()) + node.next_sibling = ExpectClose() + conversations["session resumption with empty session_id"] = conversation + + # check what happens if client generates a session_ID of its own + conversation = Connect(host, port) + node = conversation + ext = {} + ext[ExtensionType.session_ticket] = AutoEmptyExtension() + if dhe: + groups = [GroupName.secp256r1, + GroupName.ffdhe2048] + ext[ExtensionType.supported_groups] = SupportedGroupsExtension()\ + .create(groups) + ext[ExtensionType.signature_algorithms] = \ + SignatureAlgorithmsExtension().create(SIG_ALL) + ext[ExtensionType.signature_algorithms_cert] = \ + SignatureAlgorithmsCertExtension().create(SIG_ALL) + ciphers = [CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + else: + ciphers = [CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + node = node.add_child(ClientHelloGenerator(ciphers, extensions=ext)) + ext_srv = {} + ext_srv[ExtensionType.session_ticket] = None + ext_srv[ExtensionType.renegotiation_info] = None + node = node.add_child(ExpectServerHello(extensions=ext_srv, + description="first")) + node = node.add_child(ExpectCertificate()) + if dhe: + node = node.add_child(ExpectServerKeyExchange()) + node = node.add_child(ExpectServerHelloDone()) + node = node.add_child(ClientKeyExchangeGenerator()) + node = node.add_child(ChangeCipherSpecGenerator()) + node = node.add_child(FinishedGenerator()) + node = node.add_child(ExpectNewSessionTicket()) + node = node.add_child(ExpectChangeCipherSpec()) + node = node.add_child(ExpectFinished()) + node = node.add_child(AlertGenerator(AlertLevel.warning, + AlertDescription.close_notify)) + node = node.add_child(ExpectAlert()) + close = ExpectClose() + node.next_sibling = close + node = node.add_child(ExpectClose()) + node = node.add_child(Close()) + node = node.add_child(Connect(host, port)) + close.add_child(node) + + node = node.add_child(ResetHandshakeHashes()) + node = node.add_child(ResetRenegotiationInfo()) + + ext = dict(ext) + ext[ExtensionType.session_ticket] = session_ticket_ext_gen() + node = node.add_child(ClientHelloGenerator(ciphers, + session_id=getRandomBytes(32), + extensions=ext)) + ext_srv = {} + if ticket_on_resumption: + ext_srv[ExtensionType.session_ticket] = None + ext_srv[ExtensionType.renegotiation_info] = None + node = node.add_child(ExpectServerHello(extensions=ext_srv, + description="second")) + if ticket_on_resumption: + node = node.add_child(ExpectNewSessionTicket()) + node = node.add_child(ExpectChangeCipherSpec()) + node = node.add_child(ExpectFinished(description="second")) + node = node.add_child(ChangeCipherSpecGenerator()) + node = node.add_child(FinishedGenerator()) + node = node.add_child(ApplicationDataGenerator( + bytearray(b"GET / HTTP/1.0\r\n\r\n"))) + node = node.add_child(ExpectApplicationData()) + node = node.add_child(AlertGenerator(AlertLevel.warning, + AlertDescription.close_notify)) + node = node.add_child(ExpectAlert()) + node.next_sibling = ExpectClose() + conversations["session resumption with random session_id"] = conversation + + # test resumption in renegotiated handshake + conversation = Connect(host, port) + node = conversation + ext = {} + ext[ExtensionType.session_ticket] = AutoEmptyExtension() + if dhe: + groups = [GroupName.secp256r1, + GroupName.ffdhe2048] + ext[ExtensionType.supported_groups] = SupportedGroupsExtension()\ + .create(groups) + ext[ExtensionType.signature_algorithms] = \ + SignatureAlgorithmsExtension().create(SIG_ALL) + ext[ExtensionType.signature_algorithms_cert] = \ + SignatureAlgorithmsCertExtension().create(SIG_ALL) + ciphers = [CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + else: + ciphers = [CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + node = node.add_child(ClientHelloGenerator(ciphers, extensions=ext)) + ext_srv = {} + ext_srv[ExtensionType.session_ticket] = None + ext_srv[ExtensionType.renegotiation_info] = None + node = node.add_child(ExpectServerHello(extensions=ext_srv)) + node = node.add_child(ExpectCertificate()) + if dhe: + node = node.add_child(ExpectServerKeyExchange()) + node = node.add_child(ExpectServerHelloDone()) + node = node.add_child(ClientKeyExchangeGenerator()) + node = node.add_child(ChangeCipherSpecGenerator()) + node = node.add_child(FinishedGenerator()) + node = node.add_child(ExpectNewSessionTicket()) + node = node.add_child(ExpectChangeCipherSpec()) + node = node.add_child(ExpectFinished()) + node = node.add_child(ResetHandshakeHashes()) + renego_exts = OrderedDict(ext) + # use None for autogeneration of the renegotiation_info with correct + # payload + renego_exts[ExtensionType.renegotiation_info] = None + renego_exts[ExtensionType.session_ticket] = session_ticket_ext_gen() + renego_ciphers = list(ciphers) + renego_ciphers.remove(CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV) + node = node.add_child(ClientHelloGenerator( + renego_ciphers, + extensions=renego_exts, + session_id=bytearray(0))) + ext_srv = dict(ext_srv) + if not ticket_on_resumption: + del ext_srv[ExtensionType.session_ticket] + node = node.add_child(ExpectServerHello( + extensions=ext_srv, + force_resume=True, + description="second handshake")) + if ticket_on_resumption: + node = node.add_child(ExpectNewSessionTicket()) + node = node.add_child(ExpectChangeCipherSpec()) + node = node.add_child(ExpectFinished()) + node = node.add_child(ChangeCipherSpecGenerator()) + node = node.add_child(FinishedGenerator()) + node = node.add_child(ApplicationDataGenerator( + bytearray(b"GET / HTTP/1.0\r\n\r\n"))) + node = node.add_child(ExpectApplicationData()) + node = node.add_child(AlertGenerator(AlertLevel.warning, + AlertDescription.close_notify)) + node = node.add_child(ExpectAlert()) + node.next_sibling = ExpectClose() + conversations["session resumption with renegotiation"] = conversation + + # test dropping session_ticket extension in second handshake + conversation = Connect(host, port) + node = conversation + ext = {} + ext[ExtensionType.session_ticket] = AutoEmptyExtension() + if dhe: + groups = [GroupName.secp256r1, + GroupName.ffdhe2048] + ext[ExtensionType.supported_groups] = SupportedGroupsExtension()\ + .create(groups) + ext[ExtensionType.signature_algorithms] = \ + SignatureAlgorithmsExtension().create(SIG_ALL) + ext[ExtensionType.signature_algorithms_cert] = \ + SignatureAlgorithmsCertExtension().create(SIG_ALL) + ciphers = [CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + else: + ciphers = [CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + node = node.add_child(ClientHelloGenerator(ciphers, extensions=ext)) + ext_srv = {} + ext_srv[ExtensionType.session_ticket] = None + ext_srv[ExtensionType.renegotiation_info] = None + node = node.add_child(ExpectServerHello(extensions=ext_srv)) + node = node.add_child(ExpectCertificate()) + if dhe: + node = node.add_child(ExpectServerKeyExchange()) + node = node.add_child(ExpectServerHelloDone()) + node = node.add_child(ClientKeyExchangeGenerator()) + node = node.add_child(ChangeCipherSpecGenerator()) + node = node.add_child(FinishedGenerator()) + node = node.add_child(ExpectNewSessionTicket()) + node = node.add_child(ExpectChangeCipherSpec()) + node = node.add_child(ExpectFinished()) + node = node.add_child(ResetHandshakeHashes()) + renego_exts = OrderedDict(ext) + # use None for autogeneration of the renegotiation_info with correct + # payload + renego_exts[ExtensionType.renegotiation_info] = None + del renego_exts[ExtensionType.session_ticket] + renego_ciphers = list(ciphers) + renego_ciphers.remove(CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV) + node = node.add_child(ClientHelloGenerator( + renego_ciphers, + extensions=renego_exts, + session_id=bytearray(0))) + ext_srv = dict(ext_srv) + if ExtensionType.session_ticket in ext_srv: + del ext_srv[ExtensionType.session_ticket] + node = node.add_child(ExpectServerHello( + extensions=ext_srv, + description="second handshake")) + node = node.add_child(ExpectCertificate()) + if dhe: + node = node.add_child(ExpectServerKeyExchange()) + node = node.add_child(ExpectServerHelloDone()) + node = node.add_child(ClientKeyExchangeGenerator()) + node = node.add_child(ChangeCipherSpecGenerator()) + node = node.add_child(FinishedGenerator()) + node = node.add_child(ExpectChangeCipherSpec()) + node = node.add_child(ExpectFinished()) + node = node.add_child(ApplicationDataGenerator( + bytearray(b"GET / HTTP/1.0\r\n\r\n"))) + node = node.add_child(ExpectApplicationData()) + node = node.add_child(AlertGenerator(AlertLevel.warning, + AlertDescription.close_notify)) + node = node.add_child(ExpectAlert()) + node.next_sibling = ExpectClose() + conversations["renegotiation with removal of session_ticket ext"] = conversation + + # run the conversation + good = 0 + bad = 0 + xfail = 0 + xpass = 0 + failed = [] + xpassed = [] + if not num_limit: + num_limit = len(conversations) + + # make sure that sanity test is run first and last + # to verify that server was running and kept running throughout + sanity_tests = [('sanity', conversations['sanity'])] + if run_only: + if num_limit > len(run_only): + num_limit = len(run_only) + regular_tests = [(k, v) for k, v in conversations.items() if k in run_only] + else: + regular_tests = [(k, v) for k, v in conversations.items() if + (k != 'sanity') and k not in run_exclude] + sampled_tests = sample(regular_tests, min(num_limit, len(regular_tests))) + ordered_tests = chain(sanity_tests, sampled_tests, sanity_tests) + + for c_name, c_test in ordered_tests: + print("{0} ...".format(c_name)) + + runner = Runner(c_test) + + res = True + exception = None + try: + runner.run() + except Exception as exp: + exception = exp + print("Error while processing") + print(traceback.format_exc()) + res = False + + if c_name in expected_failures: + if res: + xpass += 1 + xpassed.append(c_name) + print("XPASS-expected failure but test passed\n") + else: + if expected_failures[c_name] is not None and \ + expected_failures[c_name] not in str(exception): + bad += 1 + failed.append(c_name) + print("Expected error message: {0}\n" + .format(expected_failures[c_name])) + else: + xfail += 1 + print("OK-expected failure\n") + else: + if res: + good += 1 + print("OK\n") + else: + bad += 1 + failed.append(c_name) + + print("Test s ession resumption using session tickets") + print("Use TLS 1.2 or earlier and RSA key exchange (or (EC)DHE if") + print("-d option is used)\n") + + print("Test end") + print(20 * '=') + print("version: {0}".format(version)) + print(20 * '=') + print("TOTAL: {0}".format(len(sampled_tests) + 2*len(sanity_tests))) + print("SKIP: {0}".format(len(run_exclude.intersection(conversations.keys())))) + print("PASS: {0}".format(good)) + print("XFAIL: {0}".format(xfail)) + print("FAIL: {0}".format(bad)) + print("XPASS: {0}".format(xpass)) + print(20 * '=') + sort = sorted(xpassed ,key=natural_sort_keys) + if len(sort): + print("XPASSED:\n\t{0}".format('\n\t'.join(repr(i) for i in sort))) + sort = sorted(failed, key=natural_sort_keys) + if len(sort): + print("FAILED:\n\t{0}".format('\n\t'.join(repr(i) for i in sort))) + + if bad > 0: + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/tests/test_tlsfuzzer_expect.py b/tests/test_tlsfuzzer_expect.py index b6bc297b2..e8136118d 100644 --- a/tests/test_tlsfuzzer_expect.py +++ b/tests/test_tlsfuzzer_expect.py @@ -45,7 +45,7 @@ ClientHello, Certificate, ServerHello2, ServerFinished, \ ServerKeyExchange, CertificateStatus, CertificateVerify, \ Finished, EncryptedExtensions, NewSessionTicket, Heartbeat, \ - KeyUpdate, HelloRequest, ServerHelloDone + KeyUpdate, HelloRequest, ServerHelloDone, NewSessionTicket1_0 from tlslite.extensions import SNIExtension, TLSExtension, \ SupportedGroupsExtension, ALPNExtension, ECPointFormatsExtension, \ NPNExtension, ServerKeyShareExtension, ClientKeyShareExtension, \ @@ -2955,6 +2955,7 @@ def test_process(self): nst = NewSessionTicket().create(12, 44, b'abba', b'I am a ticket', []) state = ConnectionState() + state.version = (3, 4) exp.process(state, nst) @@ -2972,6 +2973,19 @@ def test___repr___with_description(self): self.assertEqual("ExpectNewSessionTicket(description='some string')", repr(exp)) + def test_process_in_TLS1_2(self): + exp = ExpectNewSessionTicket() + + nst = NewSessionTicket1_0().create(3600, b'I am an old ticket') + + state = ConnectionState() + state.version = (3, 3) + + exp.process(state, nst) + + self.assertIn(nst, state.session_tickets) + self.assertIsNotNone(state.session_tickets[0].time) + class TestExpectVerify(unittest.TestCase): def test___init__(self): diff --git a/tests/tlslite-ng-random-subset.json b/tests/tlslite-ng-random-subset.json index a0a918172..47d97146f 100644 --- a/tests/tlslite-ng-random-subset.json +++ b/tests/tlslite-ng-random-subset.json @@ -241,6 +241,11 @@ "-x", "Protocol (3, 0) in SSLv2 compatible ClientHello", "-X", "protocol_version"], "comment" : "tlslite-ng does not support SSLv3 by default"}, + {"name" : "test-session-ticket-resumption.py", + "comment" : "tlslite-ng doesn't implement renegotiation tlslite-ng#66.", + "arguments" : ["--no-new-ticket-on-resumption", + "-x", "renegotiation with removal of session_ticket ext", "-X", "Alert(warning, no_renegotiation)", + "-x", "session resumption with renegotiation", "-X", "Alert(warning, no_renegotiation)"]}, {"name" : "test-sessionID-resumption.py"}, {"name" : "test-sessionID-resumption.py", "arguments" : ["-d"]}, diff --git a/tests/tlslite-ng.json b/tests/tlslite-ng.json index 8e9ec6e5f..779da7144 100644 --- a/tests/tlslite-ng.json +++ b/tests/tlslite-ng.json @@ -242,6 +242,11 @@ "-x", "Protocol (3, 0) in SSLv2 compatible ClientHello", "-X", "protocol_version"], "comment" : "tlslite-ng does not support SSLv3 by default"}, + {"name" : "test-session-ticket-resumption.py", + "comment" : "tlslite-ng doesn't implement renegotiation tlslite-ng#66.", + "arguments" : ["--no-new-ticket-on-resumption", + "-x", "renegotiation with removal of session_ticket ext", "-X", "Alert(warning, no_renegotiation)", + "-x", "session resumption with renegotiation", "-X", "Alert(warning, no_renegotiation)"]}, {"name" : "test-sessionID-resumption.py"}, {"name" : "test-sessionID-resumption.py", "arguments" : ["-d"]}, diff --git a/tlsfuzzer/expect.py b/tlsfuzzer/expect.py index 4e8bf5044..5084397eb 100644 --- a/tlsfuzzer/expect.py +++ b/tlsfuzzer/expect.py @@ -20,7 +20,7 @@ ChangeCipherSpec, Finished, Alert, CertificateRequest, ServerHello2,\ ServerKeyExchange, ClientHello, ServerFinished, CertificateStatus, \ CertificateVerify, EncryptedExtensions, NewSessionTicket, Heartbeat,\ - KeyUpdate, HelloRequest + KeyUpdate, HelloRequest, NewSessionTicket1_0 from tlslite.extensions import TLSExtension, ALPNExtension from tlslite.utils.codec import Parser, Writer from tlslite.utils.compat import b2a_hex @@ -276,6 +276,13 @@ def srv_ext_handler_npn(state, extension): raise AssertionError("Malformed NPN extension") +def srv_ext_handler_session_ticket(state, extension): + """Process the session_ticket extension from server.""" + del state + if extension.ticket != b"": + raise AssertionError("Malformed session_ticket extension") + + def srv_ext_handler_key_share(state, extension): """Process the key_share extension from server.""" cln_hello = state.get_last_message_of_type(ClientHello) @@ -485,6 +492,7 @@ def clnt_ext_handler_sig_algs(state, extension): ExtensionType.server_name: srv_ext_handler_sni, ExtensionType.renegotiation_info: srv_ext_handler_renego, ExtensionType.alpn: srv_ext_handler_alpn, + ExtensionType.session_ticket: srv_ext_handler_session_ticket, ExtensionType.ec_point_formats: srv_ext_handler_ec_point, ExtensionType.supports_npn: srv_ext_handler_npn, ExtensionType.key_share: srv_ext_handler_key_share, @@ -575,7 +583,7 @@ class ExpectServerHello(_ExpectExtensionsMessage): """ def __init__(self, extensions=None, version=None, resume=False, - cipher=None, server_max_protocol=None, + cipher=None, server_max_protocol=None, force_resume=False, description=None): """ Initialize the object @@ -609,6 +617,10 @@ def __init__(self, extensions=None, version=None, resume=False, current state - IOW, if the server hello should belong to a resumed session. TLS 1.2 and earlier only. In TLS 1.3 resumption is handled by providing handler for ``pre_shared_key`` extension. + + :param boolean force_resume: assume that the session is getting resumed, + even if the sessionID is empty. Applicable to TLS 1.2 and earlier + only when using session tickets and not sending a sessionID. """ super(ExpectServerHello, self).__init__(ContentType.handshake, HandshakeType.server_hello, @@ -617,6 +629,7 @@ def __init__(self, extensions=None, version=None, resume=False, self.version = version self.resume = resume self.srv_max_prot = server_max_protocol + self.force_resume = force_resume self.description = description def __str__(self): @@ -732,10 +745,13 @@ def process(self, state, msg): # extract important info state.server_random = srv_hello.random + cln_hello = state.get_last_message_of_type(ClientHello) + # check for session_id based session resumption if self.resume: assert state.session_id == srv_hello.session_id - if (state.session_id == srv_hello.session_id and + if self.force_resume or ((state.session_id == srv_hello.session_id + or cln_hello.session_id == srv_hello.session_id) and srv_hello.session_id != bytearray(0) and self._extract_version(srv_hello) < (3, 4)): # TLS 1.2 resumption, TLS 1.3 is based on PSKs @@ -754,7 +770,6 @@ def process(self, state, msg): "Expected: {0}, received: {1}.") # check if server sent cipher matches what we advertised in CH - cln_hello = state.get_last_message_of_type(ClientHello) if srv_hello.cipher_suite not in cln_hello.cipher_suites: cipher = srv_hello.cipher_suite if cipher in CipherSuite.ietfNames: @@ -1525,7 +1540,22 @@ class ExpectFinished(ExpectHandshake): to be encrypted with ``server_application_traffic_secret`` keys. """ - def __init__(self, version=None): + def __init__(self, version=None, description=None): + """ + Initialize object. + + .. note:: + The ``description`` parameter MUST be specified + as a keyword argument, i.e. read the definition as + ``(self, *, description=None)`` (see PEP 3102). + Otherwise the behaviour of this node is not guaranteed if new + arguments are added to it (as they will be added *before* + the ``description`` argument). + + :param str description: name or comment attached to the node, + it will be printed when :py:func:`str` or :py:func:`repr` is + called on the node. + """ if version in ((0, 2), (2, 0)): super(ExpectFinished, self).__init__(ContentType.handshake, SSL2HandshakeType. @@ -1534,6 +1564,7 @@ def __init__(self, version=None): super(ExpectFinished, self).__init__(ContentType.handshake, HandshakeType.finished) self.version = version + self.description = description def process(self, state, msg): """ @@ -1624,6 +1655,10 @@ def process(self, state, msg): state.cipher, c_traff_sec, s_traff_sec, None) state.msg_sock.changeReadState() + def __repr__(self): + """Return human readable representation of the object.""" + return self._repr(['description']) + class ExpectEncryptedExtensions(_ExpectExtensionsMessage): """Processing of the TLS handshake protocol Encrypted Extensions message""" @@ -1728,7 +1763,7 @@ def process(self, state, msg): class ExpectNewSessionTicket(ExpectHandshake): """Processing TLS handshake protocol new session ticket message.""" - def __init__(self, description=None): + def __init__(self, version=None, description=None): """ Initialise object. @@ -1740,6 +1775,8 @@ def __init__(self, description=None): arguments are added to it (as they will be added *before* the ``description`` argument). + :param tuple version: parse the message as in the specified TLS + version, use negotiated version by default :param str description: name or comment attached to the node, it will be printed when :py:func:`str` or :py:func:`repr` is called on the node. @@ -1748,19 +1785,32 @@ def __init__(self, description=None): ContentType.handshake, HandshakeType.new_session_ticket) self.description = description + self.version = version def process(self, state, msg): """Parse, verify and process the message.""" assert msg.contentType == ContentType.handshake - parser = Parser(msg.write()) + msg_bytes = msg.write() + parser = Parser(msg_bytes) hs_type = parser.get(1) assert hs_type == HandshakeType.new_session_ticket + if self.version is None: + self.version = state.version - ticket = NewSessionTicket().parse(parser) + if self.version < (3, 4): + ticket = NewSessionTicket1_0().parse(parser) + else: + ticket = NewSessionTicket().parse(parser) ticket.time = time.time() state.session_tickets.append(ticket) + if self.version < (3, 4): + # in TLS 1.2 and earlier tickets are part of the Handshake, so + # they need to be hashed + state.handshake_messages.append(ticket) + state.handshake_hashes.update(msg_bytes) + def __repr__(self): """Return human readable representation of object.""" return self._repr(['description']) diff --git a/tlsfuzzer/helpers.py b/tlsfuzzer/helpers.py index 69a346103..3288cc3f3 100644 --- a/tlsfuzzer/helpers.py +++ b/tlsfuzzer/helpers.py @@ -8,7 +8,7 @@ SignatureScheme, ClientCertificateType, ExtensionType from tlslite.extensions import KeyShareEntry, PreSharedKeyExtension, \ - PskIdentity, ClientKeyShareExtension + PskIdentity, ClientKeyShareExtension, SessionTicketExtension from tlslite.handshakehelpers import HandshakeHelpers from .handshake_helpers import kex_for_group @@ -18,7 +18,8 @@ 'key_share_ext_gen', 'uniqueness_check', 'RSA_SIG_ALL', 'ECDSA_SIG_ALL', 'RSA_PKCS1_ALL', 'RSA_PSS_PSS_ALL', 'RSA_PSS_RSAE_ALL', 'ECDSA_SIG_TLS1_3_ALL', 'EDDSA_SIG_ALL', - 'SIG_ALL', 'AutoEmptyExtension', 'client_cert_types_to_ids'] + 'SIG_ALL', 'AutoEmptyExtension', 'client_cert_types_to_ids', + 'session_ticket_ext_gen'] RSA_SIG_ALL = [(getattr(HashAlgorithm, x), SignatureAlgorithm.rsa) for x in @@ -328,6 +329,25 @@ def psk_session_ext_gen(psk_settings=None): return partial(_psk_session_ext_gen, psk_settings=psk_settings) +def session_ticket_ext_gen(which=-1): + """ + Create a session_ticket extension based on ticket from server. + + Session needs to have processed tickets with ExpectNewSessionTicket + nodes before. By default the last ticket will be used. + + :param int which: the subscript to use for selecting the ticket in session + `-1` for last, `0` for first, `1` for second, etc. + :return: extension generator + """ + def _session_ticket_ext_gen(state, which=which): + if not state.session_tickets: + raise ValueError("No New Session Ticket messages in session") + nst = state.session_tickets[which] + return SessionTicketExtension().create(nst.ticket) + return _session_ticket_ext_gen + + def _psk_ext_updater(state, client_hello, psk_settings): h_hash = state.handshake_hashes nst = None diff --git a/tlsfuzzer/messages.py b/tlsfuzzer/messages.py index 1c789c6a4..95224e528 100644 --- a/tlsfuzzer/messages.py +++ b/tlsfuzzer/messages.py @@ -632,7 +632,7 @@ def _generate_extensions(self, state): elif ext_id in (ExtensionType.client_hello_padding, ExtensionType.encrypt_then_mac, ExtensionType.extended_master_secret, - 35, # session_ticket + ExtensionType.session_ticket, 49, # post_handshake_auth 52): # transparency_info ext = TLSExtension().create(ext_id, bytearray()) From 451d0e0ec371ddb087616adf0054afa78d5acabe Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Mon, 31 Jul 2023 20:01:04 +0200 Subject: [PATCH 4/6] tls13-session-resumption - verify that TLS 1.2 can't be used in TLS 1.3 --- scripts/test-tls13-session-resumption.py | 166 ++++++++++++++++++++++- tlsfuzzer/helpers.py | 18 ++- 2 files changed, 177 insertions(+), 7 deletions(-) diff --git a/scripts/test-tls13-session-resumption.py b/scripts/test-tls13-session-resumption.py index a1db8fe61..ceea868f5 100644 --- a/scripts/test-tls13-session-resumption.py +++ b/scripts/test-tls13-session-resumption.py @@ -21,18 +21,20 @@ from tlsfuzzer.runner import Runner from tlsfuzzer.messages import Connect, ClientHelloGenerator, \ FinishedGenerator, ApplicationDataGenerator, AlertGenerator, \ - Close, ResetHandshakeHashes, ResetRenegotiationInfo + Close, ResetHandshakeHashes, ResetRenegotiationInfo, \ + ClientKeyExchangeGenerator, ChangeCipherSpecGenerator from tlsfuzzer.expect import ExpectServerHello, ExpectCertificate, \ ExpectChangeCipherSpec, ExpectFinished, \ ExpectAlert, ExpectApplicationData, ExpectClose, \ ExpectEncryptedExtensions, ExpectCertificateVerify, \ ExpectNewSessionTicket, srv_ext_handler_supp_vers, \ - gen_srv_ext_handler_psk, srv_ext_handler_key_share + gen_srv_ext_handler_psk, srv_ext_handler_key_share, \ + ExpectServerHelloDone from tlsfuzzer.helpers import key_share_gen, psk_session_ext_gen, \ - psk_ext_updater, RSA_SIG_ALL + psk_ext_updater, RSA_SIG_ALL, AutoEmptyExtension -version = 4 +version = 5 def help_msg(): @@ -50,6 +52,7 @@ def help_msg(): print(" -X message expect the `message` substring in exception raised during") print(" execution of preceding expected failure probe") print(" usage: [-x probe-name] [-X exception], order is compulsory!") + print(" -d negotiate (EC)DHE instead of RSA key exchange") print(" -n num run 'num' or all(if 0) tests instead of default(all)") print(" (excluding \"sanity\" tests)") print(" --help this message") @@ -62,9 +65,10 @@ def main(): run_exclude = set() expected_failures = {} last_exp_tmp = None + dhe = False argv = sys.argv[1:] - opts, args = getopt.getopt(argv, "h:p:e:x:X:n:", ["help"]) + opts, args = getopt.getopt(argv, "h:p:e:x:X:n:d", ["help"]) for opt, arg in opts: if opt == '-h': host = arg @@ -72,6 +76,8 @@ def main(): port = int(arg) elif opt == '-e': run_exclude.add(arg) + elif opt == '-d': + dhe = True elif opt == '-x': expected_failures[arg] = None last_exp_tmp = str(arg) @@ -146,6 +152,51 @@ def main(): node.add_child(ExpectClose()) conversations["sanity"] = conversation + # check if TLS 1.2 works + conversation = Connect(host, port) + node = conversation + ext = {} + ext[ExtensionType.session_ticket] = AutoEmptyExtension() + if dhe: + groups = [GroupName.secp256r1, + GroupName.ffdhe2048] + ext[ExtensionType.supported_groups] = SupportedGroupsExtension()\ + .create(groups) + ext[ExtensionType.signature_algorithms] = \ + SignatureAlgorithmsExtension().create(SIG_ALL) + ext[ExtensionType.signature_algorithms_cert] = \ + SignatureAlgorithmsCertExtension().create(SIG_ALL) + ciphers = [CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + else: + ciphers = [CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + node = node.add_child(ClientHelloGenerator(ciphers, extensions=ext)) + ext = {} + ext[ExtensionType.session_ticket] = None + ext[ExtensionType.renegotiation_info] = None + node = node.add_child(ExpectServerHello(extensions=ext)) + node = node.add_child(ExpectCertificate()) + if dhe: + node = node.add_child(ExpectServerKeyExchange()) + node = node.add_child(ExpectServerHelloDone()) + node = node.add_child(ClientKeyExchangeGenerator()) + node = node.add_child(ChangeCipherSpecGenerator()) + node = node.add_child(FinishedGenerator()) + node = node.add_child(ExpectNewSessionTicket()) + node = node.add_child(ExpectChangeCipherSpec()) + node = node.add_child(ExpectFinished()) + node = node.add_child(ApplicationDataGenerator( + bytearray(b"GET / HTTP/1.0\r\n\r\n"))) + node = node.add_child(ExpectApplicationData()) + node = node.add_child(AlertGenerator(AlertLevel.warning, + AlertDescription.close_notify)) + node = node.add_child(ExpectAlert()) + node.next_sibling = ExpectClose() + conversations["sanity - TLS 1.2"] = conversation + # resume a session conversation = Connect(host, port) node = conversation @@ -240,6 +291,111 @@ def main(): node.add_child(ExpectClose()) conversations["session resumption"] = conversation + # see if the TLS 1.2 session can't be used for TLS 1.3 + conversation = Connect(host, port) + node = conversation + ext = {} + ext[ExtensionType.session_ticket] = AutoEmptyExtension() + if dhe: + groups = [GroupName.secp256r1, + GroupName.ffdhe2048] + ext[ExtensionType.supported_groups] = SupportedGroupsExtension()\ + .create(groups) + ext[ExtensionType.signature_algorithms] = \ + SignatureAlgorithmsExtension().create(SIG_ALL) + ext[ExtensionType.signature_algorithms_cert] = \ + SignatureAlgorithmsCertExtension().create(SIG_ALL) + ciphers = [CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + else: + ciphers = [CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + node = node.add_child(ClientHelloGenerator(ciphers, extensions=ext)) + ext_srv = {} + ext_srv[ExtensionType.session_ticket] = None + ext_srv[ExtensionType.renegotiation_info] = None + node = node.add_child(ExpectServerHello(extensions=ext_srv, + description="first")) + node = node.add_child(ExpectCertificate()) + if dhe: + node = node.add_child(ExpectServerKeyExchange()) + node = node.add_child(ExpectServerHelloDone()) + node = node.add_child(ClientKeyExchangeGenerator()) + node = node.add_child(ChangeCipherSpecGenerator()) + node = node.add_child(FinishedGenerator()) + node = node.add_child(ExpectNewSessionTicket()) + node = node.add_child(ExpectChangeCipherSpec()) + node = node.add_child(ExpectFinished()) + node = node.add_child(AlertGenerator(AlertLevel.warning, + AlertDescription.close_notify)) + node = node.add_child(ExpectAlert()) + close = ExpectClose() + node.next_sibling = close + node = node.add_child(ExpectClose()) + node = node.add_child(Close()) + node = node.add_child(Connect(host, port)) + close.add_child(node) + + node = node.add_child(ResetHandshakeHashes()) + node = node.add_child(ResetRenegotiationInfo()) + + # start the second handshake + ciphers = [CipherSuite.TLS_AES_128_GCM_SHA256, + CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + ext = OrderedDict(ext) + groups = [GroupName.secp256r1] + key_shares = [] + for group in groups: + key_shares.append(key_share_gen(group)) + ext[ExtensionType.key_share] = ClientKeyShareExtension().create(key_shares) + ext[ExtensionType.supported_versions] = SupportedVersionsExtension()\ + .create([TLS_1_3_DRAFT, (3, 3)]) + ext[ExtensionType.supported_groups] = SupportedGroupsExtension()\ + .create(groups) + sig_algs = [SignatureScheme.rsa_pss_rsae_sha256, + SignatureScheme.rsa_pss_pss_sha256] + ext[ExtensionType.signature_algorithms] = SignatureAlgorithmsExtension()\ + .create(sig_algs) + ext[ExtensionType.signature_algorithms_cert] = SignatureAlgorithmsCertExtension()\ + .create(RSA_SIG_ALL) + ext[ExtensionType.psk_key_exchange_modes] = PskKeyExchangeModesExtension()\ + .create([PskKeyExchangeMode.psk_dhe_ke, PskKeyExchangeMode.psk_ke]) + ext[ExtensionType.pre_shared_key] = psk_session_ext_gen() + mods = [] + mods.append(psk_ext_updater()) + node = node.add_child(ClientHelloGenerator(ciphers, extensions=ext, + modifiers=mods)) + node = node.add_child(ExpectServerHello()) + node = node.add_child(ExpectChangeCipherSpec()) + node = node.add_child(ExpectEncryptedExtensions()) + node = node.add_child(ExpectCertificate()) + node = node.add_child(ExpectCertificateVerify()) + node = node.add_child(ExpectFinished()) + node = node.add_child(FinishedGenerator()) + node = node.add_child(ApplicationDataGenerator( + bytearray(b"GET / HTTP/1.0\r\n\r\n"))) + # ensure that the server sends at least one NST always + #node = node.add_child(ExpectNewSessionTicket()) + + # but multiple ones are OK too + cycle = ExpectNewSessionTicket() + node = node.add_child(cycle) + node.add_child(cycle) + + node.next_sibling = ExpectApplicationData() + node = node.next_sibling.add_child( + AlertGenerator(AlertLevel.warning, + AlertDescription.close_notify)) + + node = node.add_child(ExpectAlert(AlertLevel.warning, + AlertDescription.close_notify)) + node.next_sibling = ExpectClose() + node.add_child(ExpectClose()) + + conversations["use TLS 1.2 ticket in TLS 1.3"] = conversation + # run the conversation good = 0 bad = 0 diff --git a/tlsfuzzer/helpers.py b/tlsfuzzer/helpers.py index 3288cc3f3..7f2d3964f 100644 --- a/tlsfuzzer/helpers.py +++ b/tlsfuzzer/helpers.py @@ -3,6 +3,7 @@ """Helper functions for test scripts.""" import time +import random from functools import partial from tlslite.constants import HashAlgorithm, SignatureAlgorithm, \ SignatureScheme, ClientCertificateType, ExtensionType @@ -11,6 +12,7 @@ PskIdentity, ClientKeyShareExtension, SessionTicketExtension from tlslite.handshakehelpers import HandshakeHelpers from .handshake_helpers import kex_for_group +from tlslite.utils.cryptomath import getRandomBytes __all__ = ['sig_algs_to_ids', 'key_share_gen', 'psk_ext_gen', @@ -300,10 +302,14 @@ def _psk_session_ext_gen(state, psk_settings): raise ValueError("No New Session Ticket messages in session") nst = state.session_tickets[-1] + # if we're reusing TLS 1.2 ticket in TLS 1.3, it won't have the + # `ticket_age_add` field, so fake it + ticket_age_add = getattr(nst, 'ticket_age_add', random.randint(0, 2**32-1)) + # nst.time is fractional but ticket time should be in ms, not s as the # NewSessionTicket.time is ticket_time = int(time.time() * 1000 - nst.time * 1000 + - nst.ticket_age_add) % 2**32 + ticket_age_add) % 2**32 ticket_iden = PskIdentity().create(nst.ticket, ticket_time) binder_len = state.prf_size @@ -353,12 +359,20 @@ def _psk_ext_updater(state, client_hello, psk_settings): nst = None if state.session_tickets: nst = state.session_tickets[-1] + master_key = None + if nst: + try: + master_key = state.key['resumption master secret'] + except KeyError: + # we have a TLS 1.2 ticket, so we need to fake some things: + master_key = state.key['master_secret'] + nst.ticket_nonce = getRandomBytes(32) HandshakeHelpers.update_binders( client_hello, h_hash, psk_settings, [nst] if nst else None, - state.key['resumption master secret'] if nst else None) + master_key) def psk_ext_updater(psk_settings=tuple()): From 12999f26c801a6e67be0efe1f11f37bf54263ab4 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Mon, 31 Jul 2023 20:01:31 +0200 Subject: [PATCH 5/6] expect - do actually write the output on python3... --- tlsfuzzer/expect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tlsfuzzer/expect.py b/tlsfuzzer/expect.py index 5084397eb..3de6ca451 100644 --- a/tlsfuzzer/expect.py +++ b/tlsfuzzer/expect.py @@ -1946,7 +1946,7 @@ def process(self, state, msg): "expected: {1}".format(len(data), self.size)) if self.output: self.output.write("ExpectApplicationData received payload:\n") - self.output.write(data) + self.output.write(repr(data)) self.output.write("ExpectApplicationData end of payload.\n") From 093c6a4340ba6576f158da93016abe8ec8405b35 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Thu, 3 Aug 2023 19:28:16 +0200 Subject: [PATCH 6/6] require new tlslite-ng --- .github/workflows/ci.yml | 4 ++-- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7715ae8c2..eb20932a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -287,9 +287,9 @@ jobs: - name: Install dependencies (2.6) if: ${{ matrix.python-version == '2.6' }} run: | - wget https://files.pythonhosted.org/packages/7c/c9/f4a2146789a4f5161d59a597963a0a2b015a95ed25911da36acf3555c8fa/tlslite-ng-0.8.0a45.tar.gz + wget https://files.pythonhosted.org/packages/2c/57/32510e7e8b01d01fe77b68d9081f252d4a43ef5ce27dbe0ea21ad9dcec35/tlslite-ng-0.8.0a46.tar.gz wget https://files.pythonhosted.org/packages/b4/4c/f8b4ed6c61dff52294f98aaf99053dd979c1b4233d953f371afb0a2977a1/ecdsa-0.18.0b2-py2.py3-none-any.whl - pip install tlslite-ng-0.8.0a45.tar.gz ecdsa-0.18.0b2-py2.py3-none-any.whl + pip install tlslite-ng-0.8.0a46.tar.gz ecdsa-0.18.0b2-py2.py3-none-any.whl - name: Install dependencies if: ${{ matrix.python-version != '2.6' }} run: pip install -r requirements.txt diff --git a/requirements.txt b/requirements.txt index c0c58871d..5aeae9ca8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -tlslite-ng==0.8.0-alpha45 +tlslite-ng==0.8.0-alpha46