Get Unverified SSL Certificate Expiry Date with Python

Getting verified SSL information with Python (3.x) is very easy. Code examples for it are also scattered everywhere in the internet. This is one of the example where only few lines required with only built-in libraries (socket and ssl):

import datetime
import socket
import ssl

def get_num_days_before_expired(hostname: str, port: str = '443') -> int:
    """
    Get number of days before an TLS/SSL of a domain expired
    """    
    context = ssl.create_default_context()
    with socket.create_connection((hostname, port)) as sock:
        with context.wrap_socket(sock, server_hostname=hostname) as ssock:
            ssl_info = ssock.getpeercert()
            expiry_date = datetime.datetime.strptime(ssl_info['notAfter'], '%b %d %H:%M:%S %Y %Z')
            delta = expiry_date - datetime.datetime.utcnow()
            print(f'{hostname} expires in {delta.days} day(s)')
            return delta.days


if __name__ == '__main__':
    get_num_days_before_expired('google.com')

The getpeercert() function returns a map that contains a lot of information from the SSL certificate, including the notAfter which represents the expiry date. For most cases, this code would be sufficient.

But in my case, I need to get information from a domain that used SSL certficate that I generated manually using Let’s Encrypt certbot and upload it to a shared hosting. This is because the shared hosting package doesn’t include feature to automatically generate Let’s Encrypt certificates and renew them. For some reason the code above will throw exception CERTIFICATE_VERIFY_FAILED when I run against that domain.

ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1131)

From various solutions offered by the internet, using OpenSSL (pip install pyOpenSSL) package initially seemed suitable for me as it doesn’t throw any error and return notAfter information. This is one of the code I got from Stackoverflow:

import datetime
import socket
import ssl
import OpenSSL

def get_num_days_before_expired(hostname: str, port: str = '443') -> int:
    """
    Get number of days before an TLS/SSL of a domain expired
    """    
    cert = ssl.get_server_certificate((hostname, int(port)))
    x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
    expiry_date = datetime.datetime.strptime(x509.get_notAfter().decode('utf-8'), '%Y%m%d%H%M%S%z')
    delta = expiry_date - datetime.datetime.now(datetime.timezone.utc)
    print(f'{hostname} expires in {delta.days} day(s)')
    return delta.days

But for some reason, this code doesn’t return the same notAfter information with the one we see using Chrome tools for my domain. Apparently, it’s due to the behaviour of ssl.get_server_certificate() as described in this Stackoverflow answer:

While not documented as such get_server_certificate treats the given (host,port) only as the target for connection but does not set host as the server_name in the TLS SNI extension like browsers would do

https://stackoverflow.com/a/52867893/1862500

So finally, I decided to just mash up those two solutions above 😅

from urllib.request import ssl, socket
from datetime import datetime, timezone
import OpenSSL

def get_num_days_before_expired(hostname: str, port: str = '443') -> int:
    """
    Get number of days before an TLS/SSL of a domain expired
    """
    context = ssl.SSLContext()
    with socket.create_connection((hostname, port)) as sock:
        with context.wrap_socket(sock, server_hostname = hostname) as ssock:
            certificate = ssock.getpeercert(True)
            cert = ssl.DER_cert_to_PEM_cert(certificate)
            x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
            cert_expires = datetime.strptime(x509.get_notAfter().decode('utf-8'), '%Y%m%d%H%M%S%z')
            num_days = (cert_expires - datetime.now(timezone.utc)).days
            print(f'{hostname} expires in {num_days} day(s)')
            return num_days

if __name__ == '__main__':
    get_num_days_before_expired('ssis.sionministry.org')

Using this code, I’m able to retrieve correct expiry information from my unverified SSL certificate. So I can create a scheduled script to notify me when the expired date is close 🎉

I couldn’t find good documentation for the x509 object, fortunately we can still check the source code to know how to fetch other information. So we can extend the script to fetch information about the certificate and the domain.

Hopefully this can also help someone out there. Cheers! 🍻

Image by Mudassar Iqbal from Pixabay
5 2 votes
Article Rating
Subscribe
Notify of
guest
2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments