-1

What's the correct way to handle an expired certificates with Python Requests?

I want the code to differentiate between a "connection error" and connection with an "expired TLS certificate".

import requests

def conn(URL):

    try:
        response = requests.get(URL)
    except requests.exceptions.RequestException:
        print(URL, "Cannot connect")
        return False

    print(URL, "connection sucessful")
    return True

# valid cert
conn("https://www.google.com")

# unexistant domain
conn("https://unexistent-domain-example.com")

# expired cert
conn("https://expired-rsa-dv.ssl.com")
Andre
  • 598
  • 1
  • 7
  • 18

2 Answers2

1

I want the code to differentiate between a "connection error" and connection with an "expired TLS certificate".

You can look at the exception details and see if 'CERTIFICATE_VERIFY_FAILED' is there.

import requests

def conn(URL):

    try:
        response = requests.get(URL)
    except requests.exceptions.RequestException as e:
        if 'CERTIFICATE_VERIFY_FAILED' in str(e):
            print('CERTIFICATE_VERIFY_FAILED')
        print(URL, f"Cannot connect: {str(e)}")
        print('--------------------------')

        return False

    print(URL, "connection sucessful")
    return True

# valid cert
conn("https://www.google.com")


# unexistant domain
conn("https://unexistent-domain-example.com")

# expired cert
conn("https://expired-rsa-dv.ssl.com")
balderman
  • 22,927
  • 7
  • 34
  • 52
  • [`requests.exceptions.SSLError`](https://docs.python-requests.org/en/master/_modules/requests/exceptions/) – Olvin Roght Oct 03 '21 at 17:42
  • There may be reasons besides expiration for the certificate verification to fail. – President James K. Polk Oct 03 '21 at 19:57
  • @PresidentJamesK.Polk, multiple `except` clauses allowed, so I see nothing bad in separate handler for certificate problems. I find this better than check error message manually. – Olvin Roght Oct 03 '21 at 20:04
  • @OlvinRoght: OP didn't ask about certificate "problems", they asked specifically about expired certificates. – President James K. Polk Oct 03 '21 at 20:05
  • @PresidentJamesK.Polk, but code in answer will react on any certificate problem. – Olvin Roght Oct 03 '21 at 20:06
  • @OlvinRoght: That's my point. An expired certificate is a much less serious problem than, for example, an invalid signature on a certificate. – President James K. Polk Oct 03 '21 at 20:07
  • @PresidentJamesK.Polk, and I reacted on certain code. At all I would not use `requests` to check expiration date of website's certificate, retrieve cert using [`SSLSocket.getpeercert()`](https://docs.python.org/3/library/ssl.html#ssl.SSLSocket.getpeercert) and validate expiration date seems to be more generic solution. – Olvin Roght Oct 03 '21 at 20:11
  • @OlvinRoght: Yes, I think retrieving the cert and checking it for expiration is the only way. – President James K. Polk Oct 03 '21 at 20:14
1

requests is a perfect tool for requests, but your task is to check server certificate expiration date which require using lower level API. The algorithm is to retrieve server certificate, parse it and check end date.

To get certificate from server there's function ssl.get_server_certificate(). It will return certificate in PEM encoding.

There're plenty of ways how to parse PEM encoded certificate (check this question), I'd stick with "undocumented" one.

To parse time from string you can use ssl.cert_time_to_seconds().

To parse url you can use urllib.parse.urlparse(). To get current timestamp you can use time.time()

Code:

import ssl
from time import time
from urllib.parse import urlparse
from pathlib import Path

def conn(url): 
    parsed_url = urlparse(url)
    cert = ssl.get_server_certificate((parsed_url.hostname, parsed_url.port or 443))
    # save cert to temporary file (filename required for _test_decode_cert())
    temp_filename = Path(__file__).parent / "temp.crt"
    with open(temp_filename, "w") as f:
        f.write(cert)
    try:
        parsed_cert = ssl._ssl._test_decode_cert(temp_filename)
    except Exception:
        return
    finally:  # delete temporary file
        temp_filename.unlink()

    return ssl.cert_time_to_seconds(parsed_cert["notAfter"]) > time()

It'll throw an exception on any connection error, you can handle it with try .. except over get_server_certificate() call (if needed).

Olvin Roght
  • 7,677
  • 2
  • 16
  • 35
  • This answer gives the expiry date, which is handy —regardless of OP's Q. Two notes: `parsed_url.port` is None for a regular 'https://' as opposed to 443 (it's 443 if `:443`). The value of `notAfter` is something like 'May 23 12:11:59 2022 GMT', which can be parsed with `datetime.datetime.strptime(not_after '%b %d %H:%M:%S %Y %Z')` – Matteo Ferla Feb 22 '22 at 14:09
  • @MatteoFerla, you probably noticed `parsed_url.port or 443` which will use either parsed port of `443` as a default value is `parsed_url.port` will be `None` or `0`. In code I've used [`ssl.cert_time_to_seconds()`](https://docs.python.org/3/library/ssl.html#ssl.cert_time_to_seconds) which internally calls `.strptime()` with proper formatting. – Olvin Roght Feb 22 '22 at 14:17
  • Sorry —I did not at all mean to imply there was any issue. I was just clarifying for others as `parsed_url.port` behaved not as I would have expected it to (guessed from protocol). And I do not care about unix time for my usage, so I thought others may find the datetime format handy. The only glitch is that the test addresses from https://www.ssl.com/sample-valid-revoked-and-expired-ssl-tls-certificates/ seem to behave oddly, but that is due to the ssl.com site. – Matteo Ferla Feb 22 '22 at 14:25