Skip to content

Commit 8a64a1a

Browse files
committed
Add key logging for TLS 1.3 and TLS 1.2 (#523)
SSLKEYLOGFILE implementation and tests
1 parent 6805bcb commit 8a64a1a

File tree

4 files changed

+407
-4
lines changed

4 files changed

+407
-4
lines changed

tlslite/session.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ def create(self, masterSecret, sessionID, cipherSuite,
106106
appProto=bytearray(0), cl_app_secret=bytearray(0),
107107
sr_app_secret=bytearray(0), exporterMasterSecret=bytearray(0),
108108
resumptionMasterSecret=bytearray(0), tickets=None,
109-
tls_1_0_tickets=None, ec_point_format=None):
109+
tls_1_0_tickets=None, ec_point_format=None,
110+
cl_hs_traffic_secret=bytearray(0),
111+
sr_hs_traffic_secret=bytearray(0)):
110112
self.masterSecret = masterSecret
111113
self.sessionID = sessionID
112114
self.cipherSuite = cipherSuite
@@ -124,6 +126,8 @@ def create(self, masterSecret, sessionID, cipherSuite,
124126
self.sr_app_secret = sr_app_secret
125127
self.exporterMasterSecret = exporterMasterSecret
126128
self.resumptionMasterSecret = resumptionMasterSecret
129+
self.cl_handshake_traffic_secret = cl_hs_traffic_secret
130+
self.sr_handshake_traffic_secret = sr_hs_traffic_secret
127131
# NOTE we need a reference copy not a copy of object here!
128132
self.tickets = tickets
129133
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: 63 additions & 3 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,20 @@ 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, ssl_key_log_file=None):
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 ssl_key_log_file: override location for logging session secrets.
90+
If not provided the filepath pointed to by the SSLKEYLOGFILE env
91+
variable will be used.
92+
93+
:type ssl_key_log_file: str
8794
"""
8895
TLSRecordLayer.__init__(self, sock)
8996
self.serverSigAlg = None
@@ -101,6 +108,7 @@ def __init__(self, sock):
101108
self._pha_supported = False
102109
self.client_cert_compression_algo = None
103110
self.server_cert_compression_algo = None
111+
self.ssl_key_logger = SSLKeyLogger(ssl_key_log_file)
104112

105113
def keyingMaterialExporter(self, label, length=20):
106114
"""Return keying material as described in RFC 5705
@@ -414,7 +422,6 @@ def _handshakeClientAsync(self, srpParams=(), certParams=(), anonParams=(),
414422
session=None, settings=None, checker=None,
415423
nextProtos=None, serverName=None, reqTack=True,
416424
alpn=None):
417-
418425
handshaker = self._handshakeClientAsyncHelper(srpParams=srpParams,
419426
certParams=certParams,
420427
anonParams=anonParams,
@@ -427,6 +434,12 @@ def _handshakeClientAsync(self, srpParams=(), certParams=(), anonParams=(),
427434
for result in self._handshakeWrapperAsync(handshaker, checker):
428435
yield result
429436

437+
# Log client random and master secret for version < TLS1.3
438+
if self.version < (3, 4):
439+
self.ssl_key_logger.log_session_keys([(
440+
'CLIENT_RANDOM', self._clientRandom, self.session.masterSecret
441+
)])
442+
430443

431444
def _handshakeClientAsyncHelper(self, srpParams, certParams, anonParams,
432445
session, settings, serverName, nextProtos,
@@ -1323,6 +1336,14 @@ def _clientTLS13Handshake(self, settings, session, clientHello,
13231336
self._handshake_hash,
13241337
prfName)
13251338

1339+
# TLS1.3 log Client and Server traffic secrets for SSLKEYLOGFILE
1340+
self.ssl_key_logger.log_session_keys([
1341+
('CLIENT_HANDSHAKE_TRAFFIC_SECRET',
1342+
clientHello.random, cl_handshake_traffic_secret),
1343+
('SERVER_HANDSHAKE_TRAFFIC_SECRET',
1344+
clientHello.random, sr_handshake_traffic_secret)
1345+
])
1346+
13261347
# prepare for reading encrypted messages
13271348
self._recordLayer.calcTLS1_3PendingState(
13281349
serverHello.cipher_suite,
@@ -1613,6 +1634,15 @@ def _clientTLS13Handshake(self, settings, session, clientHello,
16131634
bytearray(b'exp master'),
16141635
self._handshake_hash, prfName)
16151636

1637+
1638+
# Now that we have all the TLS1.3 secrets during the handshake,
1639+
# log them if necessary
1640+
self.ssl_key_logger.log_session_keys([
1641+
('EXPORTER_SECRET', clientHello.random, exporter_master_secret),
1642+
('CLIENT_TRAFFIC_SECRET_0', clientHello.random, cl_app_traffic),
1643+
('SERVER_TRAFFIC_SECRET_0', clientHello.random, sr_app_traffic)
1644+
])
1645+
16161646
self._recordLayer.calcTLS1_3PendingState(
16171647
serverHello.cipher_suite,
16181648
cl_app_traffic,
@@ -1708,7 +1738,9 @@ def _clientTLS13Handshake(self, settings, session, clientHello,
17081738
exporterMasterSecret=exporter_master_secret,
17091739
resumptionMasterSecret=resumption_master_secret,
17101740
# NOTE it must be a reference, not a copy!
1711-
tickets=self.tickets)
1741+
tickets=self.tickets,
1742+
cl_hs_traffic_secret=cl_handshake_traffic_secret,
1743+
sr_hs_traffic_secret=sr_handshake_traffic_secret)
17121744

17131745
yield "finished" if not resuming else "resumed_and_finished"
17141746

@@ -2250,6 +2282,12 @@ def handshakeServerAsync(self, verifierDB=None,
22502282
for result in self._handshakeWrapperAsync(handshaker, checker):
22512283
yield result
22522284

2285+
# Log client random and master secret for version < TLS1.3
2286+
if self.version < (3, 4):
2287+
self.ssl_key_logger.log_session_keys([(
2288+
'CLIENT_RANDOM', self._clientRandom, self.session.masterSecret
2289+
)])
2290+
22532291

22542292
def _handshakeServerAsyncHelper(self, verifierDB,
22552293
cert_chain, privateKey, reqCert,
@@ -2956,6 +2994,15 @@ def _serverTLS13Handshake(self, settings, clientHello, cipherSuite,
29562994
bytearray(b'c hs traffic'),
29572995
self._handshake_hash,
29582996
prf_name)
2997+
2998+
# TLS1.3 log Client and Server traffic secrets for SSLKEYLOGFILE
2999+
self.ssl_key_logger.log_session_keys([
3000+
('CLIENT_HANDSHAKE_TRAFFIC_SECRET',
3001+
clientHello.random, cl_handshake_traffic_secret),
3002+
('SERVER_HANDSHAKE_TRAFFIC_SECRET',
3003+
clientHello.random, sr_handshake_traffic_secret)
3004+
])
3005+
29593006
self.version = version
29603007
self._recordLayer.calcTLS1_3PendingState(
29613008
cipherSuite,
@@ -3222,6 +3269,19 @@ def _serverTLS13Handshake(self, settings, clientHello, cipherSuite,
32223269
self._handshake_hash,
32233270
prf_name)
32243271

3272+
3273+
# Now that we have all the TLS1.3 secrets during the handshake,
3274+
# log them if necessary
3275+
self.ssl_key_logger.log_session_keys([
3276+
('EXPORTER_SECRET',
3277+
clientHello.random, exporter_master_secret),
3278+
('CLIENT_TRAFFIC_SECRET_0',
3279+
clientHello.random, cl_app_traffic),
3280+
('SERVER_TRAFFIC_SECRET_0',
3281+
clientHello.random, sr_app_traffic)
3282+
])
3283+
3284+
32253285
# verify Finished of client
32263286
cl_finished_key = HKDF_expand_label(cl_handshake_traffic_secret,
32273287
b"finished", b'',

0 commit comments

Comments
 (0)