From 8b4430c670d5c6a43e605eeb9dbbb89831029255 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Tue, 3 Jul 2018 21:20:21 +0200 Subject: [PATCH 1/6] send handshake messages in few records support for sending multiple messages of the same time in single record, coalesce the server encrypted flight and session ticket --- tlslite/tlsconnection.py | 27 +++++++++++++-------------- tlslite/tlsrecordlayer.py | 30 +++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/tlslite/tlsconnection.py b/tlslite/tlsconnection.py index f42c515b..72e5b9bd 100644 --- a/tlslite/tlsconnection.py +++ b/tlslite/tlsconnection.py @@ -2241,7 +2241,8 @@ def _serverSendTickets(self, settings): iv + encrypted_ticket, []) - for result in self._sendMsg(new_ticket): + self._queue_message(new_ticket) + for result in self._queue_flush(): yield result def _tryDecrypt(self, settings, identity): @@ -2400,12 +2401,13 @@ def _serverTLS13Handshake(self, settings, clientHello, cipherSuite, clientHello.session_id, cipherSuite, extensions=sh_extensions) - for result in self._sendMsg(serverHello): - yield result + msgs = [] + msgs.append(serverHello) if not self._ccs_sent: ccs = ChangeCipherSpec().create() - for result in self._sendMsg(ccs): - yield result + msgs.append(ccs) + for result in self._sendMsgs(msgs): + yield result # Early secret secret = secureHMAC(secret, psk, prf_name) @@ -2467,8 +2469,7 @@ def _serverTLS13Handshake(self, settings, clientHello, cipherSuite, HeartbeatMode.PEER_ALLOWED_TO_SEND)) encryptedExtensions = EncryptedExtensions().create(ee_extensions) - for result in self._sendMsg(encryptedExtensions): - yield result + self._queue_message(encryptedExtensions) if selected_psk is None: @@ -2484,13 +2485,11 @@ def _serverTLS13Handshake(self, settings, clientHello, cipherSuite, certificate_request = CertificateRequest(self.version) certificate_request.create(context=ctx, sig_algs=valid_sig_algs) - for result in self._sendMsg(certificate_request): - yield result + self._queue_message(certificate_request) certificate = Certificate(CertificateType.x509, self.version) certificate.create(serverCertChain, bytearray()) - for result in self._sendMsg(certificate): - yield result + self._queue_message(certificate) certificate_verify = CertificateVerify(self.version) @@ -2519,8 +2518,7 @@ def _serverTLS13Handshake(self, settings, clientHello, cipherSuite, yield result certificate_verify.create(signature, signature_scheme) - for result in self._sendMsg(certificate_verify): - yield result + self._queue_message(certificate_verify) finished_key = HKDF_expand_label(sr_handshake_traffic_secret, b"finished", b'', prf_size, prf_name) @@ -2530,7 +2528,8 @@ def _serverTLS13Handshake(self, settings, clientHello, cipherSuite, finished = Finished(self.version, prf_size).create(verify_data) - for result in self._sendMsg(finished): + self._queue_message(finished) + for result in self._queue_flush(): yield result self._changeReadState() diff --git a/tlslite/tlsrecordlayer.py b/tlslite/tlsrecordlayer.py index f2b9c55e..2fd85725 100644 --- a/tlslite/tlsrecordlayer.py +++ b/tlslite/tlsrecordlayer.py @@ -179,6 +179,9 @@ def __init__(self, sock): # we sent self.heartbeat_response_callback = None + self._buffer_content_type = None + self._buffer = bytearray() + @property def _send_record_limit(self): """Maximum size of payload that can be sent.""" @@ -626,7 +629,7 @@ def _sendError(self, alertDescription, errorStr=None): raise TLSLocalAlert(alert, errorStr) def _sendMsgs(self, msgs): - # send messages together + # send messages together in a single TCP write self.sock.buffer_writes = True randomizeFirstBlock = True for msg in msgs: @@ -636,7 +639,7 @@ def _sendMsgs(self, msgs): self.sock.flush() self.sock.buffer_writes = False - def _sendMsg(self, msg, randomizeFirstBlock = True): + def _sendMsg(self, msg, randomizeFirstBlock=True, update_hashes=True): """Fragment and send message through socket""" #Whenever we're connected and asked to send an app data message, #we first send the first byte of the message. This prevents @@ -654,7 +657,7 @@ def _sendMsg(self, msg, randomizeFirstBlock = True): buf = msg.write() contentType = msg.contentType #Update handshake hashes - if contentType == ContentType.handshake: + if update_hashes and contentType == ContentType.handshake: self._handshake_hash.update(buf) #Fragment big messages @@ -670,6 +673,27 @@ def _sendMsg(self, msg, randomizeFirstBlock = True): for result in self._sendMsgThroughSocket(msgFragment): yield result + def _queue_message(self, msg): + """Just queue message for sending, for record layer coalescing.""" + if self._buffer_content_type is not None and \ + self._buffer_content_type != msg.contentType: + raise ValueError("Queuing of wrong message types") + if self._buffer_content_type is None: + self._buffer_content_type = msg.contentType + + serialised_msg = msg.write() + self._buffer += serialised_msg + if msg.contentType == ContentType.handshake: + self._handshake_hash.update(serialised_msg) + + def _queue_flush(self): + """Send the queued messages.""" + msg = Message(self._buffer_content_type, self._buffer) + for result in self._sendMsg(msg, update_hashes=False): + yield result + self._buffer_content_type = None + self._buffer = bytearray() + def _sendMsgThroughSocket(self, msg): """Send message, handle errors""" From a5a81fd2dee1894477d2453429929a43d9a9b62d Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Thu, 10 Jan 2019 17:16:36 +0100 Subject: [PATCH 2/6] support sending multiple tickets to client --- tlslite/handshakesettings.py | 6 ++++ tlslite/tlsconnection.py | 69 +++++++++++++++++++----------------- 2 files changed, 42 insertions(+), 33 deletions(-) diff --git a/tlslite/handshakesettings.py b/tlslite/handshakesettings.py index e6039ef8..8b1a9dcb 100644 --- a/tlslite/handshakesettings.py +++ b/tlslite/handshakesettings.py @@ -255,6 +255,7 @@ def _init_misc_extensions(self): self.ticketCipher = "aes256gcm" self.ticketLifetime = 24 * 60 * 60 self.max_early_data = 2 ** 14 + 16 # full record + tag + self.ticket_count = 2 self.record_size_limit = 2**14 + 1 # TLS 1.3 includes content type def __init__(self): @@ -475,6 +476,10 @@ def _sanityCheckTicketSettings(other): if not 0 < other.max_early_data <= 2**64: raise ValueError("max_early_data must be between 0 and 2GiB") + if not 0 < other.ticket_count < 2**16: + raise ValueError("Incorrect amount for number of new session " + "tickets to send") + def _copy_cipher_settings(self, other): """Copy values related to cipher selection.""" other.cipherNames = self.cipherNames @@ -498,6 +503,7 @@ def _copy_extension_settings(self, other): other.ticketCipher = self.ticketCipher other.ticketLifetime = self.ticketLifetime other.max_early_data = self.max_early_data + other.ticket_count = self.ticket_count other.record_size_limit = self.record_size_limit @staticmethod diff --git a/tlslite/tlsconnection.py b/tlslite/tlsconnection.py index 72e5b9bd..d643fef0 100644 --- a/tlslite/tlsconnection.py +++ b/tlslite/tlsconnection.py @@ -2211,39 +2211,42 @@ def _serverSendTickets(self, settings): if not settings.ticketKeys: return - # prepare the ticket - ticket = SessionTicketPayload() - ticket.create(self.session.resumptionMasterSecret, - self.version, - self.session.cipherSuite, - int(time.time()), - getRandomBytes(len(settings.ticketKeys[0])), - client_cert_chain=self.session.clientCertChain) - - # encrypt the ticket - if settings.ticketCipher in ("aes128gcm", "aes256gcm"): - cipher = createAESGCM(settings.ticketKeys[0], - settings.cipherImplementations) - else: # assume chacha, enforced by handshake settings - cipher = createCHACHA20(settings.ticketKeys[0], - settings.cipherImplementations) - - # all AEADs we support require 12 byte nonces/IVs - iv = getRandomBytes(12) - - encrypted_ticket = cipher.seal(iv, ticket.write(), b'') - - # send ticket to client - new_ticket = NewSessionTicket() - new_ticket.create(settings.ticketLifetime, - getRandomNumber(1, 8**4), - ticket.nonce, - iv + encrypted_ticket, - []) - - self._queue_message(new_ticket) - for result in self._queue_flush(): - yield result + for _ in range(settings.ticket_count): + # prepare the ticket + ticket = SessionTicketPayload() + ticket.create(self.session.resumptionMasterSecret, + self.version, + self.session.cipherSuite, + int(time.time()), + getRandomBytes(len(settings.ticketKeys[0])), + client_cert_chain=self.session.clientCertChain) + + # encrypt the ticket + if settings.ticketCipher in ("aes128gcm", "aes256gcm"): + cipher = createAESGCM(settings.ticketKeys[0], + settings.cipherImplementations) + else: + assert settings.ticketCipher == "chacha20-poly1305" + cipher = createCHACHA20(settings.ticketKeys[0], + settings.cipherImplementations) + + # all AEADs we support require 12 byte nonces/IVs + iv = getRandomBytes(12) + + encrypted_ticket = cipher.seal(iv, ticket.write(), b'') + + new_ticket = NewSessionTicket() + new_ticket.create(settings.ticketLifetime, + getRandomNumber(1, 8**4), + ticket.nonce, + iv + encrypted_ticket, + []) + self._queue_message(new_ticket) + + # send tickets to client + if settings.ticket_count: + for result in self._queue_flush(): + yield result def _tryDecrypt(self, settings, identity): if not settings.ticketKeys: From bbb21985f1c6024615c8762bfbdefb86a35cce6c Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Tue, 10 Jul 2018 18:46:47 +0200 Subject: [PATCH 3/6] allow for setting the number of sent tickets in tls.py --- scripts/tls.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/scripts/tls.py b/scripts/tls.py index cfa19e0c..05476561 100755 --- a/scripts/tls.py +++ b/scripts/tls.py @@ -76,7 +76,7 @@ def printUsage(s=None): server [-k KEY] [-c CERT] [-t TACK] [-v VERIFIERDB] [-d DIR] [-l LABEL] [-L LENGTH] [--reqcert] [--param DHFILE] [--psk PSK] [--psk-ident IDENTITY] - [--psk-sha384] [--ssl3] [--max-ver VER] + [--psk-sha384] [--ssl3] [--max-ver VER] [--tickets COUNT] HOST:PORT client @@ -96,6 +96,8 @@ def printUsage(s=None): --ssl3 - enable support for SSLv3 VER - TLS version as a string, "ssl3", "tls1.0", "tls1.1", "tls1.2" or "tls1.3" + --tickets COUNT - how many tickets should server send after handshake is + finished """) sys.exit(-1) @@ -145,6 +147,7 @@ def handleArgs(argv, argString, flagsList=[]): resumption = False ssl3 = False max_ver = None + tickets = None for opt, arg in opts: if opt == "-k": @@ -198,6 +201,8 @@ def handleArgs(argv, argString, flagsList=[]): ssl3 = True elif opt == "--max-ver": max_ver = ver_to_tuple(arg) + elif opt == "--tickets": + tickets = int(arg) else: assert(False) @@ -255,6 +260,8 @@ def handleArgs(argv, argString, flagsList=[]): retList.append(ssl3) if "max-ver=" in flagsList: retList.append(max_ver) + if "tickets=" in flagsList: + retList.append(tickets) return retList @@ -450,10 +457,11 @@ def clientCmd(argv): def serverCmd(argv): (address, privateKey, cert_chain, tacks, verifierDB, directory, reqCert, expLabel, expLength, dhparam, psk, psk_ident, psk_hash, ssl3, - max_ver) = \ + max_ver, tickets) = \ handleArgs(argv, "kctbvdlL", ["reqcert", "param=", "psk=", - "psk-ident=", "psk-sha384", "ssl3", "max-ver="]) + "psk-ident=", "psk-sha384", "ssl3", "max-ver=", + "tickets="]) if (cert_chain and not privateKey) or (not cert_chain and privateKey): @@ -485,6 +493,7 @@ def serverCmd(argv): settings = HandshakeSettings() settings.useExperimentalTackExtension=True settings.dhParams = dhparam + settings.ticket_count = tickets or settings.ticket_count if psk: settings.pskConfigs = [(psk_ident, psk, psk_hash)] settings.ticketKeys = [getRandomBytes(32)] From 8eafa3b585b264f549fb64fee8d9bbe12a29412d Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Thu, 10 Jan 2019 16:45:43 +0100 Subject: [PATCH 4/6] do proper derivation for ticket encryption keys to reduce probability of key and IV collision, they need to be derived from a common secret; use TLS 1.3-like derivation mechanism to do that --- tlslite/tlsconnection.py | 50 ++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/tlslite/tlsconnection.py b/tlslite/tlsconnection.py index d643fef0..86129831 100644 --- a/tlslite/tlsconnection.py +++ b/tlslite/tlsconnection.py @@ -2206,6 +2206,32 @@ def _handshakeServerAsyncHelper(self, verifierDB, self._serverRandom = serverHello.random self._clientRandom = clientHello.random + @staticmethod + def _derive_key_iv(nonce, user_key, settings): + """Derive the IV and key for session ticket encryption.""" + if settings.ticketCipher == "aes128gcm": + prf_name = "sha256" + prf_size = 32 + else: + prf_name = "sha384" + prf_size = 48 + + # mix the nonce with the key set by user + secret = bytearray(prf_size) + secret = secureHMAC(secret, nonce, prf_name) + secret = derive_secret(secret, bytearray(b'derived'), None, prf_name) + secret = secureHMAC(secret, user_key, prf_name) + + ticket_secret = derive_secret(secret, + bytearray(b'SessionTicket secret'), + None, prf_name) + + key = HKDF_expand_label(ticket_secret, b"key", b"", len(user_key), + prf_name) + # all AEADs use 12 byte long IV + iv = HKDF_expand_label(ticket_secret, b"iv", b"", 12, prf_name) + return key, iv + def _serverSendTickets(self, settings): """Send session tickets to client.""" if not settings.ticketKeys: @@ -2222,24 +2248,28 @@ def _serverSendTickets(self, settings): client_cert_chain=self.session.clientCertChain) # encrypt the ticket + + # generate keys for the encryption + nonce = getRandomBytes(32) + key, iv = self._derive_key_iv(nonce, settings.ticketKeys[0], + settings) + if settings.ticketCipher in ("aes128gcm", "aes256gcm"): - cipher = createAESGCM(settings.ticketKeys[0], + cipher = createAESGCM(key, settings.cipherImplementations) else: assert settings.ticketCipher == "chacha20-poly1305" - cipher = createCHACHA20(settings.ticketKeys[0], + cipher = createCHACHA20(key, settings.cipherImplementations) - # all AEADs we support require 12 byte nonces/IVs - iv = getRandomBytes(12) - encrypted_ticket = cipher.seal(iv, ticket.write(), b'') + # encapsulate the ticket and send to client new_ticket = NewSessionTicket() new_ticket.create(settings.ticketLifetime, getRandomNumber(1, 8**4), ticket.nonce, - iv + encrypted_ticket, + nonce + encrypted_ticket, []) self._queue_message(new_ticket) @@ -2252,15 +2282,17 @@ def _tryDecrypt(self, settings, identity): if not settings.ticketKeys: return None, None - if len(identity.identity) < 13: + if len(identity.identity) < 33: # too small for an encrypted ticket return None, None - iv, encrypted_ticket = identity.identity[:12], identity.identity[12:] - for key in settings.ticketKeys: + nonce, encrypted_ticket = identity.identity[:32], identity.identity[32:] + for user_key in settings.ticketKeys: + key, iv = self._derive_key_iv(nonce, user_key, settings) if settings.ticketCipher in ("aes128gcm", "aes256gcm"): cipher = createAESGCM(key, settings.cipherImplementations) else: + assert settings.ticketCipher == "chacha20-poly1305" cipher = createCHACHA20(key, settings.cipherImplementations) ticket = cipher.open(iv, encrypted_ticket, b'') From ff1178f4423ac280cf6d51dc1360fcf1305dc415 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Thu, 10 Jan 2019 17:05:21 +0100 Subject: [PATCH 5/6] update documentation related to ticket handling --- tlslite/handshakesettings.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tlslite/handshakesettings.py b/tlslite/handshakesettings.py index 8b1a9dcb..21ce7698 100644 --- a/tlslite/handshakesettings.py +++ b/tlslite/handshakesettings.py @@ -186,12 +186,17 @@ class HandshakeSettings(object): tickets. First entry is the encryption key for new tickets and the default decryption key, subsequent entries are the fallback keys allowing for key rollover. The keys need to be of size appropriate - for a selected cipher in ticketCipher, 32 bytes for 'aes256gcm'. + for a selected cipher in ticketCipher, 32 bytes for 'aes256gcm' and + 'chacha20-poly1305', 16 bytes for 'aes128-gcm'. + New keys should be generated regularly and replace old ones. Key use + time should generally not be longer than 24h and key life-time should + not be longer than 48h. Leave empty to disable session ticket support on server side. :vartype ticketCipher: str :ivar ticketCipher: name of the cipher used for encrypting the session - tickets. 'aes256gcm' by default + tickets. 'aes256gcm' by default, 'aes128gcm' or 'chacha20-poly1305' + alternatively. :vartype ticketLifetime: int :ivar ticketLifetime: maximum allowed lifetime of ticket encryption key, From 08aac00fa12ccf50d625ba1090ac01f91945d4bc Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Fri, 18 Jan 2019 15:21:17 +0100 Subject: [PATCH 6/6] add explanation why 2 tickets are sent --- tlslite/handshakesettings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tlslite/handshakesettings.py b/tlslite/handshakesettings.py index 21ce7698..a8cbd369 100644 --- a/tlslite/handshakesettings.py +++ b/tlslite/handshakesettings.py @@ -260,6 +260,8 @@ def _init_misc_extensions(self): self.ticketCipher = "aes256gcm" self.ticketLifetime = 24 * 60 * 60 self.max_early_data = 2 ** 14 + 16 # full record + tag + # send two tickets so that client can quickly ramp up number of + # resumed connections (as tickets are single-use in TLS 1.3 self.ticket_count = 2 self.record_size_limit = 2**14 + 1 # TLS 1.3 includes content type