605 lines
17 KiB
Python
605 lines
17 KiB
Python
# coding: utf-8
|
|
from __future__ import unicode_literals, division, absolute_import, print_function
|
|
|
|
import re
|
|
from datetime import datetime
|
|
|
|
from ._asn1 import Certificate, int_from_bytes, timezone
|
|
from ._cipher_suites import CIPHER_SUITE_MAP
|
|
from .errors import TLSVerificationError, TLSDisconnectError, TLSError
|
|
|
|
|
|
__all__ = [
|
|
'detect_client_auth_request',
|
|
'extract_chain',
|
|
'get_dh_params_length',
|
|
'parse_alert',
|
|
'parse_handshake_messages',
|
|
'parse_session_info',
|
|
'parse_tls_records',
|
|
'raise_client_auth',
|
|
'raise_dh_params',
|
|
'raise_disconnection',
|
|
'raise_expired_not_yet_valid',
|
|
'raise_handshake',
|
|
'raise_hostname',
|
|
'raise_no_issuer',
|
|
'raise_protocol_error',
|
|
'raise_revoked',
|
|
'raise_self_signed',
|
|
'raise_verification',
|
|
'raise_weak_signature',
|
|
]
|
|
|
|
|
|
def extract_chain(server_handshake_bytes):
|
|
"""
|
|
Extracts the X.509 certificates from the server handshake bytes for use
|
|
when debugging
|
|
|
|
:param server_handshake_bytes:
|
|
A byte string of the handshake data received from the server
|
|
|
|
:return:
|
|
A list of asn1crypto.x509.Certificate objects
|
|
"""
|
|
|
|
output = []
|
|
|
|
chain_bytes = None
|
|
|
|
for record_type, _, record_data in parse_tls_records(server_handshake_bytes):
|
|
if record_type != b'\x16':
|
|
continue
|
|
for message_type, message_data in parse_handshake_messages(record_data):
|
|
if message_type == b'\x0b':
|
|
chain_bytes = message_data
|
|
break
|
|
if chain_bytes:
|
|
break
|
|
|
|
if chain_bytes:
|
|
# The first 3 bytes are the cert chain length
|
|
pointer = 3
|
|
while pointer < len(chain_bytes):
|
|
cert_length = int_from_bytes(chain_bytes[pointer:pointer + 3])
|
|
cert_start = pointer + 3
|
|
cert_end = cert_start + cert_length
|
|
pointer = cert_end
|
|
cert_bytes = chain_bytes[cert_start:cert_end]
|
|
output.append(Certificate.load(cert_bytes))
|
|
|
|
return output
|
|
|
|
|
|
def detect_client_auth_request(server_handshake_bytes):
|
|
"""
|
|
Determines if a CertificateRequest message is sent from the server asking
|
|
the client for a certificate
|
|
|
|
:param server_handshake_bytes:
|
|
A byte string of the handshake data received from the server
|
|
|
|
:return:
|
|
A boolean - if a client certificate request was found
|
|
"""
|
|
|
|
for record_type, _, record_data in parse_tls_records(server_handshake_bytes):
|
|
if record_type != b'\x16':
|
|
continue
|
|
for message_type, message_data in parse_handshake_messages(record_data):
|
|
if message_type == b'\x0d':
|
|
return True
|
|
return False
|
|
|
|
|
|
def get_dh_params_length(server_handshake_bytes):
|
|
"""
|
|
Determines the length of the DH params from the ServerKeyExchange
|
|
|
|
:param server_handshake_bytes:
|
|
A byte string of the handshake data received from the server
|
|
|
|
:return:
|
|
None or an integer of the bit size of the DH parameters
|
|
"""
|
|
|
|
output = None
|
|
|
|
dh_params_bytes = None
|
|
|
|
for record_type, _, record_data in parse_tls_records(server_handshake_bytes):
|
|
if record_type != b'\x16':
|
|
continue
|
|
for message_type, message_data in parse_handshake_messages(record_data):
|
|
if message_type == b'\x0c':
|
|
dh_params_bytes = message_data
|
|
break
|
|
if dh_params_bytes:
|
|
break
|
|
|
|
if dh_params_bytes:
|
|
output = int_from_bytes(dh_params_bytes[0:2]) * 8
|
|
|
|
return output
|
|
|
|
|
|
def parse_alert(server_handshake_bytes):
|
|
"""
|
|
Parses the handshake for protocol alerts
|
|
|
|
:param server_handshake_bytes:
|
|
A byte string of the handshake data received from the server
|
|
|
|
:return:
|
|
None or an 2-element tuple of integers:
|
|
0: 1 (warning) or 2 (fatal)
|
|
1: The alert description (see https://tools.ietf.org/html/rfc5246#section-7.2)
|
|
"""
|
|
|
|
for record_type, _, record_data in parse_tls_records(server_handshake_bytes):
|
|
if record_type != b'\x15':
|
|
continue
|
|
if len(record_data) != 2:
|
|
return None
|
|
return (int_from_bytes(record_data[0:1]), int_from_bytes(record_data[1:2]))
|
|
return None
|
|
|
|
|
|
def parse_session_info(server_handshake_bytes, client_handshake_bytes):
|
|
"""
|
|
Parse the TLS handshake from the client to the server to extract information
|
|
including the cipher suite selected, if compression is enabled, the
|
|
session id and if a new or reused session ticket exists.
|
|
|
|
:param server_handshake_bytes:
|
|
A byte string of the handshake data received from the server
|
|
|
|
:param client_handshake_bytes:
|
|
A byte string of the handshake data sent to the server
|
|
|
|
:return:
|
|
A dict with the following keys:
|
|
- "protocol": unicode string
|
|
- "cipher_suite": unicode string
|
|
- "compression": boolean
|
|
- "session_id": "new", "reused" or None
|
|
- "session_ticket: "new", "reused" or None
|
|
"""
|
|
|
|
protocol = None
|
|
cipher_suite = None
|
|
compression = False
|
|
session_id = None
|
|
session_ticket = None
|
|
|
|
server_session_id = None
|
|
client_session_id = None
|
|
|
|
for record_type, _, record_data in parse_tls_records(server_handshake_bytes):
|
|
if record_type != b'\x16':
|
|
continue
|
|
for message_type, message_data in parse_handshake_messages(record_data):
|
|
# Ensure we are working with a ServerHello message
|
|
if message_type != b'\x02':
|
|
continue
|
|
protocol = {
|
|
b'\x03\x00': "SSLv3",
|
|
b'\x03\x01': "TLSv1",
|
|
b'\x03\x02': "TLSv1.1",
|
|
b'\x03\x03': "TLSv1.2",
|
|
b'\x03\x04': "TLSv1.3",
|
|
}[message_data[0:2]]
|
|
|
|
session_id_length = int_from_bytes(message_data[34:35])
|
|
if session_id_length > 0:
|
|
server_session_id = message_data[35:35 + session_id_length]
|
|
|
|
cipher_suite_start = 35 + session_id_length
|
|
cipher_suite_bytes = message_data[cipher_suite_start:cipher_suite_start + 2]
|
|
cipher_suite = CIPHER_SUITE_MAP[cipher_suite_bytes]
|
|
|
|
compression_start = cipher_suite_start + 2
|
|
compression = message_data[compression_start:compression_start + 1] != b'\x00'
|
|
|
|
extensions_length_start = compression_start + 1
|
|
extensions_data = message_data[extensions_length_start:]
|
|
for extension_type, extension_data in _parse_hello_extensions(extensions_data):
|
|
if extension_type == 35:
|
|
session_ticket = "new"
|
|
break
|
|
break
|
|
|
|
for record_type, _, record_data in parse_tls_records(client_handshake_bytes):
|
|
if record_type != b'\x16':
|
|
continue
|
|
for message_type, message_data in parse_handshake_messages(record_data):
|
|
# Ensure we are working with a ClientHello message
|
|
if message_type != b'\x01':
|
|
continue
|
|
|
|
session_id_length = int_from_bytes(message_data[34:35])
|
|
if session_id_length > 0:
|
|
client_session_id = message_data[35:35 + session_id_length]
|
|
|
|
cipher_suite_start = 35 + session_id_length
|
|
cipher_suite_length = int_from_bytes(message_data[cipher_suite_start:cipher_suite_start + 2])
|
|
|
|
compression_start = cipher_suite_start + 2 + cipher_suite_length
|
|
compression_length = int_from_bytes(message_data[compression_start:compression_start + 1])
|
|
|
|
# On subsequent requests, the session ticket will only be seen
|
|
# in the ClientHello message
|
|
if server_session_id is None and session_ticket is None:
|
|
extensions_length_start = compression_start + 1 + compression_length
|
|
extensions_data = message_data[extensions_length_start:]
|
|
for extension_type, extension_data in _parse_hello_extensions(extensions_data):
|
|
if extension_type == 35:
|
|
session_ticket = "reused"
|
|
break
|
|
break
|
|
|
|
if server_session_id is not None:
|
|
if client_session_id is None:
|
|
session_id = "new"
|
|
else:
|
|
if client_session_id != server_session_id:
|
|
session_id = "new"
|
|
else:
|
|
session_id = "reused"
|
|
|
|
return {
|
|
"protocol": protocol,
|
|
"cipher_suite": cipher_suite,
|
|
"compression": compression,
|
|
"session_id": session_id,
|
|
"session_ticket": session_ticket,
|
|
}
|
|
|
|
|
|
def parse_tls_records(data):
|
|
"""
|
|
Creates a generator returning tuples of information about each record
|
|
in a byte string of data from a TLS client or server. Stops as soon as it
|
|
find a ChangeCipherSpec message since all data from then on is encrypted.
|
|
|
|
:param data:
|
|
A byte string of TLS records
|
|
|
|
:return:
|
|
A generator that yields 3-element tuples:
|
|
[0] Byte string of record type
|
|
[1] Byte string of protocol version
|
|
[2] Byte string of record data
|
|
"""
|
|
|
|
pointer = 0
|
|
data_len = len(data)
|
|
while pointer < data_len:
|
|
# Don't try to parse any more once the ChangeCipherSpec is found
|
|
if data[pointer:pointer + 1] == b'\x14':
|
|
break
|
|
length = int_from_bytes(data[pointer + 3:pointer + 5])
|
|
yield (
|
|
data[pointer:pointer + 1],
|
|
data[pointer + 1:pointer + 3],
|
|
data[pointer + 5:pointer + 5 + length]
|
|
)
|
|
pointer += 5 + length
|
|
|
|
|
|
def parse_handshake_messages(data):
|
|
"""
|
|
Creates a generator returning tuples of information about each message in
|
|
a byte string of data from a TLS handshake record
|
|
|
|
:param data:
|
|
A byte string of a TLS handshake record data
|
|
|
|
:return:
|
|
A generator that yields 2-element tuples:
|
|
[0] Byte string of message type
|
|
[1] Byte string of message data
|
|
"""
|
|
|
|
pointer = 0
|
|
data_len = len(data)
|
|
while pointer < data_len:
|
|
length = int_from_bytes(data[pointer + 1:pointer + 4])
|
|
yield (
|
|
data[pointer:pointer + 1],
|
|
data[pointer + 4:pointer + 4 + length]
|
|
)
|
|
pointer += 4 + length
|
|
|
|
|
|
def _parse_hello_extensions(data):
|
|
"""
|
|
Creates a generator returning tuples of information about each extension
|
|
from a byte string of extension data contained in a ServerHello ores
|
|
ClientHello message
|
|
|
|
:param data:
|
|
A byte string of a extension data from a TLS ServerHello or ClientHello
|
|
message
|
|
|
|
:return:
|
|
A generator that yields 2-element tuples:
|
|
[0] Byte string of extension type
|
|
[1] Byte string of extension data
|
|
"""
|
|
|
|
if data == b'':
|
|
return
|
|
|
|
extentions_length = int_from_bytes(data[0:2])
|
|
extensions_start = 2
|
|
extensions_end = 2 + extentions_length
|
|
|
|
pointer = extensions_start
|
|
while pointer < extensions_end:
|
|
extension_type = int_from_bytes(data[pointer:pointer + 2])
|
|
extension_length = int_from_bytes(data[pointer + 2:pointer + 4])
|
|
yield (
|
|
extension_type,
|
|
data[pointer + 4:pointer + 4 + extension_length]
|
|
)
|
|
pointer += 4 + extension_length
|
|
|
|
|
|
def raise_hostname(certificate, hostname):
|
|
"""
|
|
Raises a TLSVerificationError due to a hostname mismatch
|
|
|
|
:param certificate:
|
|
An asn1crypto.x509.Certificate object
|
|
|
|
:raises:
|
|
TLSVerificationError
|
|
"""
|
|
|
|
is_ip = re.match('^\\d+\\.\\d+\\.\\d+\\.\\d+$', hostname) or hostname.find(':') != -1
|
|
if is_ip:
|
|
hostname_type = 'IP address %s' % hostname
|
|
else:
|
|
hostname_type = 'domain name %s' % hostname
|
|
message = 'Server certificate verification failed - %s does not match' % hostname_type
|
|
valid_ips = ', '.join(certificate.valid_ips)
|
|
valid_domains = ', '.join(certificate.valid_domains)
|
|
if valid_domains:
|
|
message += ' valid domains: %s' % valid_domains
|
|
if valid_domains and valid_ips:
|
|
message += ' or'
|
|
if valid_ips:
|
|
message += ' valid IP addresses: %s' % valid_ips
|
|
raise TLSVerificationError(message, certificate)
|
|
|
|
|
|
def raise_verification(certificate):
|
|
"""
|
|
Raises a generic TLSVerificationError
|
|
|
|
:param certificate:
|
|
An asn1crypto.x509.Certificate object
|
|
|
|
:raises:
|
|
TLSVerificationError
|
|
"""
|
|
|
|
message = 'Server certificate verification failed'
|
|
raise TLSVerificationError(message, certificate)
|
|
|
|
|
|
def raise_weak_signature(certificate):
|
|
"""
|
|
Raises a TLSVerificationError when a certificate uses a weak signature
|
|
algorithm
|
|
|
|
:param certificate:
|
|
An asn1crypto.x509.Certificate object
|
|
|
|
:raises:
|
|
TLSVerificationError
|
|
"""
|
|
|
|
message = 'Server certificate verification failed - weak certificate signature algorithm'
|
|
raise TLSVerificationError(message, certificate)
|
|
|
|
|
|
def raise_client_auth():
|
|
"""
|
|
Raises a TLSError indicating client authentication is required
|
|
|
|
:raises:
|
|
TLSError
|
|
"""
|
|
|
|
message = 'TLS handshake failed - client authentication required'
|
|
raise TLSError(message)
|
|
|
|
|
|
def raise_revoked(certificate):
|
|
"""
|
|
Raises a TLSVerificationError due to the certificate being revoked
|
|
|
|
:param certificate:
|
|
An asn1crypto.x509.Certificate object
|
|
|
|
:raises:
|
|
TLSVerificationError
|
|
"""
|
|
|
|
message = 'Server certificate verification failed - certificate has been revoked'
|
|
raise TLSVerificationError(message, certificate)
|
|
|
|
|
|
def raise_no_issuer(certificate):
|
|
"""
|
|
Raises a TLSVerificationError due to no issuer certificate found in trust
|
|
roots
|
|
|
|
:param certificate:
|
|
An asn1crypto.x509.Certificate object
|
|
|
|
:raises:
|
|
TLSVerificationError
|
|
"""
|
|
|
|
message = 'Server certificate verification failed - certificate issuer not found in trusted root certificate store'
|
|
raise TLSVerificationError(message, certificate)
|
|
|
|
|
|
def raise_self_signed(certificate):
|
|
"""
|
|
Raises a TLSVerificationError due to a self-signed certificate
|
|
roots
|
|
|
|
:param certificate:
|
|
An asn1crypto.x509.Certificate object
|
|
|
|
:raises:
|
|
TLSVerificationError
|
|
"""
|
|
|
|
message = 'Server certificate verification failed - certificate is self-signed'
|
|
raise TLSVerificationError(message, certificate)
|
|
|
|
|
|
def raise_lifetime_too_long(certificate):
|
|
"""
|
|
Raises a TLSVerificationError due to a certificate lifetime exceeding
|
|
the CAB forum certificate lifetime limit
|
|
|
|
:param certificate:
|
|
An asn1crypto.x509.Certificate object
|
|
|
|
:raises:
|
|
TLSVerificationError
|
|
"""
|
|
|
|
message = 'Server certificate verification failed - certificate lifetime is too long'
|
|
raise TLSVerificationError(message, certificate)
|
|
|
|
|
|
def raise_expired_not_yet_valid(certificate):
|
|
"""
|
|
Raises a TLSVerificationError due to certificate being expired, or not yet
|
|
being valid
|
|
|
|
:param certificate:
|
|
An asn1crypto.x509.Certificate object
|
|
|
|
:raises:
|
|
TLSVerificationError
|
|
"""
|
|
|
|
validity = certificate['tbs_certificate']['validity']
|
|
not_after = validity['not_after'].native
|
|
not_before = validity['not_before'].native
|
|
|
|
now = datetime.now(timezone.utc)
|
|
|
|
if not_before > now:
|
|
formatted_before = not_before.strftime('%Y-%m-%d %H:%M:%SZ')
|
|
message = 'Server certificate verification failed - certificate not valid until %s' % formatted_before
|
|
elif not_after < now:
|
|
formatted_after = not_after.strftime('%Y-%m-%d %H:%M:%SZ')
|
|
message = 'Server certificate verification failed - certificate expired %s' % formatted_after
|
|
|
|
raise TLSVerificationError(message, certificate)
|
|
|
|
|
|
def raise_disconnection():
|
|
"""
|
|
Raises a TLSDisconnectError due to a disconnection
|
|
|
|
:raises:
|
|
TLSDisconnectError
|
|
"""
|
|
|
|
raise TLSDisconnectError('The remote end closed the connection')
|
|
|
|
|
|
def raise_protocol_error(server_handshake_bytes):
|
|
"""
|
|
Raises a TLSError due to a protocol error
|
|
|
|
:param server_handshake_bytes:
|
|
A byte string of the handshake data received from the server
|
|
|
|
:raises:
|
|
TLSError
|
|
"""
|
|
|
|
other_protocol = detect_other_protocol(server_handshake_bytes)
|
|
|
|
if other_protocol:
|
|
raise TLSError('TLS protocol error - server responded using %s' % other_protocol)
|
|
|
|
raise TLSError('TLS protocol error - server responded using a different protocol')
|
|
|
|
|
|
def raise_handshake():
|
|
"""
|
|
Raises a TLSError due to a handshake error
|
|
|
|
:raises:
|
|
TLSError
|
|
"""
|
|
|
|
raise TLSError('TLS handshake failed')
|
|
|
|
|
|
def raise_protocol_version():
|
|
"""
|
|
Raises a TLSError due to a TLS version incompatibility
|
|
|
|
:raises:
|
|
TLSError
|
|
"""
|
|
|
|
raise TLSError('TLS handshake failed - protocol version error')
|
|
|
|
|
|
def raise_dh_params():
|
|
"""
|
|
Raises a TLSError due to weak DH params
|
|
|
|
:raises:
|
|
TLSError
|
|
"""
|
|
|
|
raise TLSError('TLS handshake failed - weak DH parameters')
|
|
|
|
|
|
def detect_other_protocol(server_handshake_bytes):
|
|
"""
|
|
Looks at the server handshake bytes to try and detect a different protocol
|
|
|
|
:param server_handshake_bytes:
|
|
A byte string of the handshake data received from the server
|
|
|
|
:return:
|
|
None, or a unicode string of "ftp", "http", "imap", "pop3", "smtp"
|
|
"""
|
|
|
|
if server_handshake_bytes[0:5] == b'HTTP/':
|
|
return 'HTTP'
|
|
|
|
if server_handshake_bytes[0:4] == b'220 ':
|
|
if re.match(b'^[^\r\n]*ftp', server_handshake_bytes, re.I):
|
|
return 'FTP'
|
|
else:
|
|
return 'SMTP'
|
|
|
|
if server_handshake_bytes[0:4] == b'220-':
|
|
return 'FTP'
|
|
|
|
if server_handshake_bytes[0:4] == b'+OK ':
|
|
return 'POP3'
|
|
|
|
if server_handshake_bytes[0:4] == b'* OK' or server_handshake_bytes[0:9] == b'* PREAUTH':
|
|
return 'IMAP'
|
|
|
|
return None
|