mcp-proxy-adapter 4.1.0__py3-none-any.whl → 6.0.0__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 (101) hide show
  1. mcp_proxy_adapter/__main__.py +12 -0
  2. mcp_proxy_adapter/api/app.py +138 -11
  3. mcp_proxy_adapter/api/handlers.py +16 -1
  4. mcp_proxy_adapter/api/middleware/__init__.py +30 -29
  5. mcp_proxy_adapter/api/middleware/auth_adapter.py +235 -0
  6. mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
  7. mcp_proxy_adapter/api/middleware/factory.py +219 -0
  8. mcp_proxy_adapter/api/middleware/logging.py +32 -6
  9. mcp_proxy_adapter/api/middleware/mtls_adapter.py +305 -0
  10. mcp_proxy_adapter/api/middleware/mtls_middleware.py +296 -0
  11. mcp_proxy_adapter/api/middleware/protocol_middleware.py +135 -0
  12. mcp_proxy_adapter/api/middleware/rate_limit_adapter.py +241 -0
  13. mcp_proxy_adapter/api/middleware/roles_adapter.py +365 -0
  14. mcp_proxy_adapter/api/middleware/roles_middleware.py +381 -0
  15. mcp_proxy_adapter/api/middleware/security.py +376 -0
  16. mcp_proxy_adapter/api/middleware/token_auth_middleware.py +261 -0
  17. mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
  18. mcp_proxy_adapter/commands/__init__.py +13 -4
  19. mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
  20. mcp_proxy_adapter/commands/base.py +61 -30
  21. mcp_proxy_adapter/commands/builtin_commands.py +89 -0
  22. mcp_proxy_adapter/commands/catalog_manager.py +838 -0
  23. mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
  24. mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
  25. mcp_proxy_adapter/commands/command_registry.py +705 -345
  26. mcp_proxy_adapter/commands/dependency_manager.py +245 -0
  27. mcp_proxy_adapter/commands/health_command.py +7 -0
  28. mcp_proxy_adapter/commands/hooks.py +200 -167
  29. mcp_proxy_adapter/commands/key_management_command.py +506 -0
  30. mcp_proxy_adapter/commands/load_command.py +176 -0
  31. mcp_proxy_adapter/commands/plugins_command.py +235 -0
  32. mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
  33. mcp_proxy_adapter/commands/proxy_registration_command.py +268 -0
  34. mcp_proxy_adapter/commands/reload_command.py +48 -50
  35. mcp_proxy_adapter/commands/result.py +1 -0
  36. mcp_proxy_adapter/commands/roles_management_command.py +697 -0
  37. mcp_proxy_adapter/commands/ssl_setup_command.py +483 -0
  38. mcp_proxy_adapter/commands/token_management_command.py +529 -0
  39. mcp_proxy_adapter/commands/transport_management_command.py +144 -0
  40. mcp_proxy_adapter/commands/unload_command.py +158 -0
  41. mcp_proxy_adapter/config.py +99 -2
  42. mcp_proxy_adapter/core/auth_validator.py +606 -0
  43. mcp_proxy_adapter/core/certificate_utils.py +827 -0
  44. mcp_proxy_adapter/core/config_converter.py +405 -0
  45. mcp_proxy_adapter/core/config_validator.py +218 -0
  46. mcp_proxy_adapter/core/logging.py +11 -0
  47. mcp_proxy_adapter/core/protocol_manager.py +226 -0
  48. mcp_proxy_adapter/core/proxy_registration.py +270 -0
  49. mcp_proxy_adapter/core/role_utils.py +426 -0
  50. mcp_proxy_adapter/core/security_adapter.py +373 -0
  51. mcp_proxy_adapter/core/security_factory.py +239 -0
  52. mcp_proxy_adapter/core/settings.py +1 -0
  53. mcp_proxy_adapter/core/ssl_utils.py +233 -0
  54. mcp_proxy_adapter/core/transport_manager.py +292 -0
  55. mcp_proxy_adapter/custom_openapi.py +22 -11
  56. mcp_proxy_adapter/examples/basic_server/config.json +58 -23
  57. mcp_proxy_adapter/examples/basic_server/config_all_protocols.json +54 -0
  58. mcp_proxy_adapter/examples/basic_server/config_http.json +70 -0
  59. mcp_proxy_adapter/examples/basic_server/config_http_only.json +52 -0
  60. mcp_proxy_adapter/examples/basic_server/config_https.json +58 -0
  61. mcp_proxy_adapter/examples/basic_server/config_mtls.json +58 -0
  62. mcp_proxy_adapter/examples/basic_server/config_ssl.json +46 -0
  63. mcp_proxy_adapter/examples/basic_server/server.py +17 -1
  64. mcp_proxy_adapter/examples/custom_commands/__init__.py +1 -1
  65. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +339 -23
  66. mcp_proxy_adapter/examples/custom_commands/auto_commands/test_command.py +105 -0
  67. mcp_proxy_adapter/examples/custom_commands/catalog/commands/test_command.py +129 -0
  68. mcp_proxy_adapter/examples/custom_commands/config.json +97 -41
  69. mcp_proxy_adapter/examples/custom_commands/config_all_protocols.json +46 -0
  70. mcp_proxy_adapter/examples/custom_commands/config_https_only.json +46 -0
  71. mcp_proxy_adapter/examples/custom_commands/config_https_transport.json +33 -0
  72. mcp_proxy_adapter/examples/custom_commands/config_mtls_only.json +46 -0
  73. mcp_proxy_adapter/examples/custom_commands/config_mtls_transport.json +33 -0
  74. mcp_proxy_adapter/examples/custom_commands/config_single_transport.json +33 -0
  75. mcp_proxy_adapter/examples/custom_commands/full_help_response.json +1 -0
  76. mcp_proxy_adapter/examples/custom_commands/generated_openapi.json +629 -0
  77. mcp_proxy_adapter/examples/custom_commands/get_openapi.py +103 -0
  78. mcp_proxy_adapter/examples/custom_commands/loadable_commands/test_ignored.py +129 -0
  79. mcp_proxy_adapter/examples/custom_commands/proxy_connection_manager.py +278 -0
  80. mcp_proxy_adapter/examples/custom_commands/server.py +92 -63
  81. mcp_proxy_adapter/examples/custom_commands/simple_openapi_server.py +75 -0
  82. mcp_proxy_adapter/examples/custom_commands/start_server_with_proxy_manager.py +299 -0
  83. mcp_proxy_adapter/examples/custom_commands/start_server_with_registration.py +278 -0
  84. mcp_proxy_adapter/examples/custom_commands/test_openapi.py +27 -0
  85. mcp_proxy_adapter/examples/custom_commands/test_registry.py +23 -0
  86. mcp_proxy_adapter/examples/custom_commands/test_simple.py +19 -0
  87. mcp_proxy_adapter/examples/custom_project_example/README.md +103 -0
  88. mcp_proxy_adapter/examples/custom_project_example/README_EN.md +103 -0
  89. mcp_proxy_adapter/examples/simple_custom_commands/README.md +149 -0
  90. mcp_proxy_adapter/examples/simple_custom_commands/README_EN.md +149 -0
  91. mcp_proxy_adapter/main.py +175 -0
  92. mcp_proxy_adapter/schemas/roles_schema.json +162 -0
  93. mcp_proxy_adapter/tests/unit/test_config.py +53 -0
  94. mcp_proxy_adapter/version.py +1 -1
  95. {mcp_proxy_adapter-4.1.0.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/METADATA +2 -1
  96. mcp_proxy_adapter-6.0.0.dist-info/RECORD +179 -0
  97. mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
  98. mcp_proxy_adapter-4.1.0.dist-info/RECORD +0 -110
  99. {mcp_proxy_adapter-4.1.0.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/WHEEL +0 -0
  100. {mcp_proxy_adapter-4.1.0.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/licenses/LICENSE +0 -0
  101. {mcp_proxy_adapter-4.1.0.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,827 @@
1
+ """
2
+ Certificate Utilities
3
+
4
+ This module provides utilities for working with certificates including creation,
5
+ validation, and role extraction. Integrates with AuthValidator and RoleUtils
6
+ from previous phases.
7
+
8
+ Author: MCP Proxy Adapter Team
9
+ Version: 1.0.0
10
+ """
11
+
12
+ import logging
13
+ import os
14
+ import ipaddress
15
+ from datetime import datetime, timedelta, timezone
16
+ from typing import Dict, List, Optional, Any
17
+ from pathlib import Path
18
+
19
+ from cryptography import x509
20
+ from cryptography.hazmat.primitives import hashes, serialization
21
+ from cryptography.hazmat.primitives.asymmetric import rsa
22
+ from cryptography.x509.oid import NameOID
23
+
24
+ from .auth_validator import AuthValidator
25
+ from .role_utils import RoleUtils
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class CertificateUtils:
31
+ """
32
+ Utilities for working with certificates.
33
+
34
+ Provides methods for creating CA, server, and client certificates,
35
+ as well as validation and role extraction.
36
+ """
37
+
38
+ # Default certificate validity period (1 year)
39
+ DEFAULT_VALIDITY_DAYS = 365
40
+
41
+ # Default key size
42
+ DEFAULT_KEY_SIZE = 2048
43
+
44
+ # Custom OID for roles (same as in RoleUtils)
45
+ ROLE_EXTENSION_OID = "1.3.6.1.4.1.99999.1"
46
+
47
+ @staticmethod
48
+ def create_ca_certificate(common_name: str, output_dir: str,
49
+ validity_days: int = DEFAULT_VALIDITY_DAYS,
50
+ key_size: int = DEFAULT_KEY_SIZE) -> Dict[str, str]:
51
+ """
52
+ Create a CA certificate and private key.
53
+
54
+ Args:
55
+ common_name: Common name for the CA certificate
56
+ output_dir: Directory to save certificate and key files
57
+ validity_days: Certificate validity period in days
58
+ key_size: RSA key size in bits
59
+
60
+ Returns:
61
+ Dictionary with paths to created files
62
+
63
+ Raises:
64
+ ValueError: If parameters are invalid
65
+ OSError: If files cannot be created
66
+ """
67
+ try:
68
+ # Validate parameters
69
+ if not common_name or not common_name.strip():
70
+ raise ValueError("Common name cannot be empty")
71
+
72
+ if validity_days <= 0:
73
+ raise ValueError("Validity days must be positive")
74
+
75
+ if key_size < 1024:
76
+ raise ValueError("Key size must be at least 1024 bits")
77
+
78
+ # Create output directory if it doesn't exist
79
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
80
+
81
+ # Generate private key
82
+ private_key = rsa.generate_private_key(
83
+ public_exponent=65537,
84
+ key_size=key_size
85
+ )
86
+
87
+ # Create certificate subject
88
+ subject = issuer = x509.Name([
89
+ x509.NameAttribute(NameOID.COMMON_NAME, common_name),
90
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, "MCP Proxy Adapter CA"),
91
+ x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "Certificate Authority"),
92
+ x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
93
+ ])
94
+
95
+ # Create certificate
96
+ cert = x509.CertificateBuilder().subject_name(
97
+ subject
98
+ ).issuer_name(
99
+ issuer
100
+ ).public_key(
101
+ private_key.public_key()
102
+ ).serial_number(
103
+ x509.random_serial_number()
104
+ ).not_valid_before(
105
+ datetime.now(timezone.utc)
106
+ ).not_valid_after(
107
+ datetime.now(timezone.utc) + timedelta(days=validity_days)
108
+ ).add_extension(
109
+ x509.BasicConstraints(ca=True, path_length=None),
110
+ critical=True
111
+ ).add_extension(
112
+ x509.KeyUsage(
113
+ key_cert_sign=True,
114
+ crl_sign=True,
115
+ digital_signature=True,
116
+ key_encipherment=False,
117
+ data_encipherment=False,
118
+ key_agreement=False,
119
+ encipher_only=False,
120
+ decipher_only=False,
121
+ content_commitment=False
122
+ ),
123
+ critical=True
124
+ ).add_extension(
125
+ x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
126
+ critical=False
127
+ ).add_extension(
128
+ x509.AuthorityKeyIdentifier.from_issuer_public_key(private_key.public_key()),
129
+ critical=False
130
+ ).sign(private_key, hashes.SHA256())
131
+
132
+ # Save certificate and key
133
+ cert_path = os.path.join(output_dir, "ca.crt")
134
+ key_path = os.path.join(output_dir, "ca.key")
135
+
136
+ with open(cert_path, "wb") as f:
137
+ f.write(cert.public_bytes(serialization.Encoding.PEM))
138
+
139
+ with open(key_path, "wb") as f:
140
+ f.write(private_key.private_bytes(
141
+ encoding=serialization.Encoding.PEM,
142
+ format=serialization.PrivateFormat.PKCS8,
143
+ encryption_algorithm=serialization.NoEncryption()
144
+ ))
145
+
146
+ logger.info(f"CA certificate created: {cert_path}")
147
+
148
+ return {
149
+ "cert_path": cert_path,
150
+ "key_path": key_path,
151
+ "common_name": common_name,
152
+ "validity_days": validity_days
153
+ }
154
+
155
+ except Exception as e:
156
+ logger.error(f"Failed to create CA certificate: {e}")
157
+ raise
158
+
159
+ @staticmethod
160
+ def create_server_certificate(common_name: str, roles: List[str],
161
+ ca_cert_path: str, ca_key_path: str,
162
+ output_dir: str,
163
+ validity_days: int = DEFAULT_VALIDITY_DAYS,
164
+ key_size: int = DEFAULT_KEY_SIZE) -> Dict[str, str]:
165
+ """
166
+ Create a server certificate signed by CA.
167
+
168
+ Args:
169
+ common_name: Common name for the server certificate
170
+ roles: List of roles to include in certificate
171
+ ca_cert_path: Path to CA certificate
172
+ ca_key_path: Path to CA private key
173
+ output_dir: Directory to save certificate and key files
174
+ validity_days: Certificate validity period in days
175
+ key_size: RSA key size in bits
176
+
177
+ Returns:
178
+ Dictionary with paths to created files
179
+
180
+ Raises:
181
+ ValueError: If parameters are invalid
182
+ FileNotFoundError: If CA files not found
183
+ OSError: If files cannot be created
184
+ """
185
+ try:
186
+ # Validate parameters
187
+ if not common_name or not common_name.strip():
188
+ raise ValueError("Common name cannot be empty")
189
+
190
+ if not roles:
191
+ roles = ["server"]
192
+
193
+ if not Path(ca_cert_path).exists():
194
+ raise FileNotFoundError(f"CA certificate not found: {ca_cert_path}")
195
+
196
+ if not Path(ca_key_path).exists():
197
+ raise FileNotFoundError(f"CA key not found: {ca_key_path}")
198
+
199
+ # Create output directory if it doesn't exist
200
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
201
+
202
+ # Load CA certificate and key
203
+ with open(ca_cert_path, "rb") as f:
204
+ ca_cert = x509.load_pem_x509_certificate(f.read())
205
+
206
+ with open(ca_key_path, "rb") as f:
207
+ ca_key = serialization.load_pem_private_key(f.read(), password=None)
208
+
209
+ # Generate server private key
210
+ private_key = rsa.generate_private_key(
211
+ public_exponent=65537,
212
+ key_size=key_size
213
+ )
214
+
215
+ # Create certificate subject
216
+ subject = x509.Name([
217
+ x509.NameAttribute(NameOID.COMMON_NAME, common_name),
218
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, "MCP Proxy Adapter"),
219
+ x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "Server"),
220
+ x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
221
+ ])
222
+
223
+ # Create certificate
224
+ cert_builder = x509.CertificateBuilder().subject_name(
225
+ subject
226
+ ).issuer_name(
227
+ ca_cert.subject
228
+ ).public_key(
229
+ private_key.public_key()
230
+ ).serial_number(
231
+ x509.random_serial_number()
232
+ ).not_valid_before(
233
+ datetime.now(timezone.utc)
234
+ ).not_valid_after(
235
+ datetime.now(timezone.utc) + timedelta(days=validity_days)
236
+ ).add_extension(
237
+ x509.BasicConstraints(ca=False, path_length=None),
238
+ critical=True
239
+ ).add_extension(
240
+ x509.KeyUsage(
241
+ key_cert_sign=False,
242
+ crl_sign=False,
243
+ digital_signature=True,
244
+ key_encipherment=True,
245
+ data_encipherment=False,
246
+ key_agreement=False,
247
+ encipher_only=False,
248
+ decipher_only=False,
249
+ content_commitment=False
250
+ ),
251
+ critical=True
252
+ ).add_extension(
253
+ x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
254
+ critical=False
255
+ ).add_extension(
256
+ x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()),
257
+ critical=False
258
+ ).add_extension(
259
+ x509.SubjectAlternativeName([
260
+ x509.DNSName(common_name),
261
+ x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")),
262
+ x509.IPAddress(ipaddress.IPv6Address("::1"))
263
+ ]),
264
+ critical=False
265
+ )
266
+
267
+ # Add roles extension
268
+ if roles:
269
+ roles_data = ",".join(roles).encode('utf-8')
270
+ roles_oid = x509.ObjectIdentifier(CertificateUtils.ROLE_EXTENSION_OID)
271
+ cert_builder = cert_builder.add_extension(
272
+ x509.UnrecognizedExtension(roles_oid, roles_data),
273
+ critical=False
274
+ )
275
+
276
+ cert = cert_builder.sign(ca_key, hashes.SHA256())
277
+
278
+ # Save certificate and key
279
+ cert_path = os.path.join(output_dir, "server.crt")
280
+ key_path = os.path.join(output_dir, "server.key")
281
+
282
+ with open(cert_path, "wb") as f:
283
+ f.write(cert.public_bytes(serialization.Encoding.PEM))
284
+
285
+ with open(key_path, "wb") as f:
286
+ f.write(private_key.private_bytes(
287
+ encoding=serialization.Encoding.PEM,
288
+ format=serialization.PrivateFormat.PKCS8,
289
+ encryption_algorithm=serialization.NoEncryption()
290
+ ))
291
+
292
+ logger.info(f"Server certificate created: {cert_path}")
293
+
294
+ return {
295
+ "cert_path": cert_path,
296
+ "key_path": key_path,
297
+ "common_name": common_name,
298
+ "roles": roles,
299
+ "validity_days": validity_days
300
+ }
301
+
302
+ except Exception as e:
303
+ logger.error(f"Failed to create server certificate: {e}")
304
+ raise
305
+
306
+ @staticmethod
307
+ def create_client_certificate(common_name: str, roles: List[str],
308
+ ca_cert_path: str, ca_key_path: str,
309
+ output_dir: str,
310
+ validity_days: int = DEFAULT_VALIDITY_DAYS,
311
+ key_size: int = DEFAULT_KEY_SIZE) -> Dict[str, str]:
312
+ """
313
+ Create a client certificate signed by CA.
314
+
315
+ Args:
316
+ common_name: Common name for the client certificate
317
+ roles: List of roles to include in certificate
318
+ ca_cert_path: Path to CA certificate
319
+ ca_key_path: Path to CA private key
320
+ output_dir: Directory to save certificate and key files
321
+ validity_days: Certificate validity period in days
322
+ key_size: RSA key size in bits
323
+
324
+ Returns:
325
+ Dictionary with paths to created files
326
+
327
+ Raises:
328
+ ValueError: If parameters are invalid
329
+ FileNotFoundError: If CA files not found
330
+ OSError: If files cannot be created
331
+ """
332
+ try:
333
+ # Validate parameters
334
+ if not common_name or not common_name.strip():
335
+ raise ValueError("Common name cannot be empty")
336
+
337
+ if not roles:
338
+ roles = ["client"]
339
+
340
+ if not Path(ca_cert_path).exists():
341
+ raise FileNotFoundError(f"CA certificate not found: {ca_cert_path}")
342
+
343
+ if not Path(ca_key_path).exists():
344
+ raise FileNotFoundError(f"CA key not found: {ca_key_path}")
345
+
346
+ # Create output directory if it doesn't exist
347
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
348
+
349
+ # Load CA certificate and key
350
+ with open(ca_cert_path, "rb") as f:
351
+ ca_cert = x509.load_pem_x509_certificate(f.read())
352
+
353
+ with open(ca_key_path, "rb") as f:
354
+ ca_key = serialization.load_pem_private_key(f.read(), password=None)
355
+
356
+ # Generate client private key
357
+ private_key = rsa.generate_private_key(
358
+ public_exponent=65537,
359
+ key_size=key_size
360
+ )
361
+
362
+ # Create certificate subject
363
+ subject = x509.Name([
364
+ x509.NameAttribute(NameOID.COMMON_NAME, common_name),
365
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, "MCP Proxy Adapter"),
366
+ x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "Client"),
367
+ x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
368
+ ])
369
+
370
+ # Create certificate
371
+ cert_builder = x509.CertificateBuilder().subject_name(
372
+ subject
373
+ ).issuer_name(
374
+ ca_cert.subject
375
+ ).public_key(
376
+ private_key.public_key()
377
+ ).serial_number(
378
+ x509.random_serial_number()
379
+ ).not_valid_before(
380
+ datetime.now(timezone.utc)
381
+ ).not_valid_after(
382
+ datetime.now(timezone.utc) + timedelta(days=validity_days)
383
+ ).add_extension(
384
+ x509.BasicConstraints(ca=False, path_length=None),
385
+ critical=True
386
+ ).add_extension(
387
+ x509.KeyUsage(
388
+ key_cert_sign=False,
389
+ crl_sign=False,
390
+ digital_signature=True,
391
+ key_encipherment=True,
392
+ data_encipherment=False,
393
+ key_agreement=False,
394
+ encipher_only=False,
395
+ decipher_only=False,
396
+ content_commitment=False
397
+ ),
398
+ critical=True
399
+ ).add_extension(
400
+ x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
401
+ critical=False
402
+ ).add_extension(
403
+ x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()),
404
+ critical=False
405
+ ).add_extension(
406
+ x509.ExtendedKeyUsage([
407
+ x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH
408
+ ]),
409
+ critical=False
410
+ )
411
+
412
+ # Add roles extension
413
+ if roles:
414
+ roles_data = ",".join(roles).encode('utf-8')
415
+ roles_oid = x509.ObjectIdentifier(CertificateUtils.ROLE_EXTENSION_OID)
416
+ cert_builder = cert_builder.add_extension(
417
+ x509.UnrecognizedExtension(roles_oid, roles_data),
418
+ critical=False
419
+ )
420
+
421
+ cert = cert_builder.sign(ca_key, hashes.SHA256())
422
+
423
+ # Save certificate and key
424
+ cert_path = os.path.join(output_dir, f"{common_name}.crt")
425
+ key_path = os.path.join(output_dir, f"{common_name}.key")
426
+
427
+ with open(cert_path, "wb") as f:
428
+ f.write(cert.public_bytes(serialization.Encoding.PEM))
429
+
430
+ with open(key_path, "wb") as f:
431
+ f.write(private_key.private_bytes(
432
+ encoding=serialization.Encoding.PEM,
433
+ format=serialization.PrivateFormat.PKCS8,
434
+ encryption_algorithm=serialization.NoEncryption()
435
+ ))
436
+
437
+ logger.info(f"Client certificate created: {cert_path}")
438
+
439
+ return {
440
+ "cert_path": cert_path,
441
+ "key_path": key_path,
442
+ "common_name": common_name,
443
+ "roles": roles,
444
+ "validity_days": validity_days
445
+ }
446
+
447
+ except Exception as e:
448
+ logger.error(f"Failed to create client certificate: {e}")
449
+ raise
450
+
451
+ @staticmethod
452
+ def extract_roles_from_certificate(cert_path: str) -> List[str]:
453
+ """
454
+ Extract roles from certificate file using RoleUtils.
455
+
456
+ Args:
457
+ cert_path: Path to certificate file
458
+
459
+ Returns:
460
+ List of roles extracted from certificate
461
+ """
462
+ return RoleUtils.extract_roles_from_certificate(cert_path)
463
+
464
+ @staticmethod
465
+ def extract_roles_from_certificate_object(cert: x509.Certificate) -> List[str]:
466
+ """
467
+ Extract roles from certificate object using RoleUtils.
468
+
469
+ Args:
470
+ cert: Certificate object
471
+
472
+ Returns:
473
+ List of roles extracted from certificate
474
+ """
475
+ return RoleUtils.extract_roles_from_certificate_object(cert)
476
+
477
+ @staticmethod
478
+ def validate_certificate_chain(cert_path: str, ca_cert_path: str) -> bool:
479
+ """
480
+ Validate certificate chain using AuthValidator.
481
+
482
+ Args:
483
+ cert_path: Path to certificate to validate
484
+ ca_cert_path: Path to CA certificate
485
+
486
+ Returns:
487
+ True if certificate chain is valid, False otherwise
488
+ """
489
+ try:
490
+ validator = AuthValidator()
491
+ result = validator.validate_certificate_chain(cert_path, ca_cert_path)
492
+ return result.is_valid
493
+ except Exception as e:
494
+ logger.error(f"Failed to validate certificate chain: {e}")
495
+ return False
496
+
497
+ @staticmethod
498
+ def validate_certificate(cert_path: str) -> bool:
499
+ """
500
+ Validate certificate using AuthValidator.
501
+
502
+ Args:
503
+ cert_path: Path to certificate to validate
504
+
505
+ Returns:
506
+ True if certificate is valid, False otherwise
507
+ """
508
+ try:
509
+ validator = AuthValidator()
510
+ result = validator.validate_certificate(cert_path)
511
+ return result.is_valid
512
+ except Exception as e:
513
+ logger.error(f"Failed to validate certificate: {e}")
514
+ return False
515
+
516
+ @staticmethod
517
+ def get_certificate_info(cert_path: str) -> Dict[str, Any]:
518
+ """
519
+ Get certificate information.
520
+
521
+ Args:
522
+ cert_path: Path to certificate file
523
+
524
+ Returns:
525
+ Dictionary with certificate information
526
+ """
527
+ try:
528
+ with open(cert_path, "rb") as f:
529
+ cert_data = f.read()
530
+
531
+ cert = x509.load_pem_x509_certificate(cert_data)
532
+
533
+ # Extract roles
534
+ roles = CertificateUtils.extract_roles_from_certificate_object(cert)
535
+
536
+ # Convert subject and issuer to dictionaries
537
+ subject_dict = {}
538
+ for name_attribute in cert.subject:
539
+ subject_dict[str(name_attribute.oid)] = str(name_attribute.value)
540
+
541
+ issuer_dict = {}
542
+ for name_attribute in cert.issuer:
543
+ issuer_dict[str(name_attribute.oid)] = str(name_attribute.value)
544
+
545
+ return {
546
+ "subject": subject_dict,
547
+ "issuer": issuer_dict,
548
+ "serial_number": str(cert.serial_number),
549
+ "not_valid_before": cert.not_valid_before.isoformat(),
550
+ "not_valid_after": cert.not_valid_after.isoformat(),
551
+ "roles": roles,
552
+ "is_ca": cert.extensions.get_extension_for_oid(
553
+ x509.oid.ExtensionOID.BASIC_CONSTRAINTS
554
+ ).value.ca if cert.extensions.get_extension_for_oid(
555
+ x509.oid.ExtensionOID.BASIC_CONSTRAINTS
556
+ ) else False
557
+ }
558
+
559
+ except Exception as e:
560
+ logger.error(f"Failed to get certificate info: {e}")
561
+ return {}
562
+
563
+ @staticmethod
564
+ def generate_private_key(key_type: str, key_size: int, output_path: str) -> Dict[str, Any]:
565
+ """
566
+ Generate a private key.
567
+
568
+ Args:
569
+ key_type: Type of key (RSA, ECDSA)
570
+ key_size: Key size in bits
571
+ output_path: Path to save the private key
572
+
573
+ Returns:
574
+ Dictionary with generation result
575
+ """
576
+ try:
577
+ # Validate key type
578
+ if key_type not in ["RSA", "ECDSA"]:
579
+ return {
580
+ "success": False,
581
+ "error": "Key type must be RSA or ECDSA"
582
+ }
583
+
584
+ # Validate key size
585
+ if key_type == "RSA" and key_size < 1024:
586
+ return {
587
+ "success": False,
588
+ "error": "Key size must be at least 1024 bits"
589
+ }
590
+
591
+ if key_type == "ECDSA" and key_size not in [256, 384, 521]:
592
+ return {
593
+ "success": False,
594
+ "error": "ECDSA key size must be 256, 384, or 521 bits"
595
+ }
596
+
597
+ # Create output directory if it doesn't exist
598
+ output_dir = os.path.dirname(output_path)
599
+ if output_dir:
600
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
601
+
602
+ # Generate private key
603
+ if key_type == "RSA":
604
+ private_key = rsa.generate_private_key(
605
+ public_exponent=65537,
606
+ key_size=key_size
607
+ )
608
+ else: # ECDSA
609
+ from cryptography.hazmat.primitives.asymmetric import ec
610
+ if key_size == 256:
611
+ curve = ec.SECP256R1()
612
+ elif key_size == 384:
613
+ curve = ec.SECP384R1()
614
+ else: # 521
615
+ curve = ec.SECP521R1()
616
+
617
+ private_key = ec.generate_private_key(curve)
618
+
619
+ # Save private key
620
+ with open(output_path, "wb") as f:
621
+ f.write(private_key.private_bytes(
622
+ encoding=serialization.Encoding.PEM,
623
+ format=serialization.PrivateFormat.PKCS8,
624
+ encryption_algorithm=serialization.NoEncryption()
625
+ ))
626
+
627
+ return {
628
+ "success": True,
629
+ "key_type": key_type,
630
+ "key_size": key_size,
631
+ "key_path": output_path
632
+ }
633
+
634
+ except Exception as e:
635
+ logger.error(f"Failed to generate private key: {e}")
636
+ return {
637
+ "success": False,
638
+ "error": f"Key generation failed: {str(e)}"
639
+ }
640
+
641
+ @staticmethod
642
+ def validate_private_key(key_path: str) -> Dict[str, Any]:
643
+ """
644
+ Validate a private key.
645
+
646
+ Args:
647
+ key_path: Path to private key file
648
+
649
+ Returns:
650
+ Dictionary with validation result
651
+ """
652
+ try:
653
+ if not os.path.exists(key_path):
654
+ return {
655
+ "success": False,
656
+ "error": "Key file not found"
657
+ }
658
+
659
+ with open(key_path, "rb") as f:
660
+ key_data = f.read()
661
+
662
+ # Try to load the private key
663
+ try:
664
+ private_key = serialization.load_pem_private_key(key_data, password=None)
665
+
666
+ # Get key info
667
+ if isinstance(private_key, rsa.RSAPrivateKey):
668
+ key_type = "RSA"
669
+ key_size = private_key.key_size
670
+ else:
671
+ key_type = "ECDSA"
672
+ key_size = private_key.key_size
673
+
674
+ return {
675
+ "success": True,
676
+ "key_type": key_type,
677
+ "key_size": key_size,
678
+ "created_date": datetime.now().isoformat()
679
+ }
680
+
681
+ except Exception as e:
682
+ return {
683
+ "success": False,
684
+ "error": f"Invalid private key: {str(e)}"
685
+ }
686
+
687
+ except Exception as e:
688
+ logger.error(f"Failed to validate private key: {e}")
689
+ return {
690
+ "success": False,
691
+ "error": f"Key validation failed: {str(e)}"
692
+ }
693
+
694
+ @staticmethod
695
+ def create_encrypted_backup(key_path: str, backup_path: str, password: str) -> Dict[str, Any]:
696
+ """
697
+ Create an encrypted backup of a private key.
698
+
699
+ Args:
700
+ key_path: Path to private key file
701
+ backup_path: Path to save encrypted backup
702
+ password: Password for encryption
703
+
704
+ Returns:
705
+ Dictionary with backup result
706
+ """
707
+ try:
708
+ if not os.path.exists(key_path):
709
+ return {
710
+ "success": False,
711
+ "error": "Key file not found"
712
+ }
713
+
714
+ # Read the private key
715
+ with open(key_path, "rb") as f:
716
+ key_data = f.read()
717
+
718
+ # Load the private key
719
+ private_key = serialization.load_pem_private_key(key_data, password=None)
720
+
721
+ # Create encrypted backup
722
+ encrypted_key = private_key.private_bytes(
723
+ encoding=serialization.Encoding.PEM,
724
+ format=serialization.PrivateFormat.PKCS8,
725
+ encryption_algorithm=serialization.BestAvailableEncryption(password.encode())
726
+ )
727
+
728
+ # Save encrypted backup
729
+ with open(backup_path, "wb") as f:
730
+ f.write(encrypted_key)
731
+
732
+ return {
733
+ "success": True,
734
+ "backup_path": backup_path
735
+ }
736
+
737
+ except Exception as e:
738
+ logger.error(f"Failed to create encrypted backup: {e}")
739
+ return {
740
+ "success": False,
741
+ "error": f"Encryption failed: {str(e)}"
742
+ }
743
+
744
+ @staticmethod
745
+ def restore_encrypted_backup(backup_path: str, key_path: str, password: str) -> Dict[str, Any]:
746
+ """
747
+ Restore a private key from encrypted backup.
748
+
749
+ Args:
750
+ backup_path: Path to encrypted backup file
751
+ key_path: Path to save restored key
752
+ password: Password for decryption
753
+
754
+ Returns:
755
+ Dictionary with restore result
756
+ """
757
+ try:
758
+ if not os.path.exists(backup_path):
759
+ return {
760
+ "success": False,
761
+ "error": "Backup file not found"
762
+ }
763
+
764
+ # Read the encrypted backup
765
+ with open(backup_path, "rb") as f:
766
+ encrypted_data = f.read()
767
+
768
+ # Load the encrypted private key
769
+ private_key = serialization.load_pem_private_key(
770
+ encrypted_data,
771
+ password=password.encode()
772
+ )
773
+
774
+ # Save the decrypted key
775
+ with open(key_path, "wb") as f:
776
+ f.write(private_key.private_bytes(
777
+ encoding=serialization.Encoding.PEM,
778
+ format=serialization.PrivateFormat.PKCS8,
779
+ encryption_algorithm=serialization.NoEncryption()
780
+ ))
781
+
782
+ return {
783
+ "success": True,
784
+ "key_path": key_path
785
+ }
786
+
787
+ except Exception as e:
788
+ logger.error(f"Failed to restore encrypted backup: {e}")
789
+ return {
790
+ "success": False,
791
+ "error": f"Decryption failed: {str(e)}"
792
+ }
793
+
794
+ @staticmethod
795
+ def create_ssl_context(cert_file: str, key_file: str, ca_file: Optional[str] = None) -> Any:
796
+ """
797
+ Create SSL context for server or client.
798
+
799
+ Args:
800
+ cert_file: Path to certificate file
801
+ key_file: Path to private key file
802
+ ca_file: Path to CA certificate file (optional)
803
+
804
+ Returns:
805
+ SSL context object
806
+ """
807
+ try:
808
+ import ssl
809
+
810
+ # Create SSL context
811
+ context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
812
+ context.check_hostname = False
813
+ context.verify_mode = ssl.CERT_NONE
814
+
815
+ # Load certificate and key
816
+ context.load_cert_chain(cert_file, key_file)
817
+
818
+ # Load CA certificate if provided
819
+ if ca_file and os.path.exists(ca_file):
820
+ context.load_verify_locations(ca_file)
821
+ context.verify_mode = ssl.CERT_REQUIRED
822
+
823
+ return context
824
+
825
+ except Exception as e:
826
+ logger.error(f"Failed to create SSL context: {e}")
827
+ raise