2

I created a simple Python program to get the expiry date of SSL cert, from reference on the Internet. It works correctly for cert that is still not expired. But for cert that already expired, an error was raised during the socket handshake due to the cert expiry.

How do I get the expired cert info to extract the expiry date because the connection is refused. Is there a way to force the socket connection to establish even though the cert might be expired?

Code:

import ssl
from cryptography import x509
import sys
import socket

hostname = sys.argv[1]

context = ssl.create_default_context()

with socket.create_connection((hostname, 443)) as sock:
    with context.wrap_socket(sock, server_hostname=hostname) as ssock:
        print("SSL/TLS version:",ssock.version())
        print()
        data = ssock.getpeercert()
        print("Data:",data)
        print()
        notafter_date = data["notAfter"]
        print("Expiry date:",notafter_date)
        print()

Output for not expired cert:

$ python check_ssl_cert.py badssl.com
SSL/TLS version: TLSv1.2

Data: {'subject': ((('countryName', 'US'),), (('stateOrProvinceName', 'California'),), (('localityName', 'Walnut Creek'),), (('organizationName', 'Lucas Garron Torres'),), (('commonName', '*.badssl.com'),)), 'issuer': ((('countryName', 'US'),), (('organizationName', 'DigiCert Inc'),), (('commonName', 'DigiCert SHA2 Secure Server CA'),)), 'version': 3, 'serialNumber': '0AF06CDA37A60B641342F0A1EB1D59FD', 'notBefore': 'Mar 23 00:00:00 2020 GMT', 'notAfter': 'May 17 12:00:00 2022 GMT', 'subjectAltName': (('DNS', '*.badssl.com'), ('DNS', 'badssl.com')), 'OCSP': ('http://ocsp.digicert.com',), 'caIssuers': ('http://cacerts.digicert.com/DigiCertSHA2SecureServerCA.crt',), 'crlDistributionPoints': ('http://crl3.digicert.com/ssca-sha2-g6.crl', 'http://crl4.digicert.com/ssca-sha2-g6.crl')}

Expiry date: May 17 12:00:00 2022 GMT

Output for expired cert:

$ python check_ssl_cert.py expired.badssl.com
Traceback (most recent call last):
  File "check_ssl_cert.py", line 11, in <module>
    with context.wrap_socket(sock, server_hostname=hostname) as ssock:
  File "/usr/lib/python3.7/ssl.py", line 423, in wrap_socket
    session=session
  File "/usr/lib/python3.7/ssl.py", line 870, in _create
    self.do_handshake()
  File "/usr/lib/python3.7/ssl.py", line 1139, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1091)

As suggested for the other solution, it does not solve the issue.

I tried changing the line

data = ssock.getpeercert()

to

data = ssock.getpeercert(True)

and a DER formatted cert is returned for not expired cert, but got cert verification error for already expired cert.

Sharuzzaman Ahmat Raslan
  • 1,557
  • 2
  • 22
  • 34
  • no it does not. I tried to put ssock.getpeercert(True), and the same error is shown. I can try to add ssl.get_server_certificate command and call it trough try..except . still hoping to just use ssock.getpeercert() command only – Sharuzzaman Ahmat Raslan Feb 16 '22 at 10:20
  • 2
    "But for cert that already expired, an error was raised during the socket handshake due to the cert expiry." By design. If you use an high level library, it does all validity check for you so it will abort the TLS handshake if there is a problem. In Python have a look at PyOpenSSL module instead of internal `ssl` one, it might give you better control of what is happening (see `set_verify()`) – Patrick Mevzek Feb 16 '22 at 10:24
  • 1
    Yes, SSL (TLS, really) socket creation is abstracted away and in all high-level libraries, and the design goal here is that the connection fails hard when something is fishy. You need to use a low-level library that can do half a handshake. – Tomalak Feb 16 '22 at 10:28
  • Inserting `ssl.get_server_certificate()` was the entire point of my duplicate link. It's clear that the other things don't work. – Tomalak Feb 16 '22 at 10:53
  • `create_default_context` creates a context **with** certificate validation and that's why it fails. See [Python getting common name from URL using ssl.getpeercert()](https://stackoverflow.com/questions/45478536/python-getting-common-name-from-url-using-ssl-getpeercert) on how to reach your goal. – Steffen Ullrich Feb 16 '22 at 12:51
  • @SteffenUllrich I will review that post and figure out a solution – Sharuzzaman Ahmat Raslan Feb 16 '22 at 13:18

2 Answers2

1

I managed so create a working solution. Check my Github gist here: https://gist.github.com/sharuzzaman/8827ef0d9fff89e4e937579b2b01653f

Also the verbatim code here for quick reference

#!/bin/env python3

# check_ssl_cert.py - python get info for expired SSL cert
# Copyright 2022 Sharuzzaman Ahmat Raslan <sharuzzaman@gmail.com>

# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public License along with this program. If not, see https://www.gnu.org/licenses/.

from cryptography import x509
import socket
import ssl
import sys

hostname = sys.argv[1]

# create default context
context = ssl.create_default_context()

# override context so that it can get expired cert
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE

with socket.create_connection((hostname, 443)) as sock:
    with context.wrap_socket(sock, server_hostname=hostname) as ssock:
        print("SSL/TLS version:", ssock.version())
        print()

        # get cert in DER format
        data = ssock.getpeercert(True)
        print("Data:", data)
        print()

        # convert cert to PEM format
        pem_data = ssl.DER_cert_to_PEM_cert(data)
        print("PEM cert:", pem_data)

        # pem_data in string. convert to bytes using str.encode()
        # extract cert info from PEM format
        cert_data = x509.load_pem_x509_certificate(str.encode(pem_data))

        # show cert expiry date
        print("Expiry date:", cert_data.not_valid_after)
        print()
Sharuzzaman Ahmat Raslan
  • 1,557
  • 2
  • 22
  • 34
  • This code worked for few url but for other url I am getting error SSLError: [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1122) – Saiyam Jain Aug 02 '23 at 06:56
  • highly likely your OpenSSL library that was used by Python is not supporting SSLv3 anymore, but the server you are connecting to only support SSLv3 – Sharuzzaman Ahmat Raslan Aug 05 '23 at 05:44
  • 1
    I have found the solution attaching link to my solution https://stackoverflow.com/questions/35405092/sslerror-sslv3-alert-handshake-failure/76867961#76867961 – Saiyam Jain Aug 09 '23 at 12:50
0

I use scripts like this...

root@script:~# cat /root/bin/check-cert-mattila.eu.sh

#!/bin/bash
clear
echo "SERVER CERTIFICATE EXPIRATION DATES:"
echo "------------------------------------"
server=mail.mattila.eu
echo ""
echo "server = ${server}"
echo ""
echo ""
echo "#### WWW port (client2server) ####"
echo "------------------------------------"
echo " SSL certificate on port 443"
echo |openssl s_client -showcerts -connect $server:443 2>/dev/null | openssl x509 -dates | grep After | sed 's/notAfter=/ Expires = /'
echo ""
echo ""
echo "#### SMTP ports (server2server) ####"
echo "------------------------------------"
echo " TLS certificate on port 25"
echo |openssl s_client -showcerts -connect $server:25 2>/dev/null -starttls smtp | openssl x509 -dates | grep After | sed 's/notAfter=/ Expires = /'
echo ""
echo " SSL certificate on port 465"
echo |openssl s_client -showcerts -connect $server:465 2>/dev/null | openssl x509 -dates | grep After | sed 's/notAfter=/ Expires = /'
echo ""
echo " TLS certificate on port 587"
echo |openssl s_client -showcerts -connect $server:587 2>/dev/null -starttls smtp | openssl x509 -dates | grep After | sed 's/notAfter=/ Expires = /'
echo ""
echo ""
echo "#### IMAP ports (client2server) ####"
echo "------------------------------------"
echo " SSL certificate on port 143"
echo |openssl s_client -showcerts -connect $server:143 2>/dev/null -starttls imap | openssl x509 -dates | grep After | sed 's/notAfter=/ Expires = /'
echo ""
echo " SSL certificate on port 993"
echo |openssl s_client -showcerts -connect $server:993 2>/dev/null | openssl x509 -dates | grep After | sed 's/notAfter=/ Expires = /'
echo ""
echo ""

It will give you something like this (if your script host has up-to-date CA's).

SERVER CERTIFICATE EXPIRATION DATES:
------------------------------------

server = mail.mattila.eu


#### WWW port (client2server) ####
------------------------------------
 SSL certificate on port 443
 Expires = May  4 23:33:17 2023 GMT


#### SMTP ports (server2server) ####
------------------------------------
 TLS certificate on port 25
 Expires = May  4 23:33:17 2023 GMT

 SSL certificate on port 465
 Expires = May  4 23:33:17 2023 GMT

 TLS certificate on port 587
 Expires = May  4 23:33:17 2023 GMT


#### IMAP ports (client2server) ####
------------------------------------
 SSL certificate on port 143
 Expires = May  4 23:33:17 2023 GMT

 SSL certificate on port 993
 Expires = May  4 23:33:17 2023 GMT

This also shows correctly expired (LetsEncrypt/Certbot) certs (certifications). You can also modify it to snif out SQL/LDAP ports.

SamTzu
  • 167
  • 1
  • 7