Skip to content

Commit 03450b9

Browse files
committed
WIP: Add key logging for TLS 1.3 and TLS 1.2 (#523)
[WIP] sslkeylogfile continued add unsafe write for unrecognized platform i.e. windows
1 parent 6805bcb commit 03450b9

File tree

4 files changed

+237
-5
lines changed

4 files changed

+237
-5
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/sslkeylogging.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import os
2+
import sys
3+
import threading
4+
from .utils.compat import b2a_hex
5+
6+
7+
def posix_lock_write(file_path, lines):
8+
import fcntl
9+
with open(file_path, 'a') as f:
10+
fcntl.flock(f, fcntl.LOCK_EX)
11+
try:
12+
f.writelines(lines)
13+
f.flush()
14+
finally:
15+
fcntl.flock(f, fcntl.LOCK_UN)
16+
17+
18+
def unsafe_write(file_path, lines):
19+
with open(file_path, 'a') as f:
20+
f.writelines(lines)
21+
f.flush()
22+
23+
24+
class SSLKeyLogger:
25+
"""
26+
Write session secrets to the SSLKEYLOGFILE environment variable. Implemented to be thread-safe at the class
27+
level and uses OS level file-locking. The file-locking implementation is a function assigned to self.lock_write
28+
and determined by the value of sys.platform. Currently, POSIX is supported.
29+
30+
:param enabled: sets the logger to enabled
31+
:type enabled: bool
32+
"""
33+
_lock = threading.Lock()
34+
35+
def __init__(self, enabled=False):
36+
self.enabled = enabled
37+
self.ssl_key_logfile = os.environ.get('SSLKEYLOGFILE')
38+
self.platform = sys.platform
39+
if self.platform in ['darwin', 'linux']:
40+
self.lock_write = posix_lock_write
41+
else:
42+
# TODO warn user of unsafe?
43+
self.lock_write = unsafe_write
44+
45+
def log_session_keys(self, keys):
46+
# no-op if not enabled or if SSLKEYLOGFILE env variable isn't set
47+
if self.ssl_key_logfile is None or not self.enabled:
48+
return
49+
50+
if isinstance(keys, tuple):
51+
keys = [keys]
52+
53+
lines = [
54+
"{0} {1} {2}\n".format(
55+
label,
56+
b2a_hex(client_random).upper(),
57+
b2a_hex(secret).upper()
58+
)
59+
for label, client_random, secret in keys
60+
]
61+
62+
with self._lock:
63+
self.lock_write(self.ssl_key_logfile, lines)

tlslite/tlsconnection.py

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from .utils.cipherfactory import createAESCCM, createAESCCM_8, \
4141
createAESGCM, createCHACHA20
4242
from .utils.compression import choose_compression_send_algo
43+
from .sslkeylogging import SSLKeyLogger
4344

4445
class TLSConnection(TLSRecordLayer):
4546
"""
@@ -76,14 +77,17 @@ class TLSConnection(TLSRecordLayer):
7677
compression wasn't used then it is set to None.
7778
"""
7879

79-
def __init__(self, sock):
80+
def __init__(self, sock, logging_enabled=False):
8081
"""Create a new TLSConnection instance.
8182
8283
:param sock: The socket data will be transmitted on. The
8384
socket should already be connected. It may be in blocking or
8485
non-blocking mode.
8586
8687
:type sock: socket.socket
88+
89+
:param logging_enabled: Enable logging session secrets to the SSLKEYLOGFILE env variable.
90+
:type logging_enabled: bool
8791
"""
8892
TLSRecordLayer.__init__(self, sock)
8993
self.serverSigAlg = None
@@ -101,6 +105,7 @@ def __init__(self, sock):
101105
self._pha_supported = False
102106
self.client_cert_compression_algo = None
103107
self.server_cert_compression_algo = None
108+
self.ssl_key_logger = SSLKeyLogger(logging_enabled)
104109

105110
def keyingMaterialExporter(self, label, length=20):
106111
"""Return keying material as described in RFC 5705
@@ -414,7 +419,6 @@ def _handshakeClientAsync(self, srpParams=(), certParams=(), anonParams=(),
414419
session=None, settings=None, checker=None,
415420
nextProtos=None, serverName=None, reqTack=True,
416421
alpn=None):
417-
418422
handshaker = self._handshakeClientAsyncHelper(srpParams=srpParams,
419423
certParams=certParams,
420424
anonParams=anonParams,
@@ -427,6 +431,10 @@ def _handshakeClientAsync(self, srpParams=(), certParams=(), anonParams=(),
427431
for result in self._handshakeWrapperAsync(handshaker, checker):
428432
yield result
429433

434+
# Log client random and master secret for version < TLS1.3
435+
if self.version < (3, 4):
436+
self.ssl_key_logger.log_session_keys(('CLIENT_RANDOM', self._clientRandom, self.session.masterSecret))
437+
430438

431439
def _handshakeClientAsyncHelper(self, srpParams, certParams, anonParams,
432440
session, settings, serverName, nextProtos,
@@ -1323,6 +1331,14 @@ def _clientTLS13Handshake(self, settings, session, clientHello,
13231331
self._handshake_hash,
13241332
prfName)
13251333

1334+
# TLS1.3 log Client and Server traffic secrets for SSLKEYLOGFILE
1335+
self.ssl_key_logger.log_session_keys([
1336+
('CLIENT_HANDSHAKE_TRAFFIC_SECRET',
1337+
clientHello.random, cl_handshake_traffic_secret),
1338+
('SERVER_HANDSHAKE_TRAFFIC_SECRET',
1339+
clientHello.random, sr_handshake_traffic_secret)
1340+
])
1341+
13261342
# prepare for reading encrypted messages
13271343
self._recordLayer.calcTLS1_3PendingState(
13281344
serverHello.cipher_suite,
@@ -1613,6 +1629,15 @@ def _clientTLS13Handshake(self, settings, session, clientHello,
16131629
bytearray(b'exp master'),
16141630
self._handshake_hash, prfName)
16151631

1632+
1633+
# Now that we have all the TLS1.3 secrets during the handshake,
1634+
# log them if necessary
1635+
self.ssl_key_logger.log_session_keys([
1636+
('EXPORTER_SECRET', clientHello.random, exporter_master_secret),
1637+
('CLIENT_TRAFFIC_SECRET_0', clientHello.random, cl_app_traffic),
1638+
('SERVER_TRAFFIC_SECRET_0', clientHello.random, sr_app_traffic)
1639+
])
1640+
16161641
self._recordLayer.calcTLS1_3PendingState(
16171642
serverHello.cipher_suite,
16181643
cl_app_traffic,
@@ -1708,7 +1733,9 @@ def _clientTLS13Handshake(self, settings, session, clientHello,
17081733
exporterMasterSecret=exporter_master_secret,
17091734
resumptionMasterSecret=resumption_master_secret,
17101735
# NOTE it must be a reference, not a copy!
1711-
tickets=self.tickets)
1736+
tickets=self.tickets,
1737+
cl_hs_traffic_secret=cl_handshake_traffic_secret,
1738+
sr_hs_traffic_secret=sr_handshake_traffic_secret)
17121739

17131740
yield "finished" if not resuming else "resumed_and_finished"
17141741

@@ -2250,6 +2277,12 @@ def handshakeServerAsync(self, verifierDB=None,
22502277
for result in self._handshakeWrapperAsync(handshaker, checker):
22512278
yield result
22522279

2280+
# Log client random and master secret for version < TLS1.3
2281+
if self.version < (3, 4):
2282+
self.ssl_key_logger.log_session_keys(('CLIENT_RANDOM',
2283+
self._clientRandom,
2284+
self.session.masterSecret))
2285+
22532286

22542287
def _handshakeServerAsyncHelper(self, verifierDB,
22552288
cert_chain, privateKey, reqCert,
@@ -2956,6 +2989,15 @@ def _serverTLS13Handshake(self, settings, clientHello, cipherSuite,
29562989
bytearray(b'c hs traffic'),
29572990
self._handshake_hash,
29582991
prf_name)
2992+
2993+
# TLS1.3 log Client and Server traffic secrets for SSLKEYLOGFILE
2994+
self.ssl_key_logger.log_session_keys([
2995+
('CLIENT_HANDSHAKE_TRAFFIC_SECRET',
2996+
clientHello.random, cl_handshake_traffic_secret),
2997+
('SERVER_HANDSHAKE_TRAFFIC_SECRET',
2998+
clientHello.random, sr_handshake_traffic_secret)
2999+
])
3000+
29593001
self.version = version
29603002
self._recordLayer.calcTLS1_3PendingState(
29613003
cipherSuite,
@@ -3222,6 +3264,19 @@ def _serverTLS13Handshake(self, settings, clientHello, cipherSuite,
32223264
self._handshake_hash,
32233265
prf_name)
32243266

3267+
3268+
# Now that we have all the TLS1.3 secrets during the handshake,
3269+
# log them if necessary
3270+
self.ssl_key_logger.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()
@@ -4934,6 +4991,7 @@ def _calculate_master_secret(self, premaster_secret, cipher_suite,
49344991
output_length=48)
49354992
return secret
49364993

4994+
49374995
@staticmethod
49384996
def _pickServerKeyExchangeSig(settings, clientHello, certList=None,
49394997
private_key=None,
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
try:
2+
import unittest2 as unittest
3+
except ImportError:
4+
import unittest
5+
6+
import os
7+
from threading import Thread
8+
import tempfile
9+
import random
10+
import unittest
11+
12+
from tlslite.sslkeylogging import SSLKeyLogger
13+
from tlslite.utils.compat import b2a_hex
14+
15+
16+
class TestSslKeyLogFile(unittest.TestCase):
17+
def __init__(self, *args, **kwargs):
18+
super(TestSslKeyLogFile, self).__init__(*args, **kwargs)
19+
20+
def setUp(self):
21+
self.temp_log_file = tempfile.NamedTemporaryFile(delete=False)
22+
os.environ['SSLKEYLOGFILE'] = self.temp_log_file.name
23+
24+
def tearDown(self):
25+
os.remove(self.temp_log_file.name)
26+
if os.environ.get('SSLKEYLOGFILE'):
27+
del os.environ['SSLKEYLOGFILE']
28+
29+
def test_pre_13(self):
30+
logger_count = 3
31+
32+
loggers = []
33+
for i in range(logger_count):
34+
loggers.append(SSLKeyLogger())
35+
36+
threads = []
37+
expected_labels = []
38+
for i in range(logger_count):
39+
client_random = random.randbytes(32)
40+
master_secret = random.randbytes(48)
41+
label = ("CLIENT_RANDOM", client_random, master_secret)
42+
expected_labels.append(label)
43+
logger = loggers[i]
44+
threads.append(Thread(target=logger.log_session_keys, args=(label,)))
45+
46+
for thread in threads:
47+
thread.start()
48+
49+
for thread in threads:
50+
thread.join(10)
51+
52+
self.validate_log_file(expected_labels)
53+
54+
def test_13(self):
55+
logger_count = 3
56+
57+
loggers = []
58+
for i in range(logger_count):
59+
loggers.append(SSLKeyLogger())
60+
61+
threads = []
62+
expected_labels = []
63+
for i in range(logger_count):
64+
client_random = random.randbytes(32)
65+
labels = [
66+
("SERVER_HANDSHAKE_TRAFFIC_SECRET", client_random, random.randbytes(32)),
67+
("EXPORTER_SECRET", client_random, random.randbytes(32)),
68+
("SERVER_TRAFFIC_SECRET_0", client_random, random.randbytes(32)),
69+
("CLIENT_HANDSHAKE_TRAFFIC_SECRET", client_random, random.randbytes(32)),
70+
("CLIENT_TRAFFIC_SECRET_0", client_random, random.randbytes(32))
71+
]
72+
expected_labels.append(labels)
73+
logger = loggers[i]
74+
threads.append(Thread(target=logger.log_session_keys, args=(labels,)))
75+
76+
for thread in threads:
77+
thread.start()
78+
79+
for thread in threads:
80+
thread.join(10)
81+
82+
self.validate_log_file(expected_labels)
83+
84+
def validate_log_file(self, all_labels):
85+
"""
86+
Validates lines in SSLKEYLOGFILE for both TLS 1.2 and TLS 1.3
87+
"""
88+
with open(self.temp_log_file.name, 'r') as log_file:
89+
lines = [log_line.strip() for log_line in log_file.readlines()]
90+
for labels in all_labels:
91+
if isinstance(labels, tuple):
92+
client_random = b2a_hex(labels[1]).upper()
93+
master_secret = b2a_hex(labels[2]).upper()
94+
expected_label = "{0} {1} {2}".format(labels[0], client_random, master_secret)
95+
self.assertTrue(expected_label in lines, "Expected: {0}".format(expected_label))
96+
elif isinstance(labels, list):
97+
for label_name, client_random, secret in labels:
98+
expected_label = "{0} {1} {2}".format(
99+
label_name, b2a_hex(client_random).upper(), b2a_hex(secret).upper()
100+
)
101+
self.assertTrue(expected_label in lines, "Didn't find expected: {0}".format(expected_label))
102+
103+
104+
if __name__ == "__main__":
105+
unittest.main()

0 commit comments

Comments
 (0)