config.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. # -*- coding: utf-8 -*-
  2. """
  3. TLS-Konfiguration für sichere Verbindungen.
  4. Definiert TLS-Versionen, Cipher-Suites und Konfigurationsoptionen.
  5. """
  6. import ssl
  7. from dataclasses import dataclass, field
  8. from enum import IntEnum, auto
  9. from pathlib import Path
  10. from typing import Any
  11. class TLSVersion(IntEnum):
  12. """Unterstützte TLS-Versionen."""
  13. TLS_1_2 = auto()
  14. TLS_1_3 = auto()
  15. def to_ssl_version(self) -> int:
  16. """Konvertiert zu ssl.TLSVersion."""
  17. if self == TLSVersion.TLS_1_2:
  18. return ssl.TLSVersion.TLSv1_2
  19. return ssl.TLSVersion.TLSv1_3
  20. class CipherSuite(IntEnum):
  21. """Cipher-Suite-Profile."""
  22. MODERN = auto() # Nur moderne, sichere Ciphers
  23. INTERMEDIATE = auto() # Balance zwischen Sicherheit und Kompatibilität
  24. COMPATIBLE = auto() # Maximale Kompatibilität (weniger sicher)
  25. def get_ciphers(self) -> str:
  26. """Gibt die OpenSSL Cipher-String zurück."""
  27. if self == CipherSuite.MODERN:
  28. return (
  29. "TLS_AES_256_GCM_SHA384:"
  30. "TLS_CHACHA20_POLY1305_SHA256:"
  31. "TLS_AES_128_GCM_SHA256:"
  32. "ECDHE+AESGCM:"
  33. "DHE+AESGCM"
  34. )
  35. elif self == CipherSuite.INTERMEDIATE:
  36. return (
  37. "ECDHE+AESGCM:"
  38. "DHE+AESGCM:"
  39. "ECDHE+AES:"
  40. "DHE+AES:"
  41. "!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5"
  42. )
  43. else: # COMPATIBLE
  44. return (
  45. "ECDHE+AESGCM:"
  46. "DHE+AESGCM:"
  47. "ECDHE+AES:"
  48. "DHE+AES:"
  49. "AES256-SHA:"
  50. "AES128-SHA:"
  51. "!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5"
  52. )
  53. @dataclass
  54. class TLSConfig:
  55. """
  56. Konfiguration für TLS-Verbindungen.
  57. Attributes:
  58. enabled: TLS aktiviert
  59. cert_file: Pfad zum Zertifikat
  60. key_file: Pfad zum privaten Schlüssel
  61. ca_file: Optionaler Pfad zur CA-Datei
  62. min_version: Minimale TLS-Version
  63. cipher_suite: Cipher-Suite-Profil
  64. verify_client: Client-Zertifikat prüfen
  65. require_client_cert: Client-Zertifikat erforderlich
  66. check_hostname: Hostname prüfen
  67. session_cache_size: Session-Cache-Größe (0 = deaktiviert)
  68. session_timeout: Session-Timeout in Sekunden
  69. """
  70. enabled: bool = False
  71. cert_file: str | Path = ""
  72. key_file: str | Path = ""
  73. ca_file: str | Path = ""
  74. min_version: TLSVersion = TLSVersion.TLS_1_2
  75. cipher_suite: CipherSuite = CipherSuite.MODERN
  76. verify_client: bool = False
  77. require_client_cert: bool = False
  78. check_hostname: bool = True
  79. session_cache_size: int = 1024
  80. session_timeout: int = 86400 # 24 Stunden
  81. def __post_init__(self) -> None:
  82. """Konvertiert Pfade zu Path-Objekten."""
  83. if isinstance(self.cert_file, str) and self.cert_file:
  84. self.cert_file = Path(self.cert_file)
  85. if isinstance(self.key_file, str) and self.key_file:
  86. self.key_file = Path(self.key_file)
  87. if isinstance(self.ca_file, str) and self.ca_file:
  88. self.ca_file = Path(self.ca_file)
  89. def is_valid(self) -> tuple[bool, str]:
  90. """
  91. Prüft, ob die Konfiguration gültig ist.
  92. Returns:
  93. (gültig, Fehlermeldung)
  94. """
  95. if not self.enabled:
  96. return True, ""
  97. if not self.cert_file:
  98. return False, "Zertifikat-Datei nicht angegeben"
  99. if not self.key_file:
  100. return False, "Schlüssel-Datei nicht angegeben"
  101. if isinstance(self.cert_file, Path) and not self.cert_file.exists():
  102. return False, f"Zertifikat-Datei nicht gefunden: {self.cert_file}"
  103. if isinstance(self.key_file, Path) and not self.key_file.exists():
  104. return False, f"Schlüssel-Datei nicht gefunden: {self.key_file}"
  105. if self.ca_file:
  106. if isinstance(self.ca_file, Path) and not self.ca_file.exists():
  107. return False, f"CA-Datei nicht gefunden: {self.ca_file}"
  108. return True, ""
  109. def create_server_context(self) -> ssl.SSLContext:
  110. """
  111. Erstellt einen SSL-Kontext für Server.
  112. Returns:
  113. Konfigurierter SSL-Kontext
  114. Raises:
  115. ValueError: Bei ungültiger Konfiguration
  116. FileNotFoundError: Wenn Zertifikate nicht gefunden werden
  117. """
  118. valid, error = self.is_valid()
  119. if not valid:
  120. raise ValueError(error)
  121. # Kontext erstellen
  122. context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
  123. # Minimale TLS-Version
  124. context.minimum_version = self.min_version.to_ssl_version()
  125. # Zertifikate laden
  126. context.load_cert_chain(
  127. certfile=str(self.cert_file),
  128. keyfile=str(self.key_file)
  129. )
  130. # CA für Client-Verifizierung
  131. if self.ca_file:
  132. context.load_verify_locations(cafile=str(self.ca_file))
  133. # Client-Verifizierung
  134. if self.require_client_cert:
  135. context.verify_mode = ssl.CERT_REQUIRED
  136. elif self.verify_client:
  137. context.verify_mode = ssl.CERT_OPTIONAL
  138. else:
  139. context.verify_mode = ssl.CERT_NONE
  140. # Cipher-Suites
  141. context.set_ciphers(self.cipher_suite.get_ciphers())
  142. # Session-Cache
  143. if self.session_cache_size > 0:
  144. context.options |= ssl.OP_NO_TICKET # Deaktiviere Session-Tickets
  145. # Session-Cache wird automatisch verwaltet
  146. # Sicherheitsoptionen
  147. context.options |= ssl.OP_NO_SSLv2
  148. context.options |= ssl.OP_NO_SSLv3
  149. context.options |= ssl.OP_NO_TLSv1
  150. context.options |= ssl.OP_NO_TLSv1_1
  151. context.options |= ssl.OP_SINGLE_DH_USE
  152. context.options |= ssl.OP_SINGLE_ECDH_USE
  153. return context
  154. def create_client_context(self) -> ssl.SSLContext:
  155. """
  156. Erstellt einen SSL-Kontext für Clients.
  157. Returns:
  158. Konfigurierter SSL-Kontext
  159. """
  160. context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
  161. # Minimale TLS-Version
  162. context.minimum_version = self.min_version.to_ssl_version()
  163. # CA laden
  164. if self.ca_file:
  165. context.load_verify_locations(cafile=str(self.ca_file))
  166. else:
  167. context.load_default_certs()
  168. # Client-Zertifikat
  169. if self.cert_file and self.key_file:
  170. context.load_cert_chain(
  171. certfile=str(self.cert_file),
  172. keyfile=str(self.key_file)
  173. )
  174. # Hostname-Prüfung
  175. context.check_hostname = self.check_hostname
  176. context.verify_mode = ssl.CERT_REQUIRED if self.check_hostname else ssl.CERT_OPTIONAL
  177. # Cipher-Suites
  178. context.set_ciphers(self.cipher_suite.get_ciphers())
  179. # Sicherheitsoptionen
  180. context.options |= ssl.OP_NO_SSLv2
  181. context.options |= ssl.OP_NO_SSLv3
  182. context.options |= ssl.OP_NO_TLSv1
  183. context.options |= ssl.OP_NO_TLSv1_1
  184. return context
  185. def to_dict(self) -> dict[str, Any]:
  186. """Serialisiert die Konfiguration."""
  187. return {
  188. "enabled": self.enabled,
  189. "cert_file": str(self.cert_file) if self.cert_file else "",
  190. "key_file": str(self.key_file) if self.key_file else "",
  191. "ca_file": str(self.ca_file) if self.ca_file else "",
  192. "min_version": self.min_version.name,
  193. "cipher_suite": self.cipher_suite.name,
  194. "verify_client": self.verify_client,
  195. "require_client_cert": self.require_client_cert,
  196. "check_hostname": self.check_hostname,
  197. "session_cache_size": self.session_cache_size,
  198. "session_timeout": self.session_timeout,
  199. }
  200. @classmethod
  201. def from_dict(cls, data: dict[str, Any]) -> "TLSConfig":
  202. """Erstellt eine Konfiguration aus einem Dictionary."""
  203. min_version = TLSVersion[data.get("min_version", "TLS_1_2")]
  204. cipher_suite = CipherSuite[data.get("cipher_suite", "MODERN")]
  205. return cls(
  206. enabled=data.get("enabled", False),
  207. cert_file=data.get("cert_file", ""),
  208. key_file=data.get("key_file", ""),
  209. ca_file=data.get("ca_file", ""),
  210. min_version=min_version,
  211. cipher_suite=cipher_suite,
  212. verify_client=data.get("verify_client", False),
  213. require_client_cert=data.get("require_client_cert", False),
  214. check_hostname=data.get("check_hostname", True),
  215. session_cache_size=data.get("session_cache_size", 1024),
  216. session_timeout=data.get("session_timeout", 86400),
  217. )