From 5e40de3ee25a453e1011df0714419937eb4c53f0 Mon Sep 17 00:00:00 2001 From: rperez Date: Thu, 5 Sep 2019 16:11:44 +0200 Subject: [PATCH 1/3] Add session resumption functionalities --- scapy/layers/tls/automaton_cli.py | 185 ++++++++++++++++++++++++++--- scapy/layers/tls/automaton_srv.py | 189 +++++++++++++++++++++++++++++- scapy/layers/tls/extensions.py | 7 +- scapy/layers/tls/handshake.py | 23 +++- test/tls/example_client.py | 9 ++ test/tls/example_server.py | 3 + 6 files changed, 390 insertions(+), 26 deletions(-) diff --git a/scapy/layers/tls/automaton_cli.py b/scapy/layers/tls/automaton_cli.py index 8c3ada4eb0c..07c240a4702 100644 --- a/scapy/layers/tls/automaton_cli.py +++ b/scapy/layers/tls/automaton_cli.py @@ -20,11 +20,14 @@ from __future__ import print_function import socket import binascii +import struct +import time from scapy.config import conf from scapy.pton_ntop import inet_pton from scapy.utils import randstring, repr_hex from scapy.automaton import ATMT +from scapy.error import warning from scapy.layers.tls.automaton import _TLSAutomaton from scapy.layers.tls.basefields import _tls_version, _tls_version_options from scapy.layers.tls.session import tlsSession @@ -36,7 +39,7 @@ TLSEncryptedExtensions, TLSFinished, TLSServerHello, TLSServerHelloDone, \ TLSServerKeyExchange, TLS13Certificate, TLS13ClientHello, \ TLS13ServerHello, TLS13HelloRetryRequest, TLS13CertificateRequest, \ - _ASN1CertAndExt, TLS13KeyUpdate + _ASN1CertAndExt, TLS13KeyUpdate, TLS13NewSessionTicket from scapy.layers.tls.handshake_sslv2 import SSLv2ClientHello, \ SSLv2ServerHello, SSLv2ClientMasterKey, SSLv2ServerVerify, \ SSLv2ClientFinished, SSLv2ServerFinished, SSLv2ClientCertificate, \ @@ -46,7 +49,8 @@ TLS_Ext_PreSharedKey_CH from scapy.layers.tls.record import TLSAlert, TLSChangeCipherSpec, \ TLSApplicationData -from scapy.layers.tls.crypto.suites import _tls_cipher_suites +from scapy.layers.tls.crypto.suites import _tls_cipher_suites, \ + _tls_cipher_suites_cls from scapy.layers.tls.crypto.groups import _tls_named_groups from scapy.layers.tls.crypto.hkdf import TLS13_HKDF from scapy.modules import six @@ -76,6 +80,9 @@ class TLSClientAutomaton(_TLSAutomaton): def parse_args(self, server="127.0.0.1", dport=4433, server_name=None, mycert=None, mykey=None, client_hello=None, version=None, + resumption_master_secret=None, + session_ticket_file_in=None, + session_ticket_file_out=None, psk=None, psk_mode=None, data=None, ciphersuite=None, @@ -142,6 +149,9 @@ def parse_args(self, server="127.0.0.1", dport=4433, server_name=None, else: # Or secp256r1 otherwise self.curve = 23 + self.resumption_master_secret = resumption_master_secret + self.session_ticket_file_in = session_ticket_file_in + self.session_ticket_file_out = session_ticket_file_out self.tls13_psk_secret = psk self.tls13_psk_mode = psk_mode if curve is not None: @@ -167,6 +177,10 @@ def vprint_sessioninfo(self): self.vprint("Master secret : %s" % repr_hex(ms)) if s.server_certs: self.vprint("Server certificate chain: %r" % s.server_certs) + if s.tls_version >= 0x0304: + res_secret = s.tls13_derived_secrets["resumption_secret"] + self.vprint("Resumption master secret : %s" % + repr_hex(res_secret)) self.vprint() @ATMT.state(initial=True) @@ -188,8 +202,49 @@ def INIT_TLS_SESSION(self): self.advertised_tls_version = default_version if s.advertised_tls_version >= 0x0304: + # For out of band PSK, the PSK is given as argument + # to the automaton if self.tls13_psk_secret: s.tls13_psk_secret = binascii.unhexlify(self.tls13_psk_secret) + + # For resumed PSK, the PSK is computed from + if self.session_ticket_file_in: + with open(self.session_ticket_file_in, 'rb') as f: + + resumed_ciphersuite_len = struct.unpack("B", f.read(1))[0] + s.tls13_ticket_ciphersuite = \ + struct.unpack("!H", f.read(resumed_ciphersuite_len))[0] + + ticket_nonce_len = struct.unpack("B", f.read(1))[0] + # XXX add client_session_nonce member in tlsSession + s.client_session_nonce = f.read(ticket_nonce_len) + + client_ticket_age_len = struct.unpack("!H", f.read(2))[0] + tmp = f.read(client_ticket_age_len) + s.client_ticket_age = struct.unpack("!I", tmp)[0] + + client_ticket_age_add_len = struct.unpack("!H", f.read(2))[0] # noqa: E501 + tmp = f.read(client_ticket_age_add_len) + s.client_session_ticket_age_add = struct.unpack("!I", tmp)[0] # noqa: E501 + + ticket_len = struct.unpack("!H", f.read(2))[0] + s.client_session_ticket = f.read(ticket_len) + + if self.resumption_master_secret: + + if s.tls13_ticket_ciphersuite not in _tls_cipher_suites_cls: # noqa: E501 + warning("Unknown cipher suite %d" % s.tls13_ticket_ciphersuite) # noqa: E501 + # we do not try to set a default nor stop the execution + else: + cs_cls = _tls_cipher_suites_cls[s.tls13_ticket_ciphersuite] # noqa: E501 + + hkdf = TLS13_HKDF(cs_cls.hash_alg.name.lower()) + hash_len = hkdf.hash.digest_size + + s.tls13_psk_secret = hkdf.expand_label(binascii.unhexlify(self.resumption_master_secret), # noqa: E501 + b"resumption", + s.client_session_nonce, # noqa: E501 + hash_len) raise self.CONNECT() @ATMT.state() @@ -556,6 +611,42 @@ def should_handle_ServerData(self): elif isinstance(p, TLSAlert): print("> Received: %r" % p) raise self.CLOSE_NOTIFY() + elif isinstance(p, TLS13NewSessionTicket): + print("> Received: %r " % p) + # If arg session_ticket_file_out is set, we save + # Save the ticket for resumption... + if self.session_ticket_file_out: + # Struct of ticket file : + # * ciphersuite_len (1 byte) + # * ciphersuite (ciphersuite_len bytes) : + # we need to the store the ciphersuite for resumption + # * ticket_nonce_len (1 byte) + # * ticket_nonce (ticket_nonce_len bytes) : + # we need to store the nonce to compute the PSK + # for resumption + # * ticket_age_len (2 bytes) + # * ticket_age (ticket_age_len bytes) : + # we need to store the time we received the ticket for + # computing the obfuscated_ticket_age when resuming + # * ticket_age_add_len (2 bytes) + # * ticket_age_add (ticket_age_add_len bytes) : + # we need to store the ticket_age_add value from the + # ticket to compute the obfuscated ticket age + # * ticket_len (2 bytes) + # * ticket (ticket_len bytes) + with open(self.session_ticket_file_out, 'wb') as f: + f.write(struct.pack("B", 2)) + # we choose wcs arbitrary... + f.write(struct.pack("!H", + self.cur_session.wcs.ciphersuite.val)) + f.write(struct.pack("B", p.noncelen)) + f.write(p.ticket_nonce) + f.write(struct.pack("!H", 4)) + f.write(struct.pack("!I", int(time.time()))) + f.write(struct.pack("!H", 4)) + f.write(struct.pack("!I", p.ticket_age_add)) + f.write(struct.pack("!H", p.ticketlen)) + f.write(self.cur_session.client_session_ticket) else: print("> Received: %r" % p) self.buffer_in = self.buffer_in[1:] @@ -895,7 +986,10 @@ def tls13_should_add_ClientHello(self): ext = [] ext += TLS_Ext_SupportedVersion_CH(versions=["TLS 1.3"]) - if self.cur_session.tls13_psk_secret: + s = self.cur_session + + if s.tls13_psk_secret: + # Check if DHE is need (both for out of band and resumption PSK) if self.tls13_psk_mode == "psk_dhe_ke": ext += TLS_Ext_PSKKeyExchangeModes(kxmodes="psk_dhe_ke") ext += TLS_Ext_SupportedGroups(groups=supported_groups) @@ -904,18 +998,45 @@ def tls13_should_add_ClientHello(self): ) else: ext += TLS_Ext_PSKKeyExchangeModes(kxmodes="psk_ke") + # RFC844, section 4.2.11. # "The "pre_shared_key" extension MUST be the last extension # in the ClientHello " - hkdf = TLS13_HKDF("sha256") - hash_len = hkdf.hash.digest_size - psk_id = PSKIdentity(identity='Client_identity') - # XXX see how to not pass binder as argument - psk_binder_entry = PSKBinderEntry(binder_len=hash_len, - binder=b"\x00" * hash_len) - - ext += TLS_Ext_PreSharedKey_CH(identities=[psk_id], - binders=[psk_binder_entry]) + # Compute the pre_shared_key extension for resumption PSK + if s.client_session_ticket: + cs_cls = _tls_cipher_suites_cls[s.tls13_ticket_ciphersuite] # noqa: E501 + hkdf = TLS13_HKDF(cs_cls.hash_alg.name.lower()) + hash_len = hkdf.hash.digest_size + # We compute the client's view of the age of the ticket (ie + # the time since the receipt of the ticket) in ms + agems = int((time.time() - s.client_ticket_age) * 1000) + # Then we compute the obfuscated version of the ticket age + # by adding the "ticket_age_add" value included in the + # ticket (modulo 2^32) + obfuscated_age = ((agems + s.client_session_ticket_age_add) & + 0xffffffff) + + psk_id = PSKIdentity(identity=s.client_session_ticket, + obfuscated_ticket_age=obfuscated_age) + + psk_binder_entry = PSKBinderEntry(binder_len=hash_len, + binder=b"\x00" * hash_len) + + ext += TLS_Ext_PreSharedKey_CH(identities=[psk_id], + binders=[psk_binder_entry]) + else: + # Compute the pre_shared_key extension for out of band PSK + # (SHA256 is used as default hash function for HKDF for out + # of band PSK) + hkdf = TLS13_HKDF("sha256") + hash_len = hkdf.hash.digest_size + psk_id = PSKIdentity(identity='Client_identity') + # XXX see how to not pass binder as argument + psk_binder_entry = PSKBinderEntry(binder_len=hash_len, + binder=b"\x00" * hash_len) + + ext += TLS_Ext_PreSharedKey_CH(identities=[psk_id], + binders=[psk_binder_entry]) else: ext += TLS_Ext_SupportedGroups(groups=supported_groups) ext += TLS_Ext_KeyShare_CH( @@ -1016,14 +1137,40 @@ def tls13_should_add_ClientHello_Retry(self): else: ext += TLS_Ext_PSKKeyExchangeModes(kxmodes="psk_ke") - hkdf = TLS13_HKDF("sha256") - hash_len = hkdf.hash.digest_size - psk_id = PSKIdentity(identity='Client_identity') - psk_binder_entry = PSKBinderEntry(binder_len=hash_len, - binder=b"\x00" * hash_len) + if s.client_session_ticket: + + # XXX Retrieve parameters from first ClientHello... + cs_cls = _tls_cipher_suites_cls[s.tls13_ticket_ciphersuite] + hkdf = TLS13_HKDF(cs_cls.hash_alg.name.lower()) + hash_len = hkdf.hash.digest_size - ext += TLS_Ext_PreSharedKey_CH(identities=[psk_id], - binders=[psk_binder_entry]) + # We compute the client's view of the age of the ticket (ie + # the time since the receipt of the ticket) in ms + agems = int((time.time() - s.client_ticket_age) * 1000) + + # Then we compute the obfuscated version of the ticket age by + # adding the "ticket_age_add" value included in the ticket + # (modulo 2^32) + obfuscated_age = ((agems + s.client_session_ticket_age_add) & + 0xffffffff) + + psk_id = PSKIdentity(identity=s.client_session_ticket, + obfuscated_ticket_age=obfuscated_age) + + psk_binder_entry = PSKBinderEntry(binder_len=hash_len, + binder=b"\x00" * hash_len) + + ext += TLS_Ext_PreSharedKey_CH(identities=[psk_id], + binders=[psk_binder_entry]) + else: + hkdf = TLS13_HKDF("sha256") + hash_len = hkdf.hash.digest_size + psk_id = PSKIdentity(identity='Client_identity') + psk_binder_entry = PSKBinderEntry(binder_len=hash_len, + binder=b"\x00" * hash_len) + + ext += TLS_Ext_PreSharedKey_CH(identities=[psk_id], + binders=[psk_binder_entry]) else: ext += TLS_Ext_SupportedGroups(groups=[_tls_named_groups[selected_group]]) # noqa: E501 diff --git a/scapy/layers/tls/automaton_srv.py b/scapy/layers/tls/automaton_srv.py index c5a3f4fb07a..ec7b9bd0453 100644 --- a/scapy/layers/tls/automaton_srv.py +++ b/scapy/layers/tls/automaton_srv.py @@ -19,11 +19,15 @@ from __future__ import print_function import socket import binascii +import struct +import time +from scapy.config import conf from scapy.packet import Raw from scapy.pton_ntop import inet_pton from scapy.utils import randstring, repr_hex from scapy.automaton import ATMT +from scapy.error import warning from scapy.layers.tls.automaton import _TLSAutomaton from scapy.layers.tls.cert import PrivKeyRSA, PrivKeyECDSA from scapy.layers.tls.basefields import _tls_version @@ -31,7 +35,8 @@ from scapy.layers.tls.crypto.groups import _tls_named_groups from scapy.layers.tls.extensions import TLS_Ext_SupportedVersion_SH, \ TLS_Ext_SupportedGroups, TLS_Ext_Cookie, \ - TLS_Ext_SignatureAlgorithms, TLS_Ext_PSKKeyExchangeModes + TLS_Ext_SignatureAlgorithms, TLS_Ext_PSKKeyExchangeModes, \ + TLS_Ext_EarlyDataIndicationTicket from scapy.layers.tls.keyexchange_tls13 import TLS_Ext_KeyShare_SH, \ KeyShareEntry, TLS_Ext_KeyShare_HRR, TLS_Ext_PreSharedKey_CH, \ TLS_Ext_PreSharedKey_SH @@ -40,16 +45,22 @@ TLSServerHello, TLSServerHelloDone, TLSServerKeyExchange, \ _ASN1CertAndExt, TLS13ServerHello, TLS13Certificate, TLS13ClientHello, \ TLSEncryptedExtensions, TLS13HelloRetryRequest, TLS13CertificateRequest, \ - TLS13KeyUpdate + TLS13KeyUpdate, TLS13NewSessionTicket from scapy.layers.tls.handshake_sslv2 import SSLv2ClientCertificate, \ SSLv2ClientFinished, SSLv2ClientHello, SSLv2ClientMasterKey, \ SSLv2RequestCertificate, SSLv2ServerFinished, SSLv2ServerHello, \ SSLv2ServerVerify from scapy.layers.tls.record import TLSAlert, TLSChangeCipherSpec, \ TLSApplicationData +from scapy.layers.tls.record_tls13 import TLS13 +from scapy.layers.tls.crypto.hkdf import TLS13_HKDF from scapy.layers.tls.crypto.suites import _tls_cipher_suites_cls, \ get_usable_ciphersuites +if conf.crypto_valid: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes + class TLSServerAutomaton(_TLSAutomaton): """ @@ -81,6 +92,7 @@ def parse_args(self, server="127.0.0.1", sport=4433, client_auth=False, is_echo_server=True, max_client_idle_time=60, + session_ticket_file=None, curve=None, cookie=False, psk=None, @@ -114,6 +126,8 @@ def parse_args(self, server="127.0.0.1", sport=4433, self.cookie = cookie self.psk_secret = psk self.psk_mode = psk_mode + self.handle_session_ticket = (session_ticket_file is not None) + self.session_ticket_file = session_ticket_file for (group_id, ng) in _tls_named_groups.items(): if ng == curve: self.curve = group_id @@ -573,6 +587,96 @@ def tls13_should_add_ServerHello_from_HRR(self): def tls13_PREPARE_SERVERFLIGHT1(self): self.add_record(is_tls13=False) + def verify_psk_binder(self, psk_identity, obfuscated_age, binder): + """ + This function verifies the binder received in the 'pre_shared_key' + extension and return the resumption PSK associated with those + values. + + The arguments psk_identity, obfuscated_age and binder are taken + from 'pre_shared_key' in the ClientHello. + """ + with open(self.session_ticket_file, "rb") as f: + for line in f: + s = line.strip().split(b';') + if len(s) < 8: + continue + ticket_label = binascii.unhexlify(s[0]) + ticket_nonce = binascii.unhexlify(s[1]) + tmp = binascii.unhexlify(s[2]) + ticket_lifetime = struct.unpack("!I", tmp)[0] + tmp = binascii.unhexlify(s[3]) + ticket_age_add = struct.unpack("!I", tmp)[0] + tmp = binascii.unhexlify(s[4]) + ticket_start_time = struct.unpack("!I", tmp)[0] + resumption_secret = binascii.unhexlify(s[5]) + tmp = binascii.unhexlify(s[6]) + res_ciphersuite = struct.unpack("!H", tmp)[0] + tmp = binascii.unhexlify(s[7]) + max_early_data_size = struct.unpack("!I", tmp)[0] + + # Here psk_identity is a Ticket type but ticket_label is bytes, + # we need to convert psk_identiy to bytes in order to compare + # both strings + if psk_identity.__bytes__() == ticket_label: + + # We compute the resumed PSK associated the resumption + # secret + self.vprint("Ticket found in database !") + if res_ciphersuite not in _tls_cipher_suites_cls: + warning("Unknown cipher suite %d" % res_ciphersuite) + # we do not try to set a default nor stop the execution + else: + cs_cls = _tls_cipher_suites_cls[res_ciphersuite] + + hkdf = TLS13_HKDF(cs_cls.hash_alg.name.lower()) + hash_len = hkdf.hash.digest_size + + tls13_psk_secret = hkdf.expand_label(resumption_secret, + b"resumption", + ticket_nonce, + hash_len) + # We verify that ticket age is not expired + agesec = int((time.time() - ticket_start_time)) + # agems = agesec * 1000 + ticket_age = (obfuscated_age - ticket_age_add) % 0xffffffff # noqa: F841, E501 + + # We verify the PSK binder + s = self.cur_session + if s.tls13_retry: + handshake_context = struct.pack("B", 254) + handshake_context += struct.pack("B", 0) + handshake_context += struct.pack("B", 0) + handshake_context += struct.pack("B", hash_len) + digest = hashes.Hash(hkdf.hash, backend=default_backend()) # noqa: E501 + digest.update(s.handshake_messages[0]) + handshake_context += digest.finalize() + for m in s.handshake_messages[1:]: + if (isinstance(TLS13ClientHello) or + isinstance(TLSClientHello)): + handshake_context += m[:-hash_len - 3] + else: + handshake_context += m + else: + handshake_context = s.handshake_messages[0][:-hash_len - 3] # noqa: E501 + + # We compute the binder key + # XXX use the compute_tls13_early_secrets() function + tls13_early_secret = hkdf.extract(None, tls13_psk_secret) + binder_key = hkdf.derive_secret(tls13_early_secret, + b"res binder", + b"") + computed_binder = hkdf.compute_verify_data(binder_key, + handshake_context) # noqa: E501 + if (agesec < ticket_lifetime and + computed_binder == binder): + self.vprint("Ticket has been accepted ! ") + self.max_early_data_size = max_early_data_size + self.resumed_ciphersuite = res_ciphersuite + return tls13_psk_secret + self.vprint("Ticket has not been accepted ! Fallback to a complete handshake") # noqa: E501 + return None + @ATMT.condition(tls13_PREPARE_SERVERFLIGHT1) def tls13_should_add_ServerHello(self): @@ -586,7 +690,7 @@ def tls13_should_add_ServerHello(self): if isinstance(e, TLS_Ext_PreSharedKey_CH): psk_identity = e.identities[0].identity obfuscated_age = e.identities[0].obfuscated_ticket_age - # binder = e.binders[0].binder + binder = e.binders[0].binder # For out-of-bound PSK, obfuscated_ticket_age should be # 0. We use this field to distinguish between out-of- @@ -616,6 +720,24 @@ def tls13_should_add_ServerHello(self): server_kse = KeyShareEntry(group=group) ext += TLS_Ext_KeyShare_SH(server_share=server_kse) ext += TLS_Ext_PreSharedKey_SH(selected_identity=0) + else: + resumption_psk = self.verify_psk_binder(psk_identity, + obfuscated_age, + binder) + if resumption_psk is None: + # We did not find a ticket matching the one provided in the + # ClientHello. We fallback to a regular 1-RTT handshake + server_kse = KeyShareEntry(group=group) + ext += [TLS_Ext_KeyShare_SH(server_share=server_kse)] + else: + # 0: "psk_ke" + # 1: "psk_dhe_ke" + if psk_key_exchange_mode == 1: + server_kse = KeyShareEntry(group=group) + ext += [TLS_Ext_KeyShare_SH(server_share=server_kse)] + + ext += [TLS_Ext_PreSharedKey_SH(selected_identity=0)] + self.cur_session.tls13_psk_secret = resumption_psk else: # Standard Handshake ext += TLS_Ext_KeyShare_SH(server_share=KeyShareEntry(group=group)) @@ -796,6 +918,45 @@ def WAITING_CLIENTDATA(self): def RECEIVED_CLIENTDATA(self): pass + def save_ticket(self, ticket): + """ + This function save a ticket and others parameters in the + file given as argument to the automaton + Warning : The file is not protected and contains sensitive + information. It should be used only for testing purpose. + """ + if (not isinstance(ticket, TLS13NewSessionTicket) or + self.session_ticket_file is None): + return + + s = self.cur_session + with open(self.session_ticket_file, "ab") as f: + # ticket;ticket_nonce;obfuscated_age;start_time;resumption_secret + line = binascii.hexlify(ticket.ticket) + line += b";" + line += binascii.hexlify(ticket.ticket_nonce) + line += b";" + line += binascii.hexlify(struct.pack("!I", ticket.ticket_lifetime)) + line += b";" + line += binascii.hexlify(struct.pack("!I", ticket.ticket_age_add)) + line += b";" + line += binascii.hexlify(struct.pack("!I", int(time.time()))) + line += b";" + line += binascii.hexlify(s.tls13_derived_secrets["resumption_secret"]) # noqa: E501 + line += b";" + line += binascii.hexlify(struct.pack("!H", s.wcs.ciphersuite.val)) + line += b";" + if (ticket.ext is None or ticket.extlen is None or + ticket.extlen == 0): + line += binascii.hexlify(struct.pack("!I", 0)) + else: + for e in ticket.ext: + if isinstance(e, TLS_Ext_EarlyDataIndicationTicket): + max_size = struct.pack("!I", e.max_early_data_size) + line += binascii.hexlify(max_size) + line += b"\n" + f.write(line) + @ATMT.condition(RECEIVED_CLIENTDATA) def should_handle_ClientData(self): if not self.buffer_in: @@ -830,6 +991,10 @@ def should_handle_ClientData(self): if self.is_echo_server or recv_data.startswith(b"GET / HTTP/1.1"): self.add_record() self.add_msg(p) + if self.handle_session_ticket: + self.add_record() + ticket = TLS13NewSessionTicket(ext=[]) + self.add_msg(ticket) raise self.ADDED_SERVERDATA() raise self.HANDLED_CLIENTDATA() @@ -844,7 +1009,25 @@ def ADDED_SERVERDATA(self): @ATMT.condition(ADDED_SERVERDATA) def should_send_ServerData(self): + if self.session_ticket_file: + save_ticket = False + for p in self.buffer_out: + if isinstance(p, TLS13): + # Check if there's a NewSessionTicket to send + save_ticket = all(map(lambda x: isinstance(x, TLS13NewSessionTicket), # noqa: E501 + p.inner.msg)) + if save_ticket: + break self.flush_records() + if self.session_ticket_file and save_ticket: + # Loop backward in message send to retrieve the parsed + # NewSessionTicket. This message is not completely build before the + # flush_records() call. Other way to build this message before ? + for p in reversed(self.cur_session.handshake_messages_parsed): + if isinstance(p, TLS13NewSessionTicket): + p.show() + self.save_ticket(p) + break raise self.SENT_SERVERDATA() @ATMT.state() diff --git a/scapy/layers/tls/extensions.py b/scapy/layers/tls/extensions.py index c252f9acc33..ad5722b6e37 100644 --- a/scapy/layers/tls/extensions.py +++ b/scapy/layers/tls/extensions.py @@ -760,7 +760,12 @@ def addfield(self, pkt, s, i): i = self.adjust(pkt, f) if i == 0: # for correct build if no ext and not explicitly 0 - return s + v = pkt.tls_session.tls_version + # Xith TLS 1.3, zero lengths are always explicit. + if v is None or v < 0x0304: + return s + else: + return s + struct.pack(self.fmt, i) return s + struct.pack(self.fmt, i) diff --git a/scapy/layers/tls/handshake.py b/scapy/layers/tls/handshake.py index 4d32bfdfed5..294b7832368 100644 --- a/scapy/layers/tls/handshake.py +++ b/scapy/layers/tls/handshake.py @@ -370,9 +370,26 @@ def post_build(self, p, pay): if self.ext: for e in self.ext: if isinstance(e, TLS_Ext_PreSharedKey_CH): - hkdf = TLS13_HKDF("sha256") - hash_len = hkdf.hash.digest_size - s.compute_tls13_early_secrets(external=True) + if s.client_session_ticket: + # For a resumed PSK, the hash function use + # to compute the binder must be the same + # as the one used to establish the original + # conntection. For that, we assume that + # the ciphersuite associate with the ticket + # is given as argument to tlsSession + # (see layers/tls/automaton_cli.py for an + # example) + res_suite = s.tls13_ticket_ciphersuite + cs_cls = _tls_cipher_suites_cls[res_suite] + hkdf = TLS13_HKDF(cs_cls.hash_alg.name.lower()) + hash_len = hkdf.hash.digest_size + s.compute_tls13_early_secrets(external=False) + else: + # For out of band PSK, SHA-256 is used as default + # hash functions for HKDF + hkdf = TLS13_HKDF("sha256") + hash_len = hkdf.hash.digest_size + s.compute_tls13_early_secrets(external=True) # RFC8446 4.2.11.2 # "Each entry in the binders list is computed as an HMAC diff --git a/test/tls/example_client.py b/test/tls/example_client.py index 129a140064d..046de55f7ee 100755 --- a/test/tls/example_client.py +++ b/test/tls/example_client.py @@ -28,6 +28,12 @@ help="Disable (EC)DHE exchange with PFS") parser.add_argument("--ciphersuite", help="Ciphersuite preference") parser.add_argument("--version", help="TLS Version", default="tls13") +parser.add_argument("--ticket_in", dest='session_ticket_file_in', + help="File to read a ticket from (for TLS 1.3)") +parser.add_argument("--ticket_out", dest='session_ticket_file_out', + help="File to write a ticket to (for TLS 1.3)") +parser.add_argument("--res_master", + help="Resumption master secret (for TLS 1.3)") args = parser.parse_args() @@ -58,6 +64,9 @@ mykey=basedir+"/test/tls/pki/cli_key.pem", psk=args.psk, psk_mode=psk_mode, + resumption_master_secret=args.res_master, + session_ticket_file_in=args.session_ticket_file_in, + session_ticket_file_out=args.session_ticket_file_out, ) t.run() diff --git a/test/tls/example_server.py b/test/tls/example_server.py index 60806081ba2..bb60387d728 100755 --- a/test/tls/example_server.py +++ b/test/tls/example_server.py @@ -31,6 +31,8 @@ help="Send cookie extension in HelloRetryRequest message") parser.add_argument("--client_auth", action="store_true", help="Require client authentication") +parser.add_argument("--ticket_file", dest='session_ticket_file', + help="File to write/read a ticket to (for TLS 1.3)") args = parser.parse_args() pcs = None @@ -45,6 +47,7 @@ client_auth=args.client_auth, curve=args.curve, cookie=args.cookie, + session_ticket_file=args.session_ticket_file, psk=args.psk, psk_mode=psk_mode) t.run() From e6201498dba1cb1d327a02108f80c219e423438b Mon Sep 17 00:00:00 2001 From: gpotter Date: Thu, 30 Apr 2020 15:35:14 +0200 Subject: [PATCH 2/3] Test session resumption --- scapy/layers/tls/automaton_cli.py | 3 ++ scapy/layers/tls/automaton_srv.py | 10 ++-- test/tls/example_server.py | 3 ++ test/tls/tests_tls_netaccess.uts | 80 +++++++++++++++++++++---------- 4 files changed, 69 insertions(+), 27 deletions(-) diff --git a/scapy/layers/tls/automaton_cli.py b/scapy/layers/tls/automaton_cli.py index 07c240a4702..91ce7840fe7 100644 --- a/scapy/layers/tls/automaton_cli.py +++ b/scapy/layers/tls/automaton_cli.py @@ -562,6 +562,9 @@ def add_ClientData(self): data = self.data_to_send.pop() if data == b"quit": return + # Command to skip sending + elif data == b"wait": + raise self.WAITING_SERVERDATA() # Command to perform a key_update (for a TLS 1.3 session) elif data == b"key_update": if self.cur_session.tls_version >= 0x0304: diff --git a/scapy/layers/tls/automaton_srv.py b/scapy/layers/tls/automaton_srv.py index ec7b9bd0453..6b0d9463f9c 100644 --- a/scapy/layers/tls/automaton_srv.py +++ b/scapy/layers/tls/automaton_srv.py @@ -25,7 +25,7 @@ from scapy.config import conf from scapy.packet import Raw from scapy.pton_ntop import inet_pton -from scapy.utils import randstring, repr_hex +from scapy.utils import get_temp_file, randstring, repr_hex from scapy.automaton import ATMT from scapy.error import warning from scapy.layers.tls.automaton import _TLSAutomaton @@ -92,6 +92,7 @@ def parse_args(self, server="127.0.0.1", sport=4433, client_auth=False, is_echo_server=True, max_client_idle_time=60, + handle_session_ticket=None, session_ticket_file=None, curve=None, cookie=False, @@ -126,7 +127,11 @@ def parse_args(self, server="127.0.0.1", sport=4433, self.cookie = cookie self.psk_secret = psk self.psk_mode = psk_mode - self.handle_session_ticket = (session_ticket_file is not None) + if handle_session_ticket is None: + handle_session_ticket = (session_ticket_file is not None) + if handle_session_ticket: + session_ticket_file = session_ticket_file or get_temp_file() + self.handle_session_ticket = handle_session_ticket self.session_ticket_file = session_ticket_file for (group_id, ng) in _tls_named_groups.items(): if ng == curve: @@ -1025,7 +1030,6 @@ def should_send_ServerData(self): # flush_records() call. Other way to build this message before ? for p in reversed(self.cur_session.handshake_messages_parsed): if isinstance(p, TLS13NewSessionTicket): - p.show() self.save_ticket(p) break raise self.SENT_SERVERDATA() diff --git a/test/tls/example_server.py b/test/tls/example_server.py index bb60387d728..c73d1104785 100755 --- a/test/tls/example_server.py +++ b/test/tls/example_server.py @@ -31,6 +31,8 @@ help="Send cookie extension in HelloRetryRequest message") parser.add_argument("--client_auth", action="store_true", help="Require client authentication") +parser.add_argument("--handle_session_ticket", action="store_true", + help="Use session tickets. Auto enabled if file provided (for TLS 1.3)") # noqa: E501 parser.add_argument("--ticket_file", dest='session_ticket_file', help="File to write/read a ticket to (for TLS 1.3)") args = parser.parse_args() @@ -47,6 +49,7 @@ client_auth=args.client_auth, curve=args.curve, cookie=args.cookie, + handle_session_ticket=args.handle_session_ticket, session_ticket_file=args.session_ticket_file, psk=args.psk, psk_mode=psk_mode) diff --git a/test/tls/tests_tls_netaccess.uts b/test/tls/tests_tls_netaccess.uts index eea014ed5b6..d37391eb17d 100644 --- a/test/tls/tests_tls_netaccess.uts +++ b/test/tls/tests_tls_netaccess.uts @@ -9,8 +9,6 @@ + TLS server automaton tests ~ server -### DISCLAIMER: Those tests are slow ### - = Load server util functions from __future__ import print_function @@ -65,10 +63,11 @@ def check_output_for_data(out, err, expected_data): return (False, None) def get_file(filename): - return os.getenv("SCAPY_ROOT_DIR")+filename if not os.path.exists(filename) else filename + return os.path.abspath(os.getenv("SCAPY_ROOT_DIR")+filename if not os.path.exists(filename) else filename) -def run_tls_test_server(expected_data, q, curve=None, cookie=False, client_auth=False, psk=None): +def run_tls_test_server(expected_data, q, curve=None, cookie=False, client_auth=False, + psk=None, handle_session_ticket=False): correct = False print("Server started !") with captured_output() as (out, err): @@ -89,6 +88,7 @@ def run_tls_test_server(expected_data, q, curve=None, cookie=False, client_auth= curve=curve, cookie=cookie, client_auth=client_auth, + handle_session_ticket=handle_session_ticket, debug=5, **kwargs) # Sync threads @@ -100,17 +100,8 @@ def run_tls_test_server(expected_data, q, curve=None, cookie=False, client_auth= # Return data q.put(res) -def test_tls_server(suite="", version="", tls13=False, client_auth=False, psk=None): - msg = ("TestS_%s_data" % suite).encode() - # Run server - q_ = Queue() - th_ = threading.Thread(target=run_tls_test_server, args=(msg, q_), - kwargs={"curve": None, "cookie": False, "client_auth": client_auth, "psk": psk}) - th_.setDaemon(True) - th_.start() - # Synchronise threads - q_.get() - time.sleep(1) +def run_openssl_client(msg, suite="", version="", tls13=False, client_auth=False, + psk=None, sess_out=None): # Run client CA_f = get_file("/test/tls/pki/ca_cert.pem") mycert = get_file("/test/tls/pki/cli_cert.pem") @@ -126,6 +117,8 @@ def test_tls_server(suite="", version="", tls13=False, client_auth=False, psk=No args.extend(["-cert", mycert, "-key", mykey]) if psk: args.extend(["-psk", str(psk)]) + if sess_out: + args.extend(["-sess_out", sess_out]) p = subprocess.Popen( args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT @@ -148,6 +141,20 @@ def test_tls_server(suite="", version="", tls13=False, client_auth=False, psk=No break if _failed or not _one_success: raise RuntimeError("OpenSSL returned unexpected values") + +def test_tls_server(suite="", version="", tls13=False, client_auth=False, psk=None): + msg = ("TestS_%s_data" % suite).encode() + # Run server + q_ = Queue() + th_ = threading.Thread(target=run_tls_test_server, args=(msg, q_), + kwargs={"curve": None, "cookie": False, "client_auth": client_auth, "psk": psk}) + th_.setDaemon(True) + th_.start() + # Synchronise threads + q_.get() + time.sleep(1) + # Run openssl client + run_openssl_client(msg, suite=suite, version=version, tls13=tls13, client_auth=client_auth, psk=psk) # Wait for server th_.join(5) if th_.is_alive(): @@ -210,32 +217,45 @@ from scapy.modules.six.moves.queue import Queue send_data = cipher_suite_code = version = None def run_tls_test_client(send_data=None, cipher_suite_code=None, version=None, - client_auth=False, key_update=False): + client_auth=False, key_update=False, stop_server=True, + session_ticket_file_out=None, session_ticket_file_in=None): print("Loading client...") mycert = get_file("/test/tls/pki/cli_cert.pem") if client_auth else None mykey = get_file("/test/tls/pki/cli_key.pem") if client_auth else None commands = [send_data] if key_update: - commands += ["key_update"] - commands.extend([b"stop_server", b"quit"]) + commands.append(b"key_update") + if stop_server: + commands.append(b"stop_server") + if session_ticket_file_out: + commands.append(b"wait") + commands.append(b"quit") if version == "0002": - t = TLSClientAutomaton(data=commands, version="sslv2", debug=5, mycert=mycert, mykey=mykey) + t = TLSClientAutomaton(data=commands, version="sslv2", debug=5, mycert=mycert, mykey=mykey, + session_ticket_file_in=session_ticket_file_in, + session_ticket_file_out=session_ticket_file_out) elif version == "0304": ch = TLS13ClientHello(ciphers=int(cipher_suite_code, 16)) - t = TLSClientAutomaton(client_hello=ch, data=commands, version="tls13", debug=5, mycert=mycert, mykey=mykey) + t = TLSClientAutomaton(client_hello=ch, data=commands, version="tls13", debug=5, mycert=mycert, mykey=mykey, + session_ticket_file_in=session_ticket_file_in, + session_ticket_file_out=session_ticket_file_out) else: ch = TLSClientHello(version=int(version, 16), ciphers=int(cipher_suite_code, 16)) - t = TLSClientAutomaton(client_hello=ch, data=commands, debug=5, mycert=mycert, mykey=mykey) + t = TLSClientAutomaton(client_hello=ch, data=commands, debug=5, mycert=mycert, mykey=mykey, + session_ticket_file_in=session_ticket_file_in, + session_ticket_file_out=session_ticket_file_out) print("Running client...") t.run() -def test_tls_client(suite, version, curve=None, cookie=False, client_auth=False, key_update=False): +def test_tls_client(suite, version, curve=None, cookie=False, client_auth=False, + key_update=False, sess_in_out=False): msg = ("TestC_%s_data" % suite).encode() # Run server q_ = Queue() print("Starting server...") th_ = threading.Thread(target=run_tls_test_server, args=(msg, q_), - kwargs={"curve": None, "cookie": False, "client_auth": client_auth}) + kwargs={"curve": None, "cookie": False, "client_auth": client_auth, + "handle_session_ticket": sess_in_out}) th_.setDaemon(True) th_.start() # Synchronise threads @@ -244,7 +264,14 @@ def test_tls_client(suite, version, curve=None, cookie=False, client_auth=False, time.sleep(1) print("Thread synchronised") # Run client - run_tls_test_client(msg, suite, version, client_auth, key_update) + if sess_in_out: + file_sess = get_file("/test/session") + run_tls_test_client(msg, suite, version, client_auth, key_update, session_ticket_file_out=file_sess, + stop_server=False) + run_tls_test_client(msg, suite, version, client_auth, key_update, session_ticket_file_in=file_sess, + stop_server=True) + else: + run_tls_test_client(msg, suite, version, client_auth, key_update) # Wait for server print("Client running, waiting...") th_.join(5) @@ -309,3 +336,8 @@ test_tls_client("1305", "0304", client_auth=True) ~ crypto_advanced test_tls_client("1305", "0304", key_update=True) + += Testing TLS server and client with TLS 1.3 and TLS_AES_128_CCM_8_SHA256 and session resumption +~ crypto_advanced + +test_tls_client("1305", "0304", client_auth=True, sess_in_out=True) From f77011a12bd5e5ece8a9dc9511c50eb82bc3709d Mon Sep 17 00:00:00 2001 From: gpotter Date: Thu, 7 May 2020 00:09:26 +0200 Subject: [PATCH 3/3] Minor formatting fixes --- scapy/layers/tls/automaton_cli.py | 12 +++++++----- scapy/layers/tls/automaton_srv.py | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/scapy/layers/tls/automaton_cli.py b/scapy/layers/tls/automaton_cli.py index 91ce7840fe7..9078160dc77 100644 --- a/scapy/layers/tls/automaton_cli.py +++ b/scapy/layers/tls/automaton_cli.py @@ -202,7 +202,7 @@ def INIT_TLS_SESSION(self): self.advertised_tls_version = default_version if s.advertised_tls_version >= 0x0304: - # For out of band PSK, the PSK is given as argument + # For out of band PSK, the PSK is given as an argument # to the automaton if self.tls13_psk_secret: s.tls13_psk_secret = binascii.unhexlify(self.tls13_psk_secret) @@ -223,9 +223,11 @@ def INIT_TLS_SESSION(self): tmp = f.read(client_ticket_age_len) s.client_ticket_age = struct.unpack("!I", tmp)[0] - client_ticket_age_add_len = struct.unpack("!H", f.read(2))[0] # noqa: E501 + client_ticket_age_add_len = struct.unpack( + "!H", f.read(2))[0] tmp = f.read(client_ticket_age_add_len) - s.client_session_ticket_age_add = struct.unpack("!I", tmp)[0] # noqa: E501 + s.client_session_ticket_age_add = struct.unpack( + "!I", tmp)[0] ticket_len = struct.unpack("!H", f.read(2))[0] s.client_session_ticket = f.read(ticket_len) @@ -617,7 +619,7 @@ def should_handle_ServerData(self): elif isinstance(p, TLS13NewSessionTicket): print("> Received: %r " % p) # If arg session_ticket_file_out is set, we save - # Save the ticket for resumption... + # the ticket for resumption... if self.session_ticket_file_out: # Struct of ticket file : # * ciphersuite_len (1 byte) @@ -639,7 +641,7 @@ def should_handle_ServerData(self): # * ticket (ticket_len bytes) with open(self.session_ticket_file_out, 'wb') as f: f.write(struct.pack("B", 2)) - # we choose wcs arbitrary... + # we choose wcs arbitrarily... f.write(struct.pack("!H", self.cur_session.wcs.ciphersuite.val)) f.write(struct.pack("B", p.noncelen)) diff --git a/scapy/layers/tls/automaton_srv.py b/scapy/layers/tls/automaton_srv.py index 6b0d9463f9c..bd8ebbe9c03 100644 --- a/scapy/layers/tls/automaton_srv.py +++ b/scapy/layers/tls/automaton_srv.py @@ -128,7 +128,7 @@ def parse_args(self, server="127.0.0.1", sport=4433, self.psk_secret = psk self.psk_mode = psk_mode if handle_session_ticket is None: - handle_session_ticket = (session_ticket_file is not None) + handle_session_ticket = session_ticket_file is not None if handle_session_ticket: session_ticket_file = session_ticket_file or get_temp_file() self.handle_session_ticket = handle_session_ticket