2025-06-04 10:03:22 +02:00

280 lines
9.5 KiB
Python

#!/usr/bin/env python3
"""
SSL-Konfigurationsmodul für MYP Druckerverwaltung
Automatische Generierung von selbstsignierten SSL-Zertifikaten für localhost
Optimiert für Debian/Linux-Systeme ohne Windows-Abhängigkeiten
"""
import os
import ssl
import subprocess
import logging
from pathlib import Path
from datetime import datetime, timedelta
# Logger für SSL-Konfiguration
ssl_logger = logging.getLogger('ssl_config')
class SSLCertificateManager:
"""Verwaltet SSL-Zertifikate für die Anwendung"""
def __init__(self, app_dir="/opt/myp"):
self.app_dir = Path(app_dir)
self.ssl_dir = self.app_dir / "certs" / "localhost"
self.cert_file = self.ssl_dir / "localhost.crt"
self.key_file = self.ssl_dir / "localhost.key"
def ensure_ssl_directory(self):
"""Stellt sicher, dass das SSL-Verzeichnis existiert"""
self.ssl_dir.mkdir(parents=True, exist_ok=True)
ssl_logger.info(f"SSL-Verzeichnis erstellt: {self.ssl_dir}")
def generate_ssl_certificate(self, force_regenerate=False):
"""
Generiert ein selbstsigniertes SSL-Zertifikat für localhost
Args:
force_regenerate (bool): Erzwingt Neugenerierung auch wenn Zertifikat existiert
Returns:
bool: True wenn erfolgreich, False bei Fehler
"""
try:
# Prüfe ob Zertifikat bereits existiert und gültig ist
if not force_regenerate and self.is_certificate_valid():
ssl_logger.info("Gültiges SSL-Zertifikat bereits vorhanden")
return True
self.ensure_ssl_directory()
ssl_logger.info("Generiere neues SSL-Zertifikat für localhost...")
# OpenSSL-Konfiguration für erweiterte Attribute
openssl_config = self.ssl_dir / "openssl.conf"
self._create_openssl_config(openssl_config)
# Private Key generieren
key_cmd = [
"openssl", "genrsa",
"-out", str(self.key_file),
"2048"
]
result = subprocess.run(key_cmd, capture_output=True, text=True)
if result.returncode != 0:
ssl_logger.error(f"Private Key Generierung fehlgeschlagen: {result.stderr}")
return False
# Selbstsigniertes Zertifikat erstellen
cert_cmd = [
"openssl", "req",
"-new", "-x509",
"-key", str(self.key_file),
"-out", str(self.cert_file),
"-days", "365",
"-config", str(openssl_config),
"-extensions", "v3_req"
]
result = subprocess.run(cert_cmd, capture_output=True, text=True)
if result.returncode != 0:
ssl_logger.error(f"Zertifikat-Generierung fehlgeschlagen: {result.stderr}")
return False
# Berechtigungen setzen
os.chmod(self.key_file, 0o600) # Nur Root kann lesen
os.chmod(self.cert_file, 0o644) # Alle können lesen
# Zertifikat zu System CA-Store hinzufügen
self._add_to_system_ca_store()
ssl_logger.info(f"SSL-Zertifikat erfolgreich generiert:")
ssl_logger.info(f" Zertifikat: {self.cert_file}")
ssl_logger.info(f" Private Key: {self.key_file}")
# Aufräumen
openssl_config.unlink(missing_ok=True)
return True
except Exception as e:
ssl_logger.error(f"Fehler bei SSL-Zertifikat-Generierung: {e}")
return False
def _create_openssl_config(self, config_path):
"""Erstellt OpenSSL-Konfigurationsdatei für erweiterte Zertifikat-Attribute"""
config_content = """[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
C = DE
ST = Baden-Wuerttemberg
L = Stuttgart
O = Mercedes-Benz
OU = MYP Druckerverwaltung
CN = localhost
[v3_req]
keyUsage = keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
DNS.2 = *.localhost
DNS.3 = 127.0.0.1
IP.1 = 127.0.0.1
IP.2 = 0.0.0.0
"""
with open(config_path, 'w', encoding='utf-8') as f:
f.write(config_content)
def _add_to_system_ca_store(self):
"""Fügt das Zertifikat zum System CA-Store hinzu (nur Linux)"""
try:
if os.name != 'posix':
ssl_logger.info("System CA-Store Update nur unter Linux verfügbar")
return
system_cert_path = Path("/usr/local/share/ca-certificates/localhost.crt")
# Kopiere Zertifikat in System CA-Store
subprocess.run([
"cp", str(self.cert_file), str(system_cert_path)
], check=True)
# Aktualisiere CA-Zertifikate
subprocess.run(["update-ca-certificates"], check=True)
ssl_logger.info("Zertifikat erfolgreich zu System CA-Store hinzugefügt")
except subprocess.CalledProcessError as e:
ssl_logger.warning(f"System CA-Store Update fehlgeschlagen: {e}")
except Exception as e:
ssl_logger.warning(f"Unerwarteter Fehler beim CA-Store Update: {e}")
def is_certificate_valid(self):
"""
Prüft ob das vorhandene Zertifikat gültig ist
Returns:
bool: True wenn Zertifikat existiert und gültig ist
"""
try:
if not (self.cert_file.exists() and self.key_file.exists()):
return False
# Prüfe Zertifikat-Gültigkeit mit OpenSSL
result = subprocess.run([
"openssl", "x509",
"-in", str(self.cert_file),
"-noout", "-checkend", "86400" # Prüfe ob in nächsten 24h abläuft
], capture_output=True)
if result.returncode == 0:
ssl_logger.info("Vorhandenes SSL-Zertifikat ist gültig")
return True
else:
ssl_logger.info("Vorhandenes SSL-Zertifikat ist abgelaufen oder ungültig")
return False
except Exception as e:
ssl_logger.warning(f"Zertifikat-Validierung fehlgeschlagen: {e}")
return False
def get_ssl_context(self):
"""
Erstellt SSL-Kontext für Flask-Anwendung
Returns:
ssl.SSLContext oder tuple: SSL-Kontext oder Pfad-Tupel für Flask
"""
try:
# Stelle sicher, dass Zertifikate existieren
if not self.is_certificate_valid():
if not self.generate_ssl_certificate():
ssl_logger.error("SSL-Zertifikat-Generierung fehlgeschlagen")
return None
# Erstelle SSL-Kontext
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(str(self.cert_file), str(self.key_file))
# Sicherheitseinstellungen für Produktionsumgebung
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
context.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS')
ssl_logger.info("SSL-Kontext erfolgreich erstellt")
return context
except Exception as e:
ssl_logger.error(f"SSL-Kontext-Erstellung fehlgeschlagen: {e}")
# Fallback: Rückgabe als Tupel für Flask
if self.cert_file.exists() and self.key_file.exists():
return (str(self.cert_file), str(self.key_file))
return None
# Globale SSL-Manager-Instanz
_ssl_manager = None
def get_ssl_manager(app_dir="/opt/myp"):
"""
Singleton-Pattern für SSL-Manager
Args:
app_dir (str): Anwendungsverzeichnis
Returns:
SSLCertificateManager: SSL-Manager-Instanz
"""
global _ssl_manager
if _ssl_manager is None:
_ssl_manager = SSLCertificateManager(app_dir)
return _ssl_manager
def get_ssl_context(app_dir="/opt/myp"):
"""
Convenience-Funktion für SSL-Kontext
Args:
app_dir (str): Anwendungsverzeichnis
Returns:
ssl.SSLContext oder tuple: SSL-Kontext für Flask
"""
manager = get_ssl_manager(app_dir)
return manager.get_ssl_context()
def ensure_ssl_certificates(app_dir="/opt/myp", force_regenerate=False):
"""
Stellt sicher, dass SSL-Zertifikate vorhanden sind
Args:
app_dir (str): Anwendungsverzeichnis
force_regenerate (bool): Erzwingt Neugenerierung
Returns:
bool: True wenn erfolgreich
"""
manager = get_ssl_manager(app_dir)
return manager.generate_ssl_certificate(force_regenerate)
# Automatische Zertifikat-Generierung beim Import (nur wenn als Hauptmodul ausgeführt)
if __name__ == "__main__":
import sys
app_dir = sys.argv[1] if len(sys.argv) > 1 else "/opt/myp"
force = "--force" in sys.argv
print(f"Generiere SSL-Zertifikate für: {app_dir}")
if ensure_ssl_certificates(app_dir, force):
print("✅ SSL-Zertifikate erfolgreich generiert")
sys.exit(0)
else:
print("❌ SSL-Zertifikat-Generierung fehlgeschlagen")
sys.exit(1)