The Python ssl module is designed to leverage the OpenSSL library as installed for each particular operating system. So, as long as a Python library or executable leverages the default functionality and OpenSSL is appropriately configured, then that library or executable should be expected to work. However, it is possible for a library or executable to deviate from this functionality. Or for Python to have been packaged and installed in such a way that it is not targeting the correct paths for the OpenSSL library and certificate store.
This section reflects Python 3.5.
Resources:
This section will walk through code to demonstrate Python3 ssl client functionality.
The code below imports standard Python libraries needed for this
section. It also uses the get_default_verify_paths()
method to display the paths the ssl package will
use by default to reach the OpenSSL files.
import ssl
import socket
import pprint
import json
print(
'ssl.get_default_verify_paths()._asdict()):\n' \
+ json.dumps(
ssl.get_default_verify_paths()._asdict(),
indent = 4,
sort_keys = True
) \
)Results are below.
ssl.get_default_verify_paths()._asdict()):
{
"cafile": null,
"capath": "/usr/lib/ssl/certs",
"openssl_cafile": "/usr/lib/ssl/cert.pem",
"openssl_cafile_env": "SSL_CERT_FILE",
"openssl_capath": "/usr/lib/ssl/certs",
"openssl_capath_env": "SSL_CERT_DIR"
}
The code below creates a default SSLContext
object and displays information about it. Descriptions for the
various ssl constants are at https://docs.python.org/3.5/library/ssl.html#constants.
Note that certificates in a capath directory are
not loaded unless they have been used at least once.
ssl_context = ssl.create_default_context()
print(
'ssl_context.protocol:\n'
+ str(
ssl_context.protocol
)
)
print(
'ssl_context.verify_mode:\n'
+ str(
ssl_context.verify_mode
)
)
for verify_mode in (
('ssl.CERT_NONE', ssl.CERT_NONE),
('ssl.CERT_OPTIONAL', ssl.CERT_OPTIONAL),
('ssl.CERT_REQUIRED', ssl.CERT_REQUIRED),
):
print(
'ssl_context.verify_mode == ' + verify_mode[0] + ':\n'
+ str(
ssl_context.verify_mode == verify_mode[1]
)
)
print(
'ssl_context.check_hostname:\n'
+ str(
ssl_context.check_hostname
)
)
print(
'ssl_context.verify_flags:\n'
+ str(
ssl_context.verify_flags
)
)
print(
'ssl_context.verify_flags == ssl.VERIFY_DEFAULT:\n'
+ str(
ssl_context.verify_flags == ssl.VERIFY_DEFAULT
)
)
for verify_bit in (
('ssl.VERIFY_CRL_CHECK_LEAF', ssl.VERIFY_CRL_CHECK_LEAF),
('ssl.VERIFY_CRL_CHECK_CHAIN', ssl.VERIFY_CRL_CHECK_CHAIN),
('ssl.VERIFY_X509_STRICT', ssl.VERIFY_X509_STRICT),
('ssl.VERIFY_X509_TRUSTED_FIRST', ssl.VERIFY_X509_TRUSTED_FIRST)
):
print(
verify_bit[0] + ' bit in ssl_context.verify_flags:\n'
+ str(
(ssl_context.verify_flags & verify_bit[1]) == verify_bit[1]
)
)
print(
'ssl_context.options:\n'
+ str(
ssl_context.options
)
)
for options_bit in (
('ssl.OP_ALL', ssl.OP_ALL),
('ssl.OP_NO_SSLv2', ssl.OP_NO_SSLv2),
('ssl.OP_NO_SSLv3', ssl.OP_NO_SSLv3),
('ssl.OP_NO_TLSv1', ssl.OP_NO_TLSv1),
('ssl.OP_NO_TLSv1_1', ssl.OP_NO_TLSv1_1),
('ssl.OP_NO_TLSv1_2', ssl.OP_NO_TLSv1_2),
('ssl.OP_NO_COMPRESSION', ssl.OP_NO_COMPRESSION)
):
print(
options_bit[0] + ' bit in ssl_context.options:\n'
+ str(
(ssl_context.options & options_bit[1]) == options_bit[1]
)
)
print(
'ssl_context.cert_store_stats():\n'
+ str(
ssl_context.cert_store_stats()
)
)
print(
'ssl_context.get_ca_certs():\n'
+ str(
ssl_context.get_ca_certs()
)
)Results are below.
ssl_context.protocol:
_SSLMethod.PROTOCOL_TLS
ssl_context.verify_mode:
2
ssl_context.verify_mode == ssl.CERT_NONE:
False
ssl_context.verify_mode == ssl.CERT_OPTIONAL:
False
ssl_context.verify_mode == ssl.CERT_REQUIRED:
True
ssl_context.check_hostname:
True
ssl_context.verify_flags:
32768
ssl_context.verify_flags == ssl.VERIFY_DEFAULT:
False
ssl.VERIFY_CRL_CHECK_LEAF bit in ssl_context.verify_flags:
False
ssl.VERIFY_CRL_CHECK_CHAIN bit in ssl_context.verify_flags:
False
ssl.VERIFY_X509_STRICT bit in ssl_context.verify_flags:
False
ssl.VERIFY_X509_TRUSTED_FIRST bit in ssl_context.verify_flags:
True
ssl_context.options:
2181170175
ssl.OP_ALL bit in ssl_context.options:
True
ssl.OP_NO_SSLv2 bit in ssl_context.options:
True
ssl.OP_NO_SSLv3 bit in ssl_context.options:
True
ssl.OP_NO_TLSv1 bit in ssl_context.options:
False
ssl.OP_NO_TLSv1_1 bit in ssl_context.options:
False
ssl.OP_NO_TLSv1_2 bit in ssl_context.options:
False
ssl.OP_NO_COMPRESSION bit in ssl_context.options:
True
ssl_context.cert_store_stats():
{'x509': 0, 'x509_ca': 0, 'crl': 0}
ssl_context.get_ca_certs():
[]
The code below sets server hostname and
port variables and creates an SSLContext
object. It then attempts to make a connection. And displays
information about the connection. Note that the wrap_socket()
method has an optional do_handshake_on_connect parameter
that defaults to True.
hostname = 'www.python.org'
port = 443
ssl_sock = ssl_context.wrap_socket(
socket.socket(
socket.AF_INET
),
server_hostname=hostname
)
ssl_sock.connect((hostname, port))
print(
'ssl_sock.version():\n'
+ str(
ssl_sock.version()
)
)
print(
'ssl_sock.shared_ciphers():\n'
+ pprint.pformat(
ssl_sock.shared_ciphers(),
)
)
print(
'ssl_sock.cipher():\n'
+ str(
ssl_sock.cipher()
)
)
print(
'ssl_sock.compression():\n'
+ str(
ssl_sock.compression()
)
)
print(
'ssl_context.cert_store_stats():\n'
+ str(
ssl_context.cert_store_stats()
)
)
print(
'ssl_context.get_ca_certs():\n'
+ json.dumps(
ssl_context.get_ca_certs(),
indent = 4,
sort_keys = True
)
)Results are below.
ssl_sock.version():
TLSv1.2
ssl_sock.shared_ciphers():
[('ECDHE-ECDSA-AES256-GCM-SHA384', 'TLSv1.2', 256),
('ECDHE-RSA-AES256-GCM-SHA384', 'TLSv1.2', 256),
('ECDHE-ECDSA-AES128-GCM-SHA256', 'TLSv1.2', 128),
('ECDHE-RSA-AES128-GCM-SHA256', 'TLSv1.2', 128),
('ECDHE-ECDSA-CHACHA20-POLY1305', 'TLSv1.2', 256),
('ECDHE-RSA-CHACHA20-POLY1305', 'TLSv1.2', 256),
('DHE-DSS-AES256-GCM-SHA384', 'TLSv1.2', 256),
('DHE-RSA-AES256-GCM-SHA384', 'TLSv1.2', 256),
('DHE-DSS-AES128-GCM-SHA256', 'TLSv1.2', 128),
('DHE-RSA-AES128-GCM-SHA256', 'TLSv1.2', 128),
('DHE-RSA-CHACHA20-POLY1305', 'TLSv1.2', 256),
('ECDHE-ECDSA-AES256-CCM8', 'TLSv1.2', 256),
('ECDHE-ECDSA-AES256-CCM', 'TLSv1.2', 256),
('ECDHE-ECDSA-AES256-SHA384', 'TLSv1.2', 256),
('ECDHE-RSA-AES256-SHA384', 'TLSv1.2', 256),
('ECDHE-ECDSA-AES256-SHA', 'TLSv1.0', 256),
('ECDHE-RSA-AES256-SHA', 'TLSv1.0', 256),
('DHE-RSA-AES256-CCM8', 'TLSv1.2', 256),
('DHE-RSA-AES256-CCM', 'TLSv1.2', 256),
('DHE-RSA-AES256-SHA256', 'TLSv1.2', 256),
('DHE-DSS-AES256-SHA256', 'TLSv1.2', 256),
('DHE-RSA-AES256-SHA', 'SSLv3', 256),
('DHE-DSS-AES256-SHA', 'SSLv3', 256),
('ECDHE-ECDSA-AES128-CCM8', 'TLSv1.2', 128),
('ECDHE-ECDSA-AES128-CCM', 'TLSv1.2', 128),
('ECDHE-ECDSA-AES128-SHA256', 'TLSv1.2', 128),
('ECDHE-RSA-AES128-SHA256', 'TLSv1.2', 128),
('ECDHE-ECDSA-AES128-SHA', 'TLSv1.0', 128),
('ECDHE-RSA-AES128-SHA', 'TLSv1.0', 128),
('DHE-RSA-AES128-CCM8', 'TLSv1.2', 128),
('DHE-RSA-AES128-CCM', 'TLSv1.2', 128),
('DHE-RSA-AES128-SHA256', 'TLSv1.2', 128),
('DHE-DSS-AES128-SHA256', 'TLSv1.2', 128),
('DHE-RSA-AES128-SHA', 'SSLv3', 128),
('DHE-DSS-AES128-SHA', 'SSLv3', 128),
('ECDHE-ECDSA-CAMELLIA256-SHA384', 'TLSv1.2', 256),
('ECDHE-RSA-CAMELLIA256-SHA384', 'TLSv1.2', 256),
('ECDHE-ECDSA-CAMELLIA128-SHA256', 'TLSv1.2', 128),
('ECDHE-RSA-CAMELLIA128-SHA256', 'TLSv1.2', 128),
('DHE-RSA-CAMELLIA256-SHA256', 'TLSv1.2', 256),
('DHE-DSS-CAMELLIA256-SHA256', 'TLSv1.2', 256),
('DHE-RSA-CAMELLIA128-SHA256', 'TLSv1.2', 128),
('DHE-DSS-CAMELLIA128-SHA256', 'TLSv1.2', 128),
('DHE-RSA-CAMELLIA256-SHA', 'SSLv3', 256),
('DHE-DSS-CAMELLIA256-SHA', 'SSLv3', 256),
('DHE-RSA-CAMELLIA128-SHA', 'SSLv3', 128),
('DHE-DSS-CAMELLIA128-SHA', 'SSLv3', 128),
('AES256-GCM-SHA384', 'TLSv1.2', 256),
('AES128-GCM-SHA256', 'TLSv1.2', 128),
('AES256-CCM8', 'TLSv1.2', 256),
('AES256-CCM', 'TLSv1.2', 256),
('AES128-CCM8', 'TLSv1.2', 128),
('AES128-CCM', 'TLSv1.2', 128),
('AES256-SHA256', 'TLSv1.2', 256),
('AES128-SHA256', 'TLSv1.2', 128),
('AES256-SHA', 'SSLv3', 256),
('AES128-SHA', 'SSLv3', 128),
('CAMELLIA256-SHA256', 'TLSv1.2', 256),
('CAMELLIA128-SHA256', 'TLSv1.2', 128),
('CAMELLIA256-SHA', 'SSLv3', 256),
('CAMELLIA128-SHA', 'SSLv3', 128)]
ssl_sock.cipher():
('ECDHE-RSA-AES128-GCM-SHA256', 'TLSv1.2', 128)
ssl_sock.compression():
None
ssl_context.cert_store_stats():
{'x509': 1, 'x509_ca': 1, 'crl': 0}
ssl_context.get_ca_certs():
[
{
"issuer": [
[
[
"countryName",
"US"
]
],
[
[
"organizationName",
"DigiCert Inc"
]
],
[
[
"organizationalUnitName",
"www.digicert.com"
]
],
[
[
"commonName",
"DigiCert High Assurance EV Root CA"
]
]
],
"notAfter": "Nov 10 00:00:00 2031 GMT",
"notBefore": "Nov 10 00:00:00 2006 GMT",
"serialNumber": "02AC5C266A0B409B8F0B79F2AE462577",
"subject": [
[
[
"countryName",
"US"
]
],
[
[
"organizationName",
"DigiCert Inc"
]
],
[
[
"organizationalUnitName",
"www.digicert.com"
]
],
[
[
"commonName",
"DigiCert High Assurance EV Root CA"
]
]
],
"version": 3
}
]
The code below gets the server certificate from the
ssl.SSLContext
object. And it displays the certificate. Note that as of Python
3.4, the handshake also performs match_hostname()
when the check_hostname attribute of the
socket's context is true. And check_hostname is True in the
default context. So, there is no need to execute an additional
check.
server_cert = ssl_sock.getpeercert()
print(
'ssl_sock.getpeercert():\n'
+ pprint.pformat(
server_cert
)
)
print(
'ssl.match_hostname(server_cert, hostname):\n'
+ str(
ssl.match_hostname(server_cert, hostname)
)
)
print(ssl.match_hostname(server_cert, hostname))
Results are below.
ssl_sock.getpeercert():
{'OCSP': ('http://ocsp.digicert.com',),
'caIssuers': ('http://cacerts.digicert.com/DigiCertSHA2ExtendedValidationServerCA.crt',),
'crlDistributionPoints': ('http://crl3.digicert.com/sha2-ev-server-g2.crl',
'http://crl4.digicert.com/sha2-ev-server-g2.crl'),
'issuer': ((('countryName', 'US'),),
(('organizationName', 'DigiCert Inc'),),
(('organizationalUnitName', 'www.digicert.com'),),
(('commonName', 'DigiCert SHA2 Extended Validation Server CA'),)),
'notAfter': 'Sep 27 12:00:00 2018 GMT',
'notBefore': 'Mar 28 00:00:00 2018 GMT',
'serialNumber': '0C4A84238E7344559BB84D1E0F318883',
'subject': ((('businessCategory', 'Private Organization'),),
(('jurisdictionCountryName', 'US'),),
(('jurisdictionStateOrProvinceName', 'Delaware'),),
(('serialNumber', '3359300'),),
(('countryName', 'US'),),
(('stateOrProvinceName', 'New Hampshire'),),
(('localityName', 'Wolfeboro'),),
(('organizationName', 'Python Software Foundation'),),
(('commonName', 'www.python.org'),)),
'subjectAltName': (('DNS', 'www.python.org'),
('DNS', 'docs.python.org'),
('DNS', 'bugs.python.org'),
('DNS', 'wiki.python.org'),
('DNS', 'hg.python.org'),
('DNS', 'mail.python.org'),
('DNS', 'pypi.python.org'),
('DNS', 'packaging.python.org'),
('DNS', 'login.python.org'),
('DNS', 'discuss.python.org'),
('DNS', 'us.pycon.org'),
('DNS', 'pypi.io'),
('DNS', 'docs.pypi.io'),
('DNS', 'pypi.org'),
('DNS', 'docs.pypi.org'),
('DNS', 'donate.pypi.org'),
('DNS', 'devguide.python.org'),
('DNS', 'www.bugs.python.org'),
('DNS', 'python.org')),
'version': 3}
The code below sends a simple HEAD request to the server. Note
that the documentation indicates that the sendall()
method returns None on sucess. However, it
actually returns the number of bytes sent.
ssl_sock.sendall(
b'HEAD / HTTP/1.1\r\nHost: ' \
+ hostname.encode('utf-8') \
+ b'\r\n\r\n'
)