Skip to content

Commit 67bd0c8

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

File tree

3 files changed

+196
-4
lines changed

3 files changed

+196
-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_hs_traffic_secret=bytearray(0),
113+
sr_hs_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_hs_traffic_secret
132+
self.sr_handshake_traffic_secret = sr_hs_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: 78 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,15 @@ 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([
1333+
('CLIENT_HANDSHAKE_TRAFFIC_SECRET',
1334+
clientHello.random, cl_handshake_traffic_secret),
1335+
('SERVER_HANDSHAKE_TRAFFIC_SECRET',
1336+
clientHello.random, sr_handshake_traffic_secret)
1337+
])
1338+
13261339
# prepare for reading encrypted messages
13271340
self._recordLayer.calcTLS1_3PendingState(
13281341
serverHello.cipher_suite,
@@ -1613,6 +1626,16 @@ def _clientTLS13Handshake(self, settings, session, clientHello,
16131626
bytearray(b'exp master'),
16141627
self._handshake_hash, prfName)
16151628

1629+
1630+
# Now that we have all the TLS1.3 secrets during the handshake,
1631+
# log them if necessary
1632+
if self.sslkeylogfile:
1633+
self._log_session_keys([
1634+
('EXPORTER_SECRET', clientHello.random, exporter_master_secret),
1635+
('CLIENT_TRAFFIC_SECRET_0', clientHello.random, cl_app_traffic),
1636+
('SERVER_TRAFFIC_SECRET_0', clientHello.random, sr_app_traffic)
1637+
])
1638+
16161639
self._recordLayer.calcTLS1_3PendingState(
16171640
serverHello.cipher_suite,
16181641
cl_app_traffic,
@@ -1708,7 +1731,9 @@ def _clientTLS13Handshake(self, settings, session, clientHello,
17081731
exporterMasterSecret=exporter_master_secret,
17091732
resumptionMasterSecret=resumption_master_secret,
17101733
# NOTE it must be a reference, not a copy!
1711-
tickets=self.tickets)
1734+
tickets=self.tickets,
1735+
cl_hs_traffic_secret=cl_handshake_traffic_secret,
1736+
sr_hs_traffic_secret=sr_handshake_traffic_secret)
17121737

17131738
yield "finished" if not resuming else "resumed_and_finished"
17141739

@@ -2250,6 +2275,12 @@ def handshakeServerAsync(self, verifierDB=None,
22502275
for result in self._handshakeWrapperAsync(handshaker, checker):
22512276
yield result
22522277

2278+
# Log client random and master secret for version < TLS1.3
2279+
if self.sslkeylogfile and self.version < (3, 4):
2280+
self._log_session_keys(('CLIENT_RANDOM',
2281+
self._clientRandom,
2282+
self.session.masterSecret))
2283+
22532284

22542285
def _handshakeServerAsyncHelper(self, verifierDB,
22552286
cert_chain, privateKey, reqCert,
@@ -2956,6 +2987,16 @@ def _serverTLS13Handshake(self, settings, clientHello, cipherSuite,
29562987
bytearray(b'c hs traffic'),
29572988
self._handshake_hash,
29582989
prf_name)
2990+
2991+
# TLS1.3 log Client and Server traffic secrets for SSLKEYLOGFILE
2992+
if self.sslkeylogfile:
2993+
self._log_session_keys([
2994+
('CLIENT_HANDSHAKE_TRAFFIC_SECRET',
2995+
clientHello.random, cl_handshake_traffic_secret),
2996+
('SERVER_HANDSHAKE_TRAFFIC_SECRET',
2997+
clientHello.random, sr_handshake_traffic_secret)
2998+
])
2999+
29593000
self.version = version
29603001
self._recordLayer.calcTLS1_3PendingState(
29613002
cipherSuite,
@@ -3222,6 +3263,20 @@ def _serverTLS13Handshake(self, settings, clientHello, cipherSuite,
32223263
self._handshake_hash,
32233264
prf_name)
32243265

3266+
3267+
# Now that we have all the TLS1.3 secrets during the handshake,
3268+
# log them if necessary
3269+
if self.sslkeylogfile:
3270+
self._log_session_keys([
3271+
('EXPORTER_SECRET',
3272+
clientHello.random, exporter_master_secret),
3273+
('CLIENT_TRAFFIC_SECRET_0',
3274+
clientHello.random, cl_app_traffic),
3275+
('SERVER_TRAFFIC_SECRET_0',
3276+
clientHello.random, sr_app_traffic)
3277+
])
3278+
3279+
32253280
# verify Finished of client
32263281
cl_finished_key = HKDF_expand_label(cl_handshake_traffic_secret,
32273282
b"finished", b'',
@@ -3285,7 +3340,9 @@ def _serverTLS13Handshake(self, settings, clientHello, cipherSuite,
32853340
exporterMasterSecret=exporter_master_secret,
32863341
resumptionMasterSecret=resumption_master_secret,
32873342
# NOTE it must be a reference, not a copy
3288-
tickets=self.tickets)
3343+
tickets=self.tickets,
3344+
cl_hs_traffic_secret=cl_handshake_traffic_secret,
3345+
sr_hs_traffic_secret=sr_handshake_traffic_secret)
32893346

32903347
# switch to application_traffic_secret for client packets
32913348
self._changeReadState()
@@ -4738,6 +4795,11 @@ def _serverFinished(self, premasterSecret, clientRandom, serverRandom,
47384795
clientRandom, serverRandom,
47394796
cipherImplementations)
47404797

4798+
#Log client random and master secret if SSLKEYLOGFILE is set
4799+
if self.sslkeylogfile and self.version < (3, 3):
4800+
print('Logging session keys in serverFinished')
4801+
self._log_session_keys(("CLIENT_RANDOM", clientRandom, masterSecret))
4802+
47414803
#Exchange ChangeCipherSpec and Finished messages
47424804
for result in self._getFinished(masterSecret,
47434805
cipherSuite,
@@ -4934,6 +4996,19 @@ def _calculate_master_secret(self, premaster_secret, cipher_suite,
49344996
output_length=48)
49354997
return secret
49364998

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

0 commit comments

Comments
 (0)