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