diff --git a/src/Server.py b/src/Server.py index ef1bc00..84f7998 100644 --- a/src/Server.py +++ b/src/Server.py @@ -322,6 +322,72 @@ def init_mtls_server_context(cert_dir: str = "certs") -> ssl.SSLContext: return ctx +def reload_mtls_context(ssl_ctx: ssl.SSLContext, cert_dir: str = "certs"): + """ + Dynamically reloads server certificate chain and Root CA in an active SSLContext. + Allows in-flight TLS certificate rotation without dropping the listening socket. + """ + ca_file = os.path.join(cert_dir, "ca.crt") + srv_cert = os.path.join(cert_dir, "server.crt") + srv_key = os.path.join(cert_dir, "server.key") + + ssl_ctx.load_cert_chain(certfile=srv_cert, keyfile=srv_key) + ssl_ctx.load_verify_locations(cafile=ca_file) + + +def check_and_rotate_server_certs( + cert_dir: str = "certs", + hostnames: Optional[List[str]] = None, + threshold_days: int = 30 +) -> bool: + """ + Checks if Root CA or server TLS certificate are expiring within threshold_days. + If so, regenerates them, dynamically reloads the active SSLContext in-place, + and updates the server's in-memory CA reference so future enrollments use the new CA. + Returns True if renewed/reloaded, False otherwise. + """ + ca_renewed, srv_renewed = enrollment.check_and_renew_hub_pki( + cert_dir=cert_dir, + hostnames=hostnames, + threshold_days=threshold_days + ) + if ca_renewed or srv_renewed: + print(f"[!] Server Hub PKI certificates renewed (CA renewed: {ca_renewed}, Server cert renewed: {srv_renewed}).") + ca_cert, ca_key, ca_pem, ca_key_pem = enrollment.generate_ca_if_needed(cert_dir=cert_dir, force_renew=False) + SERVER_STATE["ca_cert"] = ca_cert + SERVER_STATE["ca_key"] = ca_key + SERVER_STATE["ca_cert_pem"] = ca_pem + + ssl_ctx = SERVER_STATE.get("ssl_ctx") + if ssl_ctx is not None: + try: + reload_mtls_context(ssl_ctx, cert_dir=cert_dir) + print("[+] In-flight mTLS SSLContext successfully reloaded with updated certificates.") + except Exception as e: + print(f"[!] Failed to reload in-flight SSLContext: {e}") + return True + return False + + +async def cert_validity_watchdog(interval_seconds: int = 43200, threshold_days: int = 30): + """ + Periodically checks the validity of Hub Root CA and Server TLS certificates (default every 12 hours). + Triggers in-flight renewal and dynamic context reloading if expiration is within threshold_days. + """ + config = SERVER_STATE.get("config", {}) + cert_dir = config.get("cert_dir", "certs") + hostnames = [config.get("tcp_host", "0.0.0.0"), "127.0.0.1", "localhost"] + while True: + try: + await asyncio.sleep(interval_seconds) + check_and_rotate_server_certs(cert_dir=cert_dir, hostnames=hostnames, threshold_days=threshold_days) + except asyncio.CancelledError: + break + except Exception as e: + print(f"[!] Exception in cert_validity_watchdog: {e}") + + + async def handle_socket_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): """ mTLS TCP socket handler. @@ -631,10 +697,20 @@ async def run_server(): print(f"[*] Hermes Reporting API available at http://{hermes_host}:{hermes_port}/api/hermes/report") print(f"[*] Client Enrollment API available at http://{hermes_host}:{hermes_port}/api/client/enroll") - await asyncio.gather( - tcp_server.serve_forever(), - uv_server.serve() - ) + watchdog_task = asyncio.create_task(cert_validity_watchdog()) + + try: + await asyncio.gather( + tcp_server.serve_forever(), + uv_server.serve(), + watchdog_task + ) + finally: + watchdog_task.cancel() + try: + await watchdog_task + except asyncio.CancelledError: + pass def main():