mcp-proxy-adapter 4.1.1__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.
- mcp_proxy_adapter/__main__.py +12 -0
- mcp_proxy_adapter/api/app.py +138 -11
- mcp_proxy_adapter/api/handlers.py +16 -1
- mcp_proxy_adapter/api/middleware/__init__.py +30 -29
- mcp_proxy_adapter/api/middleware/auth_adapter.py +235 -0
- mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
- mcp_proxy_adapter/api/middleware/factory.py +219 -0
- mcp_proxy_adapter/api/middleware/logging.py +32 -6
- mcp_proxy_adapter/api/middleware/mtls_adapter.py +305 -0
- mcp_proxy_adapter/api/middleware/mtls_middleware.py +296 -0
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +135 -0
- mcp_proxy_adapter/api/middleware/rate_limit_adapter.py +241 -0
- mcp_proxy_adapter/api/middleware/roles_adapter.py +365 -0
- mcp_proxy_adapter/api/middleware/roles_middleware.py +381 -0
- mcp_proxy_adapter/api/middleware/security.py +376 -0
- mcp_proxy_adapter/api/middleware/token_auth_middleware.py +261 -0
- mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
- mcp_proxy_adapter/commands/__init__.py +13 -4
- mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
- mcp_proxy_adapter/commands/base.py +61 -30
- mcp_proxy_adapter/commands/builtin_commands.py +89 -0
- mcp_proxy_adapter/commands/catalog_manager.py +838 -0
- mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
- mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
- mcp_proxy_adapter/commands/command_registry.py +703 -354
- mcp_proxy_adapter/commands/dependency_manager.py +245 -0
- mcp_proxy_adapter/commands/health_command.py +7 -0
- mcp_proxy_adapter/commands/hooks.py +200 -167
- mcp_proxy_adapter/commands/key_management_command.py +506 -0
- mcp_proxy_adapter/commands/load_command.py +176 -0
- mcp_proxy_adapter/commands/plugins_command.py +235 -0
- mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
- mcp_proxy_adapter/commands/proxy_registration_command.py +268 -0
- mcp_proxy_adapter/commands/reload_command.py +48 -50
- mcp_proxy_adapter/commands/result.py +1 -0
- mcp_proxy_adapter/commands/roles_management_command.py +697 -0
- mcp_proxy_adapter/commands/ssl_setup_command.py +483 -0
- mcp_proxy_adapter/commands/token_management_command.py +529 -0
- mcp_proxy_adapter/commands/transport_management_command.py +144 -0
- mcp_proxy_adapter/commands/unload_command.py +158 -0
- mcp_proxy_adapter/config.py +99 -2
- mcp_proxy_adapter/core/auth_validator.py +606 -0
- mcp_proxy_adapter/core/certificate_utils.py +827 -0
- mcp_proxy_adapter/core/config_converter.py +405 -0
- mcp_proxy_adapter/core/config_validator.py +218 -0
- mcp_proxy_adapter/core/logging.py +11 -0
- mcp_proxy_adapter/core/protocol_manager.py +226 -0
- mcp_proxy_adapter/core/proxy_registration.py +270 -0
- mcp_proxy_adapter/core/role_utils.py +426 -0
- mcp_proxy_adapter/core/security_adapter.py +373 -0
- mcp_proxy_adapter/core/security_factory.py +239 -0
- mcp_proxy_adapter/core/settings.py +1 -0
- mcp_proxy_adapter/core/ssl_utils.py +233 -0
- mcp_proxy_adapter/core/transport_manager.py +292 -0
- mcp_proxy_adapter/custom_openapi.py +22 -11
- mcp_proxy_adapter/examples/basic_server/config.json +58 -23
- mcp_proxy_adapter/examples/basic_server/config_all_protocols.json +54 -0
- mcp_proxy_adapter/examples/basic_server/config_http.json +70 -0
- mcp_proxy_adapter/examples/basic_server/config_http_only.json +52 -0
- mcp_proxy_adapter/examples/basic_server/config_https.json +58 -0
- mcp_proxy_adapter/examples/basic_server/config_mtls.json +58 -0
- mcp_proxy_adapter/examples/basic_server/config_ssl.json +46 -0
- mcp_proxy_adapter/examples/basic_server/server.py +12 -1
- mcp_proxy_adapter/examples/custom_commands/__init__.py +1 -1
- mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +339 -23
- mcp_proxy_adapter/examples/custom_commands/auto_commands/test_command.py +105 -0
- mcp_proxy_adapter/examples/custom_commands/catalog/commands/test_command.py +129 -0
- mcp_proxy_adapter/examples/custom_commands/config.json +101 -18
- mcp_proxy_adapter/examples/custom_commands/config_all_protocols.json +46 -0
- mcp_proxy_adapter/examples/custom_commands/config_https_only.json +46 -0
- mcp_proxy_adapter/examples/custom_commands/config_https_transport.json +33 -0
- mcp_proxy_adapter/examples/custom_commands/config_mtls_only.json +46 -0
- mcp_proxy_adapter/examples/custom_commands/config_mtls_transport.json +33 -0
- mcp_proxy_adapter/examples/custom_commands/config_single_transport.json +33 -0
- mcp_proxy_adapter/examples/custom_commands/full_help_response.json +1 -0
- mcp_proxy_adapter/examples/custom_commands/generated_openapi.json +629 -0
- mcp_proxy_adapter/examples/custom_commands/get_openapi.py +103 -0
- mcp_proxy_adapter/examples/custom_commands/loadable_commands/test_ignored.py +129 -0
- mcp_proxy_adapter/examples/custom_commands/proxy_connection_manager.py +278 -0
- mcp_proxy_adapter/examples/custom_commands/server.py +92 -68
- mcp_proxy_adapter/examples/custom_commands/simple_openapi_server.py +75 -0
- mcp_proxy_adapter/examples/custom_commands/start_server_with_proxy_manager.py +299 -0
- mcp_proxy_adapter/examples/custom_commands/start_server_with_registration.py +278 -0
- mcp_proxy_adapter/examples/custom_commands/test_openapi.py +27 -0
- mcp_proxy_adapter/examples/custom_commands/test_registry.py +23 -0
- mcp_proxy_adapter/examples/custom_commands/test_simple.py +19 -0
- mcp_proxy_adapter/examples/custom_project_example/README.md +103 -0
- mcp_proxy_adapter/examples/custom_project_example/README_EN.md +103 -0
- mcp_proxy_adapter/examples/simple_custom_commands/README.md +149 -0
- mcp_proxy_adapter/examples/simple_custom_commands/README_EN.md +149 -0
- mcp_proxy_adapter/main.py +175 -0
- mcp_proxy_adapter/schemas/roles_schema.json +162 -0
- mcp_proxy_adapter/tests/unit/test_config.py +53 -0
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/METADATA +2 -1
- mcp_proxy_adapter-6.0.0.dist-info/RECORD +179 -0
- mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
- mcp_proxy_adapter-4.1.1.dist-info/RECORD +0 -110
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_proxy_adapter-4.1.1.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
|