embed-client 2.0.0.0__py3-none-any.whl → 3.1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,475 @@
1
+ """
2
+ SSL/TLS Manager for embed-client.
3
+
4
+ This module provides SSL/TLS management for the embed-client, supporting
5
+ all security modes including HTTP, HTTPS, and mTLS. It integrates with
6
+ mcp_security_framework when available and provides fallback implementations.
7
+
8
+ Author: Vasiliy Zdanovskiy
9
+ email: vasilyvz@gmail.com
10
+ """
11
+
12
+ import logging
13
+ import ssl
14
+ import os
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List, Optional, Union
17
+
18
+ # Try to import mcp_security_framework components
19
+ try:
20
+ from mcp_security_framework import (
21
+ SSLConfig,
22
+ CertificateInfo,
23
+ SecurityManager
24
+ )
25
+ SECURITY_FRAMEWORK_AVAILABLE = True
26
+ except ImportError:
27
+ SECURITY_FRAMEWORK_AVAILABLE = False
28
+ print("Warning: mcp_security_framework not available. Using fallback SSL/TLS management.")
29
+
30
+ # Fallback certificate handling
31
+ try:
32
+ from cryptography import x509
33
+ from cryptography.hazmat.primitives import serialization
34
+ CRYPTOGRAPHY_AVAILABLE = True
35
+ except ImportError:
36
+ CRYPTOGRAPHY_AVAILABLE = False
37
+ print("Warning: cryptography not available. Limited SSL/TLS functionality.")
38
+
39
+
40
+ class SSLManagerError(Exception):
41
+ """Raised when SSL/TLS operations fail."""
42
+
43
+ def __init__(self, message: str, error_code: int = -32002):
44
+ self.message = message
45
+ self.error_code = error_code
46
+ super().__init__(self.message)
47
+
48
+
49
+ class ClientSSLManager:
50
+ """
51
+ Client SSL/TLS Manager.
52
+
53
+ This class provides SSL/TLS management for the embed-client,
54
+ supporting all security modes with integration to
55
+ mcp_security_framework when available.
56
+ """
57
+
58
+ def __init__(self, config: Dict[str, Any]):
59
+ """
60
+ Initialize SSL/TLS manager.
61
+
62
+ Args:
63
+ config: SSL/TLS configuration dictionary
64
+ """
65
+ self.config = config
66
+ self.logger = logging.getLogger(__name__)
67
+
68
+ # Initialize security framework components if available
69
+ self.ssl_manager = None
70
+ self.cert_manager = None
71
+
72
+ if SECURITY_FRAMEWORK_AVAILABLE:
73
+ self._initialize_security_framework()
74
+ else:
75
+ self.logger.warning("mcp_security_framework not available, using fallback SSL/TLS management")
76
+
77
+ def _initialize_security_framework(self) -> None:
78
+ """Initialize mcp_security_framework components."""
79
+ try:
80
+ # Create SSL config
81
+ ssl_config_dict = self.config.get("ssl", {})
82
+ ssl_config = SSLConfig(
83
+ enabled=ssl_config_dict.get("enabled", False),
84
+ cert_file=ssl_config_dict.get("cert_file") if ssl_config_dict.get("cert_file") and os.path.exists(ssl_config_dict.get("cert_file")) else None,
85
+ key_file=ssl_config_dict.get("key_file") if ssl_config_dict.get("key_file") and os.path.exists(ssl_config_dict.get("key_file")) else None,
86
+ ca_cert_file=ssl_config_dict.get("ca_cert_file") if ssl_config_dict.get("ca_cert_file") and os.path.exists(ssl_config_dict.get("ca_cert_file")) else None,
87
+ verify_mode=ssl_config_dict.get("verify_mode", "CERT_REQUIRED"),
88
+ check_hostname=ssl_config_dict.get("check_hostname", True),
89
+ check_expiry=ssl_config_dict.get("check_expiry", True)
90
+ )
91
+
92
+ # Create permission config (required by SecurityManager)
93
+ from mcp_security_framework import PermissionConfig
94
+ permission_config = PermissionConfig(
95
+ enabled=False,
96
+ roles_file="configs/roles.json"
97
+ )
98
+
99
+ # Create security config
100
+ from mcp_security_framework import SecurityConfig
101
+ security_config = SecurityConfig(
102
+ ssl=ssl_config,
103
+ permissions=permission_config
104
+ )
105
+
106
+ # Initialize managers
107
+ self.ssl_manager = SecurityManager(security_config)
108
+
109
+ self.logger.info("Security framework SSL/TLS components initialized successfully")
110
+
111
+ except Exception as e:
112
+ self.logger.warning(f"Failed to initialize security framework SSL/TLS: {e}")
113
+ self.ssl_manager = None
114
+ self.cert_manager = None
115
+
116
+ def create_client_ssl_context(self) -> Optional[ssl.SSLContext]:
117
+ """
118
+ Create SSL context for client connections.
119
+
120
+ Returns:
121
+ SSL context for client connections or None if SSL is disabled
122
+ """
123
+ # Check if SSL is enabled first
124
+ if not self.is_ssl_enabled():
125
+ return None
126
+
127
+ try:
128
+ # Force fallback implementation for CERT_NONE mode
129
+ ssl_config = self.config.get("ssl", {})
130
+ if ssl_config.get("verify_mode") == "CERT_NONE":
131
+ return self._create_client_ssl_context_fallback()
132
+ elif self.ssl_manager:
133
+ # Use security framework
134
+ return self.ssl_manager.create_client_context()
135
+ else:
136
+ # Fallback implementation
137
+ return self._create_client_ssl_context_fallback()
138
+
139
+ except Exception as e:
140
+ self.logger.error(f"Failed to create client SSL context: {e}")
141
+ raise SSLManagerError(f"Failed to create client SSL context: {e}")
142
+
143
+ def _create_client_ssl_context_fallback(self) -> Optional[ssl.SSLContext]:
144
+ """Fallback SSL context creation."""
145
+ ssl_config = self.config.get("ssl", {})
146
+
147
+ if not ssl_config.get("enabled", False):
148
+ return None
149
+
150
+ try:
151
+ # Configure verification
152
+ verify_mode = ssl_config.get("verify_mode", "CERT_REQUIRED")
153
+ check_hostname = ssl_config.get("check_hostname", True)
154
+
155
+ # Force check_hostname=False for CERT_NONE mode
156
+ if verify_mode == "CERT_NONE":
157
+ check_hostname = False
158
+
159
+ # Create SSL context based on verification mode
160
+ if verify_mode == "CERT_NONE":
161
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
162
+ context.check_hostname = False
163
+ context.verify_mode = ssl.CERT_NONE
164
+ else:
165
+ context = ssl.create_default_context()
166
+ context.check_hostname = check_hostname
167
+ if verify_mode == "CERT_OPTIONAL":
168
+ context.verify_mode = ssl.CERT_OPTIONAL
169
+ else: # CERT_REQUIRED
170
+ context.verify_mode = ssl.CERT_REQUIRED
171
+
172
+ # Load CA certificate if provided
173
+ ca_cert_file = ssl_config.get("ca_cert_file")
174
+ if ca_cert_file and os.path.exists(ca_cert_file):
175
+ context.load_verify_locations(ca_cert_file)
176
+
177
+ # Load client certificate if provided (for mTLS)
178
+ cert_file = ssl_config.get("cert_file")
179
+ key_file = ssl_config.get("key_file")
180
+ if cert_file and key_file and os.path.exists(cert_file) and os.path.exists(key_file):
181
+ context.load_cert_chain(cert_file, key_file)
182
+
183
+ return context
184
+
185
+ except Exception as e:
186
+ raise SSLManagerError(f"Failed to create SSL context: {e}")
187
+
188
+ def create_connector(self) -> Optional[Any]:
189
+ """
190
+ Create aiohttp connector with SSL context.
191
+
192
+ Returns:
193
+ aiohttp connector with SSL context or None if SSL is disabled
194
+ """
195
+ try:
196
+ import aiohttp
197
+
198
+ ssl_context = self.create_client_ssl_context()
199
+ if ssl_context is None:
200
+ return None
201
+
202
+ return aiohttp.TCPConnector(ssl=ssl_context)
203
+
204
+ except ImportError:
205
+ self.logger.error("aiohttp not available for connector creation")
206
+ return None
207
+ except Exception as e:
208
+ self.logger.error(f"Failed to create connector: {e}")
209
+ return None
210
+
211
+ def validate_certificate(self, cert_file: str) -> Dict[str, Any]:
212
+ """
213
+ Validate certificate file.
214
+
215
+ Args:
216
+ cert_file: Path to certificate file
217
+
218
+ Returns:
219
+ Dictionary with validation results
220
+ """
221
+ try:
222
+ if self.ssl_manager and CRYPTOGRAPHY_AVAILABLE:
223
+ # Use security framework
224
+ cert_info = extract_certificate_info(cert_file)
225
+ return {
226
+ "valid": True,
227
+ "subject": str(cert_info.subject),
228
+ "issuer": str(cert_info.issuer),
229
+ "not_valid_before": cert_info.not_valid_before.isoformat(),
230
+ "not_valid_after": cert_info.not_valid_after.isoformat(),
231
+ "serial_number": str(cert_info.serial_number),
232
+ "is_self_signed": is_certificate_self_signed(cert_file)
233
+ }
234
+ else:
235
+ # Fallback implementation
236
+ return self._validate_certificate_fallback(cert_file)
237
+
238
+ except Exception as e:
239
+ self.logger.error(f"Certificate validation failed: {e}")
240
+ return {
241
+ "valid": False,
242
+ "error": str(e)
243
+ }
244
+
245
+ def _validate_certificate_fallback(self, cert_file: str) -> Dict[str, Any]:
246
+ """Fallback certificate validation."""
247
+ if not CRYPTOGRAPHY_AVAILABLE:
248
+ return {
249
+ "valid": False,
250
+ "error": "cryptography not available for certificate validation"
251
+ }
252
+
253
+ try:
254
+ if not os.path.exists(cert_file):
255
+ return {
256
+ "valid": False,
257
+ "error": "Certificate file not found"
258
+ }
259
+
260
+ # Basic file existence and readability check
261
+ with open(cert_file, 'rb') as f:
262
+ cert_data = f.read()
263
+
264
+ # Try to parse certificate
265
+ cert = x509.load_pem_x509_certificate(cert_data)
266
+
267
+ return {
268
+ "valid": True,
269
+ "subject": str(cert.subject),
270
+ "issuer": str(cert.issuer),
271
+ "not_valid_before": cert.not_valid_before.isoformat(),
272
+ "not_valid_after": cert.not_valid_after.isoformat(),
273
+ "serial_number": str(cert.serial_number)
274
+ }
275
+
276
+ except Exception as e:
277
+ return {
278
+ "valid": False,
279
+ "error": f"Certificate validation failed: {e}"
280
+ }
281
+
282
+ def get_ssl_config(self) -> Dict[str, Any]:
283
+ """
284
+ Get current SSL configuration.
285
+
286
+ Returns:
287
+ Dictionary with SSL configuration
288
+ """
289
+ return self.config.get("ssl", {})
290
+
291
+ def is_ssl_enabled(self) -> bool:
292
+ """
293
+ Check if SSL/TLS is enabled.
294
+
295
+ Returns:
296
+ True if SSL/TLS is enabled, False otherwise
297
+ """
298
+ return self.config.get("ssl", {}).get("enabled", False)
299
+
300
+ def is_mtls_enabled(self) -> bool:
301
+ """
302
+ Check if mTLS (mutual TLS) is enabled.
303
+
304
+ Returns:
305
+ True if mTLS is enabled, False otherwise
306
+ """
307
+ ssl_config = self.config.get("ssl", {})
308
+ return (ssl_config.get("enabled", False) and
309
+ bool(ssl_config.get("cert_file")) and
310
+ bool(ssl_config.get("key_file")))
311
+
312
+ def get_certificate_info(self, cert_file: str) -> Optional[Dict[str, Any]]:
313
+ """
314
+ Get certificate information.
315
+
316
+ Args:
317
+ cert_file: Path to certificate file
318
+
319
+ Returns:
320
+ Dictionary with certificate information or None if not available
321
+ """
322
+ try:
323
+ if self.ssl_manager and CRYPTOGRAPHY_AVAILABLE:
324
+ # Use security framework
325
+ cert_info = extract_certificate_info(cert_file)
326
+ return {
327
+ "subject": str(cert_info.subject),
328
+ "issuer": str(cert_info.issuer),
329
+ "not_valid_before": cert_info.not_valid_before.isoformat(),
330
+ "not_valid_after": cert_info.not_valid_after.isoformat(),
331
+ "serial_number": str(cert_info.serial_number),
332
+ "version": cert_info.version,
333
+ "signature_algorithm": str(cert_info.signature_algorithm_oid),
334
+ "is_self_signed": is_certificate_self_signed(cert_file)
335
+ }
336
+ else:
337
+ # Fallback implementation
338
+ return self._get_certificate_info_fallback(cert_file)
339
+
340
+ except Exception as e:
341
+ self.logger.error(f"Failed to get certificate info: {e}")
342
+ return None
343
+
344
+ def _get_certificate_info_fallback(self, cert_file: str) -> Optional[Dict[str, Any]]:
345
+ """Fallback certificate info extraction."""
346
+ if not CRYPTOGRAPHY_AVAILABLE:
347
+ return None
348
+
349
+ try:
350
+ if not os.path.exists(cert_file):
351
+ return None
352
+
353
+ with open(cert_file, 'rb') as f:
354
+ cert_data = f.read()
355
+
356
+ cert = x509.load_pem_x509_certificate(cert_data)
357
+
358
+ return {
359
+ "subject": str(cert.subject),
360
+ "issuer": str(cert.issuer),
361
+ "not_valid_before": cert.not_valid_before.isoformat(),
362
+ "not_valid_after": cert.not_valid_after.isoformat(),
363
+ "serial_number": str(cert.serial_number),
364
+ "version": cert.version.name,
365
+ "signature_algorithm": str(cert.signature_algorithm_oid)
366
+ }
367
+
368
+ except Exception as e:
369
+ self.logger.error(f"Failed to extract certificate info: {e}")
370
+ return None
371
+
372
+ def validate_ssl_config(self) -> List[str]:
373
+ """
374
+ Validate SSL configuration.
375
+
376
+ Returns:
377
+ List of validation errors
378
+ """
379
+ errors = []
380
+ ssl_config = self.config.get("ssl", {})
381
+
382
+ if not ssl_config.get("enabled", False):
383
+ return errors # SSL disabled, no validation needed
384
+
385
+ # Check certificate files if mTLS is configured
386
+ cert_file = ssl_config.get("cert_file")
387
+ key_file = ssl_config.get("key_file")
388
+
389
+ if cert_file and not os.path.exists(cert_file):
390
+ errors.append(f"Certificate file not found: {cert_file}")
391
+
392
+ if key_file and not os.path.exists(key_file):
393
+ errors.append(f"Key file not found: {key_file}")
394
+
395
+ # Check CA certificate if provided
396
+ ca_cert_file = ssl_config.get("ca_cert_file")
397
+ if ca_cert_file and not os.path.exists(ca_cert_file):
398
+ errors.append(f"CA certificate file not found: {ca_cert_file}")
399
+
400
+ # Validate certificate if provided
401
+ if cert_file and os.path.exists(cert_file):
402
+ validation_result = self.validate_certificate(cert_file)
403
+ if not validation_result.get("valid", False):
404
+ errors.append(f"Certificate validation failed: {validation_result.get('error', 'Unknown error')}")
405
+
406
+ return errors
407
+
408
+ def get_supported_protocols(self) -> List[str]:
409
+ """
410
+ Get list of supported SSL/TLS protocols.
411
+
412
+ Returns:
413
+ List of supported protocol names
414
+ """
415
+ protocols = []
416
+
417
+ try:
418
+ # Check available protocols
419
+ if hasattr(ssl, 'PROTOCOL_TLSv1_2'):
420
+ protocols.append("TLSv1.2")
421
+ if hasattr(ssl, 'PROTOCOL_TLSv1_3'):
422
+ protocols.append("TLSv1.3")
423
+ if hasattr(ssl, 'PROTOCOL_TLS'):
424
+ protocols.append("TLS")
425
+
426
+ # Fallback to basic protocols
427
+ if not protocols:
428
+ protocols = ["SSLv23", "TLS"]
429
+
430
+ except Exception as e:
431
+ self.logger.warning(f"Failed to detect supported protocols: {e}")
432
+ protocols = ["TLS"] # Basic fallback
433
+
434
+ return protocols
435
+
436
+
437
+ def create_ssl_manager(config: Dict[str, Any]) -> ClientSSLManager:
438
+ """
439
+ Create SSL/TLS manager from configuration.
440
+
441
+ Args:
442
+ config: Configuration dictionary
443
+
444
+ Returns:
445
+ ClientSSLManager instance
446
+ """
447
+ return ClientSSLManager(config)
448
+
449
+
450
+ def create_ssl_context(config: Dict[str, Any]) -> Optional[ssl.SSLContext]:
451
+ """
452
+ Create SSL context from configuration.
453
+
454
+ Args:
455
+ config: Configuration dictionary
456
+
457
+ Returns:
458
+ SSL context or None if SSL is disabled
459
+ """
460
+ ssl_manager = ClientSSLManager(config)
461
+ return ssl_manager.create_client_ssl_context()
462
+
463
+
464
+ def create_connector(config: Dict[str, Any]) -> Optional[Any]:
465
+ """
466
+ Create aiohttp connector from configuration.
467
+
468
+ Args:
469
+ config: Configuration dictionary
470
+
471
+ Returns:
472
+ aiohttp connector or None if SSL is disabled
473
+ """
474
+ ssl_manager = ClientSSLManager(config)
475
+ return ssl_manager.create_connector()