4. Python3

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:

4.1. Python3 ssl Client

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