Skip to content

Commit cc7f339

Browse files
committed
WIP: Add key logging for TLS 1.3 and TLS 1.2 (#523)
1 parent c93df82 commit cc7f339

File tree

3 files changed

+173
-4
lines changed

3 files changed

+173
-4
lines changed

tlslite/session.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ def __init__(self):
9898
self.tickets = None
9999
self.tls_1_0_tickets = None
100100
self.ec_point_format = 0
101+
self.cl_handshake_traffic_secret = None
102+
self.sr_handshake_traffic_secret = None
101103

102104
def create(self, masterSecret, sessionID, cipherSuite,
103105
srpUsername, clientCertChain, serverCertChain,
@@ -106,7 +108,9 @@ def create(self, masterSecret, sessionID, cipherSuite,
106108
appProto=bytearray(0), cl_app_secret=bytearray(0),
107109
sr_app_secret=bytearray(0), exporterMasterSecret=bytearray(0),
108110
resumptionMasterSecret=bytearray(0), tickets=None,
109-
tls_1_0_tickets=None, ec_point_format=None):
111+
tls_1_0_tickets=None, ec_point_format=None,
112+
cl_handshake_traffic_secret=bytearray(0),
113+
sr_handshake_traffic_secret=bytearray(0)):
110114
self.masterSecret = masterSecret
111115
self.sessionID = sessionID
112116
self.cipherSuite = cipherSuite
@@ -124,6 +128,8 @@ def create(self, masterSecret, sessionID, cipherSuite,
124128
self.sr_app_secret = sr_app_secret
125129
self.exporterMasterSecret = exporterMasterSecret
126130
self.resumptionMasterSecret = resumptionMasterSecret
131+
self.cl_handshake_traffic_secret = cl_handshake_traffic_secret
132+
self.sr_handshake_traffic_secret = sr_handshake_traffic_secret
127133
# NOTE we need a reference copy not a copy of object here!
128134
self.tickets = tickets
129135
self.tls_1_0_tickets = tls_1_0_tickets

tlslite/tlsconnection.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def __init__(self, sock):
101101
self._pha_supported = False
102102
self.client_cert_compression_algo = None
103103
self.server_cert_compression_algo = None
104+
self.sslkeylogfile = os.environ.get('SSLKEYLOGFILE')
104105

105106
def keyingMaterialExporter(self, label, length=20):
106107
"""Return keying material as described in RFC 5705
@@ -414,7 +415,6 @@ def _handshakeClientAsync(self, srpParams=(), certParams=(), anonParams=(),
414415
session=None, settings=None, checker=None,
415416
nextProtos=None, serverName=None, reqTack=True,
416417
alpn=None):
417-
418418
handshaker = self._handshakeClientAsyncHelper(srpParams=srpParams,
419419
certParams=certParams,
420420
anonParams=anonParams,
@@ -427,6 +427,10 @@ def _handshakeClientAsync(self, srpParams=(), certParams=(), anonParams=(),
427427
for result in self._handshakeWrapperAsync(handshaker, checker):
428428
yield result
429429

430+
# Log client random and master secret for version < TLS1.3
431+
if self.sslkeylogfile and self.version < (3, 4):
432+
self._log_session_keys('CLIENT_RANDOM', self._clientRandom, self.session.masterSecret)
433+
430434

431435
def _handshakeClientAsyncHelper(self, srpParams, certParams, anonParams,
432436
session, settings, serverName, nextProtos,
@@ -1323,6 +1327,13 @@ def _clientTLS13Handshake(self, settings, session, clientHello,
13231327
self._handshake_hash,
13241328
prfName)
13251329

1330+
# TLS1.3 log Client and Server traffic secrets for SSLKEYLOGFILE
1331+
if self.sslkeylogfile:
1332+
self._log_session_keys('CLIENT_HANDSHAKE_TRAFFIC_SECRET',
1333+
clientHello.random,
1334+
cl_handshake_traffic_secret)
1335+
self._log_session_keys('SERVER_HANDSHAKE_TRAFFIC_SECRET', clientHello.random, sr_handshake_traffic_secret)
1336+
13261337
# prepare for reading encrypted messages
13271338
self._recordLayer.calcTLS1_3PendingState(
13281339
serverHello.cipher_suite,
@@ -1613,6 +1624,13 @@ def _clientTLS13Handshake(self, settings, session, clientHello,
16131624
bytearray(b'exp master'),
16141625
self._handshake_hash, prfName)
16151626

1627+
1628+
# Now that we have all the TLS1.3 secrets during the handshake, log them if necessary
1629+
if self.sslkeylogfile:
1630+
self._log_session_keys('EXPORTER_SECRET', clientHello.random, exporter_master_secret)
1631+
self._log_session_keys('CLIENT_TRAFFIC_SECRET_0', clientHello.random, cl_app_traffic)
1632+
self._log_session_keys('SERVER_TRAFFIC_SECRET_0', clientHello.random, sr_app_traffic)
1633+
16161634
self._recordLayer.calcTLS1_3PendingState(
16171635
serverHello.cipher_suite,
16181636
cl_app_traffic,
@@ -1708,7 +1726,9 @@ def _clientTLS13Handshake(self, settings, session, clientHello,
17081726
exporterMasterSecret=exporter_master_secret,
17091727
resumptionMasterSecret=resumption_master_secret,
17101728
# NOTE it must be a reference, not a copy!
1711-
tickets=self.tickets)
1729+
tickets=self.tickets,
1730+
cl_handshake_traffic_secret=cl_handshake_traffic_secret,
1731+
sr_handshake_traffic_secret=sr_handshake_traffic_secret)
17121732

17131733
yield "finished" if not resuming else "resumed_and_finished"
17141734

@@ -2250,6 +2270,10 @@ def handshakeServerAsync(self, verifierDB=None,
22502270
for result in self._handshakeWrapperAsync(handshaker, checker):
22512271
yield result
22522272

2273+
# Log client random and master secret for version < TLS1.3
2274+
if self.sslkeylogfile and self.version < (3, 4):
2275+
self._log_session_keys('CLIENT_RANDOM', self._clientRandom, self.session.masterSecret)
2276+
22532277

22542278
def _handshakeServerAsyncHelper(self, verifierDB,
22552279
cert_chain, privateKey, reqCert,
@@ -2956,6 +2980,14 @@ def _serverTLS13Handshake(self, settings, clientHello, cipherSuite,
29562980
bytearray(b'c hs traffic'),
29572981
self._handshake_hash,
29582982
prf_name)
2983+
2984+
# TLS1.3 log Client and Server traffic secrets for SSLKEYLOGFILE
2985+
if self.sslkeylogfile:
2986+
self._log_session_keys('CLIENT_HANDSHAKE_TRAFFIC_SECRET',
2987+
clientHello.random,
2988+
cl_handshake_traffic_secret)
2989+
self._log_session_keys('SERVER_HANDSHAKE_TRAFFIC_SECRET', clientHello.random, sr_handshake_traffic_secret)
2990+
29592991
self.version = version
29602992
self._recordLayer.calcTLS1_3PendingState(
29612993
cipherSuite,
@@ -3222,6 +3254,14 @@ def _serverTLS13Handshake(self, settings, clientHello, cipherSuite,
32223254
self._handshake_hash,
32233255
prf_name)
32243256

3257+
3258+
# Now that we have all the TLS1.3 secrets during the handshake, log them if necessary
3259+
if self.sslkeylogfile:
3260+
self._log_session_keys('EXPORTER_SECRET', clientHello.random, exporter_master_secret)
3261+
self._log_session_keys('CLIENT_TRAFFIC_SECRET_0', clientHello.random, cl_app_traffic)
3262+
self._log_session_keys('SERVER_TRAFFIC_SECRET_0', clientHello.random, sr_app_traffic)
3263+
3264+
32253265
# verify Finished of client
32263266
cl_finished_key = HKDF_expand_label(cl_handshake_traffic_secret,
32273267
b"finished", b'',
@@ -3285,7 +3325,9 @@ def _serverTLS13Handshake(self, settings, clientHello, cipherSuite,
32853325
exporterMasterSecret=exporter_master_secret,
32863326
resumptionMasterSecret=resumption_master_secret,
32873327
# NOTE it must be a reference, not a copy
3288-
tickets=self.tickets)
3328+
tickets=self.tickets,
3329+
cl_handshake_traffic_secret=cl_handshake_traffic_secret,
3330+
sr_handshake_traffic_secret=sr_handshake_traffic_secret)
32893331

32903332
# switch to application_traffic_secret for client packets
32913333
self._changeReadState()
@@ -4738,6 +4780,11 @@ def _serverFinished(self, premasterSecret, clientRandom, serverRandom,
47384780
clientRandom, serverRandom,
47394781
cipherImplementations)
47404782

4783+
#Log client random and master secret if SSLKEYLOGFILE is set
4784+
if self.sslkeylogfile and self.version < (3, 3):
4785+
print('Logging session keys in serverFinished')
4786+
self._log_session_keys("CLIENT_RANDOM", clientRandom, masterSecret)
4787+
47414788
#Exchange ChangeCipherSpec and Finished messages
47424789
for result in self._getFinished(masterSecret,
47434790
cipherSuite,
@@ -4934,6 +4981,10 @@ def _calculate_master_secret(self, premaster_secret, cipher_suite,
49344981
output_length=48)
49354982
return secret
49364983

4984+
def _log_session_keys(self, secret_label, client_random, secret):
4985+
with open(self.sslkeylogfile, 'a') as ssl_key_log_file:
4986+
ssl_key_log_file.write(f"{secret_label} {client_random.hex()} {secret.hex()}\n")
4987+
49374988
@staticmethod
49384989
def _pickServerKeyExchangeSig(settings, clientHello, certList=None,
49394990
private_key=None,
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
try:
2+
import unittest2 as unittest
3+
except ImportError:
4+
import unittest
5+
6+
import tempfile
7+
from socket import socket, AF_INET, SOCK_STREAM
8+
import os
9+
from typing import Dict, List
10+
11+
import unittest
12+
13+
from tlslite.api import TLSConnection, HandshakeSettings
14+
from tlslite.errors import TLSRemoteAlert
15+
16+
17+
def create_connection(hostname='www.example.com', port=443, settings=HandshakeSettings()):
18+
raw_socket = socket(AF_INET, SOCK_STREAM)
19+
raw_socket.connect((hostname, port))
20+
connection = TLSConnection(raw_socket)
21+
connection.handshakeClientCert(settings=settings)
22+
return connection
23+
24+
25+
def validate_log_file(log_file_name, labels: Dict[str, List[str]]):
26+
"""
27+
TLS1.3 labels
28+
------------------
29+
SERVER_HANDSHAKE_TRAFFIC_SECRET <client_random> <secret>
30+
EXPORTER_SECRET <client_random> <secret>
31+
SERVER_TRAFFIC_SECRET_0 <client_random> <secret>
32+
CLIENT_HANDSHAKE_TRAFFIC_SECRET <client_random> <secret>
33+
CLIENT_TRAFFIC_SECRET_0 <client_random> <secret>
34+
35+
TLS1.2 (and below)
36+
----------------
37+
CLIENT_RANDOM <client_random> <secret>
38+
"""
39+
with open(log_file_name, 'r') as log_file:
40+
for line in log_file.readlines():
41+
entry = line.split()
42+
label, client_random, secret = entry[0], entry[1], entry[2]
43+
if label in labels and labels[label][1] == client_random and labels[label][2] != secret:
44+
return False
45+
46+
return True
47+
48+
49+
class TestSslKeyLogFile(unittest.TestCase):
50+
def setUp(self):
51+
self.temp_log_file = tempfile.NamedTemporaryFile(delete_on_close=False)
52+
os.environ['SSLKEYLOGFILE'] = self.temp_log_file.name
53+
54+
def tearDown(self):
55+
# Clean up the temporary file
56+
os.remove(self.temp_log_file.name)
57+
if os.environ.get('SSLKEYLOGFILE'):
58+
del os.environ['SSLKEYLOGFILE']
59+
60+
def test_tlsv1_1(self):
61+
settings = HandshakeSettings()
62+
settings.minVersion = (3,2)
63+
settings.maxVersion = (3,2)
64+
65+
try:
66+
connection = create_connection('www.example.com', 443, settings)
67+
expected_labels = {
68+
'CLIENT_RANDOM': connection._clientRandom
69+
}
70+
self.assertTrue(validate_log_file(self.temp_log_file.name, expected_labels))
71+
connection.close()
72+
except TLSRemoteAlert as alert:
73+
print(f"TLS Remote Alert: {alert}")
74+
75+
76+
def test_tlsv1_2(self):
77+
settings = HandshakeSettings()
78+
settings.minVersion = (3,3)
79+
settings.maxVersion = (3,3)
80+
81+
try:
82+
connection = create_connection('www.example.com', 443, settings)
83+
expected_labels = {
84+
'CLIENT_RANDOM': connection._clientRandom
85+
}
86+
self.assertTrue(validate_log_file(self.temp_log_file.name, expected_labels))
87+
connection.close()
88+
except TLSRemoteAlert as alert:
89+
print(f"TLS Remote Alert: {alert}")
90+
91+
def test_tlsv1_3(self):
92+
settings = HandshakeSettings()
93+
settings.minVersion = (3, 4)
94+
settings.maxVersion = (3, 4)
95+
96+
try:
97+
connection = create_connection('www.example.com', 443, settings)
98+
expected_labels = {
99+
"SERVER_HANDSHAKE_TRAFFIC_SECRET": connection.session.sr_handshake_traffic_secret,
100+
"EXPORTER_SECRET": connection.session.exporterMasterSecret,
101+
"SERVER_TRAFFIC_SECRET_0": connection.session.sr_app_secret,
102+
"CLIENT_HANDSHAKE_TRAFFIC_SECRET": connection.session.cl_handshake_traffic_secret,
103+
"CLIENT_TRAFFIC_SECRET_0": connection.session.cl_app_secret
104+
}
105+
self.assertTrue(validate_log_file(self.temp_log_file.name, expected_labels))
106+
connection.close()
107+
except TLSRemoteAlert as alert:
108+
print(f"TLS Remote Alert: {alert}")
109+
110+
111+
if __name__ == "__main__":
112+
unittest.main()

0 commit comments

Comments
 (0)