""" AUTHOR: Khushal P Soonderji DATE: Friday, 30th May, 2025 OBJECTIVE: To provide a base class for common behaviour of Google's APIs. REFERENCES: 1. Quickstart: https://developers.google.com/gmail/api/quickstart/python DOWNLOADS: N/A """ # ***************************************************************************************************************** # ***** **** # *** IMPORT *** # ***** **** # ***************************************************************************************************************** # To make sibling directories accessible for imports: import sys sys.path.append(".") sys.path.append("..") # To check certs through cryptographic algorithms: from cryptography import x509 from cryptography.hazmat.backends import default_backend # To fetch remote certs: import ssl import socket from urllib.parse import urlparse # To work with date and time: import datetime import humanize # For debugging: from icecream import IceCreamDebugger # ***************************************************************************************************************** # ***** **** # *** MACROS / ONE-TIME INIT *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** VARIABLES *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** FUNCTIONS *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** CLASSES *** # ***** **** # ***************************************************************************************************************** class CertExpiryCheck: # Certificate variables: certificate: str | None = None not_valid_before: datetime.datetime | None = None not_valid_after: datetime.datetime | None = None exception: Exception | None = None exception_str: str | None = None # ┏┓ # ┃ ┏┓┏┓╋┏┓┓┏┏╋┏┓┏┓ # ┗┛┗┛┛┗┗┛ ┗┻┗┗┗┛┛ def __init__( self, debug: bool = False, debug_prefix: str = "Certs Exp. | " ): """ This simple class just checks if a provided certificate is valid or not and what is the date range in which it is valid. """ # Prepare the debugging utility: self._debug_prefix = debug_prefix self._printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True) if not debug: self._printer.disable() # ┓┏ ┓ # ┣┫┏┓┃┏┓┏┓┏┓┏ # ┛┗┗ ┗┣┛┗ ┛ ┛ # ┛ def enable_debug(self): self._printer.enable() def disable_debug(self): self._printer.disable() def _clear(self): self.not_valid_before = None self.not_valid_after = None self.exception = None self.exception_str = None # ┏┓ • # ┃┃┏┓┏┓┏┓┏┓┏┓╋┓┏┓┏ # ┣┛┛ ┗┛┣┛┗ ┛ ┗┗┗ ┛ # ┛ @property def is_valid(self) -> bool | None: """ Quickly tests if a loaded certificate is valid or not. :return: True if the certificate is valid, False if it isn't. Returns None if no certificate was loaded. """ # Check if the certificate was loaded correctly: if self.not_valid_after is None or self.not_valid_before is None: self._printer("Please load a certificate first.") return None # Check if the cert is within the given time limits: now = datetime.datetime.now(datetime.timezone.utc) if self.not_valid_before <= now <= self.not_valid_after: return True else: return False @property def ttl(self) -> float | None: """ Gives the time for which the certificate is valid in seconds. :return: Time for which the certificate is valid in seconds. None if the certificate was not loaded. """ # Check if the certificate was loaded correctly: if self.not_valid_after is None or self.not_valid_before is None: self._printer("Please load a certificate first.") return None # Calculate the no. of seconds left till expiry: now = datetime.datetime.now(datetime.timezone.utc) ttl_seconds = (self.not_valid_after - now).total_seconds() return ttl_seconds @property def ttl_human_readable(self) -> str | None: """ Gives human-readable validity period for certificates. e.g.: 1. valid for a month 2. expired 3 days ago 3. valid for 2 months :return: Human-readable validity period for certificates. None if the certificate was not loaded. """ # Check if the certificate was loaded correctly: if self.not_valid_after is None or self.not_valid_before is None: self._printer("Please load a certificate first.") return None # Calculate the no. of seconds left till expiry: now = datetime.datetime.now(datetime.timezone.utc) ttl_seconds = (self.not_valid_after - now).total_seconds() ttl_readable = humanize.naturaldelta(datetime.timedelta(seconds = ttl_seconds)) if ttl_seconds <= 0: ttl_readable = f"expired {ttl_readable} ago" else: ttl_readable = f"valid for {ttl_readable}" return ttl_readable # ┓ ┓ ┏┓ # ┃ ┏┓┏┏┓┃ ┃ ┏┓┏┓╋┏ # ┗┛┗┛┗┗┻┗ ┗┛┗ ┛ ┗┛ def load_local(self, path: str) -> bool: """ Loads a certificate stored on a local file path and checks for its validity. :param path: local file path where the certificate is stored. :return: True if the certificate was loaded correctly, False if the certificate wasn't loaded. """ # Note down the certificate that is being tested: self.certificate = path # Clear any previous assessment: self._clear() try: # Read the contents of the certificate file: with open(path, 'rb') as f: cert_data = f.read() # Load the certificate: cert = x509.load_pem_x509_certificate(cert_data, default_backend()) # Get the not valid before and not valid after dates: self.not_valid_before = cert.not_valid_before_utc self.not_valid_after = cert.not_valid_after_utc # All done successfully: return True except Exception as exception: self._printer(path, exception) self.exception = exception self.exception_str = str(exception) return False # ┳┓ ┏┓ # ┣┫┏┓┏┳┓┏┓╋┏┓ ┃ ┏┓┏┓╋┏ # ┛┗┗ ┛┗┗┗┛┗┗ ┗┛┗ ┛ ┗┛ def load_remote(self, hostname: str, port: int = 443) -> bool: """ Loads the certificate of a remote host and checks for its validity. :param hostname: The remote host URL. :param port: The remote host's port on which the service is running. :return: True if the certificate was loaded correctly, False if the certificate wasn't loaded. """ # Parse the URL's hostname: hostname = urlparse(hostname).hostname or hostname # Note down the certificate that is being tested: self.certificate = f"{hostname}:{port}" # Clear any previous assessment: self._clear() try: # Fetch the remote certificate: context = ssl.create_default_context() with socket.create_connection((hostname, port)) as sock: with context.wrap_socket(sock, server_hostname = hostname) as ssock: # Attempt to retrieve the certificate in DER format try: der_cert = ssock.getpeercert(binary_form = True) cert = x509.load_der_x509_certificate(der_cert, default_backend()) # If DER parsing fails, retrieve the certificate in PEM format: except ValueError: pem_cert = ssl.DER_cert_to_PEM_cert(ssock.getpeercert(binary_form = True)) cert = x509.load_pem_x509_certificate(pem_cert.encode(), default_backend()) # Get the not valid before and not valid after dates: self.not_valid_before = cert.not_valid_before_utc self.not_valid_after = cert.not_valid_after_utc # All done successfully: return True except Exception as exception: self._printer(hostname, port, exception) self.exception = exception self.exception_str = str(exception) return False # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM *** # ***** **** # ***************************************************************************************************************** if __name__ == "__main__": def show_details(my_cert: CertExpiryCheck): print("\n---\n") print("Certificate :", my_cert.certificate) print("Not Before :", my_cert.not_valid_before) print("Not After :", my_cert.not_valid_after) print("Is Valid :", my_cert.is_valid) print("TTL (seconds) :", my_cert.ttl) print("TTL (human) :", my_cert.ttl_human_readable) print("Exception :", my_cert.exception_str) # Create an instance: my_cert = CertExpiryCheck(debug = False) # Test a local path: my_cert.load_local(r"C:\Users\Khushal P Soonderji\Desktop\Dktp Files\certs\mongo\mongo_data_cert.pem") show_details(my_cert) # Test a local path: my_cert.load_local(r"C:\Users\Khushal P Soonderji\Desktop\Dktp Files\certs\kafka\privkey.pem") show_details(my_cert) # Test a local path: my_cert.load_local(r"C:\Users\Khushal P Soonderji\Desktop\Dktp Files\certs\kafka\fullchain.pem") show_details(my_cert) # Test a remote path: my_cert.load_remote("https://thecaoffice.com/", port = 443) show_details(my_cert)