mcp-proxy-adapter 4.1.1__py3-none-any.whl → 6.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.
Files changed (200) hide show
  1. mcp_proxy_adapter/__main__.py +32 -0
  2. mcp_proxy_adapter/api/app.py +290 -33
  3. mcp_proxy_adapter/api/handlers.py +32 -6
  4. mcp_proxy_adapter/api/middleware/__init__.py +38 -32
  5. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
  6. mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
  7. mcp_proxy_adapter/api/middleware/factory.py +243 -0
  8. mcp_proxy_adapter/api/middleware/logging.py +32 -6
  9. mcp_proxy_adapter/api/middleware/protocol_middleware.py +201 -0
  10. mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
  11. mcp_proxy_adapter/api/middleware/unified_security.py +197 -0
  12. mcp_proxy_adapter/api/middleware/user_info_middleware.py +158 -0
  13. mcp_proxy_adapter/commands/__init__.py +19 -4
  14. mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
  15. mcp_proxy_adapter/commands/base.py +66 -32
  16. mcp_proxy_adapter/commands/builtin_commands.py +95 -0
  17. mcp_proxy_adapter/commands/catalog_manager.py +838 -0
  18. mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
  19. mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
  20. mcp_proxy_adapter/commands/command_registry.py +711 -354
  21. mcp_proxy_adapter/commands/dependency_manager.py +245 -0
  22. mcp_proxy_adapter/commands/echo_command.py +81 -0
  23. mcp_proxy_adapter/commands/health_command.py +8 -1
  24. mcp_proxy_adapter/commands/help_command.py +21 -14
  25. mcp_proxy_adapter/commands/hooks.py +200 -167
  26. mcp_proxy_adapter/commands/key_management_command.py +506 -0
  27. mcp_proxy_adapter/commands/load_command.py +176 -0
  28. mcp_proxy_adapter/commands/plugins_command.py +235 -0
  29. mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
  30. mcp_proxy_adapter/commands/proxy_registration_command.py +409 -0
  31. mcp_proxy_adapter/commands/reload_command.py +48 -50
  32. mcp_proxy_adapter/commands/result.py +1 -0
  33. mcp_proxy_adapter/commands/role_test_command.py +141 -0
  34. mcp_proxy_adapter/commands/roles_management_command.py +697 -0
  35. mcp_proxy_adapter/commands/security_command.py +488 -0
  36. mcp_proxy_adapter/commands/ssl_setup_command.py +366 -0
  37. mcp_proxy_adapter/commands/token_management_command.py +529 -0
  38. mcp_proxy_adapter/commands/transport_management_command.py +144 -0
  39. mcp_proxy_adapter/commands/unload_command.py +158 -0
  40. mcp_proxy_adapter/config.py +394 -14
  41. mcp_proxy_adapter/core/app_factory.py +410 -0
  42. mcp_proxy_adapter/core/app_runner.py +272 -0
  43. mcp_proxy_adapter/core/auth_validator.py +606 -0
  44. mcp_proxy_adapter/core/certificate_utils.py +1045 -0
  45. mcp_proxy_adapter/core/client.py +574 -0
  46. mcp_proxy_adapter/core/client_manager.py +284 -0
  47. mcp_proxy_adapter/core/client_security.py +384 -0
  48. mcp_proxy_adapter/core/config_converter.py +405 -0
  49. mcp_proxy_adapter/core/config_validator.py +218 -0
  50. mcp_proxy_adapter/core/logging.py +19 -3
  51. mcp_proxy_adapter/core/mtls_asgi.py +156 -0
  52. mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
  53. mcp_proxy_adapter/core/protocol_manager.py +385 -0
  54. mcp_proxy_adapter/core/proxy_client.py +602 -0
  55. mcp_proxy_adapter/core/proxy_registration.py +522 -0
  56. mcp_proxy_adapter/core/role_utils.py +426 -0
  57. mcp_proxy_adapter/core/security_adapter.py +370 -0
  58. mcp_proxy_adapter/core/security_factory.py +239 -0
  59. mcp_proxy_adapter/core/security_integration.py +286 -0
  60. mcp_proxy_adapter/core/server_adapter.py +282 -0
  61. mcp_proxy_adapter/core/server_engine.py +270 -0
  62. mcp_proxy_adapter/core/settings.py +1 -0
  63. mcp_proxy_adapter/core/ssl_utils.py +234 -0
  64. mcp_proxy_adapter/core/transport_manager.py +292 -0
  65. mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
  66. mcp_proxy_adapter/custom_openapi.py +22 -11
  67. mcp_proxy_adapter/examples/__init__.py +13 -4
  68. mcp_proxy_adapter/examples/basic_framework/__init__.py +9 -0
  69. mcp_proxy_adapter/examples/basic_framework/commands/__init__.py +4 -0
  70. mcp_proxy_adapter/examples/basic_framework/hooks/__init__.py +4 -0
  71. mcp_proxy_adapter/examples/basic_framework/main.py +44 -0
  72. mcp_proxy_adapter/examples/commands/__init__.py +5 -0
  73. mcp_proxy_adapter/examples/create_certificates_simple.py +550 -0
  74. mcp_proxy_adapter/examples/debug_request_state.py +112 -0
  75. mcp_proxy_adapter/examples/debug_role_chain.py +158 -0
  76. mcp_proxy_adapter/examples/demo_client.py +275 -0
  77. mcp_proxy_adapter/examples/examples/basic_framework/__init__.py +9 -0
  78. mcp_proxy_adapter/examples/examples/basic_framework/commands/__init__.py +4 -0
  79. mcp_proxy_adapter/examples/examples/basic_framework/hooks/__init__.py +4 -0
  80. mcp_proxy_adapter/examples/examples/basic_framework/main.py +44 -0
  81. mcp_proxy_adapter/examples/examples/full_application/__init__.py +12 -0
  82. mcp_proxy_adapter/examples/examples/full_application/commands/__init__.py +7 -0
  83. mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +80 -0
  84. mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +90 -0
  85. mcp_proxy_adapter/examples/examples/full_application/hooks/__init__.py +7 -0
  86. mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +75 -0
  87. mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +71 -0
  88. mcp_proxy_adapter/examples/examples/full_application/main.py +173 -0
  89. mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +154 -0
  90. mcp_proxy_adapter/examples/full_application/__init__.py +12 -0
  91. mcp_proxy_adapter/examples/full_application/commands/__init__.py +7 -0
  92. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +80 -0
  93. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +90 -0
  94. mcp_proxy_adapter/examples/full_application/hooks/__init__.py +7 -0
  95. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +75 -0
  96. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +71 -0
  97. mcp_proxy_adapter/examples/full_application/main.py +173 -0
  98. mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +154 -0
  99. mcp_proxy_adapter/examples/generate_all_certificates.py +362 -0
  100. mcp_proxy_adapter/examples/generate_certificates.py +177 -0
  101. mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +369 -0
  102. mcp_proxy_adapter/examples/generate_test_configs.py +331 -0
  103. mcp_proxy_adapter/examples/proxy_registration_example.py +334 -0
  104. mcp_proxy_adapter/examples/run_example.py +59 -0
  105. mcp_proxy_adapter/examples/run_full_test_suite.py +318 -0
  106. mcp_proxy_adapter/examples/run_proxy_server.py +146 -0
  107. mcp_proxy_adapter/examples/run_security_tests.py +544 -0
  108. mcp_proxy_adapter/examples/run_security_tests_fixed.py +247 -0
  109. mcp_proxy_adapter/examples/scripts/config_generator.py +740 -0
  110. mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +560 -0
  111. mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +369 -0
  112. mcp_proxy_adapter/examples/security_test_client.py +782 -0
  113. mcp_proxy_adapter/examples/setup_test_environment.py +328 -0
  114. mcp_proxy_adapter/examples/test_config.py +148 -0
  115. mcp_proxy_adapter/examples/test_config_generator.py +86 -0
  116. mcp_proxy_adapter/examples/test_examples.py +281 -0
  117. mcp_proxy_adapter/examples/universal_client.py +620 -0
  118. mcp_proxy_adapter/main.py +93 -0
  119. mcp_proxy_adapter/utils/config_generator.py +1008 -0
  120. mcp_proxy_adapter/version.py +5 -2
  121. mcp_proxy_adapter-6.0.1.dist-info/METADATA +679 -0
  122. mcp_proxy_adapter-6.0.1.dist-info/RECORD +140 -0
  123. mcp_proxy_adapter-6.0.1.dist-info/entry_points.txt +2 -0
  124. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/licenses/LICENSE +2 -2
  125. mcp_proxy_adapter/api/middleware/auth.py +0 -146
  126. mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
  127. mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
  128. mcp_proxy_adapter/examples/README.md +0 -124
  129. mcp_proxy_adapter/examples/basic_server/README.md +0 -60
  130. mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
  131. mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
  132. mcp_proxy_adapter/examples/basic_server/config.json +0 -35
  133. mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
  134. mcp_proxy_adapter/examples/basic_server/server.py +0 -103
  135. mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
  136. mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
  137. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -250
  138. mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
  139. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
  140. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
  141. mcp_proxy_adapter/examples/custom_commands/config.json +0 -35
  142. mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
  143. mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
  144. mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
  145. mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
  146. mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
  147. mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
  148. mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
  149. mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
  150. mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
  151. mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
  152. mcp_proxy_adapter/examples/custom_commands/server.py +0 -228
  153. mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
  154. mcp_proxy_adapter/examples/deployment/README.md +0 -49
  155. mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
  156. mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
  157. mcp_proxy_adapter/examples/deployment/config.json +0 -29
  158. mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
  159. mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
  160. mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
  161. mcp_proxy_adapter/examples/deployment/run.sh +0 -43
  162. mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
  163. mcp_proxy_adapter/schemas/base_schema.json +0 -114
  164. mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
  165. mcp_proxy_adapter/tests/__init__.py +0 -0
  166. mcp_proxy_adapter/tests/api/__init__.py +0 -3
  167. mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
  168. mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
  169. mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
  170. mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
  171. mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
  172. mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
  173. mcp_proxy_adapter/tests/commands/__init__.py +0 -3
  174. mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
  175. mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
  176. mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
  177. mcp_proxy_adapter/tests/conftest.py +0 -131
  178. mcp_proxy_adapter/tests/functional/__init__.py +0 -3
  179. mcp_proxy_adapter/tests/functional/test_api.py +0 -253
  180. mcp_proxy_adapter/tests/integration/__init__.py +0 -3
  181. mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
  182. mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
  183. mcp_proxy_adapter/tests/performance/__init__.py +0 -3
  184. mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
  185. mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
  186. mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
  187. mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
  188. mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
  189. mcp_proxy_adapter/tests/test_base_command.py +0 -123
  190. mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
  191. mcp_proxy_adapter/tests/test_command_registry.py +0 -281
  192. mcp_proxy_adapter/tests/test_config.py +0 -127
  193. mcp_proxy_adapter/tests/test_utils.py +0 -65
  194. mcp_proxy_adapter/tests/unit/__init__.py +0 -3
  195. mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
  196. mcp_proxy_adapter/tests/unit/test_config.py +0 -217
  197. mcp_proxy_adapter-4.1.1.dist-info/METADATA +0 -200
  198. mcp_proxy_adapter-4.1.1.dist-info/RECORD +0 -110
  199. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/WHEEL +0 -0
  200. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1045 @@
1
+ """
2
+ Certificate Utilities
3
+
4
+ This module provides utilities for working with certificates including creation,
5
+ validation, and role extraction. Integrates with mcp_security_framework.
6
+
7
+ Author: MCP Proxy Adapter Team
8
+ Version: 1.0.0
9
+ """
10
+
11
+ import logging
12
+ import os
13
+ import ipaddress
14
+ from datetime import datetime, timedelta, timezone
15
+ from typing import Dict, List, Optional, Any
16
+ from pathlib import Path
17
+
18
+ # Import mcp_security_framework
19
+ try:
20
+ from mcp_security_framework.core.cert_manager import CertificateManager
21
+ from mcp_security_framework.schemas.config import (
22
+ CertificateConfig,
23
+ CAConfig,
24
+ ClientCertConfig,
25
+ ServerCertConfig
26
+ )
27
+ from mcp_security_framework.schemas.models import CertificateType
28
+ from mcp_security_framework.utils.cert_utils import (
29
+ parse_certificate,
30
+ extract_roles_from_certificate,
31
+ extract_permissions_from_certificate,
32
+ validate_certificate_chain,
33
+ get_certificate_expiry
34
+ )
35
+ SECURITY_FRAMEWORK_AVAILABLE = True
36
+ except ImportError:
37
+ SECURITY_FRAMEWORK_AVAILABLE = False
38
+ # Fallback to cryptography if mcp_security_framework is not available
39
+ from cryptography import x509
40
+ from cryptography.hazmat.primitives import hashes, serialization
41
+ from cryptography.hazmat.primitives.asymmetric import rsa
42
+ from cryptography.x509.oid import NameOID
43
+
44
+ from .auth_validator import AuthValidator
45
+ from .role_utils import RoleUtils
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+
50
+ class CertificateUtils:
51
+ """
52
+ Utilities for working with certificates.
53
+
54
+ Provides methods for creating CA, server, and client certificates,
55
+ as well as validation and role extraction using mcp_security_framework.
56
+ """
57
+
58
+ # Default certificate validity period (1 year)
59
+ DEFAULT_VALIDITY_DAYS = 365
60
+
61
+ # Default key size
62
+ DEFAULT_KEY_SIZE = 2048
63
+
64
+ # Custom OID for roles (same as in RoleUtils)
65
+ ROLE_EXTENSION_OID = "1.3.6.1.4.1.99999.1"
66
+
67
+ @staticmethod
68
+ def create_ca_certificate(common_name: str, output_dir: str,
69
+ validity_days: int = DEFAULT_VALIDITY_DAYS,
70
+ key_size: int = DEFAULT_KEY_SIZE) -> Dict[str, str]:
71
+ """
72
+ Create a CA certificate and private key using mcp_security_framework.
73
+
74
+ Args:
75
+ common_name: Common name for the CA certificate
76
+ output_dir: Directory to save certificate and key files
77
+ validity_days: Certificate validity period in days
78
+ key_size: RSA key size in bits
79
+
80
+ Returns:
81
+ Dictionary with paths to created files
82
+
83
+ Raises:
84
+ ValueError: If parameters are invalid
85
+ OSError: If files cannot be created
86
+ """
87
+ if not SECURITY_FRAMEWORK_AVAILABLE:
88
+ logger.warning("mcp_security_framework not available, using fallback method")
89
+ return CertificateUtils._create_ca_certificate_fallback(
90
+ common_name, output_dir, validity_days, key_size
91
+ )
92
+
93
+ try:
94
+ # Validate parameters
95
+ if not common_name or not common_name.strip():
96
+ raise ValueError("Common name cannot be empty")
97
+
98
+ if validity_days <= 0:
99
+ raise ValueError("Validity days must be positive")
100
+
101
+ if key_size < 1024:
102
+ raise ValueError("Key size must be at least 1024 bits")
103
+
104
+ # Create output directory if it doesn't exist
105
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
106
+
107
+ # Configure CA using mcp_security_framework
108
+ ca_config = CAConfig(
109
+ common_name=common_name,
110
+ organization="MCP Proxy Adapter CA",
111
+ organizational_unit="Certificate Authority",
112
+ country="US",
113
+ state="Default State",
114
+ locality="Default City",
115
+ validity_days=validity_days,
116
+ key_size=key_size,
117
+ key_type="RSA"
118
+ )
119
+
120
+ # Create certificate manager
121
+ cert_config = CertificateConfig(
122
+ output_dir=output_dir,
123
+ ca_cert_path=str(Path(output_dir) / f"{common_name}.crt"),
124
+ ca_key_path=str(Path(output_dir) / f"{common_name}.key")
125
+ )
126
+
127
+ cert_manager = CertificateManager(cert_config)
128
+
129
+ # Generate CA certificate
130
+ ca_pair = cert_manager.create_ca_certificate(ca_config)
131
+
132
+ return {
133
+ "cert_path": str(ca_pair.cert_path),
134
+ "key_path": str(ca_pair.key_path)
135
+ }
136
+
137
+ except Exception as e:
138
+ logger.error(f"Failed to create CA certificate: {e}")
139
+ raise
140
+
141
+ @staticmethod
142
+ def _create_ca_certificate_fallback(common_name: str, output_dir: str,
143
+ validity_days: int, key_size: int) -> Dict[str, str]:
144
+ """Fallback method using cryptography directly."""
145
+ try:
146
+ # Validate parameters
147
+ if not common_name or not common_name.strip():
148
+ raise ValueError("Common name cannot be empty")
149
+
150
+ if validity_days <= 0:
151
+ raise ValueError("Validity days must be positive")
152
+
153
+ if key_size < 1024:
154
+ raise ValueError("Key size must be at least 1024 bits")
155
+
156
+ # Create output directory if it doesn't exist
157
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
158
+
159
+ # Generate private key
160
+ private_key = rsa.generate_private_key(
161
+ public_exponent=65537,
162
+ key_size=key_size
163
+ )
164
+
165
+ # Create certificate subject
166
+ subject = issuer = x509.Name([
167
+ x509.NameAttribute(NameOID.COMMON_NAME, common_name),
168
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, "MCP Proxy Adapter CA"),
169
+ x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "Certificate Authority"),
170
+ x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
171
+ ])
172
+
173
+ # Create certificate
174
+ cert = x509.CertificateBuilder().subject_name(
175
+ subject
176
+ ).issuer_name(
177
+ issuer
178
+ ).public_key(
179
+ private_key.public_key()
180
+ ).serial_number(
181
+ x509.random_serial_number()
182
+ ).not_valid_before(
183
+ datetime.now(timezone.utc)
184
+ ).not_valid_after(
185
+ datetime.now(timezone.utc) + timedelta(days=validity_days)
186
+ ).add_extension(
187
+ x509.BasicConstraints(ca=True, path_length=None),
188
+ critical=True
189
+ ).add_extension(
190
+ x509.KeyUsage(
191
+ key_cert_sign=True,
192
+ crl_sign=True,
193
+ digital_signature=True,
194
+ key_encipherment=False,
195
+ data_encipherment=False,
196
+ key_agreement=False,
197
+ encipher_only=False,
198
+ decipher_only=False
199
+ ),
200
+ critical=True
201
+ ).sign(private_key, hashes.SHA256())
202
+
203
+ # Save certificate and key
204
+ cert_path = Path(output_dir) / f"{common_name}.crt"
205
+ key_path = Path(output_dir) / f"{common_name}.key"
206
+
207
+ with open(cert_path, "wb") as f:
208
+ f.write(cert.public_bytes(serialization.Encoding.PEM))
209
+
210
+ with open(key_path, "wb") as f:
211
+ f.write(private_key.private_bytes(
212
+ encoding=serialization.Encoding.PEM,
213
+ format=serialization.PrivateFormat.PKCS8,
214
+ encryption_algorithm=serialization.NoEncryption()
215
+ ))
216
+
217
+ return {
218
+ "cert_path": str(cert_path),
219
+ "key_path": str(key_path)
220
+ }
221
+
222
+ except Exception as e:
223
+ logger.error(f"Failed to create CA certificate (fallback): {e}")
224
+ raise
225
+
226
+ @staticmethod
227
+ def create_server_certificate(common_name: str, roles: List[str],
228
+ ca_cert_path: str, ca_key_path: str,
229
+ output_dir: str,
230
+ validity_days: int = DEFAULT_VALIDITY_DAYS,
231
+ key_size: int = DEFAULT_KEY_SIZE) -> Dict[str, str]:
232
+ """
233
+ Create a server certificate signed by CA.
234
+
235
+ Args:
236
+ common_name: Common name for the server certificate
237
+ roles: List of roles to include in certificate
238
+ ca_cert_path: Path to CA certificate
239
+ ca_key_path: Path to CA private key
240
+ output_dir: Directory to save certificate and key files
241
+ validity_days: Certificate validity period in days
242
+ key_size: RSA key size in bits
243
+
244
+ Returns:
245
+ Dictionary with paths to created files
246
+
247
+ Raises:
248
+ ValueError: If parameters are invalid
249
+ FileNotFoundError: If CA files not found
250
+ OSError: If files cannot be created
251
+ """
252
+ try:
253
+ # Validate parameters
254
+ if not common_name or not common_name.strip():
255
+ raise ValueError("Common name cannot be empty")
256
+
257
+ if not roles:
258
+ roles = ["server"]
259
+
260
+ if not Path(ca_cert_path).exists():
261
+ raise FileNotFoundError(f"CA certificate not found: {ca_cert_path}")
262
+
263
+ if not Path(ca_key_path).exists():
264
+ raise FileNotFoundError(f"CA key not found: {ca_key_path}")
265
+
266
+ # Create output directory if it doesn't exist
267
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
268
+
269
+ # Load CA certificate and key
270
+ with open(ca_cert_path, "rb") as f:
271
+ ca_cert = x509.load_pem_x509_certificate(f.read())
272
+
273
+ with open(ca_key_path, "rb") as f:
274
+ ca_key = serialization.load_pem_private_key(f.read(), password=None)
275
+
276
+ # Generate server private key
277
+ private_key = rsa.generate_private_key(
278
+ public_exponent=65537,
279
+ key_size=key_size
280
+ )
281
+
282
+ # Create certificate subject
283
+ subject = x509.Name([
284
+ x509.NameAttribute(NameOID.COMMON_NAME, common_name),
285
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, "MCP Proxy Adapter"),
286
+ x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "Server"),
287
+ x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
288
+ ])
289
+
290
+ # Create certificate
291
+ cert_builder = x509.CertificateBuilder().subject_name(
292
+ subject
293
+ ).issuer_name(
294
+ ca_cert.subject
295
+ ).public_key(
296
+ private_key.public_key()
297
+ ).serial_number(
298
+ x509.random_serial_number()
299
+ ).not_valid_before(
300
+ datetime.now(timezone.utc)
301
+ ).not_valid_after(
302
+ datetime.now(timezone.utc) + timedelta(days=validity_days)
303
+ ).add_extension(
304
+ x509.BasicConstraints(ca=False, path_length=None),
305
+ critical=True
306
+ ).add_extension(
307
+ x509.KeyUsage(
308
+ key_cert_sign=False,
309
+ crl_sign=False,
310
+ digital_signature=True,
311
+ key_encipherment=True,
312
+ data_encipherment=False,
313
+ key_agreement=False,
314
+ encipher_only=False,
315
+ decipher_only=False,
316
+ content_commitment=False
317
+ ),
318
+ critical=True
319
+ ).add_extension(
320
+ x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
321
+ critical=False
322
+ ).add_extension(
323
+ x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()),
324
+ critical=False
325
+ ).add_extension(
326
+ x509.SubjectAlternativeName([
327
+ x509.DNSName(common_name),
328
+ x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")),
329
+ x509.IPAddress(ipaddress.IPv6Address("::1"))
330
+ ]),
331
+ critical=False
332
+ )
333
+
334
+ # Add roles extension
335
+ if roles:
336
+ roles_data = ",".join(roles).encode('utf-8')
337
+ roles_oid = x509.ObjectIdentifier(CertificateUtils.ROLE_EXTENSION_OID)
338
+ cert_builder = cert_builder.add_extension(
339
+ x509.UnrecognizedExtension(roles_oid, roles_data),
340
+ critical=False
341
+ )
342
+
343
+ cert = cert_builder.sign(ca_key, hashes.SHA256())
344
+
345
+ # Save certificate and key
346
+ cert_path = os.path.join(output_dir, "server.crt")
347
+ key_path = os.path.join(output_dir, "server.key")
348
+
349
+ with open(cert_path, "wb") as f:
350
+ f.write(cert.public_bytes(serialization.Encoding.PEM))
351
+
352
+ with open(key_path, "wb") as f:
353
+ f.write(private_key.private_bytes(
354
+ encoding=serialization.Encoding.PEM,
355
+ format=serialization.PrivateFormat.PKCS8,
356
+ encryption_algorithm=serialization.NoEncryption()
357
+ ))
358
+
359
+ logger.info(f"Server certificate created: {cert_path}")
360
+
361
+ return {
362
+ "cert_path": cert_path,
363
+ "key_path": key_path,
364
+ "common_name": common_name,
365
+ "roles": roles,
366
+ "validity_days": validity_days
367
+ }
368
+
369
+ except Exception as e:
370
+ logger.error(f"Failed to create server certificate: {e}")
371
+ raise
372
+
373
+ @staticmethod
374
+ def create_client_certificate(common_name: str, ca_cert_path: str, ca_key_path: str,
375
+ output_dir: str, roles: List[str] = None,
376
+ validity_days: int = DEFAULT_VALIDITY_DAYS,
377
+ key_size: int = DEFAULT_KEY_SIZE) -> Dict[str, str]:
378
+ """
379
+ Create a client certificate and private key using mcp_security_framework.
380
+
381
+ Args:
382
+ common_name: Common name for the client certificate
383
+ ca_cert_path: Path to CA certificate
384
+ ca_key_path: Path to CA private key
385
+ output_dir: Directory to save certificate and key files
386
+ roles: List of roles to include in certificate
387
+ validity_days: Certificate validity period in days
388
+ key_size: RSA key size in bits
389
+
390
+ Returns:
391
+ Dictionary with paths to created files
392
+
393
+ Raises:
394
+ ValueError: If parameters are invalid
395
+ OSError: If files cannot be created
396
+ """
397
+ if not SECURITY_FRAMEWORK_AVAILABLE:
398
+ logger.warning("mcp_security_framework not available, using fallback method")
399
+ return CertificateUtils._create_client_certificate_fallback(
400
+ common_name, ca_cert_path, ca_key_path, output_dir, roles, validity_days, key_size
401
+ )
402
+
403
+ try:
404
+ # Validate parameters
405
+ if not common_name or not common_name.strip():
406
+ raise ValueError("Common name cannot be empty")
407
+
408
+ if not Path(ca_cert_path).exists():
409
+ raise ValueError(f"CA certificate not found: {ca_cert_path}")
410
+
411
+ if not Path(ca_key_path).exists():
412
+ raise ValueError(f"CA private key not found: {ca_key_path}")
413
+
414
+ if validity_days <= 0:
415
+ raise ValueError("Validity days must be positive")
416
+
417
+ if key_size < 1024:
418
+ raise ValueError("Key size must be at least 1024 bits")
419
+
420
+ # Create output directory if it doesn't exist
421
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
422
+
423
+ # Configure client certificate using mcp_security_framework
424
+ client_config = ClientCertConfig(
425
+ common_name=common_name,
426
+ organization="MCP Proxy Adapter Client",
427
+ organizational_unit="Client Certificates",
428
+ country="US",
429
+ state="Default State",
430
+ locality="Default City",
431
+ validity_days=validity_days,
432
+ key_size=key_size,
433
+ key_type="RSA",
434
+ roles=roles or [],
435
+ permissions=[] # Permissions can be added later if needed
436
+ )
437
+
438
+ # Create certificate manager
439
+ cert_config = CertificateConfig(
440
+ output_dir=output_dir,
441
+ ca_cert_path=ca_cert_path,
442
+ ca_key_path=ca_key_path
443
+ )
444
+
445
+ cert_manager = CertificateManager(cert_config)
446
+
447
+ # Generate client certificate
448
+ client_pair = cert_manager.create_client_certificate(client_config)
449
+
450
+ return {
451
+ "cert_path": str(client_pair.cert_path),
452
+ "key_path": str(client_pair.key_path)
453
+ }
454
+
455
+ except Exception as e:
456
+ logger.error(f"Failed to create client certificate: {e}")
457
+ raise
458
+
459
+ @staticmethod
460
+ def _create_client_certificate_fallback(common_name: str, ca_cert_path: str, ca_key_path: str,
461
+ output_dir: str, roles: List[str] = None,
462
+ validity_days: int = DEFAULT_VALIDITY_DAYS,
463
+ key_size: int = DEFAULT_KEY_SIZE) -> Dict[str, str]:
464
+ """Fallback method using cryptography directly."""
465
+ try:
466
+ # Validate parameters
467
+ if not common_name or not common_name.strip():
468
+ raise ValueError("Common name cannot be empty")
469
+
470
+ if not Path(ca_cert_path).exists():
471
+ raise ValueError(f"CA certificate not found: {ca_cert_path}")
472
+
473
+ if not Path(ca_key_path).exists():
474
+ raise ValueError(f"CA private key not found: {ca_key_path}")
475
+
476
+ if validity_days <= 0:
477
+ raise ValueError("Validity days must be positive")
478
+
479
+ if key_size < 1024:
480
+ raise ValueError("Key size must be at least 1024 bits")
481
+
482
+ # Create output directory if it doesn't exist
483
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
484
+
485
+ # Load CA certificate and key
486
+ with open(ca_cert_path, "rb") as f:
487
+ ca_cert = x509.load_pem_x509_certificate(f.read())
488
+
489
+ with open(ca_key_path, "rb") as f:
490
+ ca_key = serialization.load_pem_private_key(f.read(), password=None)
491
+
492
+ # Generate client private key
493
+ private_key = rsa.generate_private_key(
494
+ public_exponent=65537,
495
+ key_size=key_size
496
+ )
497
+
498
+ # Create certificate subject
499
+ subject = x509.Name([
500
+ x509.NameAttribute(NameOID.COMMON_NAME, common_name),
501
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, "MCP Proxy Adapter Client"),
502
+ x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "Client Certificates"),
503
+ x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
504
+ ])
505
+
506
+ # Create certificate
507
+ cert_builder = x509.CertificateBuilder().subject_name(
508
+ subject
509
+ ).issuer_name(
510
+ ca_cert.subject
511
+ ).public_key(
512
+ private_key.public_key()
513
+ ).serial_number(
514
+ x509.random_serial_number()
515
+ ).not_valid_before(
516
+ datetime.now(timezone.utc)
517
+ ).not_valid_after(
518
+ datetime.now(timezone.utc) + timedelta(days=validity_days)
519
+ ).add_extension(
520
+ x509.BasicConstraints(ca=False, path_length=None),
521
+ critical=True
522
+ ).add_extension(
523
+ x509.KeyUsage(
524
+ key_cert_sign=False,
525
+ crl_sign=False,
526
+ digital_signature=True,
527
+ key_encipherment=True,
528
+ data_encipherment=False,
529
+ key_agreement=False,
530
+ encipher_only=False,
531
+ decipher_only=False
532
+ ),
533
+ critical=True
534
+ ).add_extension(
535
+ x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH]),
536
+ critical=False
537
+ )
538
+
539
+ # Add roles extension if provided
540
+ if roles:
541
+ roles_str = ",".join(roles)
542
+ roles_oid = x509.ObjectIdentifier(CertificateUtils.ROLE_EXTENSION_OID)
543
+ cert_builder = cert_builder.add_extension(
544
+ x509.UnrecognizedExtension(roles_oid, roles_str.encode()),
545
+ critical=False
546
+ )
547
+
548
+ cert = cert_builder.sign(ca_key, hashes.SHA256())
549
+
550
+ # Save certificate and key
551
+ cert_path = Path(output_dir) / f"{common_name}.crt"
552
+ key_path = Path(output_dir) / f"{common_name}.key"
553
+
554
+ with open(cert_path, "wb") as f:
555
+ f.write(cert.public_bytes(serialization.Encoding.PEM))
556
+
557
+ with open(key_path, "wb") as f:
558
+ f.write(private_key.private_bytes(
559
+ encoding=serialization.Encoding.PEM,
560
+ format=serialization.PrivateFormat.PKCS8,
561
+ encryption_algorithm=serialization.NoEncryption()
562
+ ))
563
+
564
+ return {
565
+ "cert_path": str(cert_path),
566
+ "key_path": str(key_path)
567
+ }
568
+
569
+ except Exception as e:
570
+ logger.error(f"Failed to create client certificate (fallback): {e}")
571
+ raise
572
+
573
+ @staticmethod
574
+ def extract_roles_from_certificate(cert_path: str) -> List[str]:
575
+ """
576
+ Extract roles from certificate using mcp_security_framework.
577
+
578
+ Args:
579
+ cert_path: Path to certificate file
580
+
581
+ Returns:
582
+ List of roles found in certificate
583
+ """
584
+ if not SECURITY_FRAMEWORK_AVAILABLE:
585
+ logger.warning("mcp_security_framework not available, using fallback method")
586
+ return RoleUtils.extract_roles_from_certificate(cert_path)
587
+
588
+ try:
589
+ return extract_roles_from_certificate(cert_path)
590
+ except Exception as e:
591
+ logger.error(f"Failed to extract roles from certificate: {e}")
592
+ return []
593
+
594
+ @staticmethod
595
+ def extract_roles_from_certificate_object(cert) -> List[str]:
596
+ """
597
+ Extract roles from certificate object using mcp_security_framework.
598
+
599
+ Args:
600
+ cert: Certificate object
601
+
602
+ Returns:
603
+ List of roles found in certificate
604
+ """
605
+ if not SECURITY_FRAMEWORK_AVAILABLE:
606
+ logger.warning("mcp_security_framework not available, using fallback method")
607
+ return RoleUtils.extract_roles_from_certificate_object(cert)
608
+
609
+ try:
610
+ # Convert certificate object to PEM format for mcp_security_framework
611
+ cert_pem = cert.public_bytes(serialization.Encoding.PEM)
612
+ return extract_roles_from_certificate(cert_pem)
613
+ except Exception as e:
614
+ logger.error(f"Failed to extract roles from certificate object: {e}")
615
+ return []
616
+
617
+ @staticmethod
618
+ def extract_permissions_from_certificate(cert_path: str) -> List[str]:
619
+ """
620
+ Extract permissions from certificate using mcp_security_framework.
621
+
622
+ Args:
623
+ cert_path: Path to certificate file
624
+
625
+ Returns:
626
+ List of permissions found in certificate
627
+ """
628
+ if not SECURITY_FRAMEWORK_AVAILABLE:
629
+ logger.warning("mcp_security_framework not available, permissions extraction not supported")
630
+ return []
631
+
632
+ try:
633
+ return extract_permissions_from_certificate(cert_path)
634
+ except Exception as e:
635
+ logger.error(f"Failed to extract permissions from certificate: {e}")
636
+ return []
637
+
638
+ @staticmethod
639
+ def validate_certificate_chain(cert_path: str, ca_cert_path: str) -> bool:
640
+ """
641
+ Validate certificate chain using mcp_security_framework.
642
+
643
+ Args:
644
+ cert_path: Path to certificate to validate
645
+ ca_cert_path: Path to CA certificate
646
+
647
+ Returns:
648
+ True if chain is valid, False otherwise
649
+ """
650
+ if not SECURITY_FRAMEWORK_AVAILABLE:
651
+ logger.warning("mcp_security_framework not available, using fallback validation")
652
+ return CertificateUtils._validate_certificate_chain_fallback(cert_path, ca_cert_path)
653
+
654
+ try:
655
+ return validate_certificate_chain(cert_path, ca_cert_path)
656
+ except Exception as e:
657
+ logger.error(f"Failed to validate certificate chain: {e}")
658
+ return False
659
+
660
+ @staticmethod
661
+ def _validate_certificate_chain_fallback(cert_path: str, ca_cert_path: str) -> bool:
662
+ """Fallback certificate chain validation using cryptography."""
663
+ try:
664
+ # Load certificates
665
+ with open(cert_path, "rb") as f:
666
+ cert = x509.load_pem_x509_certificate(f.read())
667
+
668
+ with open(ca_cert_path, "rb") as f:
669
+ ca_cert = x509.load_pem_x509_certificate(f.read())
670
+
671
+ # Simple validation: check if certificate is issued by CA
672
+ return cert.issuer == ca_cert.subject
673
+
674
+ except Exception as e:
675
+ logger.error(f"Failed to validate certificate chain (fallback): {e}")
676
+ return False
677
+
678
+ @staticmethod
679
+ def get_certificate_expiry(cert_path: str) -> Optional[datetime]:
680
+ """
681
+ Get certificate expiry date using mcp_security_framework.
682
+
683
+ Args:
684
+ cert_path: Path to certificate file
685
+
686
+ Returns:
687
+ Certificate expiry date or None if not available
688
+ """
689
+ if not SECURITY_FRAMEWORK_AVAILABLE:
690
+ logger.warning("mcp_security_framework not available, using fallback method")
691
+ return CertificateUtils._get_certificate_expiry_fallback(cert_path)
692
+
693
+ try:
694
+ expiry_info = get_certificate_expiry(cert_path)
695
+ if isinstance(expiry_info, dict) and 'expiry_date' in expiry_info:
696
+ return expiry_info['expiry_date']
697
+ return None
698
+ except Exception as e:
699
+ logger.error(f"Failed to get certificate expiry: {e}")
700
+ return None
701
+
702
+ @staticmethod
703
+ def _get_certificate_expiry_fallback(cert_path: str) -> Optional[datetime]:
704
+ """Fallback method to get certificate expiry using cryptography."""
705
+ try:
706
+ with open(cert_path, "rb") as f:
707
+ cert = x509.load_pem_x509_certificate(f.read())
708
+
709
+ return cert.not_valid_after
710
+
711
+ except Exception as e:
712
+ logger.error(f"Failed to get certificate expiry (fallback): {e}")
713
+ return None
714
+
715
+ @staticmethod
716
+ def validate_certificate(cert_path: str) -> bool:
717
+ """
718
+ Validate certificate using AuthValidator.
719
+
720
+ Args:
721
+ cert_path: Path to certificate to validate
722
+
723
+ Returns:
724
+ True if certificate is valid, False otherwise
725
+ """
726
+ try:
727
+ validator = AuthValidator()
728
+ result = validator.validate_certificate(cert_path)
729
+ return result.is_valid
730
+ except Exception as e:
731
+ logger.error(f"Failed to validate certificate: {e}")
732
+ return False
733
+
734
+ @staticmethod
735
+ def get_certificate_info(cert_path: str) -> Dict[str, Any]:
736
+ """
737
+ Get certificate information.
738
+
739
+ Args:
740
+ cert_path: Path to certificate file
741
+
742
+ Returns:
743
+ Dictionary with certificate information
744
+ """
745
+ try:
746
+ with open(cert_path, "rb") as f:
747
+ cert_data = f.read()
748
+
749
+ cert = x509.load_pem_x509_certificate(cert_data)
750
+
751
+ # Extract roles
752
+ roles = CertificateUtils.extract_roles_from_certificate_object(cert)
753
+
754
+ # Convert subject and issuer to dictionaries
755
+ subject_dict = {}
756
+ for name_attribute in cert.subject:
757
+ subject_dict[str(name_attribute.oid)] = str(name_attribute.value)
758
+
759
+ issuer_dict = {}
760
+ for name_attribute in cert.issuer:
761
+ issuer_dict[str(name_attribute.oid)] = str(name_attribute.value)
762
+
763
+ return {
764
+ "subject": subject_dict,
765
+ "issuer": issuer_dict,
766
+ "serial_number": str(cert.serial_number),
767
+ "not_valid_before": cert.not_valid_before.isoformat(),
768
+ "not_valid_after": cert.not_valid_after.isoformat(),
769
+ "roles": roles,
770
+ "is_ca": cert.extensions.get_extension_for_oid(
771
+ x509.oid.ExtensionOID.BASIC_CONSTRAINTS
772
+ ).value.ca if cert.extensions.get_extension_for_oid(
773
+ x509.oid.ExtensionOID.BASIC_CONSTRAINTS
774
+ ) else False
775
+ }
776
+
777
+ except Exception as e:
778
+ logger.error(f"Failed to get certificate info: {e}")
779
+ return {}
780
+
781
+ @staticmethod
782
+ def generate_private_key(key_type: str, key_size: int, output_path: str) -> Dict[str, Any]:
783
+ """
784
+ Generate a private key.
785
+
786
+ Args:
787
+ key_type: Type of key (RSA, ECDSA)
788
+ key_size: Key size in bits
789
+ output_path: Path to save the private key
790
+
791
+ Returns:
792
+ Dictionary with generation result
793
+ """
794
+ try:
795
+ # Validate key type
796
+ if key_type not in ["RSA", "ECDSA"]:
797
+ return {
798
+ "success": False,
799
+ "error": "Key type must be RSA or ECDSA"
800
+ }
801
+
802
+ # Validate key size
803
+ if key_type == "RSA" and key_size < 1024:
804
+ return {
805
+ "success": False,
806
+ "error": "Key size must be at least 1024 bits"
807
+ }
808
+
809
+ if key_type == "ECDSA" and key_size not in [256, 384, 521]:
810
+ return {
811
+ "success": False,
812
+ "error": "ECDSA key size must be 256, 384, or 521 bits"
813
+ }
814
+
815
+ # Create output directory if it doesn't exist
816
+ output_dir = os.path.dirname(output_path)
817
+ if output_dir:
818
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
819
+
820
+ # Generate private key
821
+ if key_type == "RSA":
822
+ private_key = rsa.generate_private_key(
823
+ public_exponent=65537,
824
+ key_size=key_size
825
+ )
826
+ else: # ECDSA
827
+ from cryptography.hazmat.primitives.asymmetric import ec
828
+ if key_size == 256:
829
+ curve = ec.SECP256R1()
830
+ elif key_size == 384:
831
+ curve = ec.SECP384R1()
832
+ else: # 521
833
+ curve = ec.SECP521R1()
834
+
835
+ private_key = ec.generate_private_key(curve)
836
+
837
+ # Save private key
838
+ with open(output_path, "wb") as f:
839
+ f.write(private_key.private_bytes(
840
+ encoding=serialization.Encoding.PEM,
841
+ format=serialization.PrivateFormat.PKCS8,
842
+ encryption_algorithm=serialization.NoEncryption()
843
+ ))
844
+
845
+ return {
846
+ "success": True,
847
+ "key_type": key_type,
848
+ "key_size": key_size,
849
+ "key_path": output_path
850
+ }
851
+
852
+ except Exception as e:
853
+ logger.error(f"Failed to generate private key: {e}")
854
+ return {
855
+ "success": False,
856
+ "error": f"Key generation failed: {str(e)}"
857
+ }
858
+
859
+ @staticmethod
860
+ def validate_private_key(key_path: str) -> Dict[str, Any]:
861
+ """
862
+ Validate a private key.
863
+
864
+ Args:
865
+ key_path: Path to private key file
866
+
867
+ Returns:
868
+ Dictionary with validation result
869
+ """
870
+ try:
871
+ if not os.path.exists(key_path):
872
+ return {
873
+ "success": False,
874
+ "error": "Key file not found"
875
+ }
876
+
877
+ with open(key_path, "rb") as f:
878
+ key_data = f.read()
879
+
880
+ # Try to load the private key
881
+ try:
882
+ private_key = serialization.load_pem_private_key(key_data, password=None)
883
+
884
+ # Get key info
885
+ if isinstance(private_key, rsa.RSAPrivateKey):
886
+ key_type = "RSA"
887
+ key_size = private_key.key_size
888
+ else:
889
+ key_type = "ECDSA"
890
+ key_size = private_key.key_size
891
+
892
+ return {
893
+ "success": True,
894
+ "key_type": key_type,
895
+ "key_size": key_size,
896
+ "created_date": datetime.now().isoformat()
897
+ }
898
+
899
+ except Exception as e:
900
+ return {
901
+ "success": False,
902
+ "error": f"Invalid private key: {str(e)}"
903
+ }
904
+
905
+ except Exception as e:
906
+ logger.error(f"Failed to validate private key: {e}")
907
+ return {
908
+ "success": False,
909
+ "error": f"Key validation failed: {str(e)}"
910
+ }
911
+
912
+ @staticmethod
913
+ def create_encrypted_backup(key_path: str, backup_path: str, password: str) -> Dict[str, Any]:
914
+ """
915
+ Create an encrypted backup of a private key.
916
+
917
+ Args:
918
+ key_path: Path to private key file
919
+ backup_path: Path to save encrypted backup
920
+ password: Password for encryption
921
+
922
+ Returns:
923
+ Dictionary with backup result
924
+ """
925
+ try:
926
+ if not os.path.exists(key_path):
927
+ return {
928
+ "success": False,
929
+ "error": "Key file not found"
930
+ }
931
+
932
+ # Read the private key
933
+ with open(key_path, "rb") as f:
934
+ key_data = f.read()
935
+
936
+ # Load the private key
937
+ private_key = serialization.load_pem_private_key(key_data, password=None)
938
+
939
+ # Create encrypted backup
940
+ encrypted_key = private_key.private_bytes(
941
+ encoding=serialization.Encoding.PEM,
942
+ format=serialization.PrivateFormat.PKCS8,
943
+ encryption_algorithm=serialization.BestAvailableEncryption(password.encode())
944
+ )
945
+
946
+ # Save encrypted backup
947
+ with open(backup_path, "wb") as f:
948
+ f.write(encrypted_key)
949
+
950
+ return {
951
+ "success": True,
952
+ "backup_path": backup_path
953
+ }
954
+
955
+ except Exception as e:
956
+ logger.error(f"Failed to create encrypted backup: {e}")
957
+ return {
958
+ "success": False,
959
+ "error": f"Encryption failed: {str(e)}"
960
+ }
961
+
962
+ @staticmethod
963
+ def restore_encrypted_backup(backup_path: str, key_path: str, password: str) -> Dict[str, Any]:
964
+ """
965
+ Restore a private key from encrypted backup.
966
+
967
+ Args:
968
+ backup_path: Path to encrypted backup file
969
+ key_path: Path to save restored key
970
+ password: Password for decryption
971
+
972
+ Returns:
973
+ Dictionary with restore result
974
+ """
975
+ try:
976
+ if not os.path.exists(backup_path):
977
+ return {
978
+ "success": False,
979
+ "error": "Backup file not found"
980
+ }
981
+
982
+ # Read the encrypted backup
983
+ with open(backup_path, "rb") as f:
984
+ encrypted_data = f.read()
985
+
986
+ # Load the encrypted private key
987
+ private_key = serialization.load_pem_private_key(
988
+ encrypted_data,
989
+ password=password.encode()
990
+ )
991
+
992
+ # Save the decrypted key
993
+ with open(key_path, "wb") as f:
994
+ f.write(private_key.private_bytes(
995
+ encoding=serialization.Encoding.PEM,
996
+ format=serialization.PrivateFormat.PKCS8,
997
+ encryption_algorithm=serialization.NoEncryption()
998
+ ))
999
+
1000
+ return {
1001
+ "success": True,
1002
+ "key_path": key_path
1003
+ }
1004
+
1005
+ except Exception as e:
1006
+ logger.error(f"Failed to restore encrypted backup: {e}")
1007
+ return {
1008
+ "success": False,
1009
+ "error": f"Decryption failed: {str(e)}"
1010
+ }
1011
+
1012
+ @staticmethod
1013
+ def create_ssl_context(cert_file: str, key_file: str, ca_file: Optional[str] = None) -> Any:
1014
+ """
1015
+ Create SSL context for server or client.
1016
+
1017
+ Args:
1018
+ cert_file: Path to certificate file
1019
+ key_file: Path to private key file
1020
+ ca_file: Path to CA certificate file (optional)
1021
+
1022
+ Returns:
1023
+ SSL context object
1024
+ """
1025
+ try:
1026
+ import ssl
1027
+
1028
+ # Create SSL context
1029
+ context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
1030
+ context.check_hostname = False
1031
+ context.verify_mode = ssl.CERT_NONE
1032
+
1033
+ # Load certificate and key
1034
+ context.load_cert_chain(cert_file, key_file)
1035
+
1036
+ # Load CA certificate if provided
1037
+ if ca_file and os.path.exists(ca_file):
1038
+ context.load_verify_locations(ca_file)
1039
+ context.verify_mode = ssl.CERT_REQUIRED
1040
+
1041
+ return context
1042
+
1043
+ except Exception as e:
1044
+ logger.error(f"Failed to create SSL context: {e}")
1045
+ raise