Files
aza/AzA march 2026 - Kopie (6)/aza_tls.py
2026-04-16 13:32:32 +02:00

176 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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()