From b3456703e5415d6afb7c725d416136a1d1fe1def 2024-05-09 18:17:36 From: x Date: 2024-05-09 18:17:36 Subject: [PATCH] feat: replace web with more generic SSL verificator and handlers --- diff --git a/backend/check_domains.py b/backend/check_domains.py index 5a9b44cc51c8edeb2f62af2defeb920e8118421e..f05df8735d2a1f2b757a1ed7cdb8500a257f8d76 100644 --- a/backend/check_domains.py +++ b/backend/check_domains.py @@ -1,14 +1,11 @@ #!/usr/bin/env python3 import json import ssl -import socket import os from rich.console import Console -from cryptography import x509 -import web +from web import SSLVerificator from mail import MailVerificator -import tls_utils if __name__ == "__main__": console = Console() @@ -22,43 +19,10 @@ if __name__ == "__main__": console.log("[white]Checking web domains...") + ssl = SSLVerificator(context) for web_domain in input["domains"]["web"]: - # Initiate TLS connection - with context.wrap_socket(socket.socket(), server_hostname=web_domain) as s: - try: - s.connect((web_domain, 443)) - cert = s.getpeercert() - except ssl.SSLCertVerificationError as e: - saved = e - if e.verify_code == 10: - expiry = web.web_noconn_expiry_days(web_domain)[1] - if(expiry != None): - # TODO: add the TLS expiry stuff here - # possibly a list of domains that have expired - # if its already in here, dont add it again - console.log("[red bold underline]" + web_domain, "expired", abs(expiry), "days ago.", style="red") - elif e.verify_code == 23: - console.log("[red bold underline]" + web_domain, "was revoked.", style="red") - elif e.verify_code == 18: - console.log("[red bold underline]" + web_domain, "is self-signed.", style="red") - elif e.verify_code == 19: - console.log("[red bold underline]" + web_domain, "invalid: root not trusted.", style="red") - else: - console.log("[red bold underline]" + web_domain, "failed verification:", e.verify_message + ".", style="red") - continue - except ssl.SSLError as e: - console.log("[orange bold underline]" + web_domain, "could not establish a secure connection:", e.reason, style="orange") - continue - except Exception as e: - print(e) - continue - - validity = tls_utils.get_validity_days(cert)[1] - # Print expiry date - console.log("[green bold underline]" + web_domain, "expires in", validity, "days", style="green") - # TODO: remove known expired certs - # If the cert was expired before, we know that it is now valid - # -> remove it from the list of expirjuded certs + result = ssl.connect(web_domain, 443) + result.print(console) mail = MailVerificator(context) for smtp_entry in input["domains"]["smtp"]: diff --git a/backend/mail.py b/backend/mail.py index f8ecd52b4f00555c4eaba8ef93b43630b4755b55..dfc9f8c283a4d40370467dfc36104ae5c4311080 100644 --- a/backend/mail.py +++ b/backend/mail.py @@ -14,7 +14,7 @@ class MailHandler(ABC): self.port = port self.context = context - def connect(self, verification: bool) -> tuple[bool, int]: + def connect(self, verification: bool) -> int: connection = self.protocol_init(self.host, self.port) if verification: connection.starttls(**self.protocol_starttls_args()) @@ -22,7 +22,7 @@ class MailHandler(ABC): connection.starttls() cert = connection.sock.getpeercert() self.protocol_close(connection) - return tls_utils.get_validity_days(cert) + return tls_utils.get_validity_days(cert)[1] @abstractmethod def protocol_init(self, host, port): @@ -66,11 +66,11 @@ class MailVerificator: def connect(self, domain: str, port: int, protocol: str) -> TLSDetails: mail = MailHandler.create_handler(protocol)(domain, port, self.context) try: - expiry = mail.connect(True)[1] + expiry = mail.connect(True) return TLSDetails(domain_name=domain, expires_in_days=expiry) except ssl.SSLCertVerificationError as e: - if (e.verify_code == 10): - expiry = mail.connect(False)[1] + if (e.verify_code == tls_utils.EXPIRED_VERIFY_CODE): + expiry = mail.connect(False) return TLSDetails(domain_name=domain, expires_in_days=expiry) else: error = "failed verification:", e.verify_message + "." diff --git a/backend/tls_utils.py b/backend/tls_utils.py index 8e6ae78ef00284f356c46dac20096e38151e58d4..041340fc2b21e0665325499422181d66d4e0b5ba 100644 --- a/backend/tls_utils.py +++ b/backend/tls_utils.py @@ -3,18 +3,27 @@ from rich.console import Console import datetime import math +EXPIRED = 10 +REVOKED = 23 +SELF_SIGNED = 18 +ROOT_NOT_TRUSTED = 19 + class TLSDetails: domain_name = None expires_in_days = None error_message = None + connection_error = False - def __init__(self, domain_name : str = None, expires_in_days : str = None, error_message : str = None): + def __init__(self, domain_name : str = None, expires_in_days : str = None, error_message : str = None, connection_error : bool = False): self.domain_name = domain_name self.expires_in_days = expires_in_days self.error_message = error_message + self.connection_error = connection_error def print(self, console: Console): - if self.error_message != None: + if self.connection_error: + console.log("[orange bold underline]" + self.domain_name, self.error_message, style="orange") + elif self.error_message != None: console.log("[red bold underline]" + self.domain_name, self.error_message, style="red") elif self.expires_in_days < 0: console.log("[red bold underline]" + self.domain_name, "expired", abs(self.expires_in_days), "days ago.", style="red") diff --git a/backend/web.py b/backend/web.py index 60b3c3cce11a587cb1a3b0b44158a0e7e161cdca..d1d8626b54ec6436542b29e585496bf093a946c2 100644 --- a/backend/web.py +++ b/backend/web.py @@ -2,17 +2,51 @@ import ssl from rich.console import Console from cryptography import x509 +import socket -import tls_utils as tls_utils +import tls_utils +from tls_utils import TLSDetails -def web_noconn_expiry_days(web_domain: str) -> int | None: - try: - pem_cert = ssl.get_server_certificate((web_domain, 443), timeout=5) - cert = x509.load_pem_x509_certificate(pem_cert.encode()) - except Exception as e: - console = Console() - console.log("Could not grab server cert for", "[orange bold underline]"+web_domain, ":", e, style="orange") - return None - - not_after = cert.not_valid_after.timestamp() - return tls_utils.get_expiry_timestamps(not_after) \ No newline at end of file +class SSLHandler: + def __init__(self, host: str, port: int, context: ssl.SSLContext): + self.host = host + self.port = port + self.context = context + + def connect(self, verification: bool) -> int: + if verification: + with self.context.wrap_socket(socket.socket(), server_hostname=self.host) as s: + s.connect((self.host, self.port)) + cert = s.getpeercert() + return tls_utils.get_validity_days(cert)[1] + else: + pem_cert = ssl.get_server_certificate((self.host, self.port), timeout=5) + cert = x509.load_pem_x509_certificate(pem_cert.encode()) + not_after = cert.not_valid_after.timestamp() + return tls_utils.get_expiry_timestamps(not_after)[1] + +class SSLVerificator: + def __init__(self, context: ssl.SSLContext): + self.context = context + + def connect(self, domain: str, port: int) -> TLSDetails: + handler = SSLHandler(domain, port, self.context) + try: + expiry = handler.connect(True) + return TLSDetails(domain_name=domain, expires_in_days=expiry) + except ssl.SSLCertVerificationError as e: + if e.verify_code == tls_utils.EXPIRED: + expiry = handler.connect(False) + return TLSDetails(domain_name=domain, expires_in_days=expiry) + elif e.verify_code == tls_utils.REVOKED: + return TLSDetails(domain_name=domain, error_message="was revoked.") + elif e.verify_code == tls_utils.SELF_SIGNED: + return TLSDetails(domain_name=domain, error_message="is self-signed.") + elif e.verify_code == tls_utils.ROOT_NOT_TRUSTED: + return TLSDetails(domain_name=domain, error_message="invalid: root not trusted.") + else: + return TLSDetails(domain_name=domain, error_message="failed verification: " + e.verify_message + ".") + except ssl.SSLError as e: + return TLSDetails(domain_name=domain, error_message="could not establish a secure connection: " + e.reason + ".") + except Exception as e: + return TLSDetails(domain_name=domain, error_message="could not connect: " + str(e) + ".") \ No newline at end of file