Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send handshake messages in as few records as possible, send multiple new session tickets #287

Merged
merged 6 commits into from
Jan 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions scripts/tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)]
Expand Down
17 changes: 15 additions & 2 deletions tlslite/handshakesettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -255,6 +260,9 @@ 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
tomato42 marked this conversation as resolved.
Show resolved Hide resolved
self.record_size_limit = 2**14 + 1 # TLS 1.3 includes content type

def __init__(self):
Expand Down Expand Up @@ -475,6 +483,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
Expand All @@ -498,6 +510,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
Expand Down
130 changes: 82 additions & 48 deletions tlslite/tlsconnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -2206,57 +2206,93 @@ 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:
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,
[])

for result in self._sendMsg(new_ticket):
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

# 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(key,
settings.cipherImplementations)
else:
assert settings.ticketCipher == "chacha20-poly1305"
cipher = createCHACHA20(key,
settings.cipherImplementations)

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,
nonce + 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:
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'')
Expand Down Expand Up @@ -2400,12 +2436,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)
Expand Down Expand Up @@ -2467,8 +2504,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:

Expand All @@ -2484,13 +2520,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)

Expand Down Expand Up @@ -2519,8 +2553,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)
Expand All @@ -2530,7 +2563,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()
Expand Down
30 changes: 27 additions & 3 deletions tlslite/tlsrecordlayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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"""

Expand Down