Skip to content

Commit

Permalink
bpo-34271: Add ssl debugging helpers (pythonGH-10031)
Browse files Browse the repository at this point in the history
The ssl module now can dump key material to a keylog file and trace TLS
protocol messages with a tracing callback. The default and stdlib
contexts also support SSLKEYLOGFILE env var.

The msg_callback and related enums are private members. The feature
is designed for internal debugging and not for end users.

Signed-off-by: Christian Heimes <[email protected]>
  • Loading branch information
tiran authored May 31, 2019
1 parent e9b51c0 commit c7f7069
Show file tree
Hide file tree
Showing 7 changed files with 677 additions and 18 deletions.
23 changes: 23 additions & 0 deletions Doc/library/ssl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ purposes.
*cadata* is given) or uses :meth:`SSLContext.load_default_certs` to load
default CA certificates.

When :attr:`~SSLContext.keylog_filename` is supported and the environment
variable :envvar:`SSLKEYLOGFILE` is set, :func:`create_default_context`
enables key logging.

.. note::
The protocol, options, cipher and other settings may change to more
restrictive values anytime without prior deprecation. The values
Expand Down Expand Up @@ -172,6 +176,10 @@ purposes.

3DES was dropped from the default cipher string.

.. versionchanged:: 3.8

Support for key logging to :envvar:`SSLKEYLOGFILE` was added.


Exceptions
^^^^^^^^^^
Expand Down Expand Up @@ -1056,6 +1064,7 @@ Constants

SSL 3.0 to TLS 1.3.


SSL Sockets
-----------

Expand Down Expand Up @@ -1901,6 +1910,20 @@ to speed up repeated connections from the same clients.

This features requires OpenSSL 0.9.8f or newer.

.. attribute:: SSLContext.keylog_filename

Write TLS keys to a keylog file, whenever key material is generated or
received. The keylog file is designed for debugging purposes only. The
file format is specified by NSS and used by many traffic analyzers such
as Wireshark. The log file is opened in append-only mode. Writes are
synchronized between threads, but not between processes.

.. versionadded:: 3.8

.. note::

This features requires OpenSSL 1.1.1 or newer.

.. attribute:: SSLContext.maximum_version

A :class:`TLSVersion` enum member representing the highest supported
Expand Down
172 changes: 171 additions & 1 deletion Lib/ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,90 @@ class TLSVersion(_IntEnum):
MAXIMUM_SUPPORTED = _ssl.PROTO_MAXIMUM_SUPPORTED


class _TLSContentType(_IntEnum):
"""Content types (record layer)
See RFC 8446, section B.1
"""
CHANGE_CIPHER_SPEC = 20
ALERT = 21
HANDSHAKE = 22
APPLICATION_DATA = 23
# pseudo content types
HEADER = 0x100
INNER_CONTENT_TYPE = 0x101


class _TLSAlertType(_IntEnum):
"""Alert types for TLSContentType.ALERT messages
See RFC 8466, section B.2
"""
CLOSE_NOTIFY = 0
UNEXPECTED_MESSAGE = 10
BAD_RECORD_MAC = 20
DECRYPTION_FAILED = 21
RECORD_OVERFLOW = 22
DECOMPRESSION_FAILURE = 30
HANDSHAKE_FAILURE = 40
NO_CERTIFICATE = 41
BAD_CERTIFICATE = 42
UNSUPPORTED_CERTIFICATE = 43
CERTIFICATE_REVOKED = 44
CERTIFICATE_EXPIRED = 45
CERTIFICATE_UNKNOWN = 46
ILLEGAL_PARAMETER = 47
UNKNOWN_CA = 48
ACCESS_DENIED = 49
DECODE_ERROR = 50
DECRYPT_ERROR = 51
EXPORT_RESTRICTION = 60
PROTOCOL_VERSION = 70
INSUFFICIENT_SECURITY = 71
INTERNAL_ERROR = 80
INAPPROPRIATE_FALLBACK = 86
USER_CANCELED = 90
NO_RENEGOTIATION = 100
MISSING_EXTENSION = 109
UNSUPPORTED_EXTENSION = 110
CERTIFICATE_UNOBTAINABLE = 111
UNRECOGNIZED_NAME = 112
BAD_CERTIFICATE_STATUS_RESPONSE = 113
BAD_CERTIFICATE_HASH_VALUE = 114
UNKNOWN_PSK_IDENTITY = 115
CERTIFICATE_REQUIRED = 116
NO_APPLICATION_PROTOCOL = 120


class _TLSMessageType(_IntEnum):
"""Message types (handshake protocol)
See RFC 8446, section B.3
"""
HELLO_REQUEST = 0
CLIENT_HELLO = 1
SERVER_HELLO = 2
HELLO_VERIFY_REQUEST = 3
NEWSESSION_TICKET = 4
END_OF_EARLY_DATA = 5
HELLO_RETRY_REQUEST = 6
ENCRYPTED_EXTENSIONS = 8
CERTIFICATE = 11
SERVER_KEY_EXCHANGE = 12
CERTIFICATE_REQUEST = 13
SERVER_DONE = 14
CERTIFICATE_VERIFY = 15
CLIENT_KEY_EXCHANGE = 16
FINISHED = 20
CERTIFICATE_URL = 21
CERTIFICATE_STATUS = 22
SUPPLEMENTAL_DATA = 23
KEY_UPDATE = 24
NEXT_PROTO = 67
MESSAGE_HASH = 254
CHANGE_CIPHER_SPEC = 0x0101


if sys.platform == "win32":
from _ssl import enum_certificates, enum_crls

Expand Down Expand Up @@ -523,6 +607,83 @@ def hostname_checks_common_name(self, value):
def hostname_checks_common_name(self):
return True

@property
def _msg_callback(self):
"""TLS message callback
The message callback provides a debugging hook to analyze TLS
connections. The callback is called for any TLS protocol message
(header, handshake, alert, and more), but not for application data.
Due to technical limitations, the callback can't be used to filter
traffic or to abort a connection. Any exception raised in the
callback is delayed until the handshake, read, or write operation
has been performed.
def msg_cb(conn, direction, version, content_type, msg_type, data):
pass
conn
:class:`SSLSocket` or :class:`SSLObject` instance
direction
``read`` or ``write``
version
:class:`TLSVersion` enum member or int for unknown version. For a
frame header, it's the header version.
content_type
:class:`_TLSContentType` enum member or int for unsupported
content type.
msg_type
Either a :class:`_TLSContentType` enum number for a header
message, a :class:`_TLSAlertType` enum member for an alert
message, a :class:`_TLSMessageType` enum member for other
messages, or int for unsupported message types.
data
Raw, decrypted message content as bytes
"""
inner = super()._msg_callback
if inner is not None:
return inner.user_function
else:
return None

@_msg_callback.setter
def _msg_callback(self, callback):
if callback is None:
super(SSLContext, SSLContext)._msg_callback.__set__(self, None)
return

if not hasattr(callback, '__call__'):
raise TypeError(f"{callback} is not callable.")

def inner(conn, direction, version, content_type, msg_type, data):
try:
version = TLSVersion(version)
except TypeError:
pass

try:
content_type = _TLSContentType(content_type)
except TypeError:
pass

if content_type == _TLSContentType.HEADER:
msg_enum = _TLSContentType
elif content_type == _TLSContentType.ALERT:
msg_enum = _TLSAlertType
else:
msg_enum = _TLSMessageType
try:
msg_type = msg_enum(msg_type)
except TypeError:
pass

return callback(conn, direction, version,
content_type, msg_type, data)

inner.user_function = callback

super(SSLContext, SSLContext)._msg_callback.__set__(self, inner)

@property
def protocol(self):
return _SSLMethod(super().protocol)
Expand Down Expand Up @@ -576,6 +737,11 @@ def create_default_context(purpose=Purpose.SERVER_AUTH, *, cafile=None,
# CERT_OPTIONAL or CERT_REQUIRED. Let's try to load default system
# root CA certificates for the given purpose. This may fail silently.
context.load_default_certs(purpose)
# OpenSSL 1.1.1 keylog file
if hasattr(context, 'keylog_filename'):
keylogfile = os.environ.get('SSLKEYLOGFILE')
if keylogfile and not sys.flags.ignore_environment:
context.keylog_filename = keylogfile
return context

def _create_unverified_context(protocol=PROTOCOL_TLS, *, cert_reqs=CERT_NONE,
Expand Down Expand Up @@ -617,7 +783,11 @@ def _create_unverified_context(protocol=PROTOCOL_TLS, *, cert_reqs=CERT_NONE,
# CERT_OPTIONAL or CERT_REQUIRED. Let's try to load default system
# root CA certificates for the given purpose. This may fail silently.
context.load_default_certs(purpose)

# OpenSSL 1.1.1 keylog file
if hasattr(context, 'keylog_filename'):
keylogfile = os.environ.get('SSLKEYLOGFILE')
if keylogfile and not sys.flags.ignore_environment:
context.keylog_filename = keylogfile
return context

# Used by http.client if no context is explicitly passed.
Expand Down
Loading

0 comments on commit c7f7069

Please sign in to comment.