mcp-proxy-adapter 6.3.4__py3-none-any.whl → 6.3.6__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.
- mcp_proxy_adapter/__init__.py +9 -5
- mcp_proxy_adapter/__main__.py +1 -1
- mcp_proxy_adapter/api/app.py +227 -176
- mcp_proxy_adapter/api/handlers.py +68 -60
- mcp_proxy_adapter/api/middleware/__init__.py +7 -5
- mcp_proxy_adapter/api/middleware/base.py +19 -16
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +44 -34
- mcp_proxy_adapter/api/middleware/error_handling.py +57 -67
- mcp_proxy_adapter/api/middleware/factory.py +50 -52
- mcp_proxy_adapter/api/middleware/logging.py +46 -30
- mcp_proxy_adapter/api/middleware/performance.py +19 -16
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +80 -50
- mcp_proxy_adapter/api/middleware/transport_middleware.py +26 -24
- mcp_proxy_adapter/api/middleware/unified_security.py +70 -51
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +43 -34
- mcp_proxy_adapter/api/schemas.py +69 -43
- mcp_proxy_adapter/api/tool_integration.py +83 -63
- mcp_proxy_adapter/api/tools.py +60 -50
- mcp_proxy_adapter/commands/__init__.py +15 -6
- mcp_proxy_adapter/commands/auth_validation_command.py +107 -110
- mcp_proxy_adapter/commands/base.py +108 -112
- mcp_proxy_adapter/commands/builtin_commands.py +28 -18
- mcp_proxy_adapter/commands/catalog_manager.py +394 -265
- mcp_proxy_adapter/commands/cert_monitor_command.py +222 -204
- mcp_proxy_adapter/commands/certificate_management_command.py +210 -213
- mcp_proxy_adapter/commands/command_registry.py +275 -226
- mcp_proxy_adapter/commands/config_command.py +48 -33
- mcp_proxy_adapter/commands/dependency_container.py +22 -23
- mcp_proxy_adapter/commands/dependency_manager.py +65 -56
- mcp_proxy_adapter/commands/echo_command.py +15 -15
- mcp_proxy_adapter/commands/health_command.py +31 -29
- mcp_proxy_adapter/commands/help_command.py +97 -61
- mcp_proxy_adapter/commands/hooks.py +65 -49
- mcp_proxy_adapter/commands/key_management_command.py +148 -147
- mcp_proxy_adapter/commands/load_command.py +58 -40
- mcp_proxy_adapter/commands/plugins_command.py +80 -54
- mcp_proxy_adapter/commands/protocol_management_command.py +60 -48
- mcp_proxy_adapter/commands/proxy_registration_command.py +107 -115
- mcp_proxy_adapter/commands/reload_command.py +43 -37
- mcp_proxy_adapter/commands/result.py +26 -33
- mcp_proxy_adapter/commands/role_test_command.py +26 -26
- mcp_proxy_adapter/commands/roles_management_command.py +176 -173
- mcp_proxy_adapter/commands/security_command.py +134 -122
- mcp_proxy_adapter/commands/settings_command.py +47 -56
- mcp_proxy_adapter/commands/ssl_setup_command.py +109 -129
- mcp_proxy_adapter/commands/token_management_command.py +129 -158
- mcp_proxy_adapter/commands/transport_management_command.py +41 -36
- mcp_proxy_adapter/commands/unload_command.py +42 -37
- mcp_proxy_adapter/config.py +36 -35
- mcp_proxy_adapter/core/__init__.py +19 -21
- mcp_proxy_adapter/core/app_factory.py +30 -9
- mcp_proxy_adapter/core/app_runner.py +81 -64
- mcp_proxy_adapter/core/auth_validator.py +176 -182
- mcp_proxy_adapter/core/certificate_utils.py +469 -426
- mcp_proxy_adapter/core/client.py +155 -126
- mcp_proxy_adapter/core/client_manager.py +60 -54
- mcp_proxy_adapter/core/client_security.py +120 -91
- mcp_proxy_adapter/core/config_converter.py +176 -143
- mcp_proxy_adapter/core/config_validator.py +12 -4
- mcp_proxy_adapter/core/crl_utils.py +21 -7
- mcp_proxy_adapter/core/errors.py +64 -20
- mcp_proxy_adapter/core/logging.py +34 -29
- mcp_proxy_adapter/core/mtls_asgi.py +29 -25
- mcp_proxy_adapter/core/mtls_asgi_app.py +66 -54
- mcp_proxy_adapter/core/protocol_manager.py +154 -104
- mcp_proxy_adapter/core/proxy_client.py +202 -144
- mcp_proxy_adapter/core/proxy_registration.py +7 -3
- mcp_proxy_adapter/core/role_utils.py +139 -125
- mcp_proxy_adapter/core/security_adapter.py +88 -77
- mcp_proxy_adapter/core/security_factory.py +50 -44
- mcp_proxy_adapter/core/security_integration.py +72 -24
- mcp_proxy_adapter/core/server_adapter.py +68 -64
- mcp_proxy_adapter/core/server_engine.py +71 -53
- mcp_proxy_adapter/core/settings.py +68 -58
- mcp_proxy_adapter/core/ssl_utils.py +69 -56
- mcp_proxy_adapter/core/transport_manager.py +72 -60
- mcp_proxy_adapter/core/unified_config_adapter.py +201 -150
- mcp_proxy_adapter/core/utils.py +4 -2
- mcp_proxy_adapter/custom_openapi.py +107 -99
- mcp_proxy_adapter/examples/basic_framework/main.py +9 -2
- mcp_proxy_adapter/examples/commands/__init__.py +1 -1
- mcp_proxy_adapter/examples/create_certificates_simple.py +182 -71
- mcp_proxy_adapter/examples/debug_request_state.py +38 -19
- mcp_proxy_adapter/examples/debug_role_chain.py +53 -20
- mcp_proxy_adapter/examples/demo_client.py +48 -36
- mcp_proxy_adapter/examples/examples/basic_framework/main.py +9 -2
- mcp_proxy_adapter/examples/examples/full_application/__init__.py +1 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +22 -10
- mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +24 -17
- mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +16 -3
- mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +13 -3
- mcp_proxy_adapter/examples/examples/full_application/main.py +27 -2
- mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +48 -14
- mcp_proxy_adapter/examples/full_application/__init__.py +1 -0
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +22 -10
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +24 -17
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +16 -3
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +13 -3
- mcp_proxy_adapter/examples/full_application/main.py +27 -2
- mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +48 -14
- mcp_proxy_adapter/examples/generate_all_certificates.py +198 -73
- mcp_proxy_adapter/examples/generate_certificates.py +31 -16
- mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +220 -74
- mcp_proxy_adapter/examples/generate_test_configs.py +68 -91
- mcp_proxy_adapter/examples/proxy_registration_example.py +76 -75
- mcp_proxy_adapter/examples/run_example.py +23 -5
- mcp_proxy_adapter/examples/run_full_test_suite.py +109 -71
- mcp_proxy_adapter/examples/run_proxy_server.py +22 -9
- mcp_proxy_adapter/examples/run_security_tests.py +103 -41
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +72 -36
- mcp_proxy_adapter/examples/scripts/config_generator.py +288 -187
- mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +185 -72
- mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +220 -74
- mcp_proxy_adapter/examples/security_test_client.py +196 -127
- mcp_proxy_adapter/examples/setup_test_environment.py +17 -29
- mcp_proxy_adapter/examples/test_config.py +19 -4
- mcp_proxy_adapter/examples/test_config_generator.py +23 -7
- mcp_proxy_adapter/examples/test_examples.py +84 -56
- mcp_proxy_adapter/examples/universal_client.py +119 -62
- mcp_proxy_adapter/openapi.py +108 -115
- mcp_proxy_adapter/utils/config_generator.py +429 -274
- mcp_proxy_adapter/version.py +1 -2
- {mcp_proxy_adapter-6.3.4.dist-info → mcp_proxy_adapter-6.3.6.dist-info}/METADATA +1 -1
- mcp_proxy_adapter-6.3.6.dist-info/RECORD +144 -0
- mcp_proxy_adapter-6.3.6.dist-info/top_level.txt +2 -0
- mcp_proxy_adapter_issue_package/demonstrate_issue.py +178 -0
- mcp_proxy_adapter-6.3.4.dist-info/RECORD +0 -143
- mcp_proxy_adapter-6.3.4.dist-info/top_level.txt +0 -1
- {mcp_proxy_adapter-6.3.4.dist-info → mcp_proxy_adapter-6.3.6.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.3.4.dist-info → mcp_proxy_adapter-6.3.6.dist-info}/entry_points.txt +0 -0
- {mcp_proxy_adapter-6.3.4.dist-info → mcp_proxy_adapter-6.3.6.dist-info}/licenses/LICENSE +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(
|
69
|
-
|
70
|
-
|
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(
|
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(
|
143
|
-
|
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
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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 =
|
175
|
-
|
176
|
-
|
177
|
-
issuer
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
)
|
200
|
-
|
201
|
-
)
|
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(
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
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(
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
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
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
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 =
|
292
|
-
|
293
|
-
|
294
|
-
ca_cert.subject
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
)
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
x509.
|
330
|
-
|
331
|
-
|
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(
|
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(
|
354
|
-
|
355
|
-
|
356
|
-
|
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(
|
375
|
-
|
376
|
-
|
377
|
-
|
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(
|
426
|
+
logger.warning(
|
427
|
+
"mcp_security_framework not available, using fallback method"
|
428
|
+
)
|
399
429
|
return CertificateUtils._create_client_certificate_fallback(
|
400
|
-
common_name,
|
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(
|
461
|
-
|
462
|
-
|
463
|
-
|
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
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
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 =
|
508
|
-
|
509
|
-
|
510
|
-
ca_cert.subject
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
)
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
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(
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
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(
|
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(
|
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(
|
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(
|
652
|
-
|
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(
|
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
|
696
|
-
return expiry_info[
|
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":
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
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(
|
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
|
-
|
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(
|
840
|
-
|
841
|
-
|
842
|
-
|
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
|
-
|
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(
|
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
|
-
|
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(
|
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
|
-
|
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(
|
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
|
-
|
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(
|
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
|
-
|
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(
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
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(
|
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
|