176 lines
5.6 KiB
Python
176 lines
5.6 KiB
Python
|
|
# -*- 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()
|