mcp-proxy-adapter 6.9.28__py3-none-any.whl → 6.9.30__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.
Potentially problematic release.
This version of mcp-proxy-adapter might be problematic. Click here for more details.
- mcp_proxy_adapter/__init__.py +10 -0
- mcp_proxy_adapter/__main__.py +8 -21
- mcp_proxy_adapter/api/app.py +10 -913
- mcp_proxy_adapter/api/core/__init__.py +18 -0
- mcp_proxy_adapter/api/core/app_factory.py +243 -0
- mcp_proxy_adapter/api/core/lifespan_manager.py +55 -0
- mcp_proxy_adapter/api/core/registration_manager.py +166 -0
- mcp_proxy_adapter/api/core/ssl_context_factory.py +88 -0
- mcp_proxy_adapter/api/handlers.py +78 -199
- mcp_proxy_adapter/api/middleware/__init__.py +1 -44
- mcp_proxy_adapter/api/middleware/base.py +0 -42
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +0 -85
- mcp_proxy_adapter/api/middleware/error_handling.py +1 -127
- mcp_proxy_adapter/api/middleware/factory.py +0 -94
- mcp_proxy_adapter/api/middleware/logging.py +0 -112
- mcp_proxy_adapter/api/middleware/performance.py +0 -35
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +2 -98
- mcp_proxy_adapter/api/middleware/transport_middleware.py +0 -37
- mcp_proxy_adapter/api/middleware/unified_security.py +10 -10
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +0 -118
- mcp_proxy_adapter/api/openapi/__init__.py +21 -0
- mcp_proxy_adapter/api/openapi/command_integration.py +105 -0
- mcp_proxy_adapter/api/openapi/openapi_generator.py +40 -0
- mcp_proxy_adapter/api/openapi/openapi_registry.py +62 -0
- mcp_proxy_adapter/api/openapi/schema_loader.py +116 -0
- mcp_proxy_adapter/api/schemas.py +0 -61
- mcp_proxy_adapter/api/tool_integration.py +0 -117
- mcp_proxy_adapter/api/tools.py +0 -46
- mcp_proxy_adapter/cli/__init__.py +12 -0
- mcp_proxy_adapter/cli/commands/__init__.py +15 -0
- mcp_proxy_adapter/cli/commands/client.py +100 -0
- mcp_proxy_adapter/cli/commands/config_generate.py +21 -0
- mcp_proxy_adapter/cli/commands/config_validate.py +36 -0
- mcp_proxy_adapter/cli/commands/generate.py +259 -0
- mcp_proxy_adapter/cli/commands/server.py +174 -0
- mcp_proxy_adapter/cli/commands/sets.py +128 -0
- mcp_proxy_adapter/cli/commands/testconfig.py +177 -0
- mcp_proxy_adapter/cli/examples/__init__.py +8 -0
- mcp_proxy_adapter/cli/examples/http_basic.py +82 -0
- mcp_proxy_adapter/cli/examples/https_token.py +96 -0
- mcp_proxy_adapter/cli/examples/mtls_roles.py +103 -0
- mcp_proxy_adapter/cli/main.py +63 -0
- mcp_proxy_adapter/cli/parser.py +324 -0
- mcp_proxy_adapter/cli/validators.py +231 -0
- mcp_proxy_adapter/client/jsonrpc_client.py +406 -0
- mcp_proxy_adapter/client/proxy.py +45 -0
- mcp_proxy_adapter/commands/__init__.py +44 -28
- mcp_proxy_adapter/commands/auth_validation_command.py +7 -344
- mcp_proxy_adapter/commands/base.py +19 -43
- mcp_proxy_adapter/commands/builtin_commands.py +0 -75
- mcp_proxy_adapter/commands/catalog/__init__.py +20 -0
- mcp_proxy_adapter/commands/catalog/catalog_loader.py +34 -0
- mcp_proxy_adapter/commands/catalog/catalog_manager.py +122 -0
- mcp_proxy_adapter/commands/catalog/catalog_syncer.py +149 -0
- mcp_proxy_adapter/commands/catalog/command_catalog.py +43 -0
- mcp_proxy_adapter/commands/catalog/dependency_manager.py +37 -0
- mcp_proxy_adapter/commands/catalog_manager.py +58 -928
- mcp_proxy_adapter/commands/cert_monitor_command.py +0 -88
- mcp_proxy_adapter/commands/certificate_management_command.py +0 -45
- mcp_proxy_adapter/commands/command_registry.py +172 -904
- mcp_proxy_adapter/commands/config_command.py +0 -28
- mcp_proxy_adapter/commands/dependency_container.py +1 -70
- mcp_proxy_adapter/commands/dependency_manager.py +0 -128
- mcp_proxy_adapter/commands/echo_command.py +0 -34
- mcp_proxy_adapter/commands/health_command.py +0 -3
- mcp_proxy_adapter/commands/help_command.py +0 -159
- mcp_proxy_adapter/commands/hooks.py +0 -137
- mcp_proxy_adapter/commands/key_management_command.py +0 -25
- mcp_proxy_adapter/commands/load_command.py +7 -78
- mcp_proxy_adapter/commands/plugins_command.py +0 -16
- mcp_proxy_adapter/commands/protocol_management_command.py +0 -28
- mcp_proxy_adapter/commands/proxy_registration_command.py +0 -88
- mcp_proxy_adapter/commands/queue_commands.py +750 -0
- mcp_proxy_adapter/commands/registration_status_command.py +0 -43
- mcp_proxy_adapter/commands/registry/__init__.py +18 -0
- mcp_proxy_adapter/commands/registry/command_info.py +103 -0
- mcp_proxy_adapter/commands/registry/command_loader.py +207 -0
- mcp_proxy_adapter/commands/registry/command_manager.py +119 -0
- mcp_proxy_adapter/commands/registry/command_registry.py +217 -0
- mcp_proxy_adapter/commands/reload_command.py +0 -80
- mcp_proxy_adapter/commands/result.py +25 -77
- mcp_proxy_adapter/commands/role_test_command.py +0 -44
- mcp_proxy_adapter/commands/roles_management_command.py +0 -199
- mcp_proxy_adapter/commands/security_command.py +0 -30
- mcp_proxy_adapter/commands/settings_command.py +0 -68
- mcp_proxy_adapter/commands/ssl_setup_command.py +0 -42
- mcp_proxy_adapter/commands/token_management_command.py +0 -1
- mcp_proxy_adapter/commands/transport_management_command.py +0 -20
- mcp_proxy_adapter/commands/unload_command.py +0 -71
- mcp_proxy_adapter/config.py +15 -626
- mcp_proxy_adapter/core/__init__.py +5 -39
- mcp_proxy_adapter/core/app_factory.py +14 -36
- mcp_proxy_adapter/core/app_runner.py +0 -27
- mcp_proxy_adapter/core/auth_validator.py +1 -93
- mcp_proxy_adapter/core/certificate/__init__.py +20 -0
- mcp_proxy_adapter/core/certificate/certificate_creator.py +371 -0
- mcp_proxy_adapter/core/certificate/certificate_extractor.py +183 -0
- mcp_proxy_adapter/core/certificate/certificate_utils.py +249 -0
- mcp_proxy_adapter/core/certificate/certificate_validator.py +110 -0
- mcp_proxy_adapter/core/certificate/ssl_context_manager.py +70 -0
- mcp_proxy_adapter/core/certificate_utils.py +64 -903
- mcp_proxy_adapter/core/client.py +10 -9
- mcp_proxy_adapter/core/client_manager.py +0 -19
- mcp_proxy_adapter/core/client_security.py +0 -2
- mcp_proxy_adapter/core/config/__init__.py +18 -0
- mcp_proxy_adapter/core/config/config.py +195 -0
- mcp_proxy_adapter/core/config/config_factory.py +22 -0
- mcp_proxy_adapter/core/config/config_loader.py +66 -0
- mcp_proxy_adapter/core/config/feature_manager.py +31 -0
- mcp_proxy_adapter/core/config/simple_config.py +112 -0
- mcp_proxy_adapter/core/config/simple_config_generator.py +50 -0
- mcp_proxy_adapter/core/config/simple_config_validator.py +96 -0
- mcp_proxy_adapter/core/config_converter.py +0 -186
- mcp_proxy_adapter/core/config_validator.py +96 -1238
- mcp_proxy_adapter/core/errors.py +7 -42
- mcp_proxy_adapter/core/job_manager.py +54 -0
- mcp_proxy_adapter/core/logging.py +2 -22
- mcp_proxy_adapter/core/mtls_asgi.py +0 -20
- mcp_proxy_adapter/core/mtls_asgi_app.py +0 -12
- mcp_proxy_adapter/core/mtls_proxy.py +0 -80
- mcp_proxy_adapter/core/mtls_server.py +3 -173
- mcp_proxy_adapter/core/protocol_manager.py +1 -191
- mcp_proxy_adapter/core/proxy/__init__.py +22 -0
- mcp_proxy_adapter/core/proxy/auth_manager.py +27 -0
- mcp_proxy_adapter/core/proxy/proxy_registration_manager.py +137 -0
- mcp_proxy_adapter/core/proxy/registration_client.py +60 -0
- mcp_proxy_adapter/core/proxy/ssl_manager.py +101 -0
- mcp_proxy_adapter/core/proxy_client.py +0 -1
- mcp_proxy_adapter/core/proxy_registration.py +36 -913
- mcp_proxy_adapter/core/role_utils.py +0 -308
- mcp_proxy_adapter/core/security_adapter.py +1 -36
- mcp_proxy_adapter/core/security_factory.py +1 -150
- mcp_proxy_adapter/core/security_integration.py +0 -33
- mcp_proxy_adapter/core/server_adapter.py +1 -40
- mcp_proxy_adapter/core/server_engine.py +2 -173
- mcp_proxy_adapter/core/settings.py +0 -127
- mcp_proxy_adapter/core/signal_handler.py +0 -65
- mcp_proxy_adapter/core/ssl_utils.py +19 -137
- mcp_proxy_adapter/core/transport_manager.py +0 -151
- mcp_proxy_adapter/core/unified_config_adapter.py +1 -193
- mcp_proxy_adapter/core/utils.py +1 -182
- mcp_proxy_adapter/core/validation/__init__.py +21 -0
- mcp_proxy_adapter/core/validation/config_validator.py +211 -0
- mcp_proxy_adapter/core/validation/file_validator.py +73 -0
- mcp_proxy_adapter/core/validation/protocol_validator.py +191 -0
- mcp_proxy_adapter/core/validation/security_validator.py +58 -0
- mcp_proxy_adapter/core/validation/validation_result.py +27 -0
- mcp_proxy_adapter/custom_openapi.py +33 -652
- mcp_proxy_adapter/examples/bugfix_certificate_config.py +0 -23
- mcp_proxy_adapter/examples/check_config.py +0 -2
- mcp_proxy_adapter/examples/client_usage_example.py +164 -0
- mcp_proxy_adapter/examples/config_builder.py +13 -2
- mcp_proxy_adapter/examples/config_cli.py +0 -1
- mcp_proxy_adapter/examples/create_test_configs.py +0 -46
- mcp_proxy_adapter/examples/debug_request_state.py +0 -1
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +0 -47
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +0 -45
- mcp_proxy_adapter/examples/full_application/commands/echo_command.py +0 -12
- mcp_proxy_adapter/examples/full_application/commands/help_command.py +0 -12
- mcp_proxy_adapter/examples/full_application/commands/list_command.py +0 -7
- mcp_proxy_adapter/examples/full_application/hooks/__init__.py +0 -2
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +0 -59
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +0 -54
- mcp_proxy_adapter/examples/full_application/main.py +186 -150
- mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +0 -107
- mcp_proxy_adapter/examples/full_application/test_minimal_server.py +0 -24
- mcp_proxy_adapter/examples/full_application/test_server.py +0 -58
- mcp_proxy_adapter/examples/generate_config.py +65 -11
- mcp_proxy_adapter/examples/queue_demo_simple.py +632 -0
- mcp_proxy_adapter/examples/queue_integration_example.py +578 -0
- mcp_proxy_adapter/examples/queue_server_demo.py +82 -0
- mcp_proxy_adapter/examples/queue_server_example.py +85 -0
- mcp_proxy_adapter/examples/queue_server_simple.py +173 -0
- mcp_proxy_adapter/examples/required_certificates.py +0 -2
- mcp_proxy_adapter/examples/run_full_test_suite.py +0 -29
- mcp_proxy_adapter/examples/run_proxy_server.py +31 -71
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +0 -27
- mcp_proxy_adapter/examples/security_test/__init__.py +18 -0
- mcp_proxy_adapter/examples/security_test/auth_manager.py +14 -0
- mcp_proxy_adapter/examples/security_test/ssl_context_manager.py +28 -0
- mcp_proxy_adapter/examples/security_test/test_client.py +159 -0
- mcp_proxy_adapter/examples/security_test/test_result.py +22 -0
- mcp_proxy_adapter/examples/security_test_client.py +24 -1075
- mcp_proxy_adapter/examples/setup/__init__.py +24 -0
- mcp_proxy_adapter/examples/setup/certificate_manager.py +215 -0
- mcp_proxy_adapter/examples/setup/config_generator.py +12 -0
- mcp_proxy_adapter/examples/setup/config_validator.py +118 -0
- mcp_proxy_adapter/examples/setup/environment_setup.py +62 -0
- mcp_proxy_adapter/examples/setup/test_files_generator.py +10 -0
- mcp_proxy_adapter/examples/setup/test_runner.py +89 -0
- mcp_proxy_adapter/examples/setup_test_environment.py +133 -1425
- mcp_proxy_adapter/examples/test_config.py +0 -3
- mcp_proxy_adapter/examples/test_config_builder.py +25 -405
- mcp_proxy_adapter/examples/test_examples.py +0 -1
- mcp_proxy_adapter/examples/test_framework_complete.py +0 -2
- mcp_proxy_adapter/examples/test_mcp_server.py +0 -1
- mcp_proxy_adapter/examples/test_protocol_examples.py +0 -1
- mcp_proxy_adapter/examples/universal_client.py +0 -6
- mcp_proxy_adapter/examples/update_config_certificates.py +0 -1
- mcp_proxy_adapter/examples/validate_generator_compatibility.py +0 -1
- mcp_proxy_adapter/examples/validate_generator_compatibility_simple.py +0 -187
- mcp_proxy_adapter/integrations/__init__.py +25 -0
- mcp_proxy_adapter/integrations/queuemgr_integration.py +462 -0
- mcp_proxy_adapter/main.py +70 -62
- mcp_proxy_adapter/openapi.py +0 -22
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.30.dist-info}/METADATA +2 -1
- mcp_proxy_adapter-6.9.30.dist-info/RECORD +235 -0
- {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.30.dist-info}/entry_points.txt +1 -1
- mcp_proxy_adapter-6.9.28.dist-info/RECORD +0 -149
- {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.30.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.30.dist-info}/top_level.txt +0 -0
|
@@ -1,56 +1,26 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Author: Vasiliy Zdanovskiy
|
|
3
|
+
email: vasilyvz@gmail.com
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
validation, and role extraction. Integrates with mcp_security_framework.
|
|
6
|
-
|
|
7
|
-
Author: MCP Proxy Adapter Team
|
|
8
|
-
Version: 1.0.0
|
|
5
|
+
Main certificate utilities for MCP Proxy Adapter.
|
|
9
6
|
"""
|
|
10
7
|
|
|
11
8
|
import logging
|
|
12
|
-
import
|
|
13
|
-
import ipaddress
|
|
14
|
-
from datetime import datetime, timedelta, timezone
|
|
15
|
-
from typing import Dict, List, Optional, Any
|
|
9
|
+
from datetime import datetime
|
|
16
10
|
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional, Any
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
CertificateConfig,
|
|
23
|
-
CAConfig,
|
|
24
|
-
ClientCertConfig,
|
|
25
|
-
ServerCertConfig,
|
|
26
|
-
)
|
|
27
|
-
from mcp_security_framework.schemas.models import CertificateType
|
|
28
|
-
from mcp_security_framework.utils.cert_utils import (
|
|
29
|
-
parse_certificate,
|
|
30
|
-
extract_roles_from_certificate,
|
|
31
|
-
extract_permissions_from_certificate,
|
|
32
|
-
validate_certificate_chain,
|
|
33
|
-
get_certificate_expiry,
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
SECURITY_FRAMEWORK_AVAILABLE = True
|
|
37
|
-
except ImportError:
|
|
38
|
-
SECURITY_FRAMEWORK_AVAILABLE = False
|
|
39
|
-
# Fallback to cryptography if mcp_security_framework is not available
|
|
40
|
-
from cryptography import x509
|
|
41
|
-
from cryptography.hazmat.primitives import hashes, serialization
|
|
42
|
-
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
43
|
-
from cryptography.x509.oid import NameOID
|
|
44
|
-
|
|
45
|
-
from .auth_validator import AuthValidator
|
|
46
|
-
from .role_utils import RoleUtils
|
|
13
|
+
from .certificate_creator import CertificateCreator
|
|
14
|
+
from .certificate_validator import CertificateValidator
|
|
15
|
+
from .certificate_extractor import CertificateExtractor
|
|
16
|
+
from .ssl_context_manager import SSLContextManager
|
|
47
17
|
|
|
48
18
|
logger = logging.getLogger(__name__)
|
|
49
19
|
|
|
50
20
|
|
|
51
21
|
class CertificateUtils:
|
|
52
22
|
"""
|
|
53
|
-
|
|
23
|
+
Main utilities for working with certificates.
|
|
54
24
|
|
|
55
25
|
Provides methods for creating CA, server, and client certificates,
|
|
56
26
|
as well as validation and role extraction using mcp_security_framework.
|
|
@@ -73,7 +43,7 @@ class CertificateUtils:
|
|
|
73
43
|
key_size: int = DEFAULT_KEY_SIZE,
|
|
74
44
|
) -> Dict[str, str]:
|
|
75
45
|
"""
|
|
76
|
-
Create a CA certificate and private key
|
|
46
|
+
Create a CA certificate and private key.
|
|
77
47
|
|
|
78
48
|
Args:
|
|
79
49
|
common_name: Common name for the CA certificate
|
|
@@ -83,542 +53,75 @@ class CertificateUtils:
|
|
|
83
53
|
|
|
84
54
|
Returns:
|
|
85
55
|
Dictionary with paths to created files
|
|
86
|
-
|
|
87
|
-
Raises:
|
|
88
|
-
ValueError: If parameters are invalid
|
|
89
|
-
OSError: If files cannot be created
|
|
90
56
|
"""
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
95
|
-
return CertificateUtils._create_ca_certificate_fallback(
|
|
96
|
-
common_name, output_dir, validity_days, key_size
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
try:
|
|
100
|
-
# Validate parameters
|
|
101
|
-
if not common_name or not common_name.strip():
|
|
102
|
-
raise ValueError("Common name cannot be empty")
|
|
103
|
-
|
|
104
|
-
if validity_days <= 0:
|
|
105
|
-
raise ValueError("Validity days must be positive")
|
|
106
|
-
|
|
107
|
-
if key_size < 1024:
|
|
108
|
-
raise ValueError("Key size must be at least 1024 bits")
|
|
109
|
-
|
|
110
|
-
# Create output directory if it doesn't exist
|
|
111
|
-
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
112
|
-
|
|
113
|
-
# Configure CA using mcp_security_framework
|
|
114
|
-
ca_config = CAConfig(
|
|
115
|
-
common_name=common_name,
|
|
116
|
-
organization="MCP Proxy Adapter CA",
|
|
117
|
-
organizational_unit="Certificate Authority",
|
|
118
|
-
country="US",
|
|
119
|
-
state="Default State",
|
|
120
|
-
locality="Default City",
|
|
121
|
-
validity_days=validity_days,
|
|
122
|
-
key_size=key_size,
|
|
123
|
-
key_type="RSA",
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
# Create certificate manager
|
|
127
|
-
cert_config = CertificateConfig(
|
|
128
|
-
output_dir=output_dir,
|
|
129
|
-
ca_cert_path=str(Path(output_dir) / f"{common_name}.crt"),
|
|
130
|
-
ca_key_path=str(Path(output_dir) / f"{common_name}.key"),
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
cert_manager = CertificateManager(cert_config)
|
|
134
|
-
|
|
135
|
-
# Generate CA certificate
|
|
136
|
-
ca_pair = cert_manager.create_ca_certificate(ca_config)
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
"cert_path": str(ca_pair.cert_path),
|
|
140
|
-
"key_path": str(ca_pair.key_path),
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
except Exception as e:
|
|
144
|
-
get_global_logger().error(f"Failed to create CA certificate: {e}")
|
|
145
|
-
raise
|
|
146
|
-
|
|
147
|
-
@staticmethod
|
|
148
|
-
def _create_ca_certificate_fallback(
|
|
149
|
-
common_name: str, output_dir: str, validity_days: int, key_size: int
|
|
150
|
-
) -> Dict[str, str]:
|
|
151
|
-
"""Fallback method using cryptography directly."""
|
|
152
|
-
try:
|
|
153
|
-
# Validate parameters
|
|
154
|
-
if not common_name or not common_name.strip():
|
|
155
|
-
raise ValueError("Common name cannot be empty")
|
|
156
|
-
|
|
157
|
-
if validity_days <= 0:
|
|
158
|
-
raise ValueError("Validity days must be positive")
|
|
159
|
-
|
|
160
|
-
if key_size < 1024:
|
|
161
|
-
raise ValueError("Key size must be at least 1024 bits")
|
|
162
|
-
|
|
163
|
-
# Create output directory if it doesn't exist
|
|
164
|
-
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
165
|
-
|
|
166
|
-
# Generate private key
|
|
167
|
-
private_key = rsa.generate_private_key(
|
|
168
|
-
public_exponent=65537, key_size=key_size
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
# Create certificate subject
|
|
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
|
-
|
|
185
|
-
# Create certificate
|
|
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
|
-
|
|
215
|
-
# Save certificate and key
|
|
216
|
-
cert_path = Path(output_dir) / f"{common_name}.crt"
|
|
217
|
-
key_path = Path(output_dir) / f"{common_name}.key"
|
|
218
|
-
|
|
219
|
-
with open(cert_path, "wb") as f:
|
|
220
|
-
f.write(cert.public_bytes(serialization.Encoding.PEM))
|
|
221
|
-
|
|
222
|
-
with open(key_path, "wb") as f:
|
|
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
|
-
|
|
233
|
-
except Exception as e:
|
|
234
|
-
get_global_logger().error(f"Failed to create CA certificate (fallback): {e}")
|
|
235
|
-
raise
|
|
57
|
+
return CertificateCreator.create_ca_certificate(
|
|
58
|
+
common_name, output_dir, validity_days, key_size
|
|
59
|
+
)
|
|
236
60
|
|
|
237
61
|
@staticmethod
|
|
238
62
|
def create_server_certificate(
|
|
239
63
|
common_name: str,
|
|
240
|
-
|
|
64
|
+
output_dir: str,
|
|
241
65
|
ca_cert_path: str,
|
|
242
66
|
ca_key_path: str,
|
|
243
|
-
output_dir: str,
|
|
244
67
|
validity_days: int = DEFAULT_VALIDITY_DAYS,
|
|
245
68
|
key_size: int = DEFAULT_KEY_SIZE,
|
|
69
|
+
san_dns: Optional[List[str]] = None,
|
|
70
|
+
san_ip: Optional[List[str]] = None,
|
|
246
71
|
) -> Dict[str, str]:
|
|
247
72
|
"""
|
|
248
73
|
Create a server certificate signed by CA.
|
|
249
74
|
|
|
250
75
|
Args:
|
|
251
76
|
common_name: Common name for the server certificate
|
|
252
|
-
|
|
77
|
+
output_dir: Directory to save certificate and key files
|
|
253
78
|
ca_cert_path: Path to CA certificate
|
|
254
79
|
ca_key_path: Path to CA private key
|
|
255
|
-
output_dir: Directory to save certificate and key files
|
|
256
80
|
validity_days: Certificate validity period in days
|
|
257
81
|
key_size: RSA key size in bits
|
|
82
|
+
san_dns: List of DNS names for SAN extension
|
|
83
|
+
san_ip: List of IP addresses for SAN extension
|
|
258
84
|
|
|
259
85
|
Returns:
|
|
260
86
|
Dictionary with paths to created files
|
|
261
|
-
|
|
262
|
-
Raises:
|
|
263
|
-
ValueError: If parameters are invalid
|
|
264
|
-
FileNotFoundError: If CA files not found
|
|
265
|
-
OSError: If files cannot be created
|
|
266
87
|
"""
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if not roles:
|
|
273
|
-
roles = ["server"]
|
|
274
|
-
|
|
275
|
-
if not Path(ca_cert_path).exists():
|
|
276
|
-
raise FileNotFoundError(f"CA certificate not found: {ca_cert_path}")
|
|
277
|
-
|
|
278
|
-
if not Path(ca_key_path).exists():
|
|
279
|
-
raise FileNotFoundError(f"CA key not found: {ca_key_path}")
|
|
280
|
-
|
|
281
|
-
# Create output directory if it doesn't exist
|
|
282
|
-
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
283
|
-
|
|
284
|
-
# Load CA certificate and key
|
|
285
|
-
with open(ca_cert_path, "rb") as f:
|
|
286
|
-
ca_cert = x509.load_pem_x509_certificate(f.read())
|
|
287
|
-
|
|
288
|
-
with open(ca_key_path, "rb") as f:
|
|
289
|
-
ca_key = serialization.load_pem_private_key(f.read(), password=None)
|
|
290
|
-
|
|
291
|
-
# Generate server private key
|
|
292
|
-
private_key = rsa.generate_private_key(
|
|
293
|
-
public_exponent=65537, key_size=key_size
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
# Create certificate subject
|
|
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
|
-
|
|
306
|
-
# Create certificate
|
|
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
|
-
)
|
|
354
|
-
)
|
|
355
|
-
|
|
356
|
-
# Add roles extension
|
|
357
|
-
if roles:
|
|
358
|
-
roles_data = ",".join(roles).encode("utf-8")
|
|
359
|
-
roles_oid = x509.ObjectIdentifier(CertificateUtils.ROLE_EXTENSION_OID)
|
|
360
|
-
cert_builder = cert_builder.add_extension(
|
|
361
|
-
x509.UnrecognizedExtension(roles_oid, roles_data), critical=False
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
cert = cert_builder.sign(ca_key, hashes.SHA256())
|
|
365
|
-
|
|
366
|
-
# Save certificate and key
|
|
367
|
-
cert_path = os.path.join(output_dir, "server.crt")
|
|
368
|
-
key_path = os.path.join(output_dir, "server.key")
|
|
369
|
-
|
|
370
|
-
with open(cert_path, "wb") as f:
|
|
371
|
-
f.write(cert.public_bytes(serialization.Encoding.PEM))
|
|
372
|
-
|
|
373
|
-
with open(key_path, "wb") as f:
|
|
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
|
-
|
|
382
|
-
get_global_logger().info(f"Server certificate created: {cert_path}")
|
|
383
|
-
|
|
384
|
-
return {
|
|
385
|
-
"cert_path": cert_path,
|
|
386
|
-
"key_path": key_path,
|
|
387
|
-
"common_name": common_name,
|
|
388
|
-
"roles": roles,
|
|
389
|
-
"validity_days": validity_days,
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
except Exception as e:
|
|
393
|
-
get_global_logger().error(f"Failed to create server certificate: {e}")
|
|
394
|
-
raise
|
|
88
|
+
return CertificateCreator.create_server_certificate(
|
|
89
|
+
common_name, output_dir, ca_cert_path, ca_key_path,
|
|
90
|
+
validity_days, key_size, san_dns, san_ip
|
|
91
|
+
)
|
|
395
92
|
|
|
396
93
|
@staticmethod
|
|
397
94
|
def create_client_certificate(
|
|
398
95
|
common_name: str,
|
|
96
|
+
output_dir: str,
|
|
399
97
|
ca_cert_path: str,
|
|
400
98
|
ca_key_path: str,
|
|
401
|
-
output_dir: str,
|
|
402
|
-
roles: List[str] = None,
|
|
403
99
|
validity_days: int = DEFAULT_VALIDITY_DAYS,
|
|
404
100
|
key_size: int = DEFAULT_KEY_SIZE,
|
|
405
101
|
) -> Dict[str, str]:
|
|
406
102
|
"""
|
|
407
|
-
Create a client certificate
|
|
103
|
+
Create a client certificate signed by CA.
|
|
408
104
|
|
|
409
105
|
Args:
|
|
410
106
|
common_name: Common name for the client certificate
|
|
107
|
+
output_dir: Directory to save certificate and key files
|
|
411
108
|
ca_cert_path: Path to CA certificate
|
|
412
109
|
ca_key_path: Path to CA private key
|
|
413
|
-
output_dir: Directory to save certificate and key files
|
|
414
|
-
roles: List of roles to include in certificate
|
|
415
110
|
validity_days: Certificate validity period in days
|
|
416
111
|
key_size: RSA key size in bits
|
|
417
112
|
|
|
418
113
|
Returns:
|
|
419
114
|
Dictionary with paths to created files
|
|
420
|
-
|
|
421
|
-
Raises:
|
|
422
|
-
ValueError: If parameters are invalid
|
|
423
|
-
OSError: If files cannot be created
|
|
424
115
|
"""
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
return CertificateUtils._create_client_certificate_fallback(
|
|
430
|
-
common_name,
|
|
431
|
-
ca_cert_path,
|
|
432
|
-
ca_key_path,
|
|
433
|
-
output_dir,
|
|
434
|
-
roles,
|
|
435
|
-
validity_days,
|
|
436
|
-
key_size,
|
|
437
|
-
)
|
|
438
|
-
|
|
439
|
-
try:
|
|
440
|
-
# Validate parameters
|
|
441
|
-
if not common_name or not common_name.strip():
|
|
442
|
-
raise ValueError("Common name cannot be empty")
|
|
443
|
-
|
|
444
|
-
if not Path(ca_cert_path).exists():
|
|
445
|
-
raise ValueError(f"CA certificate not found: {ca_cert_path}")
|
|
446
|
-
|
|
447
|
-
if not Path(ca_key_path).exists():
|
|
448
|
-
raise ValueError(f"CA private key not found: {ca_key_path}")
|
|
449
|
-
|
|
450
|
-
if validity_days <= 0:
|
|
451
|
-
raise ValueError("Validity days must be positive")
|
|
452
|
-
|
|
453
|
-
if key_size < 1024:
|
|
454
|
-
raise ValueError("Key size must be at least 1024 bits")
|
|
455
|
-
|
|
456
|
-
# Create output directory if it doesn't exist
|
|
457
|
-
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
458
|
-
|
|
459
|
-
# Configure client certificate using mcp_security_framework
|
|
460
|
-
client_config = ClientCertConfig(
|
|
461
|
-
common_name=common_name,
|
|
462
|
-
organization="MCP Proxy Adapter Client",
|
|
463
|
-
organizational_unit="Client Certificates",
|
|
464
|
-
country="US",
|
|
465
|
-
state="Default State",
|
|
466
|
-
locality="Default City",
|
|
467
|
-
validity_days=validity_days,
|
|
468
|
-
key_size=key_size,
|
|
469
|
-
key_type="RSA",
|
|
470
|
-
roles=roles or [],
|
|
471
|
-
permissions=[], # Permissions can be added later if needed
|
|
472
|
-
)
|
|
473
|
-
|
|
474
|
-
# Create certificate manager
|
|
475
|
-
cert_config = CertificateConfig(
|
|
476
|
-
output_dir=output_dir,
|
|
477
|
-
ca_cert_path=ca_cert_path,
|
|
478
|
-
ca_key_path=ca_key_path,
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
cert_manager = CertificateManager(cert_config)
|
|
482
|
-
|
|
483
|
-
# Generate client certificate
|
|
484
|
-
client_pair = cert_manager.create_client_certificate(client_config)
|
|
485
|
-
|
|
486
|
-
return {
|
|
487
|
-
"cert_path": str(client_pair.cert_path),
|
|
488
|
-
"key_path": str(client_pair.key_path),
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
except Exception as e:
|
|
492
|
-
get_global_logger().error(f"Failed to create client certificate: {e}")
|
|
493
|
-
raise
|
|
494
|
-
|
|
495
|
-
@staticmethod
|
|
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]:
|
|
505
|
-
"""Fallback method using cryptography directly."""
|
|
506
|
-
try:
|
|
507
|
-
# Validate parameters
|
|
508
|
-
if not common_name or not common_name.strip():
|
|
509
|
-
raise ValueError("Common name cannot be empty")
|
|
510
|
-
|
|
511
|
-
if not Path(ca_cert_path).exists():
|
|
512
|
-
raise ValueError(f"CA certificate not found: {ca_cert_path}")
|
|
513
|
-
|
|
514
|
-
if not Path(ca_key_path).exists():
|
|
515
|
-
raise ValueError(f"CA private key not found: {ca_key_path}")
|
|
516
|
-
|
|
517
|
-
if validity_days <= 0:
|
|
518
|
-
raise ValueError("Validity days must be positive")
|
|
519
|
-
|
|
520
|
-
if key_size < 1024:
|
|
521
|
-
raise ValueError("Key size must be at least 1024 bits")
|
|
522
|
-
|
|
523
|
-
# Create output directory if it doesn't exist
|
|
524
|
-
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
525
|
-
|
|
526
|
-
# Load CA certificate and key
|
|
527
|
-
with open(ca_cert_path, "rb") as f:
|
|
528
|
-
ca_cert = x509.load_pem_x509_certificate(f.read())
|
|
529
|
-
|
|
530
|
-
with open(ca_key_path, "rb") as f:
|
|
531
|
-
ca_key = serialization.load_pem_private_key(f.read(), password=None)
|
|
532
|
-
|
|
533
|
-
# Generate client private key
|
|
534
|
-
private_key = rsa.generate_private_key(
|
|
535
|
-
public_exponent=65537, key_size=key_size
|
|
536
|
-
)
|
|
537
|
-
|
|
538
|
-
# Create certificate subject
|
|
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
|
-
|
|
552
|
-
# Create certificate
|
|
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
|
-
)
|
|
583
|
-
)
|
|
584
|
-
|
|
585
|
-
# Add roles extension if provided
|
|
586
|
-
if roles:
|
|
587
|
-
roles_str = ",".join(roles)
|
|
588
|
-
roles_oid = x509.ObjectIdentifier(CertificateUtils.ROLE_EXTENSION_OID)
|
|
589
|
-
cert_builder = cert_builder.add_extension(
|
|
590
|
-
x509.UnrecognizedExtension(roles_oid, roles_str.encode()),
|
|
591
|
-
critical=False,
|
|
592
|
-
)
|
|
593
|
-
|
|
594
|
-
cert = cert_builder.sign(ca_key, hashes.SHA256())
|
|
595
|
-
|
|
596
|
-
# Save certificate and key
|
|
597
|
-
cert_path = Path(output_dir) / f"{common_name}.crt"
|
|
598
|
-
key_path = Path(output_dir) / f"{common_name}.key"
|
|
599
|
-
|
|
600
|
-
with open(cert_path, "wb") as f:
|
|
601
|
-
f.write(cert.public_bytes(serialization.Encoding.PEM))
|
|
602
|
-
|
|
603
|
-
with open(key_path, "wb") as f:
|
|
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
|
-
|
|
614
|
-
except Exception as e:
|
|
615
|
-
get_global_logger().error(f"Failed to create client certificate (fallback): {e}")
|
|
616
|
-
raise
|
|
116
|
+
return CertificateCreator.create_client_certificate(
|
|
117
|
+
common_name, output_dir, ca_cert_path, ca_key_path,
|
|
118
|
+
validity_days, key_size
|
|
119
|
+
)
|
|
617
120
|
|
|
618
121
|
@staticmethod
|
|
619
122
|
def extract_roles_from_certificate(cert_path: str) -> List[str]:
|
|
620
123
|
"""
|
|
621
|
-
Extract roles from certificate
|
|
124
|
+
Extract roles from certificate.
|
|
622
125
|
|
|
623
126
|
Args:
|
|
624
127
|
cert_path: Path to certificate file
|
|
@@ -626,22 +129,12 @@ class CertificateUtils:
|
|
|
626
129
|
Returns:
|
|
627
130
|
List of roles found in certificate
|
|
628
131
|
"""
|
|
629
|
-
|
|
630
|
-
get_global_logger().warning(
|
|
631
|
-
"mcp_security_framework not available, using fallback method"
|
|
632
|
-
)
|
|
633
|
-
return RoleUtils.extract_roles_from_certificate(cert_path)
|
|
634
|
-
|
|
635
|
-
try:
|
|
636
|
-
return extract_roles_from_certificate(cert_path)
|
|
637
|
-
except Exception as e:
|
|
638
|
-
get_global_logger().error(f"Failed to extract roles from certificate: {e}")
|
|
639
|
-
return []
|
|
132
|
+
return CertificateExtractor.extract_roles_from_certificate(cert_path)
|
|
640
133
|
|
|
641
134
|
@staticmethod
|
|
642
135
|
def extract_roles_from_certificate_object(cert) -> List[str]:
|
|
643
136
|
"""
|
|
644
|
-
Extract roles from certificate object
|
|
137
|
+
Extract roles from certificate object.
|
|
645
138
|
|
|
646
139
|
Args:
|
|
647
140
|
cert: Certificate object
|
|
@@ -649,24 +142,12 @@ class CertificateUtils:
|
|
|
649
142
|
Returns:
|
|
650
143
|
List of roles found in certificate
|
|
651
144
|
"""
|
|
652
|
-
|
|
653
|
-
get_global_logger().warning(
|
|
654
|
-
"mcp_security_framework not available, using fallback method"
|
|
655
|
-
)
|
|
656
|
-
return RoleUtils.extract_roles_from_certificate_object(cert)
|
|
657
|
-
|
|
658
|
-
try:
|
|
659
|
-
# Convert certificate object to PEM format for mcp_security_framework
|
|
660
|
-
cert_pem = cert.public_bytes(serialization.Encoding.PEM)
|
|
661
|
-
return extract_roles_from_certificate(cert_pem)
|
|
662
|
-
except Exception as e:
|
|
663
|
-
get_global_logger().error(f"Failed to extract roles from certificate object: {e}")
|
|
664
|
-
return []
|
|
145
|
+
return CertificateExtractor.extract_roles_from_certificate_object(cert)
|
|
665
146
|
|
|
666
147
|
@staticmethod
|
|
667
148
|
def extract_permissions_from_certificate(cert_path: str) -> List[str]:
|
|
668
149
|
"""
|
|
669
|
-
Extract permissions from certificate
|
|
150
|
+
Extract permissions from certificate.
|
|
670
151
|
|
|
671
152
|
Args:
|
|
672
153
|
cert_path: Path to certificate file
|
|
@@ -674,119 +155,47 @@ class CertificateUtils:
|
|
|
674
155
|
Returns:
|
|
675
156
|
List of permissions found in certificate
|
|
676
157
|
"""
|
|
677
|
-
|
|
678
|
-
get_global_logger().warning(
|
|
679
|
-
"mcp_security_framework not available, permissions extraction not supported"
|
|
680
|
-
)
|
|
681
|
-
return []
|
|
682
|
-
|
|
683
|
-
try:
|
|
684
|
-
return extract_permissions_from_certificate(cert_path)
|
|
685
|
-
except Exception as e:
|
|
686
|
-
get_global_logger().error(f"Failed to extract permissions from certificate: {e}")
|
|
687
|
-
return []
|
|
158
|
+
return CertificateExtractor.extract_permissions_from_certificate(cert_path)
|
|
688
159
|
|
|
689
160
|
@staticmethod
|
|
690
161
|
def validate_certificate_chain(cert_path: str, ca_cert_path: str) -> bool:
|
|
691
162
|
"""
|
|
692
|
-
Validate certificate chain
|
|
163
|
+
Validate certificate chain.
|
|
693
164
|
|
|
694
165
|
Args:
|
|
695
|
-
cert_path: Path to certificate
|
|
696
|
-
ca_cert_path: Path to CA certificate
|
|
166
|
+
cert_path: Path to certificate file
|
|
167
|
+
ca_cert_path: Path to CA certificate file
|
|
697
168
|
|
|
698
169
|
Returns:
|
|
699
|
-
True if chain is valid, False otherwise
|
|
170
|
+
True if certificate chain is valid, False otherwise
|
|
700
171
|
"""
|
|
701
|
-
|
|
702
|
-
get_global_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
|
-
|
|
709
|
-
try:
|
|
710
|
-
return validate_certificate_chain(cert_path, ca_cert_path)
|
|
711
|
-
except Exception as e:
|
|
712
|
-
get_global_logger().error(f"Failed to validate certificate chain: {e}")
|
|
713
|
-
return False
|
|
714
|
-
|
|
715
|
-
@staticmethod
|
|
716
|
-
def _validate_certificate_chain_fallback(cert_path: str, ca_cert_path: str) -> bool:
|
|
717
|
-
"""Fallback certificate chain validation using cryptography."""
|
|
718
|
-
try:
|
|
719
|
-
# Load certificates
|
|
720
|
-
with open(cert_path, "rb") as f:
|
|
721
|
-
cert = x509.load_pem_x509_certificate(f.read())
|
|
722
|
-
|
|
723
|
-
with open(ca_cert_path, "rb") as f:
|
|
724
|
-
ca_cert = x509.load_pem_x509_certificate(f.read())
|
|
725
|
-
|
|
726
|
-
# Simple validation: check if certificate is issued by CA
|
|
727
|
-
return cert.issuer == ca_cert.subject
|
|
728
|
-
|
|
729
|
-
except Exception as e:
|
|
730
|
-
get_global_logger().error(f"Failed to validate certificate chain (fallback): {e}")
|
|
731
|
-
return False
|
|
172
|
+
return CertificateValidator.validate_certificate_chain(cert_path, ca_cert_path)
|
|
732
173
|
|
|
733
174
|
@staticmethod
|
|
734
175
|
def get_certificate_expiry(cert_path: str) -> Optional[datetime]:
|
|
735
176
|
"""
|
|
736
|
-
Get certificate expiry date
|
|
177
|
+
Get certificate expiry date.
|
|
737
178
|
|
|
738
179
|
Args:
|
|
739
180
|
cert_path: Path to certificate file
|
|
740
181
|
|
|
741
182
|
Returns:
|
|
742
|
-
Certificate expiry date or None if
|
|
183
|
+
Certificate expiry date or None if error
|
|
743
184
|
"""
|
|
744
|
-
|
|
745
|
-
get_global_logger().warning(
|
|
746
|
-
"mcp_security_framework not available, using fallback method"
|
|
747
|
-
)
|
|
748
|
-
return CertificateUtils._get_certificate_expiry_fallback(cert_path)
|
|
749
|
-
|
|
750
|
-
try:
|
|
751
|
-
expiry_info = get_certificate_expiry(cert_path)
|
|
752
|
-
if isinstance(expiry_info, dict) and "expiry_date" in expiry_info:
|
|
753
|
-
return expiry_info["expiry_date"]
|
|
754
|
-
return None
|
|
755
|
-
except Exception as e:
|
|
756
|
-
get_global_logger().error(f"Failed to get certificate expiry: {e}")
|
|
757
|
-
return None
|
|
758
|
-
|
|
759
|
-
@staticmethod
|
|
760
|
-
def _get_certificate_expiry_fallback(cert_path: str) -> Optional[datetime]:
|
|
761
|
-
"""Fallback method to get certificate expiry using cryptography."""
|
|
762
|
-
try:
|
|
763
|
-
with open(cert_path, "rb") as f:
|
|
764
|
-
cert = x509.load_pem_x509_certificate(f.read())
|
|
765
|
-
|
|
766
|
-
return cert.not_valid_after
|
|
767
|
-
|
|
768
|
-
except Exception as e:
|
|
769
|
-
get_global_logger().error(f"Failed to get certificate expiry (fallback): {e}")
|
|
770
|
-
return None
|
|
185
|
+
return CertificateValidator.get_certificate_expiry(cert_path)
|
|
771
186
|
|
|
772
187
|
@staticmethod
|
|
773
188
|
def validate_certificate(cert_path: str) -> bool:
|
|
774
189
|
"""
|
|
775
|
-
Validate certificate
|
|
190
|
+
Validate certificate file.
|
|
776
191
|
|
|
777
192
|
Args:
|
|
778
|
-
cert_path: Path to certificate
|
|
193
|
+
cert_path: Path to certificate file
|
|
779
194
|
|
|
780
195
|
Returns:
|
|
781
196
|
True if certificate is valid, False otherwise
|
|
782
197
|
"""
|
|
783
|
-
|
|
784
|
-
validator = AuthValidator()
|
|
785
|
-
result = validator.validate_certificate(cert_path)
|
|
786
|
-
return result.is_valid
|
|
787
|
-
except Exception as e:
|
|
788
|
-
get_global_logger().error(f"Failed to validate certificate: {e}")
|
|
789
|
-
return False
|
|
198
|
+
return CertificateValidator.validate_certificate(cert_path)
|
|
790
199
|
|
|
791
200
|
@staticmethod
|
|
792
201
|
def get_certificate_info(cert_path: str) -> Dict[str, Any]:
|
|
@@ -799,260 +208,28 @@ class CertificateUtils:
|
|
|
799
208
|
Returns:
|
|
800
209
|
Dictionary with certificate information
|
|
801
210
|
"""
|
|
802
|
-
|
|
803
|
-
with open(cert_path, "rb") as f:
|
|
804
|
-
cert_data = f.read()
|
|
805
|
-
|
|
806
|
-
cert = x509.load_pem_x509_certificate(cert_data)
|
|
807
|
-
|
|
808
|
-
# Extract roles
|
|
809
|
-
roles = CertificateUtils.extract_roles_from_certificate_object(cert)
|
|
810
|
-
|
|
811
|
-
# Convert subject and issuer to dictionaries
|
|
812
|
-
subject_dict = {}
|
|
813
|
-
for name_attribute in cert.subject:
|
|
814
|
-
subject_dict[str(name_attribute.oid)] = str(name_attribute.value)
|
|
815
|
-
|
|
816
|
-
issuer_dict = {}
|
|
817
|
-
for name_attribute in cert.issuer:
|
|
818
|
-
issuer_dict[str(name_attribute.oid)] = str(name_attribute.value)
|
|
819
|
-
|
|
820
|
-
return {
|
|
821
|
-
"subject": subject_dict,
|
|
822
|
-
"issuer": issuer_dict,
|
|
823
|
-
"serial_number": str(cert.serial_number),
|
|
824
|
-
"not_valid_before": cert.not_valid_before.isoformat(),
|
|
825
|
-
"not_valid_after": cert.not_valid_after.isoformat(),
|
|
826
|
-
"roles": roles,
|
|
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
|
-
),
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
except Exception as e:
|
|
839
|
-
get_global_logger().error(f"Failed to get certificate info: {e}")
|
|
840
|
-
return {}
|
|
841
|
-
|
|
842
|
-
@staticmethod
|
|
843
|
-
def generate_private_key(
|
|
844
|
-
key_type: str, key_size: int, output_path: str
|
|
845
|
-
) -> Dict[str, Any]:
|
|
846
|
-
"""
|
|
847
|
-
Generate a private key.
|
|
848
|
-
|
|
849
|
-
Args:
|
|
850
|
-
key_type: Type of key (RSA, ECDSA)
|
|
851
|
-
key_size: Key size in bits
|
|
852
|
-
output_path: Path to save the private key
|
|
853
|
-
|
|
854
|
-
Returns:
|
|
855
|
-
Dictionary with generation result
|
|
856
|
-
"""
|
|
857
|
-
try:
|
|
858
|
-
# Validate key type
|
|
859
|
-
if key_type not in ["RSA", "ECDSA"]:
|
|
860
|
-
return {"success": False, "error": "Key type must be RSA or ECDSA"}
|
|
861
|
-
|
|
862
|
-
# Validate key size
|
|
863
|
-
if key_type == "RSA" and key_size < 1024:
|
|
864
|
-
return {
|
|
865
|
-
"success": False,
|
|
866
|
-
"error": "Key size must be at least 1024 bits",
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
if key_type == "ECDSA" and key_size not in [256, 384, 521]:
|
|
870
|
-
return {
|
|
871
|
-
"success": False,
|
|
872
|
-
"error": "ECDSA key size must be 256, 384, or 521 bits",
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
# Create output directory if it doesn't exist
|
|
876
|
-
output_dir = os.path.dirname(output_path)
|
|
877
|
-
if output_dir:
|
|
878
|
-
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
879
|
-
|
|
880
|
-
# Generate private key
|
|
881
|
-
if key_type == "RSA":
|
|
882
|
-
private_key = rsa.generate_private_key(
|
|
883
|
-
public_exponent=65537, key_size=key_size
|
|
884
|
-
)
|
|
885
|
-
else: # ECDSA
|
|
886
|
-
from cryptography.hazmat.primitives.asymmetric import ec
|
|
887
|
-
|
|
888
|
-
if key_size == 256:
|
|
889
|
-
curve = ec.SECP256R1()
|
|
890
|
-
elif key_size == 384:
|
|
891
|
-
curve = ec.SECP384R1()
|
|
892
|
-
else: # 521
|
|
893
|
-
curve = ec.SECP521R1()
|
|
894
|
-
|
|
895
|
-
private_key = ec.generate_private_key(curve)
|
|
896
|
-
|
|
897
|
-
# Save private key
|
|
898
|
-
with open(output_path, "wb") as f:
|
|
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
|
-
|
|
907
|
-
return {
|
|
908
|
-
"success": True,
|
|
909
|
-
"key_type": key_type,
|
|
910
|
-
"key_size": key_size,
|
|
911
|
-
"key_path": output_path,
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
except Exception as e:
|
|
915
|
-
get_global_logger().error(f"Failed to generate private key: {e}")
|
|
916
|
-
return {"success": False, "error": f"Key generation failed: {str(e)}"}
|
|
211
|
+
return CertificateValidator.get_certificate_info(cert_path)
|
|
917
212
|
|
|
918
213
|
@staticmethod
|
|
919
214
|
def validate_private_key(key_path: str) -> Dict[str, Any]:
|
|
920
215
|
"""
|
|
921
|
-
Validate
|
|
216
|
+
Validate private key file.
|
|
922
217
|
|
|
923
218
|
Args:
|
|
924
219
|
key_path: Path to private key file
|
|
925
220
|
|
|
926
221
|
Returns:
|
|
927
|
-
Dictionary with validation
|
|
222
|
+
Dictionary with validation results
|
|
928
223
|
"""
|
|
929
|
-
|
|
930
|
-
if not os.path.exists(key_path):
|
|
931
|
-
return {"success": False, "error": "Key file not found"}
|
|
932
|
-
|
|
933
|
-
with open(key_path, "rb") as f:
|
|
934
|
-
key_data = f.read()
|
|
935
|
-
|
|
936
|
-
# Try to load the private key
|
|
937
|
-
try:
|
|
938
|
-
private_key = serialization.load_pem_private_key(
|
|
939
|
-
key_data, password=None
|
|
940
|
-
)
|
|
941
|
-
|
|
942
|
-
# Get key info
|
|
943
|
-
if isinstance(private_key, rsa.RSAPrivateKey):
|
|
944
|
-
key_type = "RSA"
|
|
945
|
-
key_size = private_key.key_size
|
|
946
|
-
else:
|
|
947
|
-
key_type = "ECDSA"
|
|
948
|
-
key_size = private_key.key_size
|
|
949
|
-
|
|
950
|
-
return {
|
|
951
|
-
"success": True,
|
|
952
|
-
"key_type": key_type,
|
|
953
|
-
"key_size": key_size,
|
|
954
|
-
"created_date": datetime.now().isoformat(),
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
except Exception as e:
|
|
958
|
-
return {"success": False, "error": f"Invalid private key: {str(e)}"}
|
|
959
|
-
|
|
960
|
-
except Exception as e:
|
|
961
|
-
get_global_logger().error(f"Failed to validate private key: {e}")
|
|
962
|
-
return {"success": False, "error": f"Key validation failed: {str(e)}"}
|
|
963
|
-
|
|
964
|
-
@staticmethod
|
|
965
|
-
def create_encrypted_backup(
|
|
966
|
-
key_path: str, backup_path: str, password: str
|
|
967
|
-
) -> Dict[str, Any]:
|
|
968
|
-
"""
|
|
969
|
-
Create an encrypted backup of a private key.
|
|
970
|
-
|
|
971
|
-
Args:
|
|
972
|
-
key_path: Path to private key file
|
|
973
|
-
backup_path: Path to save encrypted backup
|
|
974
|
-
password: Password for encryption
|
|
975
|
-
|
|
976
|
-
Returns:
|
|
977
|
-
Dictionary with backup result
|
|
978
|
-
"""
|
|
979
|
-
try:
|
|
980
|
-
if not os.path.exists(key_path):
|
|
981
|
-
return {"success": False, "error": "Key file not found"}
|
|
982
|
-
|
|
983
|
-
# Read the private key
|
|
984
|
-
with open(key_path, "rb") as f:
|
|
985
|
-
key_data = f.read()
|
|
986
|
-
|
|
987
|
-
# Load the private key
|
|
988
|
-
private_key = serialization.load_pem_private_key(key_data, password=None)
|
|
989
|
-
|
|
990
|
-
# Create encrypted backup
|
|
991
|
-
encrypted_key = private_key.private_bytes(
|
|
992
|
-
encoding=serialization.Encoding.PEM,
|
|
993
|
-
format=serialization.PrivateFormat.PKCS8,
|
|
994
|
-
encryption_algorithm=serialization.BestAvailableEncryption(
|
|
995
|
-
password.encode()
|
|
996
|
-
),
|
|
997
|
-
)
|
|
998
|
-
|
|
999
|
-
# Save encrypted backup
|
|
1000
|
-
with open(backup_path, "wb") as f:
|
|
1001
|
-
f.write(encrypted_key)
|
|
1002
|
-
|
|
1003
|
-
return {"success": True, "backup_path": backup_path}
|
|
1004
|
-
|
|
1005
|
-
except Exception as e:
|
|
1006
|
-
get_global_logger().error(f"Failed to create encrypted backup: {e}")
|
|
1007
|
-
return {"success": False, "error": f"Encryption failed: {str(e)}"}
|
|
1008
|
-
|
|
1009
|
-
@staticmethod
|
|
1010
|
-
def restore_encrypted_backup(
|
|
1011
|
-
backup_path: str, key_path: str, password: str
|
|
1012
|
-
) -> Dict[str, Any]:
|
|
1013
|
-
"""
|
|
1014
|
-
Restore a private key from encrypted backup.
|
|
1015
|
-
|
|
1016
|
-
Args:
|
|
1017
|
-
backup_path: Path to encrypted backup file
|
|
1018
|
-
key_path: Path to save restored key
|
|
1019
|
-
password: Password for decryption
|
|
1020
|
-
|
|
1021
|
-
Returns:
|
|
1022
|
-
Dictionary with restore result
|
|
1023
|
-
"""
|
|
1024
|
-
try:
|
|
1025
|
-
if not os.path.exists(backup_path):
|
|
1026
|
-
return {"success": False, "error": "Backup file not found"}
|
|
1027
|
-
|
|
1028
|
-
# Read the encrypted backup
|
|
1029
|
-
with open(backup_path, "rb") as f:
|
|
1030
|
-
encrypted_data = f.read()
|
|
1031
|
-
|
|
1032
|
-
# Load the encrypted private key
|
|
1033
|
-
private_key = serialization.load_pem_private_key(
|
|
1034
|
-
encrypted_data, password=password.encode()
|
|
1035
|
-
)
|
|
1036
|
-
|
|
1037
|
-
# Save the decrypted key
|
|
1038
|
-
with open(key_path, "wb") as f:
|
|
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
|
-
|
|
1049
|
-
except Exception as e:
|
|
1050
|
-
get_global_logger().error(f"Failed to restore encrypted backup: {e}")
|
|
1051
|
-
return {"success": False, "error": f"Decryption failed: {str(e)}"}
|
|
224
|
+
return CertificateValidator.validate_private_key(key_path)
|
|
1052
225
|
|
|
1053
226
|
@staticmethod
|
|
1054
227
|
def create_ssl_context(
|
|
1055
|
-
cert_file:
|
|
228
|
+
cert_file: Optional[str] = None,
|
|
229
|
+
key_file: Optional[str] = None,
|
|
230
|
+
ca_cert_file: Optional[str] = None,
|
|
231
|
+
verify_mode: int = 0, # ssl.CERT_NONE
|
|
232
|
+
check_hostname: bool = False,
|
|
1056
233
|
) -> Any:
|
|
1057
234
|
"""
|
|
1058
235
|
Create SSL context for server or client.
|
|
@@ -1060,29 +237,13 @@ class CertificateUtils:
|
|
|
1060
237
|
Args:
|
|
1061
238
|
cert_file: Path to certificate file
|
|
1062
239
|
key_file: Path to private key file
|
|
1063
|
-
|
|
240
|
+
ca_cert_file: Path to CA certificate file
|
|
241
|
+
verify_mode: SSL verification mode
|
|
242
|
+
check_hostname: Whether to check hostname
|
|
1064
243
|
|
|
1065
244
|
Returns:
|
|
1066
|
-
SSL context
|
|
245
|
+
SSL context
|
|
1067
246
|
"""
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
# Create SSL context
|
|
1072
|
-
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
|
1073
|
-
context.check_hostname = False
|
|
1074
|
-
context.verify_mode = ssl.CERT_NONE
|
|
1075
|
-
|
|
1076
|
-
# Load certificate and key
|
|
1077
|
-
context.load_cert_chain(cert_file, key_file)
|
|
1078
|
-
|
|
1079
|
-
# Load CA certificate if provided
|
|
1080
|
-
if ca_file and os.path.exists(ca_file):
|
|
1081
|
-
context.load_verify_locations(ca_file)
|
|
1082
|
-
context.verify_mode = ssl.CERT_REQUIRED
|
|
1083
|
-
|
|
1084
|
-
return context
|
|
1085
|
-
|
|
1086
|
-
except Exception as e:
|
|
1087
|
-
get_global_logger().error(f"Failed to create SSL context: {e}")
|
|
1088
|
-
raise
|
|
247
|
+
return SSLContextManager.create_ssl_context(
|
|
248
|
+
cert_file, key_file, ca_cert_file, verify_mode, check_hostname
|
|
249
|
+
)
|