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' )