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
function returns a map that contains a lot of information from the SSL certificate, including the getpeercert()
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
https://stackoverflow.com/a/52867893/1862500get_server_certificate
treats the given(host,port)
only as the target for connection but does not sethost
as theserver_name
in the TLS SNI extension like browsers would do
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