manager.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. # -*- coding: utf-8 -*-
  2. """
  3. TLS-Manager für Zertifikatsverwaltung.
  4. Bietet Funktionen zum Laden, Prüfen und Verwalten von Zertifikaten.
  5. """
  6. import ssl
  7. from dataclasses import dataclass
  8. from datetime import datetime
  9. from pathlib import Path
  10. from typing import Any
  11. from trixy_core.network.tls.config import TLSConfig
  12. @dataclass
  13. class CertificateInfo:
  14. """
  15. Informationen über ein Zertifikat.
  16. Attributes:
  17. subject: Subject des Zertifikats
  18. issuer: Aussteller des Zertifikats
  19. serial_number: Seriennummer
  20. not_before: Gültig ab
  21. not_after: Gültig bis
  22. version: Zertifikats-Version
  23. san: Subject Alternative Names
  24. fingerprint_sha256: SHA-256 Fingerprint
  25. """
  26. subject: dict[str, str]
  27. issuer: dict[str, str]
  28. serial_number: int
  29. not_before: datetime
  30. not_after: datetime
  31. version: int
  32. san: list[str]
  33. fingerprint_sha256: str
  34. @property
  35. def common_name(self) -> str:
  36. """Common Name (CN) des Subjects."""
  37. return self.subject.get("CN", "")
  38. @property
  39. def is_valid(self) -> bool:
  40. """Prüft, ob das Zertifikat zeitlich gültig ist."""
  41. now = datetime.now()
  42. return self.not_before <= now <= self.not_after
  43. @property
  44. def days_until_expiry(self) -> int:
  45. """Tage bis zum Ablauf."""
  46. delta = self.not_after - datetime.now()
  47. return delta.days
  48. @property
  49. def is_self_signed(self) -> bool:
  50. """Prüft, ob das Zertifikat selbstsigniert ist."""
  51. return self.subject == self.issuer
  52. class TLSManager:
  53. """
  54. Manager für TLS-Operationen und Zertifikatsverwaltung.
  55. Example:
  56. manager = TLSManager(TLSConfig(
  57. enabled=True,
  58. cert_file="certs/server.crt",
  59. key_file="certs/server.key"
  60. ))
  61. # Server-Kontext erstellen
  62. context = manager.get_server_context()
  63. # Zertifikat-Info abrufen
  64. info = manager.get_certificate_info()
  65. print(f"Gültig bis: {info.not_after}")
  66. # Prüfung auf baldigen Ablauf
  67. if manager.is_expiring_soon(days=30):
  68. print("Zertifikat läuft bald ab!")
  69. """
  70. def __init__(self, config: TLSConfig) -> None:
  71. """
  72. Initialisiert den TLS-Manager.
  73. Args:
  74. config: TLS-Konfiguration
  75. """
  76. self._config = config
  77. self._server_context: ssl.SSLContext | None = None
  78. self._client_context: ssl.SSLContext | None = None
  79. self._cert_info: CertificateInfo | None = None
  80. @property
  81. def config(self) -> TLSConfig:
  82. """Die TLS-Konfiguration."""
  83. return self._config
  84. @property
  85. def is_enabled(self) -> bool:
  86. """Prüft, ob TLS aktiviert ist."""
  87. return self._config.enabled
  88. def get_server_context(self) -> ssl.SSLContext | None:
  89. """
  90. Gibt den Server-SSL-Kontext zurück.
  91. Returns:
  92. SSL-Kontext oder None wenn TLS deaktiviert
  93. """
  94. if not self._config.enabled:
  95. return None
  96. if self._server_context is None:
  97. self._server_context = self._config.create_server_context()
  98. return self._server_context
  99. def get_client_context(self) -> ssl.SSLContext | None:
  100. """
  101. Gibt den Client-SSL-Kontext zurück.
  102. Returns:
  103. SSL-Kontext oder None wenn TLS deaktiviert
  104. """
  105. if not self._config.enabled:
  106. return None
  107. if self._client_context is None:
  108. self._client_context = self._config.create_client_context()
  109. return self._client_context
  110. def reload(self) -> None:
  111. """Lädt die SSL-Kontexte neu."""
  112. self._server_context = None
  113. self._client_context = None
  114. self._cert_info = None
  115. def get_certificate_info(self) -> CertificateInfo | None:
  116. """
  117. Gibt Informationen über das Server-Zertifikat zurück.
  118. Returns:
  119. CertificateInfo oder None
  120. """
  121. if not self._config.enabled or not self._config.cert_file:
  122. return None
  123. if self._cert_info is not None:
  124. return self._cert_info
  125. try:
  126. self._cert_info = self._load_certificate_info(
  127. Path(self._config.cert_file)
  128. )
  129. return self._cert_info
  130. except Exception:
  131. return None
  132. def _load_certificate_info(self, cert_path: Path) -> CertificateInfo:
  133. """
  134. Lädt Zertifikats-Informationen aus einer PEM-Datei.
  135. Args:
  136. cert_path: Pfad zur Zertifikatsdatei
  137. Returns:
  138. CertificateInfo
  139. """
  140. import hashlib
  141. # Zertifikat laden via temporären Kontext
  142. context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
  143. context.check_hostname = False
  144. context.verify_mode = ssl.CERT_NONE
  145. # PEM lesen
  146. with open(cert_path, 'rb') as f:
  147. pem_data = f.read()
  148. # Zertifikat parsen
  149. from ssl import PEM_cert_to_DER_cert, DER_cert_to_PEM_cert
  150. import re
  151. # PEM-Block extrahieren
  152. pem_match = re.search(
  153. b'-----BEGIN CERTIFICATE-----(.+?)-----END CERTIFICATE-----',
  154. pem_data,
  155. re.DOTALL
  156. )
  157. if not pem_match:
  158. raise ValueError("Kein gültiges PEM-Zertifikat gefunden")
  159. # DER für Fingerprint
  160. import base64
  161. der_data = base64.b64decode(pem_match.group(1))
  162. fingerprint = hashlib.sha256(der_data).hexdigest().upper()
  163. fingerprint = ':'.join(fingerprint[i:i+2] for i in range(0, len(fingerprint), 2))
  164. # Zertifikat über ssl laden für Details
  165. # Wir nutzen einen Workaround: Socket mit getpeercert simulieren
  166. # Alternative: pyOpenSSL oder cryptography nutzen
  167. # Einfaches Parsing für grundlegende Info
  168. # Für vollständige Unterstützung wäre cryptography-Bibliothek nötig
  169. cert_info = CertificateInfo(
  170. subject={"CN": self._extract_cn_from_pem(pem_data)},
  171. issuer={"CN": self._extract_cn_from_pem(pem_data, issuer=True)},
  172. serial_number=0,
  173. not_before=datetime.now(),
  174. not_after=datetime.now(),
  175. version=3,
  176. san=[],
  177. fingerprint_sha256=fingerprint
  178. )
  179. return cert_info
  180. def _extract_cn_from_pem(self, pem_data: bytes, issuer: bool = False) -> str:
  181. """
  182. Extrahiert den CN aus einem PEM-Zertifikat.
  183. Dies ist eine vereinfachte Implementierung.
  184. Für vollständige Unterstützung sollte cryptography verwendet werden.
  185. Args:
  186. pem_data: PEM-Daten
  187. issuer: True für Issuer-CN, False für Subject-CN
  188. Returns:
  189. Common Name oder "Unknown"
  190. """
  191. # Sehr einfache Extraktion - nicht für Produktion geeignet
  192. # In einer echten Implementierung würde man cryptography nutzen
  193. import re
  194. pattern = rb'CN\s*=\s*([^,/\n]+)'
  195. matches = re.findall(pattern, pem_data)
  196. if matches:
  197. # Bei selbstsignierten Zertifikaten ist Subject = Issuer
  198. return matches[0].decode('utf-8', errors='replace').strip()
  199. return "Unknown"
  200. def is_expiring_soon(self, days: int = 30) -> bool:
  201. """
  202. Prüft, ob das Zertifikat bald abläuft.
  203. Args:
  204. days: Anzahl Tage
  205. Returns:
  206. True wenn Ablauf innerhalb der angegebenen Tage
  207. """
  208. info = self.get_certificate_info()
  209. if info is None:
  210. return False
  211. return info.days_until_expiry <= days
  212. def verify_chain(self, cert_path: Path, ca_path: Path | None = None) -> tuple[bool, str]:
  213. """
  214. Verifiziert die Zertifikatskette.
  215. Args:
  216. cert_path: Pfad zum Zertifikat
  217. ca_path: Optionaler Pfad zur CA
  218. Returns:
  219. (gültig, Fehlermeldung)
  220. """
  221. try:
  222. context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
  223. context.verify_mode = ssl.CERT_REQUIRED
  224. context.check_hostname = False
  225. if ca_path:
  226. context.load_verify_locations(cafile=str(ca_path))
  227. else:
  228. context.load_default_certs()
  229. # Zertifikat laden
  230. context.load_cert_chain(certfile=str(cert_path))
  231. return True, ""
  232. except ssl.SSLError as e:
  233. return False, str(e)
  234. except FileNotFoundError as e:
  235. return False, f"Datei nicht gefunden: {e.filename}"
  236. except Exception as e:
  237. return False, str(e)
  238. def get_status(self) -> dict[str, Any]:
  239. """
  240. Gibt den Status des TLS-Managers zurück.
  241. Returns:
  242. Status-Dictionary
  243. """
  244. info = self.get_certificate_info()
  245. return {
  246. "enabled": self._config.enabled,
  247. "cert_file": str(self._config.cert_file) if self._config.cert_file else None,
  248. "min_version": self._config.min_version.name,
  249. "cipher_suite": self._config.cipher_suite.name,
  250. "certificate": {
  251. "common_name": info.common_name if info else None,
  252. "is_valid": info.is_valid if info else None,
  253. "days_until_expiry": info.days_until_expiry if info else None,
  254. "is_self_signed": info.is_self_signed if info else None,
  255. "fingerprint": info.fingerprint_sha256 if info else None,
  256. } if info else None
  257. }
  258. def create_development_certificates(
  259. output_dir: Path,
  260. common_name: str = "localhost",
  261. days_valid: int = 365
  262. ) -> tuple[Path, Path]:
  263. """
  264. Erstellt selbstsignierte Entwicklungszertifikate.
  265. Nutzt den CertificateGenerator für die Erstellung.
  266. Args:
  267. output_dir: Ausgabeverzeichnis
  268. common_name: Common Name
  269. days_valid: Gültigkeitsdauer
  270. Returns:
  271. (cert_path, key_path)
  272. """
  273. from trixy_core.network.tls.generator import CertificateGenerator, CertificateRequest
  274. generator = CertificateGenerator()
  275. request = CertificateRequest(
  276. common_name=common_name,
  277. organization="Trixy Development",
  278. organizational_unit="Development",
  279. country="DE",
  280. days_valid=days_valid,
  281. san_dns=[common_name, "localhost"],
  282. san_ip=["127.0.0.1", "::1"]
  283. )
  284. cert_path, key_path = generator.generate_self_signed(
  285. request=request,
  286. output_dir=output_dir
  287. )
  288. return cert_path, key_path