# -*- coding: utf-8 -*- """ AZA – Zentrale TLS-Konfiguration für alle Backend-Services. Alle TLS-Parameter werden über Umgebungsvariablen gesteuert: AZA_TLS_CERTFILE – Pfad zum Zertifikat (PEM) AZA_TLS_KEYFILE – Pfad zum Private Key (PEM) AZA_TLS_MIN_VERSION – Minimale TLS-Version ("1.2" oder "1.3", Default: "1.2") AZA_TLS_REQUIRE – "1" = Server startet nicht ohne Zertifikat (Default: "1") DEV-Modus (Self-Signed): python aza_tls.py Erzeugt dev-cert.pem + dev-key.pem AZA_TLS_CERTFILE=dev-cert.pem AZA_TLS_KEYFILE=dev-key.pem python backend_main.py """ import os import ssl import sys import datetime AZA_TLS_CERTFILE = os.getenv("AZA_TLS_CERTFILE", "").strip() AZA_TLS_KEYFILE = os.getenv("AZA_TLS_KEYFILE", "").strip() AZA_TLS_MIN_VERSION = os.getenv("AZA_TLS_MIN_VERSION", "1.2").strip() AZA_TLS_REQUIRE = os.getenv("AZA_TLS_REQUIRE", "1").strip() _TLS_VERSIONS = { "1.2": ssl.TLSVersion.TLSv1_2, "1.3": ssl.TLSVersion.TLSv1_3, } _STRONG_CIPHERS = ":".join([ "ECDHE+AESGCM", "ECDHE+CHACHA20", "DHE+AESGCM", "DHE+CHACHA20", "!aNULL", "!eNULL", "!MD5", "!DSS", "!RC4", "!3DES", ]) def tls_required() -> bool: return AZA_TLS_REQUIRE == "1" def has_tls_config() -> bool: return bool(AZA_TLS_CERTFILE and AZA_TLS_KEYFILE) def check_tls_or_exit(): """Beendet den Prozess wenn TLS erforderlich aber nicht konfiguriert ist.""" if tls_required() and not has_tls_config(): print( "FEHLER: TLS ist erforderlich (AZA_TLS_REQUIRE=1), aber " "AZA_TLS_CERTFILE und/oder AZA_TLS_KEYFILE sind nicht gesetzt.\n" "Setzen Sie die Umgebungsvariablen oder deaktivieren Sie die " "Pflicht mit AZA_TLS_REQUIRE=0 (nur für Entwicklung).", file=sys.stderr, ) sys.exit(1) if has_tls_config(): if not os.path.isfile(AZA_TLS_CERTFILE): print(f"FEHLER: Zertifikatsdatei nicht gefunden: {AZA_TLS_CERTFILE}", file=sys.stderr) sys.exit(1) if not os.path.isfile(AZA_TLS_KEYFILE): print(f"FEHLER: Key-Datei nicht gefunden: {AZA_TLS_KEYFILE}", file=sys.stderr) sys.exit(1) def create_ssl_context() -> ssl.SSLContext: """Erstellt einen gehärteten SSL-Kontext für Server.""" if not has_tls_config(): raise RuntimeError("TLS nicht konfiguriert (AZA_TLS_CERTFILE / AZA_TLS_KEYFILE fehlen)") ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) min_ver = _TLS_VERSIONS.get(AZA_TLS_MIN_VERSION) if min_ver is None: raise ValueError(f"Ungültige TLS-Version: {AZA_TLS_MIN_VERSION} (erlaubt: 1.2, 1.3)") ctx.minimum_version = min_ver ctx.maximum_version = ssl.TLSVersion.TLSv1_3 ctx.set_ciphers(_STRONG_CIPHERS) ctx.load_cert_chain(certfile=AZA_TLS_CERTFILE, keyfile=AZA_TLS_KEYFILE) return ctx def get_uvicorn_ssl_kwargs() -> dict: """Gibt die SSL-Parameter für uvicorn.run() zurück.""" if not has_tls_config(): return {} return { "ssl_certfile": AZA_TLS_CERTFILE, "ssl_keyfile": AZA_TLS_KEYFILE, "ssl_ciphers": _STRONG_CIPHERS, } def generate_dev_cert(base_dir: str = None): """Erzeugt ein Self-Signed-Zertifikat für Entwicklung.""" try: from cryptography import x509 from cryptography.x509.oid import NameOID from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa except ImportError: print("FEHLER: 'cryptography' Paket wird benötigt: pip install cryptography", file=sys.stderr) sys.exit(1) if base_dir is None: base_dir = os.path.dirname(os.path.abspath(__file__)) cert_path = os.path.join(base_dir, "dev-cert.pem") key_path = os.path.join(base_dir, "dev-key.pem") key = rsa.generate_private_key(public_exponent=65537, key_size=4096) subject = issuer = x509.Name([ x509.NameAttribute(NameOID.ORGANIZATION_NAME, "AZA MedWork DEV"), x509.NameAttribute(NameOID.COMMON_NAME, "localhost"), ]) now = datetime.datetime.now(datetime.timezone.utc) cert = ( x509.CertificateBuilder() .subject_name(subject) .issuer_name(issuer) .public_key(key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(now) .not_valid_after(now + datetime.timedelta(days=365)) .add_extension( x509.SubjectAlternativeName([ x509.DNSName("localhost"), x509.DNSName("127.0.0.1"), x509.IPAddress(ipaddress_from_str("127.0.0.1")), ]), critical=False, ) .sign(key, hashes.SHA256()) ) with open(key_path, "wb") as f: f.write(key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption(), )) with open(cert_path, "wb") as f: f.write(cert.public_bytes(serialization.Encoding.PEM)) print(f"DEV-Zertifikat erstellt:") print(f" Zertifikat: {cert_path}") print(f" Key: {key_path}") print(f" Gültigkeit: 365 Tage") print(f" Schlüssel: RSA 4096-bit") print(f" Signatur: SHA-256") print() print(f"Verwendung:") print(f' set AZA_TLS_CERTFILE={cert_path}') print(f' set AZA_TLS_KEYFILE={key_path}') return cert_path, key_path def ipaddress_from_str(addr: str): """Hilfsfunktion für IP-Adresse in SAN.""" import ipaddress return ipaddress.IPv4Address(addr) if __name__ == "__main__": generate_dev_cert()