Getting certificate chain with Python 3.3 SSL module

15,281

Solution 1

Thanks to the contributing answer by Aleksi, I found a bug/feature request that already requested this very thing: http://bugs.python.org/issue18233. Though the changes haven't been finalized, yet, they do have a patch that makes this available:

This is the test code which I've stolen from some forgotten source and reassembled:

import socket

from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23
from ssl import SSLContext  # Modern SSL?
from ssl import HAS_SNI  # Has SNI?

from pprint import pprint

def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
                    ca_certs=None, server_hostname=None,
                    ssl_version=None):
    context = SSLContext(ssl_version)
    context.verify_mode = cert_reqs

    if ca_certs:
        try:
            context.load_verify_locations(ca_certs)
        # Py32 raises IOError
        # Py33 raises FileNotFoundError
        except Exception as e:  # Reraise as SSLError
            raise SSLError(e)

    if certfile:
        # FIXME: This block needs a test.
        context.load_cert_chain(certfile, keyfile)

    if HAS_SNI:  # Platform-specific: OpenSSL with enabled SNI
        return (context, context.wrap_socket(sock, server_hostname=server_hostname))

    return (context, context.wrap_socket(sock))

hostname = 'www.google.com'
print("Hostname: %s" % (hostname))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((hostname, 443))

(context, ssl_socket) = ssl_wrap_socket(s,
                                       ssl_version=2, 
                                       cert_reqs=2, 
                                       ca_certs='/usr/local/lib/python3.3/dist-packages/requests/cacert.pem', 
                                       server_hostname=hostname)

pprint(ssl_socket.getpeercertchain())

s.close()

Output:

Hostname: www.google.com
({'issuer': ((('countryName', 'US'),),
             (('organizationName', 'Google Inc'),),
             (('commonName', 'Google Internet Authority G2'),)),
  'notAfter': 'Sep 11 11:04:38 2014 GMT',
  'notBefore': 'Sep 11 11:04:38 2013 GMT',
  'serialNumber': '50C71E48BCC50676',
  'subject': ((('countryName', 'US'),),
              (('stateOrProvinceName', 'California'),),
              (('localityName', 'Mountain View'),),
              (('organizationName', 'Google Inc'),),
              (('commonName', 'www.google.com'),)),
  'subjectAltName': (('DNS', 'www.google.com'),),
  'version': 3},
 {'issuer': ((('countryName', 'US'),),
             (('organizationName', 'GeoTrust Inc.'),),
             (('commonName', 'GeoTrust Global CA'),)),
  'notAfter': 'Apr  4 15:15:55 2015 GMT',
  'notBefore': 'Apr  5 15:15:55 2013 GMT',
  'serialNumber': '023A69',
  'subject': ((('countryName', 'US'),),
              (('organizationName', 'Google Inc'),),
              (('commonName', 'Google Internet Authority G2'),)),
  'version': 3},
 {'issuer': ((('countryName', 'US'),),
             (('organizationName', 'Equifax'),),
             (('organizationalUnitName',
               'Equifax Secure Certificate Authority'),)),
  'notAfter': 'Aug 21 04:00:00 2018 GMT',
  'notBefore': 'May 21 04:00:00 2002 GMT',
  'serialNumber': '12BBE6',
  'subject': ((('countryName', 'US'),),
              (('organizationName', 'GeoTrust Inc.'),),
              (('commonName', 'GeoTrust Global CA'),)),
  'version': 3},
 {'issuer': ((('countryName', 'US'),),
             (('organizationName', 'Equifax'),),
             (('organizationalUnitName',
               'Equifax Secure Certificate Authority'),)),
  'notAfter': 'Aug 22 16:41:51 2018 GMT',
  'notBefore': 'Aug 22 16:41:51 1998 GMT',
  'serialNumber': '35DEF4CF',
  'subject': ((('countryName', 'US'),),
              (('organizationName', 'Equifax'),),
              (('organizationalUnitName',
                'Equifax Secure Certificate Authority'),)),
  'version': 3})

Solution 2

The answer above did not work out of the box.

After going through many options, I found this to be the simplest approach which requires minimum 3rd party libraries.

pip install pyopenssl certifi

import socket
from OpenSSL import SSL
import certifi

hostname = 'www.google.com'
port = 443


context = SSL.Context(method=SSL.TLSv1_METHOD)
context.load_verify_locations(cafile=certifi.where())

conn = SSL.Connection(context, socket=socket.socket(socket.AF_INET, socket.SOCK_STREAM))
conn.settimeout(5)
conn.connect((hostname, port))
conn.setblocking(1)
conn.do_handshake()
conn.set_tlsext_host_name(hostname.encode())
for (idx, cert) in enumerate(conn.get_peer_cert_chain()):
    print(f'{idx} subject: {cert.get_subject()}')
    print(f'  issuer: {cert.get_issuer()})')
    print(f'  fingerprint: {cert.digest("sha1")}')

conn.close()

Here is a link to the original idea https://gist.github.com/brandond/f3d28734a40c49833176207b17a44786

Here is a reference which brought me here How to get response SSL certificate from requests in python?

Solution 3

I'm not sure, but I think that part of the OpenSSL API just isn't available in Python's ssl-module.

It seems that the function SSL_get_peer_cert_chain is used to access the certificate chain in OpenSSL. See, for example, the section of openssl s_client that prints the output you included. On the other hand, grepping the source of Python's ssl-module for SSL_get_peer_cert_chain yields no matches.

M2Crypto and pyOpenSSL both seem to include a get_peer_cert_chain function, if you're willing to look at other (and non-stdlib) libraries. I can't vouch for them personally, though, since I haven't used them much.

Share:
15,281
Dustin Oprea
Author by

Dustin Oprea

A boundlessly curious engineer.

Updated on June 18, 2022

Comments

  • Dustin Oprea
    Dustin Oprea almost 2 years

    I can get the standard certificate information for an SSL connection in Python 3.3 via the getpeercert() method on the SSL socket. However, it doesn't seem to provide the chain like OpenSSL's "s_client" tool does.

    Is there some way I can get this so that I can see if my IA certificate was configured properly?

    s_client command-line:

    openssl s_client -connect google.com:443
    

    s_client result (just the first few lines):

    $ openssl s_client -connect google.com:443
    CONNECTED(00000003)
    depth=2 C = US, O = GeoTrust Inc., CN = GeoTrust Global CA
    verify error:num=20:unable to get local issuer certificate
    verify return:0
    ---
    Certificate chain
     0 s:/C=US/ST=California/L=Mountain View/O=Google Inc/CN=*.google.com
       i:/C=US/O=Google Inc/CN=Google Internet Authority G2
     1 s:/C=US/O=Google Inc/CN=Google Internet Authority G2
       i:/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA
     2 s:/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA
       i:/C=US/O=Equifax/OU=Equifax Secure Certificate Authority
    ---
    

    Python 3.3 code:

    import socket
    
    from ssl import SSLContext  # Modern SSL?
    from ssl import HAS_SNI  # Has SNI?
    
    from pprint import pprint
    
    def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
                        ca_certs=None, server_hostname=None,
                        ssl_version=None):
    
        context = SSLContext(ssl_version)
        context.verify_mode = cert_reqs
    
        if ca_certs:
            try:
                context.load_verify_locations(ca_certs)
            # Py32 raises IOError
            # Py33 raises FileNotFoundError
            except Exception as e:  # Reraise as SSLError
                raise ssl.SSLError(e)
    
        if certfile:
            # FIXME: This block needs a test.
            context.load_cert_chain(certfile, keyfile)
    
        if HAS_SNI:  # Platform-specific: OpenSSL with enabled SNI
            return context.wrap_socket(sock, server_hostname=server_hostname)
    
        return context.wrap_socket(sock)
    
    hostname = 'www.google.com'
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((hostname, 443))
    
    sslSocket = ssl_wrap_socket(s,
                                ssl_version=2, 
                                cert_reqs=2, 
                                ca_certs='/usr/local/lib/python3.3/dist-packages/requests/cacert.pem', 
                                server_hostname=hostname)
    
    pprint(sslSocket.getpeercert())
    s.close()
    

    Code result:

    {'issuer': ((('countryName', 'US'),),
                (('organizationName', 'Google Inc'),),
                (('commonName', 'Google Internet Authority G2'),)),
     'notAfter': 'Sep 25 15:09:31 2014 GMT',
     'notBefore': 'Sep 25 15:09:31 2013 GMT',
     'serialNumber': '13A87ADB3E733D3B',
     'subject': ((('countryName', 'US'),),
                 (('stateOrProvinceName', 'California'),),
                 (('localityName', 'Mountain View'),),
                 (('organizationName', 'Google Inc'),),
                 (('commonName', 'www.google.com'),)),
     'subjectAltName': (('DNS', 'www.google.com'),),
     'version': 3}
    
  • Dustin Oprea
    Dustin Oprea over 10 years
    I was going to add a feature request, but there was already a recent ticket open for exactly this. See my answer.