Skip to content

Commit bdc72fc

Browse files
committed
Add key logging for TLS 1.3 and TLS 1.2 (#523)
SSLKEYLOGFILE implementation and tests
1 parent 30eb5ea commit bdc72fc

File tree

5 files changed

+426
-10
lines changed

5 files changed

+426
-10
lines changed

tests/tlstest.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2853,12 +2853,17 @@ def connect():
28532853
testConnServer(connection)
28542854
connection.close()
28552855
finally:
2856-
try:
2857-
os.remove(db_name)
2858-
except FileNotFoundError:
2859-
# dbm module may create files with different names depending on
2860-
# platform
2861-
os.remove(db_name + ".dat")
2856+
def quiet_remove(db_name):
2857+
try:
2858+
os.remove(db_name)
2859+
except OSError:
2860+
pass
2861+
2862+
# dbm module may create files with different names depending on
2863+
# platform
2864+
candidates = [db_name, db_name + ".dat", db_name + ".db"]
2865+
for candidate in candidates:
2866+
quiet_remove(candidate)
28622867

28632868
test_no += 1
28642869

tlslite/session.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ def create(self, masterSecret, sessionID, cipherSuite,
112112
sr_app_secret=bytearray(0), exporterMasterSecret=bytearray(0),
113113
resumptionMasterSecret=bytearray(0), tickets=None,
114114
tls_1_0_tickets=None, ec_point_format=None,
115-
delegated_credential=None):
115+
delegated_credential=None,
116+
cl_hs_traffic_secret=bytearray(0),
117+
sr_hs_traffic_secret=bytearray(0)):
116118
self.masterSecret = masterSecret
117119
self.sessionID = sessionID
118120
self.cipherSuite = cipherSuite
@@ -130,6 +132,8 @@ def create(self, masterSecret, sessionID, cipherSuite,
130132
self.sr_app_secret = sr_app_secret
131133
self.exporterMasterSecret = exporterMasterSecret
132134
self.resumptionMasterSecret = resumptionMasterSecret
135+
self.cl_handshake_traffic_secret = cl_hs_traffic_secret
136+
self.sr_handshake_traffic_secret = sr_hs_traffic_secret
133137
# NOTE we need a reference copy not a copy of object here!
134138
self.tickets = tickets
135139
self.tls_1_0_tickets = tls_1_0_tickets

tlslite/sslkeylogging.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 file pointed to by the SSLKEYLOGFILE
27+
environment variable. Implemented to be thread-safe at the class level and
28+
uses OS level file-locking. The file-locking implementation is a function
29+
assigned to self.lock_write and determined by the value of sys.platform.
30+
31+
Currently, POSIX is supported for thread and process safety. If enabled for
32+
other systems, safety is not guaranteed.
33+
34+
:param logfile_override: specify the filepath for logging
35+
:type logfile_override: str
36+
"""
37+
_lock = threading.Lock()
38+
39+
def __init__(self, logfile_override=None):
40+
self.ssl_key_logfile = os.environ.get('SSLKEYLOGFILE')
41+
if logfile_override:
42+
self.ssl_key_logfile = logfile_override
43+
self.platform = sys.platform
44+
if self.platform in ['darwin', 'linux']:
45+
self.lock_write = posix_lock_write
46+
else:
47+
self.lock_write = unsafe_write
48+
49+
def log_session_keys(self, keys):
50+
"""
51+
Log session keys to the SSL key log file, if configured.
52+
53+
Write session keys in the `SSLKEYLOGFILE` format, allowing tools like
54+
Wireshark to decrypt captured traffic. Each entry is written as a line
55+
with the format: ``<LABEL> <CLIENT_RANDOM> <SECRET>``.
56+
57+
If neither the `SSLKEYLOGFILE` environment variable nor a
58+
`logfile_override` is set, this method is a no-op.
59+
60+
:param keys: List of (label, client_random, secret) tuples.
61+
:type keys: list[tuple[str, bytes, bytes]]
62+
"""
63+
if not self.ssl_key_logfile:
64+
return
65+
66+
lines = [
67+
"{0} {1} {2}\n".format(
68+
label,
69+
b2a_hex(client_random).upper(),
70+
b2a_hex(secret).upper()
71+
)
72+
for label, client_random, secret in keys
73+
]
74+
75+
with self._lock:
76+
self.lock_write(self.ssl_key_logfile, lines)

tlslite/tlsconnection.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from .utils.cipherfactory import createAESCCM, createAESCCM_8, \
4444
createAESGCM, createCHACHA20
4545
from .utils.compression import choose_compression_send_algo
46+
from .sslkeylogging import SSLKeyLogger
4647

4748

4849
class TLSConnection(TLSRecordLayer):
@@ -80,14 +81,20 @@ class TLSConnection(TLSRecordLayer):
8081
compression wasn't used then it is set to None.
8182
"""
8283

83-
def __init__(self, sock):
84+
def __init__(self, sock, ssl_key_log_file=None):
8485
"""Create a new TLSConnection instance.
8586
8687
:param sock: The socket data will be transmitted on. The
8788
socket should already be connected. It may be in blocking or
8889
non-blocking mode.
8990
9091
:type sock: socket.socket
92+
93+
:param ssl_key_log_file: override location for logging session secrets.
94+
If not provided the filepath pointed to by the SSLKEYLOGFILE env
95+
variable will be used.
96+
97+
:type ssl_key_log_file: str
9198
"""
9299
TLSRecordLayer.__init__(self, sock)
93100
self.serverSigAlg = None
@@ -105,6 +112,7 @@ def __init__(self, sock):
105112
self._pha_supported = False
106113
self.client_cert_compression_algo = None
107114
self.server_cert_compression_algo = None
115+
self.ssl_key_logger = SSLKeyLogger(ssl_key_log_file)
108116

109117
def keyingMaterialExporter(self, label, length=20):
110118
"""Return keying material as described in RFC 5705
@@ -418,7 +426,6 @@ def _handshakeClientAsync(self, srpParams=(), certParams=(), anonParams=(),
418426
session=None, settings=None, checker=None,
419427
nextProtos=None, serverName=None, reqTack=True,
420428
alpn=None):
421-
422429
handshaker = self._handshakeClientAsyncHelper(srpParams=srpParams,
423430
certParams=certParams,
424431
anonParams=anonParams,
@@ -431,6 +438,16 @@ def _handshakeClientAsync(self, srpParams=(), certParams=(), anonParams=(),
431438
for result in self._handshakeWrapperAsync(handshaker, checker):
432439
yield result
433440

441+
# Log client random and master secret for version < TLS1.3
442+
# in the case of SRP fault, the session instance will be None
443+
if self.session is not None:
444+
if self.version < (3, 4):
445+
self.ssl_key_logger.log_session_keys([(
446+
'CLIENT_RANDOM',
447+
self._clientRandom,
448+
self.session.masterSecret
449+
)])
450+
434451

435452
def _handshakeClientAsyncHelper(self, srpParams, certParams, anonParams,
436453
session, settings, serverName, nextProtos,
@@ -1332,6 +1349,14 @@ def _clientTLS13Handshake(self, settings, session, clientHello,
13321349
self._handshake_hash,
13331350
prfName)
13341351

1352+
# TLS1.3 log Client and Server traffic secrets for SSLKEYLOGFILE
1353+
self.ssl_key_logger.log_session_keys([
1354+
('CLIENT_HANDSHAKE_TRAFFIC_SECRET',
1355+
clientHello.random, cl_handshake_traffic_secret),
1356+
('SERVER_HANDSHAKE_TRAFFIC_SECRET',
1357+
clientHello.random, sr_handshake_traffic_secret)
1358+
])
1359+
13351360
# prepare for reading encrypted messages
13361361
self._recordLayer.calcTLS1_3PendingState(
13371362
serverHello.cipher_suite,
@@ -1656,6 +1681,15 @@ def _clientTLS13Handshake(self, settings, session, clientHello,
16561681
bytearray(b'exp master'),
16571682
self._handshake_hash, prfName)
16581683

1684+
1685+
# Now that we have all the TLS1.3 secrets during the handshake,
1686+
# log them if necessary
1687+
self.ssl_key_logger.log_session_keys([
1688+
('EXPORTER_SECRET', clientHello.random, exporter_master_secret),
1689+
('CLIENT_TRAFFIC_SECRET_0', clientHello.random, cl_app_traffic),
1690+
('SERVER_TRAFFIC_SECRET_0', clientHello.random, sr_app_traffic)
1691+
])
1692+
16591693
self._recordLayer.calcTLS1_3PendingState(
16601694
serverHello.cipher_suite,
16611695
cl_app_traffic,
@@ -1752,7 +1786,9 @@ def _clientTLS13Handshake(self, settings, session, clientHello,
17521786
resumptionMasterSecret=resumption_master_secret,
17531787
# NOTE it must be a reference, not a copy!
17541788
tickets=self.tickets,
1755-
delegated_credential=delegated_credential)
1789+
delegated_credential=delegated_credential,
1790+
cl_hs_traffic_secret=cl_handshake_traffic_secret,
1791+
sr_hs_traffic_secret=sr_handshake_traffic_secret)
17561792

17571793
yield "finished" if not resuming else "resumed_and_finished"
17581794

@@ -2305,6 +2341,16 @@ def handshakeServerAsync(self, verifierDB=None,
23052341
for result in self._handshakeWrapperAsync(handshaker, checker):
23062342
yield result
23072343

2344+
# Log client random and master secret for version < TLS1.3
2345+
# in the case of SRP fault, the session instance will be None
2346+
if self.session is not None:
2347+
if self.version < (3, 4):
2348+
self.ssl_key_logger.log_session_keys([(
2349+
'CLIENT_RANDOM',
2350+
self._clientRandom,
2351+
self.session.masterSecret
2352+
)])
2353+
23082354

23092355
def _handshakeServerAsyncHelper(self, verifierDB,
23102356
cert_chain, privateKey, reqCert,
@@ -3032,6 +3078,15 @@ def _serverTLS13Handshake(self, settings, clientHello, cipherSuite,
30323078
bytearray(b'c hs traffic'),
30333079
self._handshake_hash,
30343080
prf_name)
3081+
3082+
# TLS1.3 log Client and Server traffic secrets for SSLKEYLOGFILE
3083+
self.ssl_key_logger.log_session_keys([
3084+
('CLIENT_HANDSHAKE_TRAFFIC_SECRET',
3085+
clientHello.random, cl_handshake_traffic_secret),
3086+
('SERVER_HANDSHAKE_TRAFFIC_SECRET',
3087+
clientHello.random, sr_handshake_traffic_secret)
3088+
])
3089+
30353090
self.version = version
30363091
self._recordLayer.calcTLS1_3PendingState(
30373092
cipherSuite,
@@ -3310,6 +3365,19 @@ def _serverTLS13Handshake(self, settings, clientHello, cipherSuite,
33103365
self._handshake_hash,
33113366
prf_name)
33123367

3368+
3369+
# Now that we have all the TLS1.3 secrets during the handshake,
3370+
# log them if necessary
3371+
self.ssl_key_logger.log_session_keys([
3372+
('EXPORTER_SECRET',
3373+
clientHello.random, exporter_master_secret),
3374+
('CLIENT_TRAFFIC_SECRET_0',
3375+
clientHello.random, cl_app_traffic),
3376+
('SERVER_TRAFFIC_SECRET_0',
3377+
clientHello.random, sr_app_traffic)
3378+
])
3379+
3380+
33133381
# verify Finished of client
33143382
cl_finished_key = HKDF_expand_label(cl_handshake_traffic_secret,
33153383
b"finished", b'',

0 commit comments

Comments
 (0)