test_network_tls.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests für TLS/SSL Module.
  4. Testet TLS-Konfiguration, Zertifikats-Manager und Generator.
  5. """
  6. import os
  7. import ssl
  8. import tempfile
  9. from pathlib import Path
  10. import pytest
  11. from trixy_core.network.tls.config import (
  12. TLSConfig,
  13. TLSVersion,
  14. CipherSuite,
  15. )
  16. from trixy_core.network.tls.manager import (
  17. TLSManager,
  18. CertificateInfo,
  19. create_development_certificates,
  20. )
  21. from trixy_core.network.tls.generator import (
  22. CertificateGenerator,
  23. CertificateRequest,
  24. KeyType,
  25. )
  26. # =============================================================================
  27. # TLS Config Tests
  28. # =============================================================================
  29. class TestTLSVersion:
  30. """Tests für TLSVersion."""
  31. def test_tls_1_2_to_ssl(self):
  32. """TLS 1.2 zu ssl.TLSVersion."""
  33. assert TLSVersion.TLS_1_2.to_ssl_version() == ssl.TLSVersion.TLSv1_2
  34. def test_tls_1_3_to_ssl(self):
  35. """TLS 1.3 zu ssl.TLSVersion."""
  36. assert TLSVersion.TLS_1_3.to_ssl_version() == ssl.TLSVersion.TLSv1_3
  37. class TestCipherSuite:
  38. """Tests für CipherSuite."""
  39. def test_modern_ciphers(self):
  40. """Moderne Cipher-Suites."""
  41. ciphers = CipherSuite.MODERN.get_ciphers()
  42. assert "TLS_AES_256_GCM_SHA384" in ciphers
  43. assert "ECDHE" in ciphers
  44. def test_intermediate_ciphers(self):
  45. """Intermediate Cipher-Suites."""
  46. ciphers = CipherSuite.INTERMEDIATE.get_ciphers()
  47. assert "ECDHE" in ciphers
  48. assert "!aNULL" in ciphers # Keine anonymen Ciphers
  49. def test_compatible_ciphers(self):
  50. """Compatible Cipher-Suites."""
  51. ciphers = CipherSuite.COMPATIBLE.get_ciphers()
  52. assert "AES" in ciphers
  53. class TestTLSConfig:
  54. """Tests für TLSConfig."""
  55. def test_default_config(self):
  56. """Standard-Konfiguration."""
  57. config = TLSConfig()
  58. assert config.enabled is False
  59. assert config.min_version == TLSVersion.TLS_1_2
  60. assert config.cipher_suite == CipherSuite.MODERN
  61. def test_config_with_paths(self):
  62. """Konfiguration mit Pfaden."""
  63. config = TLSConfig(
  64. enabled=True,
  65. cert_file="/path/to/cert.crt",
  66. key_file="/path/to/key.key",
  67. )
  68. assert isinstance(config.cert_file, Path)
  69. assert config.cert_file == Path("/path/to/cert.crt")
  70. def test_validation_disabled(self):
  71. """Validierung bei deaktiviertem TLS."""
  72. config = TLSConfig(enabled=False)
  73. valid, error = config.is_valid()
  74. assert valid is True
  75. assert error == ""
  76. def test_validation_missing_cert(self):
  77. """Validierung ohne Zertifikat."""
  78. config = TLSConfig(enabled=True, key_file="/path/to/key")
  79. valid, error = config.is_valid()
  80. assert valid is False
  81. assert "Zertifikat" in error
  82. def test_validation_missing_key(self):
  83. """Validierung ohne Schlüssel."""
  84. config = TLSConfig(enabled=True, cert_file="/path/to/cert")
  85. valid, error = config.is_valid()
  86. assert valid is False
  87. assert "Schlüssel" in error
  88. def test_validation_file_not_found(self):
  89. """Validierung mit nicht existierender Datei."""
  90. config = TLSConfig(
  91. enabled=True,
  92. cert_file="/nonexistent/cert.crt",
  93. key_file="/nonexistent/key.key",
  94. )
  95. valid, error = config.is_valid()
  96. assert valid is False
  97. assert "nicht gefunden" in error
  98. def test_serialization(self):
  99. """Serialisierung und Deserialisierung."""
  100. config = TLSConfig(
  101. enabled=True,
  102. cert_file="/path/to/cert.crt",
  103. key_file="/path/to/key.key",
  104. min_version=TLSVersion.TLS_1_3,
  105. cipher_suite=CipherSuite.INTERMEDIATE,
  106. verify_client=True,
  107. )
  108. data = config.to_dict()
  109. restored = TLSConfig.from_dict(data)
  110. assert restored.enabled == config.enabled
  111. assert restored.min_version == config.min_version
  112. assert restored.cipher_suite == config.cipher_suite
  113. assert restored.verify_client == config.verify_client
  114. # =============================================================================
  115. # Certificate Request Tests
  116. # =============================================================================
  117. class TestCertificateRequest:
  118. """Tests für CertificateRequest."""
  119. def test_default_request(self):
  120. """Standard-Anforderung."""
  121. request = CertificateRequest(common_name="localhost")
  122. assert request.common_name == "localhost"
  123. assert request.organization == "Trixy"
  124. assert request.key_type == KeyType.RSA_2048
  125. assert request.days_valid == 365
  126. def test_subject_string(self):
  127. """Subject-String generieren."""
  128. request = CertificateRequest(
  129. common_name="trixy.local",
  130. organization="Test Org",
  131. country="DE",
  132. )
  133. subject = request.get_subject_string()
  134. assert "/CN=trixy.local" in subject
  135. assert "/O=Test Org" in subject
  136. assert "/C=DE" in subject
  137. def test_san_extension(self):
  138. """SAN-Extension generieren."""
  139. request = CertificateRequest(
  140. common_name="trixy.local",
  141. san_dns=["localhost", "trixy.local"],
  142. san_ip=["127.0.0.1", "::1"],
  143. )
  144. san = request.get_san_extension()
  145. assert "DNS:localhost" in san
  146. assert "DNS:trixy.local" in san
  147. assert "IP:127.0.0.1" in san
  148. assert "IP:::1" in san
  149. def test_san_default_cn(self):
  150. """SAN Default = CN wenn leer."""
  151. request = CertificateRequest(common_name="example.com")
  152. san = request.get_san_extension()
  153. assert "DNS:example.com" in san
  154. # =============================================================================
  155. # Certificate Generator Tests
  156. # =============================================================================
  157. class TestCertificateGenerator:
  158. """Tests für CertificateGenerator."""
  159. def test_openssl_availability(self):
  160. """OpenSSL-Verfügbarkeit prüfen."""
  161. generator = CertificateGenerator()
  162. # Sollte auf den meisten Systemen True sein
  163. result = generator.is_openssl_available()
  164. assert isinstance(result, bool)
  165. @pytest.mark.skipif(
  166. not CertificateGenerator().is_openssl_available(),
  167. reason="OpenSSL nicht verfügbar"
  168. )
  169. def test_generate_self_signed(self):
  170. """Selbstsigniertes Zertifikat generieren."""
  171. with tempfile.TemporaryDirectory() as tmpdir:
  172. generator = CertificateGenerator()
  173. request = CertificateRequest(
  174. common_name="test.local",
  175. organization="Test",
  176. days_valid=30,
  177. san_dns=["test.local", "localhost"],
  178. san_ip=["127.0.0.1"],
  179. )
  180. cert_path, key_path = generator.generate_self_signed(
  181. request=request,
  182. output_dir=Path(tmpdir),
  183. )
  184. assert cert_path.exists()
  185. assert key_path.exists()
  186. # Key-Berechtigungen prüfen
  187. key_mode = os.stat(key_path).st_mode & 0o777
  188. assert key_mode == 0o600
  189. @pytest.mark.skipif(
  190. not CertificateGenerator().is_openssl_available(),
  191. reason="OpenSSL nicht verfügbar"
  192. )
  193. def test_generate_different_key_types(self):
  194. """Verschiedene Schlüsseltypen."""
  195. with tempfile.TemporaryDirectory() as tmpdir:
  196. generator = CertificateGenerator()
  197. for key_type in [KeyType.RSA_2048, KeyType.RSA_4096]:
  198. request = CertificateRequest(
  199. common_name=f"test-{key_type.name}.local",
  200. key_type=key_type,
  201. )
  202. cert_path, key_path = generator.generate_self_signed(
  203. request=request,
  204. output_dir=Path(tmpdir),
  205. cert_filename=f"cert_{key_type.name}.crt",
  206. key_filename=f"key_{key_type.name}.key",
  207. )
  208. assert cert_path.exists()
  209. assert key_path.exists()
  210. @pytest.mark.skipif(
  211. not CertificateGenerator().is_openssl_available(),
  212. reason="OpenSSL nicht verfügbar"
  213. )
  214. def test_generate_ecdsa(self):
  215. """ECDSA-Schlüssel generieren."""
  216. with tempfile.TemporaryDirectory() as tmpdir:
  217. generator = CertificateGenerator()
  218. request = CertificateRequest(
  219. common_name="ecdsa.local",
  220. key_type=KeyType.ECDSA_P256,
  221. )
  222. cert_path, key_path = generator.generate_self_signed(
  223. request=request,
  224. output_dir=Path(tmpdir),
  225. )
  226. assert cert_path.exists()
  227. assert key_path.exists()
  228. # =============================================================================
  229. # TLS Manager Tests
  230. # =============================================================================
  231. class TestTLSManager:
  232. """Tests für TLSManager."""
  233. def test_disabled_tls(self):
  234. """TLS deaktiviert."""
  235. config = TLSConfig(enabled=False)
  236. manager = TLSManager(config)
  237. assert manager.is_enabled is False
  238. assert manager.get_server_context() is None
  239. assert manager.get_client_context() is None
  240. @pytest.mark.skipif(
  241. not CertificateGenerator().is_openssl_available(),
  242. reason="OpenSSL nicht verfügbar"
  243. )
  244. def test_create_server_context(self):
  245. """Server-Kontext erstellen."""
  246. with tempfile.TemporaryDirectory() as tmpdir:
  247. # Zertifikat generieren
  248. generator = CertificateGenerator()
  249. request = CertificateRequest(common_name="localhost")
  250. cert_path, key_path = generator.generate_self_signed(
  251. request=request,
  252. output_dir=Path(tmpdir),
  253. )
  254. # Manager erstellen
  255. config = TLSConfig(
  256. enabled=True,
  257. cert_file=cert_path,
  258. key_file=key_path,
  259. )
  260. manager = TLSManager(config)
  261. context = manager.get_server_context()
  262. assert context is not None
  263. assert isinstance(context, ssl.SSLContext)
  264. @pytest.mark.skipif(
  265. not CertificateGenerator().is_openssl_available(),
  266. reason="OpenSSL nicht verfügbar"
  267. )
  268. def test_certificate_info(self):
  269. """Zertifikats-Info abrufen."""
  270. with tempfile.TemporaryDirectory() as tmpdir:
  271. generator = CertificateGenerator()
  272. request = CertificateRequest(
  273. common_name="test.local",
  274. organization="Test Org",
  275. )
  276. cert_path, key_path = generator.generate_self_signed(
  277. request=request,
  278. output_dir=Path(tmpdir),
  279. )
  280. config = TLSConfig(
  281. enabled=True,
  282. cert_file=cert_path,
  283. key_file=key_path,
  284. )
  285. manager = TLSManager(config)
  286. info = manager.get_certificate_info()
  287. assert info is not None
  288. assert info.fingerprint_sha256 != ""
  289. @pytest.mark.skipif(
  290. not CertificateGenerator().is_openssl_available(),
  291. reason="OpenSSL nicht verfügbar"
  292. )
  293. def test_reload(self):
  294. """Kontexte neu laden."""
  295. with tempfile.TemporaryDirectory() as tmpdir:
  296. generator = CertificateGenerator()
  297. request = CertificateRequest(common_name="localhost")
  298. cert_path, key_path = generator.generate_self_signed(
  299. request=request,
  300. output_dir=Path(tmpdir),
  301. )
  302. config = TLSConfig(
  303. enabled=True,
  304. cert_file=cert_path,
  305. key_file=key_path,
  306. )
  307. manager = TLSManager(config)
  308. # Ersten Kontext erstellen
  309. ctx1 = manager.get_server_context()
  310. # Reload
  311. manager.reload()
  312. # Neuen Kontext erstellen
  313. ctx2 = manager.get_server_context()
  314. # Sollten verschiedene Objekte sein
  315. assert ctx1 is not ctx2
  316. @pytest.mark.skipif(
  317. not CertificateGenerator().is_openssl_available(),
  318. reason="OpenSSL nicht verfügbar"
  319. )
  320. def test_get_status(self):
  321. """Status abrufen."""
  322. with tempfile.TemporaryDirectory() as tmpdir:
  323. generator = CertificateGenerator()
  324. request = CertificateRequest(common_name="localhost")
  325. cert_path, key_path = generator.generate_self_signed(
  326. request=request,
  327. output_dir=Path(tmpdir),
  328. )
  329. config = TLSConfig(
  330. enabled=True,
  331. cert_file=cert_path,
  332. key_file=key_path,
  333. )
  334. manager = TLSManager(config)
  335. status = manager.get_status()
  336. assert status["enabled"] is True
  337. assert status["min_version"] == "TLS_1_2"
  338. # =============================================================================
  339. # Development Certificates Helper Tests
  340. # =============================================================================
  341. class TestDevelopmentCertificates:
  342. """Tests für Entwicklungs-Zertifikate."""
  343. @pytest.mark.skipif(
  344. not CertificateGenerator().is_openssl_available(),
  345. reason="OpenSSL nicht verfügbar"
  346. )
  347. def test_create_development_certificates(self):
  348. """Entwicklungszertifikate erstellen."""
  349. with tempfile.TemporaryDirectory() as tmpdir:
  350. cert_path, key_path = create_development_certificates(
  351. output_dir=Path(tmpdir),
  352. common_name="dev.local",
  353. days_valid=7,
  354. )
  355. assert cert_path.exists()
  356. assert key_path.exists()
  357. # Zertifikat-Inhalt prüfen
  358. cert_content = cert_path.read_text()
  359. assert "BEGIN CERTIFICATE" in cert_content
  360. # Schlüssel-Inhalt prüfen
  361. key_content = key_path.read_text()
  362. assert "PRIVATE KEY" in key_content
  363. # =============================================================================
  364. # SSL Context Creation Tests
  365. # =============================================================================
  366. class TestSSLContextCreation:
  367. """Tests für SSL-Context-Erstellung."""
  368. @pytest.mark.skipif(
  369. not CertificateGenerator().is_openssl_available(),
  370. reason="OpenSSL nicht verfügbar"
  371. )
  372. def test_server_context_options(self):
  373. """Server-Kontext-Optionen prüfen."""
  374. with tempfile.TemporaryDirectory() as tmpdir:
  375. generator = CertificateGenerator()
  376. request = CertificateRequest(common_name="localhost")
  377. cert_path, key_path = generator.generate_self_signed(
  378. request=request,
  379. output_dir=Path(tmpdir),
  380. )
  381. config = TLSConfig(
  382. enabled=True,
  383. cert_file=cert_path,
  384. key_file=key_path,
  385. min_version=TLSVersion.TLS_1_2,
  386. cipher_suite=CipherSuite.MODERN,
  387. )
  388. context = config.create_server_context()
  389. # Minimale Version prüfen
  390. assert context.minimum_version == ssl.TLSVersion.TLSv1_2
  391. # Sicherheitsoptionen prüfen
  392. # Hinweis: OP_NO_SSLv2 ist 0 in modernem Python (SSLv2 komplett entfernt)
  393. assert context.options & ssl.OP_NO_SSLv3
  394. assert context.options & ssl.OP_NO_TLSv1
  395. assert context.options & ssl.OP_NO_TLSv1_1
  396. @pytest.mark.skipif(
  397. not CertificateGenerator().is_openssl_available(),
  398. reason="OpenSSL nicht verfügbar"
  399. )
  400. def test_client_context_options(self):
  401. """Client-Kontext-Optionen prüfen."""
  402. with tempfile.TemporaryDirectory() as tmpdir:
  403. generator = CertificateGenerator()
  404. request = CertificateRequest(common_name="localhost")
  405. cert_path, key_path = generator.generate_self_signed(
  406. request=request,
  407. output_dir=Path(tmpdir),
  408. )
  409. config = TLSConfig(
  410. enabled=True,
  411. cert_file=cert_path,
  412. key_file=key_path,
  413. ca_file=cert_path, # Selbstsigniert = CA
  414. check_hostname=False,
  415. )
  416. context = config.create_client_context()
  417. assert context.check_hostname is False
  418. assert context.minimum_version >= ssl.TLSVersion.TLSv1_2
  419. # =============================================================================
  420. # Edge Cases
  421. # =============================================================================
  422. class TestEdgeCases:
  423. """Tests für Randfälle."""
  424. def test_invalid_openssl_path(self):
  425. """Ungültiger OpenSSL-Pfad."""
  426. generator = CertificateGenerator(openssl_path="/nonexistent/openssl")
  427. assert generator.is_openssl_available() is False
  428. def test_empty_config(self):
  429. """Leere Konfiguration."""
  430. config = TLSConfig()
  431. data = config.to_dict()
  432. restored = TLSConfig.from_dict(data)
  433. assert restored.enabled is False
  434. def test_path_string_conversion(self):
  435. """String zu Path Konvertierung."""
  436. config = TLSConfig(
  437. enabled=True,
  438. cert_file="relative/path/cert.crt",
  439. key_file="relative/path/key.key",
  440. )
  441. assert isinstance(config.cert_file, Path)
  442. assert isinstance(config.key_file, Path)