From 551ab1ab327ad4dd596fe51807dc136090ee00a4 Mon Sep 17 00:00:00 2001 From: rperez Date: Tue, 3 Sep 2019 10:31:57 +0200 Subject: [PATCH 1/2] add KeyUpdate --- scapy/layers/tls/automaton_cli.py | 9 ++++++++- scapy/layers/tls/automaton_srv.py | 16 ++++++++++++++-- scapy/layers/tls/handshake.py | 19 +++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/scapy/layers/tls/automaton_cli.py b/scapy/layers/tls/automaton_cli.py index bf59c36f831..52966c29f4c 100644 --- a/scapy/layers/tls/automaton_cli.py +++ b/scapy/layers/tls/automaton_cli.py @@ -35,7 +35,7 @@ TLSEncryptedExtensions, TLSFinished, TLSServerHello, TLSServerHelloDone, \ TLSServerKeyExchange, TLS13Certificate, TLS13ClientHello, \ TLS13ServerHello, TLS13HelloRetryRequest, TLS13CertificateRequest, \ - _ASN1CertAndExt + _ASN1CertAndExt, TLS13KeyUpdate from scapy.layers.tls.handshake_sslv2 import SSLv2ClientHello, \ SSLv2ServerHello, SSLv2ClientMasterKey, SSLv2ServerVerify, \ SSLv2ClientFinished, SSLv2ServerFinished, SSLv2ClientCertificate, \ @@ -496,6 +496,13 @@ def add_ClientData(self): data = self.data_to_send.pop() if data == b"quit": return + # Command to perform a key_update (for a TLS 1.3 session) + elif data == b"key_update": + if self.cur_session.tls_version >= 0x0304: + self.add_record() + self.add_msg(TLS13KeyUpdate(request_update="update_requested")) + raise self.ADDED_CLIENTDATA() + if self.linebreak: data += b"\n" self.add_record() diff --git a/scapy/layers/tls/automaton_srv.py b/scapy/layers/tls/automaton_srv.py index 625d6dd0e08..1129fbeffbd 100644 --- a/scapy/layers/tls/automaton_srv.py +++ b/scapy/layers/tls/automaton_srv.py @@ -37,7 +37,8 @@ TLSCertificateVerify, TLSClientHello, TLSClientKeyExchange, TLSFinished, \ TLSServerHello, TLSServerHelloDone, TLSServerKeyExchange, \ _ASN1CertAndExt, TLS13ServerHello, TLS13Certificate, TLS13ClientHello, \ - TLSEncryptedExtensions, TLS13HelloRetryRequest, TLS13CertificateRequest + TLSEncryptedExtensions, TLS13HelloRetryRequest, TLS13CertificateRequest, \ + TLS13KeyUpdate from scapy.layers.tls.handshake_sslv2 import SSLv2ClientCertificate, \ SSLv2ClientFinished, SSLv2ClientHello, SSLv2ClientMasterKey, \ SSLv2RequestCertificate, SSLv2ServerFinished, SSLv2ServerHello, \ @@ -145,11 +146,16 @@ def http_sessioninfo(self): s += "Version : %s\n" % v cs = self.cur_session.wcs.ciphersuite.name s += "Cipher suite : %s\n" % cs - ms = self.cur_session.master_secret + if self.cur_session.tls_version < 0x0304: + ms = self.cur_session.master_secret + else: + ms = self.cur_session.tls13_master_secret + s += "Master secret : %s\n" % repr_hex(ms) body = "
%s
\r\n\r\n" % s answer = (header + body) % len(body) return answer + return answer @ATMT.state(initial=True) def INITIAL(self): @@ -768,6 +774,12 @@ def should_handle_ClientData(self): elif isinstance(p, TLSAlert): print("> Received: %r" % p) raise self.CLOSE_NOTIFY() + elif isinstance(p, TLS13KeyUpdate): + print("> Received: %r" % p) + p = TLS13KeyUpdate(request_update=0) + self.add_record() + self.add_msg(p) + raise self.ADDED_SERVERDATA() else: print("> Received: %r" % p) diff --git a/scapy/layers/tls/handshake.py b/scapy/layers/tls/handshake.py index 86bb6614527..9e7cdf8afd1 100644 --- a/scapy/layers/tls/handshake.py +++ b/scapy/layers/tls/handshake.py @@ -1511,6 +1511,25 @@ class TLS13KeyUpdate(_TLSHandshake): ThreeBytesField("msglen", None), ByteEnumField("request_update", 0, _key_update_request)] + def post_build_tls_session_update(self, msg_str): + s = self.tls_session + s.pwcs = writeConnState(ciphersuite=type(s.wcs.ciphersuite), + connection_end=s.connection_end, + tls_version=s.tls_version) + s.triggered_pwcs_commit = True + s.compute_tls13_next_traffic_secrets(s.connection_end, "write") + + def post_dissection_tls_session_update(self, msg_str): + s = self.tls_session + s.prcs = writeConnState(ciphersuite=type(s.rcs.ciphersuite), + connection_end=s.connection_end, + tls_version=s.tls_version) + s.triggered_prcs_commit = True + if s.connection_end == "server": + s.compute_tls13_next_traffic_secrets("client", "read") + elif s.connection_end == "client": + s.compute_tls13_next_traffic_secrets("server", "read") + ############################################################################### # All handshake messages defined in this module # From 7839ca942ddb034b11ac42565c1143c27cda9cfc Mon Sep 17 00:00:00 2001 From: rperez Date: Wed, 4 Sep 2019 13:44:39 +0200 Subject: [PATCH 2/2] Add external PSK + middlebox compatibility --- scapy/layers/tls/automaton.py | 9 ++- scapy/layers/tls/automaton_cli.py | 123 +++++++++++++++++++++++------- scapy/layers/tls/automaton_srv.py | 107 ++++++++++++++++++-------- scapy/layers/tls/handshake.py | 116 ++++++++++++++++++++++++---- scapy/layers/tls/record.py | 7 +- scapy/layers/tls/record_tls13.py | 14 ---- scapy/layers/tls/session.py | 4 + test/tls/example_client.py | 44 ++++++++--- test/tls/example_server.py | 13 +++- test/tls/tests_tls_netaccess.uts | 70 ++++++++++++----- 10 files changed, 382 insertions(+), 125 deletions(-) diff --git a/scapy/layers/tls/automaton.py b/scapy/layers/tls/automaton.py index 3b951fe42ee..6c4748ae410 100644 --- a/scapy/layers/tls/automaton.py +++ b/scapy/layers/tls/automaton.py @@ -200,11 +200,11 @@ def raise_on_packet(self, pkt_cls, state, get_next_msg=True): self.buffer_in = self.buffer_in[1:] raise state() - def add_record(self, is_sslv2=None, is_tls13=None): + def add_record(self, is_sslv2=None, is_tls13=None, is_tls12=None): """ Add a new TLS or SSLv2 or TLS 1.3 record to the packets buffered out. """ - if is_sslv2 is None and is_tls13 is None: + if is_sslv2 is None and is_tls13 is None and is_tls12 is None: v = (self.cur_session.tls_version or self.cur_session.advertised_tls_version) if v in [0x0200, 0x0002]: @@ -215,6 +215,11 @@ def add_record(self, is_sslv2=None, is_tls13=None): self.buffer_out.append(SSLv2(tls_session=self.cur_session)) elif is_tls13: self.buffer_out.append(TLS13(tls_session=self.cur_session)) + # For TLS 1.3 middlebox compatibility, TLS record version must + # be 0x0303 + elif is_tls12: + self.buffer_out.append(TLS(version="TLS 1.2", + tls_session=self.cur_session)) else: self.buffer_out.append(TLS(tls_session=self.cur_session)) diff --git a/scapy/layers/tls/automaton_cli.py b/scapy/layers/tls/automaton_cli.py index 52966c29f4c..8c3ada4eb0c 100644 --- a/scapy/layers/tls/automaton_cli.py +++ b/scapy/layers/tls/automaton_cli.py @@ -19,6 +19,7 @@ from __future__ import print_function import socket +import binascii from scapy.config import conf from scapy.pton_ntop import inet_pton @@ -29,7 +30,7 @@ from scapy.layers.tls.session import tlsSession from scapy.layers.tls.extensions import TLS_Ext_SupportedGroups, \ TLS_Ext_SupportedVersion_CH, TLS_Ext_SignatureAlgorithms, \ - TLS_Ext_SupportedVersion_SH + TLS_Ext_SupportedVersion_SH, TLS_Ext_PSKKeyExchangeModes from scapy.layers.tls.handshake import TLSCertificate, TLSCertificateRequest, \ TLSCertificateVerify, TLSClientHello, TLSClientKeyExchange, \ TLSEncryptedExtensions, TLSFinished, TLSServerHello, TLSServerHelloDone, \ @@ -41,11 +42,13 @@ SSLv2ClientFinished, SSLv2ServerFinished, SSLv2ClientCertificate, \ SSLv2RequestCertificate from scapy.layers.tls.keyexchange_tls13 import TLS_Ext_KeyShare_CH, \ - KeyShareEntry, TLS_Ext_KeyShare_HRR + KeyShareEntry, TLS_Ext_KeyShare_HRR, PSKIdentity, PSKBinderEntry, \ + 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.groups import _tls_named_groups +from scapy.layers.tls.crypto.hkdf import TLS13_HKDF from scapy.modules import six from scapy.packet import Raw from scapy.compat import bytes_encode @@ -73,6 +76,7 @@ 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, + psk=None, psk_mode=None, data=None, ciphersuite=None, curve=None, @@ -138,6 +142,8 @@ def parse_args(self, server="127.0.0.1", dport=4433, server_name=None, else: # Or secp256r1 otherwise self.curve = 23 + self.tls13_psk_secret = psk + self.tls13_psk_mode = psk_mode if curve is not None: for (group_id, ng) in _tls_named_groups.items(): if ng == curve: @@ -171,14 +177,19 @@ def INITIAL(self): @ATMT.state() def INIT_TLS_SESSION(self): self.cur_session = tlsSession(connection_end="client") - self.cur_session.client_certs = self.mycert - self.cur_session.client_key = self.mykey + s = self.cur_session + s.client_certs = self.mycert + s.client_key = self.mykey v = self.advertised_tls_version if v: - self.cur_session.advertised_tls_version = v + s.advertised_tls_version = v else: - default_version = self.cur_session.advertised_tls_version + default_version = s.advertised_tls_version self.advertised_tls_version = default_version + + if s.advertised_tls_version >= 0x0304: + if self.tls13_psk_secret: + s.tls13_psk_secret = binascii.unhexlify(self.tls13_psk_secret) raise self.CONNECT() @ATMT.state() @@ -872,21 +883,47 @@ def tls13_should_add_ClientHello(self): if conf.crypto_valid_advanced: supported_groups.append("x25519") self.add_record(is_tls13=False) - ext = [TLS_Ext_SupportedVersion_CH(versions=["TLS 1.3"]), - TLS_Ext_SupportedGroups(groups=supported_groups), - TLS_Ext_KeyShare_CH(client_shares=[KeyShareEntry(group=self.curve)]), # noqa: E501 - TLS_Ext_SignatureAlgorithms(sig_algs=["sha256+rsaepss", - "sha256+rsa"])] if self.client_hello: - if not self.client_hello.ext: - self.client_hello.ext = ext p = self.client_hello else: if self.ciphersuite is None: c = 0x1301 else: c = self.ciphersuite - p = TLS13ClientHello(ciphers=c, ext=ext) + p = TLS13ClientHello(ciphers=c) + + ext = [] + ext += TLS_Ext_SupportedVersion_CH(versions=["TLS 1.3"]) + + if self.cur_session.tls13_psk_secret: + if self.tls13_psk_mode == "psk_dhe_ke": + ext += TLS_Ext_PSKKeyExchangeModes(kxmodes="psk_dhe_ke") + ext += TLS_Ext_SupportedGroups(groups=supported_groups) + ext += TLS_Ext_KeyShare_CH( + client_shares=[KeyShareEntry(group=self.curve)] + ) + 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]) + else: + ext += TLS_Ext_SupportedGroups(groups=supported_groups) + ext += TLS_Ext_KeyShare_CH( + client_shares=[KeyShareEntry(group=self.curve)] + ) + ext += TLS_Ext_SignatureAlgorithms(sig_algs=["sha256+rsaepss", + "sha256+rsa"]) + p.ext = ext self.add_msg(p) raise self.TLS13_ADDED_CLIENTHELLO() @@ -967,10 +1004,32 @@ def tls13_should_add_ClientHello_Retry(self): selected_version = e.version if not selected_group or not selected_version: raise self.CLOSE_NOTIFY() - ext = [TLS_Ext_SupportedVersion_CH(versions=[_tls_version[selected_version]]), # noqa: E501 - TLS_Ext_SupportedGroups(groups=[_tls_named_groups[selected_group]]), # noqa: E501 - TLS_Ext_KeyShare_CH(client_shares=[KeyShareEntry(group=selected_group)]), # noqa: E501 - TLS_Ext_SignatureAlgorithms(sig_algs=["sha256+rsaepss"])] + + ext = [] + ext += TLS_Ext_SupportedVersion_CH(versions=[_tls_version[selected_version]]) # noqa: E501 + + if s.tls13_psk_secret: + if self.tls13_psk_mode == "psk_dhe_ke": + ext += TLS_Ext_PSKKeyExchangeModes(kxmodes="psk_dhe_ke"), + ext += TLS_Ext_SupportedGroups(groups=[_tls_named_groups[selected_group]]) # noqa: E501 + ext += TLS_Ext_KeyShare_CH(client_shares=[KeyShareEntry(group=selected_group)]) # noqa: E501 + 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) + + 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 + ext += TLS_Ext_KeyShare_CH(client_shares=[KeyShareEntry(group=selected_group)]) # noqa: E501 + ext += TLS_Ext_SignatureAlgorithms(sig_algs=["sha256+rsaepss"]) + p = TLS13ClientHello(ciphers=ciphersuite, ext=ext) self.add_msg(p) raise self.TLS13_ADDED_CLIENTHELLO() @@ -979,21 +1038,22 @@ def tls13_should_add_ClientHello_Retry(self): def TLS13_HANDLED_SERVERHELLO(self): pass - @ATMT.state() - def TLS13_WAITING_ENCRYPTEDEXTENSIONS(self): - self.get_next_msg() - - @ATMT.condition(TLS13_WAITING_ENCRYPTEDEXTENSIONS) - def tls13_should_handle_EncryptedExtensions(self): - self.raise_on_packet(TLSEncryptedExtensions, - self.TLS13_WAITING_CERTIFICATE) - @ATMT.condition(TLS13_HANDLED_SERVERHELLO, prio=1) def tls13_should_handle_encrytpedExtensions(self): self.raise_on_packet(TLSEncryptedExtensions, self.TLS13_HANDLED_ENCRYPTEDEXTENSIONS) @ATMT.condition(TLS13_HANDLED_SERVERHELLO, prio=2) + def tls13_should_handle_ChangeCipherSpec(self): + self.raise_on_packet(TLSChangeCipherSpec, + self.TLS13_HANDLED_CHANGE_CIPHER_SPEC) + + @ATMT.state() + def TLS13_HANDLED_CHANGE_CIPHER_SPEC(self): + self.cur_session.middlebox_compatibility = True + raise self.TLS13_HANDLED_SERVERHELLO() + + @ATMT.condition(TLS13_HANDLED_SERVERHELLO, prio=3) def tls13_missing_encryptedExtension(self): self.vprint("Missing TLS 1.3 EncryptedExtensions message!") raise self.CLOSE_NOTIFY() @@ -1015,6 +1075,12 @@ def tls13_should_handle_certificateRequest_from_encryptedExtensions(self): def tls13_should_handle_certificate_from_encryptedExtensions(self): self.tls13_should_handle_Certificate() + @ATMT.condition(TLS13_HANDLED_ENCRYPTEDEXTENSIONS, prio=3) + def tls13_should_handle_finished_from_encryptedExtensions(self): + if self.cur_session.tls13_psk_secret: + self.raise_on_packet(TLSFinished, + self.TLS13_HANDLED_FINISHED) + @ATMT.state() def TLS13_HANDLED_CERTIFICATEREQUEST(self): pass @@ -1056,6 +1122,9 @@ def TLS13_HANDLED_FINISHED(self): @ATMT.state() def TLS13_PREPARE_CLIENTFLIGHT2(self): + if self.cur_session.middlebox_compatibility: + self.add_record(is_tls12=True) + self.add_msg(TLSChangeCipherSpec()) self.add_record(is_tls13=True) @ATMT.condition(TLS13_PREPARE_CLIENTFLIGHT2, prio=1) diff --git a/scapy/layers/tls/automaton_srv.py b/scapy/layers/tls/automaton_srv.py index 1129fbeffbd..c5a3f4fb07a 100644 --- a/scapy/layers/tls/automaton_srv.py +++ b/scapy/layers/tls/automaton_srv.py @@ -18,6 +18,7 @@ from __future__ import print_function import socket +import binascii from scapy.packet import Raw from scapy.pton_ntop import inet_pton @@ -30,9 +31,10 @@ 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_SignatureAlgorithms, TLS_Ext_PSKKeyExchangeModes from scapy.layers.tls.keyexchange_tls13 import TLS_Ext_KeyShare_SH, \ - KeyShareEntry, TLS_Ext_KeyShare_HRR + KeyShareEntry, TLS_Ext_KeyShare_HRR, TLS_Ext_PreSharedKey_CH, \ + TLS_Ext_PreSharedKey_SH from scapy.layers.tls.handshake import TLSCertificate, TLSCertificateRequest, \ TLSCertificateVerify, TLSClientHello, TLSClientKeyExchange, TLSFinished, \ TLSServerHello, TLSServerHelloDone, TLSServerKeyExchange, \ @@ -81,6 +83,8 @@ def parse_args(self, server="127.0.0.1", sport=4433, max_client_idle_time=60, curve=None, cookie=False, + psk=None, + psk_mode=None, **kargs): super(TLSServerAutomaton, self).parse_args(mycert=mycert, @@ -108,6 +112,8 @@ def parse_args(self, server="127.0.0.1", sport=4433, self.max_client_idle_time = max_client_idle_time self.curve = None self.cookie = cookie + self.psk_secret = psk + self.psk_mode = psk_mode for (group_id, ng) in _tls_named_groups.items(): if ng == curve: self.curve = group_id @@ -155,7 +161,6 @@ def http_sessioninfo(self): body = "
%s
\r\n\r\n" % s answer = (header + body) % len(body) return answer - return answer @ATMT.state(initial=True) def INITIAL(self): @@ -570,6 +575,27 @@ def tls13_PREPARE_SERVERFLIGHT1(self): @ATMT.condition(tls13_PREPARE_SERVERFLIGHT1) def tls13_should_add_ServerHello(self): + + psk_identity = None + psk_key_exchange_mode = None + obfuscated_age = None + # XXX check ClientHello extensions... + for m in reversed(self.cur_session.handshake_messages_parsed): + if isinstance(m, (TLS13ClientHello, TLSClientHello)): + for e in m.ext: + 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 + + # For out-of-bound PSK, obfuscated_ticket_age should be + # 0. We use this field to distinguish between out-of- + # bound PSK and resumed PSK + is_out_of_band_psk = (obfuscated_age == 0) + + if isinstance(e, TLS_Ext_PSKKeyExchangeModes): + psk_key_exchange_mode = e.kxmodes[0] + if isinstance(self.mykey, PrivKeyRSA): kx = "RSA" elif isinstance(self.mykey, PrivKeyECDSA): @@ -577,8 +603,22 @@ def tls13_should_add_ServerHello(self): usable_suites = get_usable_ciphersuites(self.cur_pkt.ciphers, kx) c = usable_suites[0] group = next(iter(self.cur_session.tls13_client_pubshares)) - ext = [TLS_Ext_SupportedVersion_SH(version="TLS 1.3"), - TLS_Ext_KeyShare_SH(server_share=KeyShareEntry(group=group))] + ext = [TLS_Ext_SupportedVersion_SH(version="TLS 1.3")] + if (psk_identity and obfuscated_age and psk_key_exchange_mode): + s = self.cur_session + if is_out_of_band_psk: + # Handshake with external PSK authentication + # XXX test that self.psk_secret is set + s.tls13_psk_secret = binascii.unhexlify(self.psk_secret) + # 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) + else: + # Standard Handshake + ext += TLS_Ext_KeyShare_SH(server_share=KeyShareEntry(group=group)) if self.cur_session.sid is not None: p = TLS13ServerHello(cipher=c, sid=self.cur_session.sid, ext=ext) @@ -589,6 +629,13 @@ def tls13_should_add_ServerHello(self): @ATMT.state() def tls13_ADDED_SERVERHELLO(self): + # If the client proposed a non-empty session ID in his ClientHello + # he requested the middlebox compatibility mode (RFC8446, appendix D.4) + # In this case, the server should send a dummy ChangeCipherSpec in + # between the ServerHello and the encrypted handshake messages + if self.cur_session.sid is not None: + self.add_record(is_tls12=True) + self.add_msg(TLSChangeCipherSpec()) pass @ATMT.condition(tls13_ADDED_SERVERHELLO) @@ -615,11 +662,15 @@ def tls13_ADDED_CERTIFICATEREQUEST(self): @ATMT.condition(tls13_ADDED_CERTIFICATEREQUEST) def tls13_should_add_Certificate(self): - certs = [] - for c in self.cur_session.server_certs: - certs += _ASN1CertAndExt(cert=c) - - self.add_msg(TLS13Certificate(certs=certs)) + # If a PSK is set, an extension pre_shared_key + # was send in the ServerHello. No certificate should + # be send here + if not self.cur_session.tls13_psk_secret: + certs = [] + for c in self.cur_session.server_certs: + certs += _ASN1CertAndExt(cert=c) + + self.add_msg(TLS13Certificate(certs=certs)) raise self.tls13_ADDED_CERTIFICATE() @ATMT.state() @@ -628,7 +679,8 @@ def tls13_ADDED_CERTIFICATE(self): @ATMT.condition(tls13_ADDED_CERTIFICATE) def tls13_should_add_CertificateVerifiy(self): - self.add_msg(TLSCertificateVerify()) + if not self.cur_session.tls13_psk_secret: + self.add_msg(TLSCertificateVerify()) raise self.tls13_ADDED_CERTIFICATEVERIFY() @ATMT.state() @@ -647,27 +699,6 @@ def tls13_ADDED_SERVERFINISHED(self): @ATMT.condition(tls13_ADDED_SERVERFINISHED) def tls13_should_send_ServerFlight1(self): self.flush_records() - raise self.tls13_HANDLED_SERVERFLIGHT1() - - @ATMT.state() - def tls13_HANDLED_SERVERFLIGHT1(self): - pass - - @ATMT.condition(tls13_HANDLED_SERVERFLIGHT1, prio=1) - def tls13_should_handle_ChangeCipherSpec(self): - self.raise_on_packet(TLSChangeCipherSpec, - self.tls13_HANDLED_CHANGECIPHERSPEC) - - @ATMT.state() - def tls13_HANDLED_CHANGECIPHERSPEC(self): - pass - - @ATMT.condition(tls13_HANDLED_SERVERFLIGHT1, prio=2) - def tls13_missing_ChangeCipherSpec(self): - raise self.tls13_WAITING_CLIENTFLIGHT2() - - @ATMT.condition(tls13_HANDLED_CHANGECIPHERSPEC) - def tls13_should_wait_ClientFlight2(self): raise self.tls13_WAITING_CLIENTFLIGHT2() @ATMT.state() @@ -684,7 +715,17 @@ def tls13_should_handle_ClientFlight2(self): self.raise_on_packet(TLS13Certificate, self.TLS13_HANDLED_CLIENTCERTIFICATE) + # For Middlebox compatibility (see RFC8446, appendix D.4) + # a dummy ChangeCipherSpec record can be send. In this case, + # this function just read the ChangeCipherSpec message and + # go back in a previous state continuing with the next TLS 1.3 + # record @ATMT.condition(tls13_RECEIVED_CLIENTFLIGHT2, prio=2) + def tls13_should_handle_ClientCCS(self): + self.raise_on_packet(TLSChangeCipherSpec, + self.tls13_RECEIVED_CLIENTFLIGHT2) + + @ATMT.condition(tls13_RECEIVED_CLIENTFLIGHT2, prio=3) def tls13_no_ClientCertificate(self): if self.client_auth: raise self.TLS13_MISSING_CLIENTCERTIFICATE() @@ -840,7 +881,7 @@ def close_session_final(self): self.flush_records() except Exception: self.vprint("Could not send termination Alert, maybe the client left?") # noqa: E501 - # We might call shutdown, but unit tests with s_client fail with this. + # We might call shutdown, but unit tests with s_client fail with this # self.socket.shutdown(1) self.socket.close() raise self.FINAL() diff --git a/scapy/layers/tls/handshake.py b/scapy/layers/tls/handshake.py index 9e7cdf8afd1..4d32bfdfed5 100644 --- a/scapy/layers/tls/handshake.py +++ b/scapy/layers/tls/handshake.py @@ -12,6 +12,7 @@ from __future__ import absolute_import import math +import os import struct from scapy.error import log_runtime, warning @@ -41,6 +42,7 @@ SigAndHashAlgsLenField) from scapy.layers.tls.session import (_GenericTLSSessionInheritance, readConnState, writeConnState) +from scapy.layers.tls.keyexchange_tls13 import TLS_Ext_PreSharedKey_CH from scapy.layers.tls.crypto.compression import (_tls_compression_algs, _tls_compression_algs_cls, Comp_NULL, _GenericComp, @@ -293,7 +295,6 @@ def tls_session_update(self, msg_str): along with the raw string representing this handshake message. """ super(TLSClientHello, self).tls_session_update(msg_str) - s = self.tls_session s.advertised_tls_version = self.version # This ClientHello could be a 1.3 one. Let's store the sid @@ -311,6 +312,9 @@ def tls_session_update(self, msg_str): for e in self.ext: if isinstance(e, TLS_Ext_SupportedVersion_CH): s.advertised_tls_version = e.versions[0] + if s.sid: + s.middlebox_compatibility = True + if isinstance(e, TLS_Ext_SignatureAlgorithms): s.advertised_sig_algs = e.sig_algs @@ -355,7 +359,49 @@ class TLS13ClientHello(_TLSHandshake): def post_build(self, p, pay): if self.random_bytes is None: p = p[:6] + randstring(32) + p[6 + 32:] - return super(TLS13ClientHello, self).post_build(p, pay) + # We don't call the post_build function from class _TLSHandshake + # to compute the message length because we need that value now + # for the HMAC in binder + tmp_len = len(p) + if self.msglen is None: + sz = tmp_len - 4 + p = struct.pack("!I", (orb(p[0]) << 24) | sz) + p[4:] + s = self.tls_session + 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) + + # RFC8446 4.2.11.2 + # "Each entry in the binders list is computed as an HMAC + # over a transcript hash (see Section 4.4.1) containing a + # partial ClientHello up to and including the + # PreSharedKeyExtension.identities field." + # PSK Binders field is : + # - PSK Binders length (2 bytes) + # - First PSK Binder length (1 byte) + + # HMAC (hash_len bytes) + # The PSK Binder is computed in the same way as the + # Finished message with binder_key as BaseKey + + handshake_context = b"" + if s.tls13_retry: + for m in s.handshake_messages: + handshake_context += m + handshake_context += p[:-hash_len - 3] + + binder_key = s.tls13_derived_secrets["binder_key"] + psk_binder = hkdf.compute_verify_data(binder_key, + handshake_context) + + # Here, we replaced the last 32 bytes of the packet by the + # new HMAC values computed over the ClientHello (without + # the binders) + p = p[:-hash_len] + psk_binder + + return p + pay def tls_session_update(self, msg_str): """ @@ -367,6 +413,8 @@ def tls_session_update(self, msg_str): if self.sidlen and self.sidlen > 0: s.sid = self.sid + s.middlebox_compatibility = True + self.random_bytes = msg_str[10:38] s.client_random = self.random_bytes if self.ext: @@ -549,17 +597,20 @@ def tls_session_update(self, msg_str): cs_cls = _tls_cipher_suites_cls[cs_val] connection_end = s.connection_end - if connection_end == "server": s.pwcs = writeConnState(ciphersuite=cs_cls, connection_end=connection_end, tls_version=s.tls_version) - s.triggered_pwcs_commit = True + + if not s.middlebox_compatibility: + s.triggered_pwcs_commit = True elif connection_end == "client": + s.prcs = readConnState(ciphersuite=cs_cls, connection_end=connection_end, tls_version=s.tls_version) - s.triggered_prcs_commit = True + if not s.middlebox_compatibility: + s.triggered_prcs_commit = True if s.tls13_early_secret is None: # In case the connState was not pre-initialized, we could not @@ -595,9 +646,15 @@ def tls_session_update(self, msg_str): s.tls13_client_pubshares = {} # If the server responds to a ClientHello with a HelloRetryRequest # The value of the first ClientHello is replaced by a message_hash - cs_cls = _tls_cipher_suites_cls[self.cipher] - hkdf = TLS13_HKDF(cs_cls.hash_alg.name.lower()) - hash_len = hkdf.hash.digest_size + if s.client_session_ticket: + 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 + else: + cs_cls = _tls_cipher_suites_cls[self.cipher] + hkdf = TLS13_HKDF(cs_cls.hash_alg.name.lower()) + hash_len = hkdf.hash.digest_size + handshake_context = struct.pack("B", 254) handshake_context += struct.pack("B", 0) handshake_context += struct.pack("B", 0) @@ -646,12 +703,14 @@ def post_build_tls_session_update(self, msg_str): connection_end=connection_end, tls_version=s.tls_version) - s.triggered_prcs_commit = True chts = s.tls13_derived_secrets["client_handshake_traffic_secret"] # noqa: E501 s.prcs.tls13_derive_keys(chts) - s.rcs = self.tls_session.prcs - s.triggered_prcs_commit = False + if not s.middlebox_compatibility: + s.rcs = self.tls_session.prcs + s.triggered_prcs_commit = False + else: + s.triggered_prcs_commit = True def post_dissection_tls_session_update(self, msg_str): self.tls_session_update(msg_str) @@ -675,13 +734,13 @@ def post_dissection_tls_session_update(self, msg_str): s.pwcs = writeConnState(ciphersuite=type(s.rcs.ciphersuite), connection_end=connection_end, tls_version=s.tls_version) - - s.triggered_pwcs_commit = True chts = s.tls13_derived_secrets["client_handshake_traffic_secret"] # noqa: E501 s.pwcs.tls13_derive_keys(chts) - - s.wcs = self.tls_session.pwcs - s.triggered_pwcs_commit = False + if not s.middlebox_compatibility: + s.wcs = self.tls_session.pwcs + s.triggered_pwcs_commit = False + else: + s.triggered_prcs_commit = True ############################################################################### # Certificate # ############################################################################### @@ -1482,6 +1541,31 @@ class TLS13NewSessionTicket(_TLSHandshake): (pkt.ticketlen or 0) - # noqa: E501 pkt.noncelen or 0) - 13)] # noqa: E501 + def build(self): + fval = self.getfieldval("ticket") + if fval == b"": + # Here, the ticket is just a random 48-byte label + # The ticket may also be a self-encrypted and self-authenticated + # value + self.ticket = os.urandom(48) + + fval = self.getfieldval("ticket_nonce") + if fval == b"": + # Nonce is randomly chosen + self.ticket_nonce = os.urandom(32) + + fval = self.getfieldval("ticket_lifetime") + if fval == 0xffffffff: + # ticket_lifetime is set to 12 hours + self.ticket_lifetime = 43200 + + fval = self.getfieldval("ticket_age_add") + if fval == 0: + # ticket_age_add is a random 32-bit value + self.ticket_age_add = struct.unpack("!I", os.urandom(4))[0] + + return _TLSHandshake.build(self) + def post_dissection_tls_session_update(self, msg_str): self.tls_session_update(msg_str) if self.tls_session.connection_end == "client": diff --git a/scapy/layers/tls/record.py b/scapy/layers/tls/record.py index d258a7dc270..79de0bc569a 100644 --- a/scapy/layers/tls/record.py +++ b/scapy/layers/tls/record.py @@ -204,10 +204,12 @@ def addfield(self, pkt, s, val): res += self.i2m(pkt, p) # Add TLS13ClientHello in case of HelloRetryRequest + # Add ChangeCipherSpec for middlebox compatibility if (isinstance(pkt, _GenericTLSSessionInheritance) and _tls_version_check(pkt.tls_session.tls_version, 0x0304) and not isinstance(pkt.msg[0], TLS13ServerHello) and - not isinstance(pkt.msg[0], TLS13ClientHello)): + not isinstance(pkt.msg[0], TLS13ClientHello) and + not isinstance(pkt.msg[0], TLSChangeCipherSpec)): return s + res if not pkt.type: @@ -306,7 +308,8 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): return SSLv2 s = kargs.get("tls_session", None) if s and _tls_version_check(s.tls_version, 0x0304): - if s.rcs and not isinstance(s.rcs.cipher, Cipher_NULL): + if (s.rcs and not isinstance(s.rcs.cipher, Cipher_NULL) and + byte0 == 0x17): from scapy.layers.tls.record_tls13 import TLS13 return TLS13 if plen < 5: diff --git a/scapy/layers/tls/record_tls13.py b/scapy/layers/tls/record_tls13.py index deb7478ccfa..7e2211092e9 100644 --- a/scapy/layers/tls/record_tls13.py +++ b/scapy/layers/tls/record_tls13.py @@ -61,16 +61,6 @@ def pre_dissect(self, s): return s -class TLSInnerChangeCipherSpec(_GenericTLSSessionInheritance): - __slots__ = ["type"] - name = "TLS Inner Plaintext (CCS)" - fields_desc = [_TLSMsgListField("msg", [], length_from=lambda x: 1)] - - def __init__(self, _pkt=None, *args, **kwargs): - self.type = 0x14 - super(TLSInnerChangeCipherSpec, self).__init__(_pkt, *args, **kwargs) - - class _TLSInnerPlaintextField(PacketField): def __init__(self, name, default, *args, **kargs): super(_TLSInnerPlaintextField, self).__init__(name, @@ -78,13 +68,9 @@ def __init__(self, name, default, *args, **kargs): TLSInnerPlaintext) def m2i(self, pkt, m): - if pkt.type == 0x14: - return TLSInnerChangeCipherSpec(m, tls_session=pkt.tls_session) return self.cls(m, tls_session=pkt.tls_session) def getfield(self, pkt, s): - if pkt.type == 0x14: - return super(_TLSInnerPlaintextField, self).getfield(pkt, s) tag_len = pkt.tls_session.rcs.mac_len frag_len = pkt.len - tag_len if frag_len < 1: diff --git a/scapy/layers/tls/session.py b/scapy/layers/tls/session.py index 24b2d111b8a..b9e6bf978bc 100644 --- a/scapy/layers/tls/session.py +++ b/scapy/layers/tls/session.py @@ -439,6 +439,10 @@ def __init__(self, self.tls13_handshake_secret = None self.tls13_master_secret = None self.tls13_derived_secrets = {} + self.post_handshake_auth = False + self.tls13_ticket_ciphersuite = None + self.tls13_retry = False + self.middlebox_compatibility = False # Handshake messages needed for Finished computation/validation. # No record layer headers, no HelloRequests, no ChangeCipherSpecs. diff --git a/test/tls/example_client.py b/test/tls/example_client.py index b3503cf15a3..129a140064d 100755 --- a/test/tls/example_client.py +++ b/test/tls/example_client.py @@ -5,10 +5,7 @@ """ Basic TLS client. A ciphersuite may be commanded via a first argument. -Default protocol version is TLS 1.2. - -For instance, "sudo ./client_simple.py c014" will try to connect to any TLS -server at 127.0.0.1:4433, with suite TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA. +Default protocol version is TLS 1.3. """ import os @@ -18,24 +15,49 @@ sys.path=[basedir]+sys.path from scapy.layers.tls.automaton_cli import TLSClientAutomaton +from scapy.layers.tls.basefields import _tls_version_options from scapy.layers.tls.handshake import TLSClientHello, TLS13ClientHello +from argparse import ArgumentParser + +psk = None +parser = ArgumentParser(description='Simple TLS Client') +parser.add_argument("--psk", + help="External PSK for symmetric authentication (for TLS 1.3)") # noqa: E501 +parser.add_argument("--no_pfs", action="store_true", + help="Disable (EC)DHE exchange with PFS") +parser.add_argument("--ciphersuite", help="Ciphersuite preference") +parser.add_argument("--version", help="TLS Version", default="tls13") + +args = parser.parse_args() + +# By default, PFS is set +if args.no_pfs: + psk_mode = "psk_ke" +else: + psk_mode = "psk_dhe_ke" + +v = _tls_version_options.get(args.version, None) +if not v: + sys.exit("Unrecognized TLS version option.") + + -if len(sys.argv) == 2: - ciphers = int(sys.argv[1], 16) +if args.ciphersuite: + ciphers = int(args.ciphersuite, 16) if ciphers not in list(range(0x1301, 0x1306)): ch = TLSClientHello(ciphers=ciphers) - version = "tls12" else: ch = TLS13ClientHello(ciphers=ciphers) - version = "tls13" else: ch = None - version = "tls13" t = TLSClientAutomaton(client_hello=ch, - version=version, + version=args.version, mycert=basedir+"/test/tls/pki/cli_cert.pem", - mykey=basedir+"/test/tls/pki/cli_key.pem") + mykey=basedir+"/test/tls/pki/cli_key.pem", + psk=args.psk, + psk_mode=psk_mode, + ) t.run() diff --git a/test/tls/example_server.py b/test/tls/example_server.py index 5d8c6320f2c..60806081ba2 100755 --- a/test/tls/example_server.py +++ b/test/tls/example_server.py @@ -21,6 +21,10 @@ from argparse import ArgumentParser parser = ArgumentParser(description='Simple TLS Server') +parser.add_argument("--psk", + help="External PSK for symmetric authentication (for TLS 1.3)") # noqa: E501 +parser.add_argument("--no_pfs", action="store_true", + help="Disable (EC)DHE exchange with PFS") # args.curve must be a value in the dict _tls_named_curves (see tls/crypto/groups.py) parser.add_argument("--curve", help="ECC curve to advertise (ex: secp256r1...") parser.add_argument("--cookie", action="store_true", @@ -30,11 +34,18 @@ args = parser.parse_args() pcs = None +# PFS is set by default... +if args.no_pfs and args.psk: + psk_mode = "psk_ke" +else: + psk_mode = "psk_dhe_ke" t = TLSServerAutomaton(mycert=basedir+'/test/tls/pki/srv_cert.pem', mykey=basedir+'/test/tls/pki/srv_key.pem', preferred_ciphersuite=pcs, client_auth=args.client_auth, curve=args.curve, - cookie=args.cookie) + cookie=args.cookie, + psk=args.psk, + psk_mode=psk_mode) t.run() diff --git a/test/tls/tests_tls_netaccess.uts b/test/tls/tests_tls_netaccess.uts index ad88b5c77d8..eea014ed5b6 100644 --- a/test/tls/tests_tls_netaccess.uts +++ b/test/tls/tests_tls_netaccess.uts @@ -51,12 +51,15 @@ def check_output_for_data(out, err, expected_data): return (False, errored) output = out.s.strip() if expected_data: - s = re.search("> Received: '([^']*)'", output) - if s: - data = s.group(1) - print("Test output: %s" % data) - if expected_data in data: - return (True, data) + expected_data = plain_str(expected_data) + print("Testing for output: '%s'" % expected_data) + p = re.compile(r"> Received: b?'([^']*)'") + for s in p.finditer(output): + if s: + data = s.group(1) + print("Found: %s" % data) + if expected_data in data: + return (True, data) return (False, output) else: return (False, None) @@ -65,7 +68,7 @@ def get_file(filename): return 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): +def run_tls_test_server(expected_data, q, curve=None, cookie=False, client_auth=False, psk=None): correct = False print("Server started !") with captured_output() as (out, err): @@ -77,12 +80,17 @@ def run_tls_test_server(expected_data, q, curve=None, cookie=False, client_auth= print(mycert) assert os.path.exists(mycert) assert os.path.exists(mykey) + kwargs = dict() + if psk: + kwargs["psk"] = psk + kwargs["psk_mode"] = "psk_dhe_ke" t = TLSServerAutomaton(mycert=mycert, mykey=mykey, curve=curve, cookie=cookie, client_auth=client_auth, - debug=5) + debug=5, + **kwargs) # Sync threads q.put(True) # Run server automaton @@ -92,11 +100,12 @@ 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): +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_, None, False, client_auth)) + 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 @@ -115,6 +124,8 @@ def test_tls_server(suite="", version="", tls13=False, client_auth=False): ] if client_auth: args.extend(["-cert", mycert, "-key", mykey]) + if psk: + args.extend(["-psk", str(psk)]) p = subprocess.Popen( args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT @@ -134,6 +145,7 @@ def test_tls_server(suite="", version="", tls13=False, client_auth=False): break else: _one_success = True + break if _failed or not _one_success: raise RuntimeError("OpenSSL returned unexpected values") # Wait for server @@ -143,7 +155,9 @@ def test_tls_server(suite="", version="", tls13=False, client_auth=False): # Analyse values if q_.empty(): raise RuntimeError("Missing return values") - print(q_.get()) + ret = q_.get(timeout=5) + print(ret) + assert ret[0] = Testing TLS server with TLS 1.0 and TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA @@ -176,6 +190,11 @@ test_tls_server("TLS_AES_256_GCM_SHA384", "-tls1_3", tls13=True) test_tls_server("TLS_AES_256_GCM_SHA384", "-tls1_3", tls13=True, client_auth=True) += Testing TLS server with TLS 1.3 and ECDHE-PSK-AES256-CBC-SHA384 and PSK +~ open_ssl_client + +test_tls_server("ECDHE-PSK-AES256-CBC-SHA384", "-tls1_3", tls13=False, psk="1a2b3c4d") + + TLS client automaton tests ~ client @@ -190,27 +209,33 @@ 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): +def run_tls_test_client(send_data=None, cipher_suite_code=None, version=None, + client_auth=False, key_update=False): 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"]) if version == "0002": - t = TLSClientAutomaton(data=[send_data, b"stop_server", b"quit"], version="sslv2", debug=5, mycert=mycert, mykey=mykey) + t = TLSClientAutomaton(data=commands, version="sslv2", debug=5, mycert=mycert, mykey=mykey) elif version == "0304": ch = TLS13ClientHello(ciphers=int(cipher_suite_code, 16)) - t = TLSClientAutomaton(client_hello=ch, data=[send_data, b"stop_server", b"quit"], version="tls13", debug=5, mycert=mycert, mykey=mykey) + t = TLSClientAutomaton(client_hello=ch, data=commands, version="tls13", debug=5, mycert=mycert, mykey=mykey) else: ch = TLSClientHello(version=int(version, 16), ciphers=int(cipher_suite_code, 16)) - t = TLSClientAutomaton(client_hello=ch, data=[send_data, b"stop_server", b"quit"], debug=5, mycert=mycert, mykey=mykey) + t = TLSClientAutomaton(client_hello=ch, data=commands, debug=5, mycert=mycert, mykey=mykey) print("Running client...") t.run() -def test_tls_client(suite, version, curve=None, cookie=False, client_auth=False): +def test_tls_client(suite, version, curve=None, cookie=False, client_auth=False, key_update=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_, curve, cookie, client_auth)) + th_ = threading.Thread(target=run_tls_test_server, args=(msg, q_), + kwargs={"curve": None, "cookie": False, "client_auth": client_auth}) th_.setDaemon(True) th_.start() # Synchronise threads @@ -219,7 +244,7 @@ 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) + run_tls_test_client(msg, suite, version, client_auth, key_update) # Wait for server print("Client running, waiting...") th_.join(5) @@ -228,7 +253,9 @@ def test_tls_client(suite, version, curve=None, cookie=False, client_auth=False) # Return values if q_.empty(): raise RuntimeError("Missing return value") - return q_.get(timeout=5) + ret = q_.get(timeout=5) + print(ret) + assert ret[0] = Testing TLS server and client with SSLv2 and SSL_CK_DES_192_EDE3_CBC_WITH_MD5 @@ -277,3 +304,8 @@ test_tls_client("1302", "0304", curve="secp256r1", cookie=True) ~ crypto_advanced test_tls_client("1305", "0304", client_auth=True) + += Testing TLS server and client with TLS 1.3 and TLS_AES_128_CCM_8_SHA256 and key update +~ crypto_advanced + +test_tls_client("1305", "0304", key_update=True)