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()
|