Using Python's pyOpenSSL to verify SSL certificates downloaded from a host

From November 2020 the Chain of Trust can be verified without calling OpenSSL with Python's subprocess.

17 December 2020 Updated 17 December 2020
post main image

While writing a script to check if websites correctly redirected to 'https:/www.' I thought to add some SSL certificate checks as well. This means I had to verify SSL certificates downloaded from a host. Is the certificate really for this website? Show me the expiration date. Is the certificate chain correct? And can we trust the certificate(s)?

Initially I got stuck where many people got stuck because of the following. The intermediate certificates you download from a host cannot be trusted and pyOpenSSL did not use the 'untrusted' flag when verifying a certificate chain. Meaning that the certificate can be flagged trusted while it is not.

The only way around was to run the OpenSSL verify command using subprocess. Not what we want but at least we could do it. There are other ways but they are much more complex. This was reported and discussed in 2016. With the release of pyOpenSSL 20.0.0 (2020-11-27) the following changes were made:

  • a new method 'load_locations()' to X509Store to set trusted certificate file bundles and/or directories
  • a new parameter 'chain' to X509StoreContext where you can add untrusted certificates
  • a new method 'get_verified_chain()' toX509StoreContext returning the complete validated chain

With these changes we now finally can verify the Chain of Trust.

Python and cryptography is not easy

Websites, services have moved from HTTP to HTTPS. This means when you connect to a service you must check the certificate(s) as well. Is Python doing this for you? The Python requests library is doing this automatically for you. You don't even have to add a parameter like 'verify=True'.

But here I am looking for a way to check the SSL certificates in my own Python script. Below I describe some ways to do this and some Python code I wrote to investigate this. This is running on my Ubuntu 18.04 PC.

Relevant reading, watching

Maybe you want to start by reading the articles 'Chain of Fools: An Exploration of Certificate Chain Validation Mishaps', and '[Cryptography-dev] on how (not) to chain certs with openssl + pyopenssl', and watching a nice video 'Digital Certificates: Chain of Trust', see links below.

The CertInfo class

In the examples below I use a class CertInfo(). This is a class I wrote to extract information from a certificate. The Python code is at the end of this article.

The certificate chain

A certificate chain is a linked list of certificates. In every certificate there are two items that specify how they are linked:

  • Subject-CN (common name)
  • Issuer-CN (common name)

Starting with the server certificate, it is issued by the Issuer-CN. The server certificate is also called end-entity certificate, leaf certificate or subscriber certificate. The next certificate we downloaded must have a Subject-CN identical to the server certificate's Issuer-CN, etc.

Below, the first certificate downloaded from the host is host_cert[0], the second host_cert[1], etc. The last certificate is the root certificate root_cert.


INFO - ------------------------------------------------------------
INFO - Dumping certs: ...
INFO - * got 2 certs from host
INFO - * got root_cert
INFO - host_cert[0]
INFO - * Subject-CN: *
INFO - * Issuer-CN: DigiCert SHA2 Secure Server CA
INFO - host_cert[1]
INFO - * Subject-CN: DigiCert SHA2 Secure Server CA
INFO - * Issuer-CN: DigiCert Global Root CA
INFO - root_cert
INFO - * Subject-CN: DigiCert Global Root CA
INFO - * Issuer-CN: DigiCert Global Root CA


INFO - ------------------------------------------------------------
INFO - Dumping certs: ...
INFO - * got 3 certs from host
INFO - * got root_cert
INFO - host_cert[0]
INFO - * Subject-CN:
INFO - * Issuer-CN: Sectigo RSA Domain Validation Secure Server CA
INFO - host_cert[1]
INFO - * Subject-CN: Sectigo RSA Domain Validation Secure Server CA
INFO - * Issuer-CN: USERTrust RSA Certification Authority
INFO - host_cert[2]
INFO - * Subject-CN: USERTrust RSA Certification Authority
INFO - * Issuer-CN: AAA Certificate Services
INFO - root_cert
INFO - * Subject-CN: AAA Certificate Services
INFO - * Issuer-CN: AAA Certificate Services


INFO - ------------------------------------------------------------
INFO - Dumping certs: ...
INFO - * got 3 certs from host
INFO - * got root_cert
INFO - host_cert[0]
INFO - * Subject-CN:
INFO - * Issuer-CN: DigiCert TLS RSA SHA256 2020 CA1
INFO - host_cert[1]
INFO - * Subject-CN: DigiCert TLS RSA SHA256 2020 CA1
INFO - * Issuer-CN: DigiCert Global Root CA
INFO - host_cert[2]
INFO - * Subject-CN: DigiCert Global Root CA
INFO - * Issuer-CN: DigiCert Global Root CA
INFO - root_cert
INFO - * Subject-CN: DigiCert Global Root CA
INFO - * Issuer-CN: DigiCert Global Root CA

Using this data we can check if the chain starting at the server certificate ends at the root certificate. If we want to verify if the certificates are correctly configured, it is essential that we do this because OpenSSL does not check this sequence! Note that has one intermediate certificate and and have two intermediate certificates. is a special case, I will discuss this below.

Here is Python code to check the certificate chain:

    def check_chain_order(self):

        if self.root_cert is None:
            return False

        # link issuer-subject, start with server_cert
        for i, cert in enumerate(self.host_certs):
            if i < 1:
                # we need two
            ci1 = CertInfo(self.host_certs[i - 1])
            ci2 = CertInfo(self.host_certs[i])
            if ci1.issuer_cn != ci2.subject_cn:
                return False

        # link issuer-subject, root_cert
        ci1 = CertInfo(self.host_certs[-1])
        ci2 = CertInfo(self.root_cert)
        if ci1.issuer_cn != ci2.subject_cn:
            return False

        return True

Getting the root certificate

To verify the certificate chain we also need the root certificate. In many cases you can include a CA root path to a directory on your device and the verification function will look up the root certificate automatically. On my PC I can set the OpenSSL CApath parameter to:


But what if we want to get the CA root certficate for use in the certificate chain verify function as described above? I did not see how I can do this with pyOpenSSL but it can be done in other ways.

First, we can do this with OpenSSL and subprocess. The OpenSSL command is:

openssl x509 -noout -issuer_hash -in cert.pem

where is the last (intermediate) certificate fetched from the host (assuming the certificate order is correct). There are some limitations but in most cases this will work. This returns a hexadecimal number like:


With a '.0' appended this is a symlink to the root certificate:

ls -l /etc/ssl/certs/4a6481c9.0


lrwxrwxrwx 1 root root 27 okt 14  2017 /etc/ssl/certs/4a6481c9.0 -> GlobalSign_Root_CA_-_R2.pem

If you want to know why this works like this then see the utility c_rehash that comes with OpenSSL. This builds a hash for fast look up of root certificates.

In Python I use subprocess to run the OpenSSL command, and then write the root file in my directory for easy access. Note that I use STDIN to feed the PEM to OpenSSL and use STDOUT to capture the result.

    def get_root_cert(self, pem):
        cmd = ['openssl', 'x509', '-noout', '-issuer_hash']
        p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        out = p.communicate(input=pem.encode())[0].decode('utf-8').strip()

        if p.returncode != 0:
            return None

        root_pem_file = os.path.join(os.sep, 'etc', 'ssl', 'certs', out + '.0')
        if not os.path.isfile(root_pem_file):
            return None

        with open(root_pem_file, 'r') as fh:
            root_pem =

        with open('root.pem', 'w') as fh:
        return crypto.load_certificate(crypto.FILETYPE_PEM, root_pem)

Another way of getting the root certificate if we do have a root certificate bundle on our system is to download this bundle, for example from the Curl website:

This file contains the CA root PEMs. If we want to use this we should extract the certificates and then construct a dictionary with (index) key Subject-CN and value PEM. The first time we build and store this in a file (using Pickle), the next times we load this file, index the directory and return the PEM of the CA root certificate.

    def get_root_cert(self, pem):
        root_subject_cn2pems_file = 'root_subject_cn2pems.pickle'
        cacert_file = 'cacert.pem'

        cert = crypto.load_certificate(crypto.FILETYPE_PEM, pem)
        ci = CertInfo(cert)
        issuer_cn = ci.issuer_cn

        if os.path.isfile(root_subject_cn2pems_file):
            # use stored root pems
            with open(root_subject_cn2pems_file, 'rb') as fh:
                root_subject_cn2pems = pickle.load(fh)
            # create root pems and store
            with open(cacert_file, 'r') as fh:
                capems =

            pem_begin = '-----BEGIN CERTIFICATE-----'
            pem_end = '-----END CERTIFICATE-----'

            root_subject_cn2pems = {}
            for part in capems.split(pem_begin)[1:]:
                if pem_end not in part:
                pem_part, rem = part.split(pem_end, 1)
                pem = str(pem_begin + pem_part + pem_end)

                cert = crypto.load_certificate(crypto.FILETYPE_PEM, pem)
                ci = CertInfo(cert)
                if ci.subject_cn is None:
                root_subject_cn2pems[ci.subject_cn] = pem

            with open(root_subject_cn2pems_file, 'wb') as fh:
                pickle.dump(root_subject_cn2pems, fh)

        # try to get root pem
        if issuer_cn not in root_subject_cn2pems:
            return None

        root_pem = root_subject_cn2pems[issuer_cn]
        with open('root.pem', 'w') as fh:
        return crypto.load_certificate(crypto.FILETYPE_PEM, root_pem)

Again, CertInfo() is a class I wrote to extract information from a certificate. The Python code is at the end of this article.

Verifying the Chain of Trust

It is not enough to check if the server certificate links to a root certificate on your PC, phone, device. There is also what is called 'Chain of Trust'. Certificates we download from a host can not be trusted. The only certificate that be trusted is the root certificate that is on your device, PC, in a special directory, on my Ubuntu PC:


Using the root certificate we can check if we can trust the next (intermediate) certificate. If trusted then we use this intermediate certificate and check if the next certificate can be trusted. We do this until we arrive at the server certificate.

The OpenSSL command for the website:

openssl verify -x509_strict -CApath /etc/ssl/certs -untrusted 1.pem 0.pem

If we have the root certificate, we can do:

openssl verify -x509_strict -no-CApath -CAfile root.pem -untrusted 1.pem 0.pem

The 'untrusted' flag tells OpenSSL that 1.pem cannot be trusted and must be trusted before checking 0.pem. Similary, we can check the chain for the website which has two intermediate certificates:

openssl verify -x509_strict -no-CApath -CAfile root.pem -untrusted 2.pem -untrusted 1.pem 0.pem

Note that if you swap the intermediate certificates 1.pem and 2.pem for the website, then the result is the same in both cases. This means that OpenSSL is first trying to find the chain order and then starts the trust operation. In this case we can also concatenate the two intermediate certificates into 12.pem and run:

openssl verify -x509_strict -no-CApath -CAfile root.pem -untrusted 12.pem 0.pem

The SSL certificates I downloaded from the host are in self.host_certs and also stored in files 0.pem, 1.pem, ... Using Python's subprocess, the code is:

    def chain_is_trusted(self):
        cmd = ['openssl', 'verify', '-x509_strict', '-no-CApath', '-CAfile', 'root.pem']
        for i in range(self.host_certs_len - 1, 0, -1):
                str(i) + '.pem',        
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        out = p.communicate()[0].decode('utf-8').strip()

        if p.returncode == 0:
            return True
        return False

This is ugly and requires us to store PEM files first before calling subprocess.

Since pyOpenSSL 20.0.0 we can do this much more elegant:

    def chain_is_trusted(self):

        store = crypto.X509Store()
        store.load_locations(None, capath='/etc/ssl/certs')

        # server cert
        server_cert = self.host_certs[0]

        # intermediate certs
        untrusted_certs = self.host_certs[1:]

        store_ctx = crypto.X509StoreContext(store, server_cert, chain=untrusted_certs)

            # optional
            # certs = store_ctx.get_verified_chain()
            return True
        except crypto.X509StoreContextError as e:
        return False

The verify_certificate() method generates an Exception is if the chain cannot be verified.

The Chain of Trust, step-by-step

Above we checked the Chain of Trust with a single OpenSSL command. But we can also do this another way. Starting with the trusted root certificate on my PC we first try to trust the last intermediate certificate from the host. If this passes, we use the now trusted intermediate certificate to try to trust the next untrusted certificate all the way up to the server certificate. With OpenSSL we use the flag 'partial_chain'.

For example for the website we can issue the following commands:

openssl verify -x509_strict -no-CApath -CAfile root.pem -partial_chain 2.pem

openssl verify -x509_strict -no-CApath -CAfile 2.pem -partial_chain 1.pem

openssl verify -x509_strict -no-CApath -CAfile 1.pem -partial_chain 0.pem

Self-signed (intermediate) certificates

When an intermediate certificate is self-signed, OpenSSL stops validation(?). This means we want to check if a certificate downloaded from the host is self-signed.

A certificate is self-signed when:

  • the Subject-CN and the Issuer-CN match
  • the subjectKeyIdentifier and the authorityKeyIdentifier match
  • the certificate contains a key usage extension with the KU_KEY_CERT_SIGN bit set

I implemented only the the first two, no idea the the moment how to do the third item.

While testing I found for

subjectKeyIdentifier: None
authorityKeyIdentifier: None

but for

subjectKeyIdentifier: 03:DE:50:35:56:D1:4C:BB:66:F0:A3:E2:1B:1B:C3:97:B2:3D:D1:55
authorityKeyIdentifier: keyid:03:DE:50:35:56:D1:4C:BB:66:F0:A3:E2:1B:1B:C3:97:B2:3D:D1:55

I assume I must remove here the leading 'keyid:' before comparing the subjectKeyIdentifier against the authorityKeyIdentifier.

    def cert_is_self_signed(self, cert):
        ci = CertInfo(cert)

        # strip optional keyid from authority_key_identifier
        keyid = 'keyid:'
        keyid_len = len(keyid)
        extension_authority_key_identifier = ci.extension_authority_key_identifier
        if extension_authority_key_identifier is not None:
            if extension_authority_key_identifier[:keyid_len] == keyid:
                extension_authority_key_identifier = extension_authority_key_identifier[keyid_len:]

        # subject_key_identifier, authority_key_identifier: both None or match
        if (ci.subject_cn == ci.issuer_cn) and \
            ((ci.extension_subject_key_identifier is None and ci.extension_authority_key_identifier is None) or \
            (ci.extension_subject_key_identifier == extension_authority_key_identifier)):
                return True
        return False

More about self-signed certificates can be found in the article 'How to know if certificate is self-signed', see links below.

Here they also refer to RFC 3280:

'A certificate is self-issued if the DNs that appear in the subject and issuer fields are identical and are not empty. In general, the issuer and subject of the certificates that make up a path are different for each certificate. However, a CA may issue a certificate to itself to support key rollover or changes in certificate policies. These self-issued certificates are not counted when evaluating path length or name constraints.'

'The keyIdentifier field of the authorityKeyIdentifier extension MUST be included in all certificates generated by conforming CAs to facilitate certification path construction. There is one exception; where a CA distributes its public key in the form of a "self-signed" certificate, the authority key identifier MAY be omitted. The signature on a self-signed certificate is generated with the private key associated with the certificate's subject public key. (This proves that the issuer possesses both the public and private keys.) In this case, the subject and authority key identifiers would be identical, but only the subject key identifier is needed for certification path building.'

Confusion with,

During tests I also used and The host returned three certificates. I was checking the result by replacing certificates and see what happens.

openssl verify -x509_strict -no-CApath -CAfile root.pem -untrusted 2.pem -untrusted 1.pem 0.pem

It was a mess. I could remove intermediate certificate 2.pem and OpenSSL still said: OK. I could replace 1.pem by another random certificate and OpenSSL still said: OK. What was going on here?

Then I looked at the certificates returned by the host and found that the third certificate, 2.pem, was self-signed and was identical to the root certificate on my PC. This means that these domains send me a root certificate? OpenSSL does not complain, browsers do not complain.

I did not investigate this further but I thought to mention this to prevent headaches ... :-(

The wrong host check

The wrong host check must make sure that the certificate is for this host. I did not see how this can be done with pyOpenSSL, in fact I did not see a way to do this with OpenSSL. OpenSSL does not warn you if the certificate is for a different host.

We can use Wget:



ERROR: no certificate subject alternative name matches requested host name ‘’.

Or, we can use Curl:

curl -L


curl: (51) SSL: no alternative certificate subject name matches target host name ''

In Python we can do this with the Python requests library. A wrong host gives the exception:



HTTPSConnectionPool(host='', port=443): Max retries exceeded with url: / (Caused by SSLError(CertificateError("hostname '' doesn't match either of '*', ''",),))

Note that the Exception 'hostname ... doesn't match' only appears if other tests pass. If there is a problem with the certificates somewhere you get an Exception 'certificate verify failed'.

The CertInfo() class

I wrote some Python code to play with this. Most important is the CertInfo() class. Here I decode the certificate.


import datetime
from OpenSSL import crypto

class CertInfo:
    def __init__(
        self.cert = cert

    def decode_x509name_obj(self, o):
        parts = []
        for c in o.get_components():
            parts.append(c[0].decode('utf-8') + '=' + c[1].decode('utf-8'))
        return ', '.join(parts)

    def cert_date_to_gmt_date(self, d):
        return datetime.datetime.strptime(d.decode('ascii'), '%Y%m%d%H%M%SZ')

    def cert_date_to_gmt_date_string(self, d):
        return self.cert_date_to_gmt_date(d).strftime("%Y-%m-%d %H:%M:%S GMT")

    def get_item(self, item, extension=None, return_as=None, algo=None):
            if item == 'subject':
                return self.decode_x509name_obj(self.cert.get_subject())

            elif item == 'subject_o':
                return self.cert.get_subject().O.strip()

            elif item == 'subject_cn':
                return self.cert.get_subject().CN.strip()

            elif item == 'extensions':
                ext_count = self.cert.get_extension_count()
                if extension is None:
                    ext_infos = []
                    for i in range (0, ext_count):
                        ext = self.cert.get_extension(i)
                    return ext_infos

                for i in range (0, ext_count):
                    ext = self.cert.get_extension(i)
                    if extension in str(ext.get_short_name()):
                        return ext.__str__().strip()
                return None

            elif item == 'version':
                return self.cert.get_version()

            elif item == 'pubkey_type':
                pk_type = self.cert.get_pubkey().type()
                if pk_type == crypto.TYPE_RSA:
                    return 'RSA'
                elif pk_type == crypto.TYPE_DSA:
                    return 'DSA'
                return 'Unknown'

            elif item == 'pubkey_pem':
                return crypto.dump_publickey(crypto.FILETYPE_PEM, self.cert.get_pubkey()).decode('utf-8')

            elif item == 'serial_number':
                return self.cert.get_serial_number()

            elif item == 'not_before':
                not_before = self.cert.get_notBefore()
                if return_as == 'string':
                    return self.cert_date_to_gmt_date_string(not_before)
                return self.cert_date_to_gmt_date(not_before)

            elif item == 'not_after':
                not_after = self.cert.get_notAfter()
                if return_as == 'string':
                    return self.cert_date_to_gmt_date_string(not_after)
                return self.cert_date_to_gmt_date(not_after)

            elif item == 'has_expired':
                return self.cert.has_expired()

            elif item == 'issuer':
                return self.decode_x509name_obj(self.cert.get_issuer())

            elif item == 'issuer_o':
                return self.cert.get_issuer().O.strip()

            elif item == 'issuer_cn':
                return self.cert.get_issuer().CN.strip()

            elif item == 'signature_algorithm':
                return self.cert.get_signature_algorithm().decode('utf-8')

            elif item == 'digest':
                # ['md5', 'sha1', 'sha256', 'sha512']
                return self.cert.digest(algo)

            elif item == 'pem':
                return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert).decode('utf-8')

                return None

        except Exception as e:
            logger.error('item = {}, exception, e = {}'.format(item, e))
            return None

    def subject(self):
        return self.get_item('subject')

    def subject_o(self):
        return self.get_item('subject_o')

    def subject_cn(self):
        return self.get_item('subject_cn')

    def subject_name_hash(self):
        return self.get_item('subject_name_hash')

    def extension_count(self):
        return self.get_item('extension_count')

    def extensions(self):
        return self.get_item('extensions')

    def extension_basic_constraints(self):
        return self.get_item('extensions', extension='basicConstraints')

    def extension_subject_key_identifier(self):
        return self.get_item('extensions', extension='subjectKeyIdentifier')

    def extension_authority_key_identifier(self):
        return self.get_item('extensions', extension='authorityKeyIdentifier')

    def extension_subject_alt_name(self):
        return self.get_item('extensions', extension='subjectAltName')

    def version(self):
        return self.get_item('version')

    def pubkey_type(self):
        return self.get_item('pubkey_type')

    def pubkey_pem(self):
        return self.get_item('pubkey_pem')

    def serial_number(self):
        return self.get_item('serial_number')

    def not_before(self):
        return self.get_item('not_before')

    def not_before_s(self):
        return self.get_item('not_before', return_as='string')

    def not_after(self):
        return self.get_item('not_after')

    def not_after_s(self):
        return self.get_item('not_after', return_as='string')

    def has_expired(self):
        return self.get_item('has_expired')

    def issuer(self):
        return self.get_item('issuer')

    def issuer_o(self):
        return self.get_item('issuer_o')

    def issuer_cn(self):
        return self.get_item('issuer_cn')

    def signature_algorithm(self):
        return self.get_item('signature_algorithm')

    def digest_sha256(self):
        return self.get_item('digest', algo='sha256')

    def pem(self):
        return self.get_item('pem')

Not all properties of the certificate are available. For example, I did not find an easy way to get the Signature.

To print the certificate items:

    def print_cert_items(self, cert_id, cert):
        def format_cert_items(m):
            return '{}: {}'.format(m[0], m[1])

        ci = CertInfo(cert)

        cert_items = [
            ('Subject', ci.subject),
            ('Subject-CN', ci.subject_cn),
            ('Subject name hash', ci.subject_name_hash),
            ('Issuer', ci.issuer),
            ('Issuer-CN', ci.issuer_cn),
            ('Extensions', ci.extensions),
            ('Extension-basicConstraints', ci.extension_basic_constraints),
            ('Extension-subjectKeyIdentifier', ci.extension_subject_key_identifier),
            ('Extension-authorityKeyIdentifier', ci.extension_authority_key_identifier),
            ('Extension-subjectAltName (SAN)', ci.extension_subject_alt_name),
            ('Version', ci.version),
            ('Serial_number', ci.serial_number),
            ('Public key-type', ci.pubkey_type),
            ('Public key-pem', ci.pubkey_pem),
            ('Not before', ci.not_before_s),
            ('Not after', ci.not_after_s),
            ('Has expired', ci.has_expired),
            ('Signature algortihm', ci.signature_algorithm),
            ('Digest-sha256', ci.digest_sha256),
            ('PEM', ci.pem),

        cert_item_lines = map(format_cert_items, cert_items)


This was a long journey. I was looking for a Python pyOpenSSL only solution to verify the Chain of Trust without subprocess and the whole time bumped into pages telling me that this could not be done with pyOpenSSL only. After this I checked the pyOpenSSL repository on and it said that this had been added very recent. The changelog showed the parameters and methods and after reading the docs it was easy to implement.

I also found ways to check the certificate chain, whether a certificate is self-signed certificates, expired and a way to check for a wrong host. Now on to extending my server check script ...

Links / credits

[Cryptography-dev] on how (not) to chain certs with openssl + pyopenssl


Chain of Fools: An Exploration of Certificate Chain Validation Mishaps

Cheat Sheet - OpenSSL

Digital Certificates: Chain of Trust

Get or build PEM certificate chain in Python

Get your certificate chain right

How to know if certificate is self-signed

How to validate / verify an X509 Certificate chain of trust in Python?

PyOpenSSL - how can I get SAN(Subject Alternative Names) list

Use openssl to individually verify components of a certificate chain

Verify a certificate chain using openssl verify

X509StoreContext objects

Leave a comment

Comment anonymously or log in to comment.


Leave a reply

Reply anonymously or log in to reply.