mcp-proxy-adapter 6.2.35__py3-none-any.whl → 6.3.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/config.py +1 -0
- mcp_proxy_adapter/core/app_factory.py +127 -86
- mcp_proxy_adapter/core/config_validator.py +92 -55
- mcp_proxy_adapter/core/crl_utils.py +348 -0
- mcp_proxy_adapter/core/security_integration.py +130 -66
- mcp_proxy_adapter/core/ssl_utils.py +37 -5
- mcp_proxy_adapter/examples/scripts/config_generator.py +1 -0
- mcp_proxy_adapter/main.py +19 -10
- mcp_proxy_adapter/utils/config_generator.py +1 -0
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-6.2.35.dist-info → mcp_proxy_adapter-6.3.0.dist-info}/METADATA +1 -1
- {mcp_proxy_adapter-6.2.35.dist-info → mcp_proxy_adapter-6.3.0.dist-info}/RECORD +16 -15
- {mcp_proxy_adapter-6.2.35.dist-info → mcp_proxy_adapter-6.3.0.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.2.35.dist-info → mcp_proxy_adapter-6.3.0.dist-info}/entry_points.txt +0 -0
- {mcp_proxy_adapter-6.2.35.dist-info → mcp_proxy_adapter-6.3.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_proxy_adapter-6.2.35.dist-info → mcp_proxy_adapter-6.3.0.dist-info}/top_level.txt +0 -0
@@ -7,8 +7,8 @@ with invalid or insecure configurations.
|
|
7
7
|
|
8
8
|
import os
|
9
9
|
import json
|
10
|
-
|
11
|
-
from
|
10
|
+
import uuid
|
11
|
+
from typing import Dict, Any, List
|
12
12
|
|
13
13
|
from mcp_proxy_adapter.core.logging import logger
|
14
14
|
|
@@ -16,220 +16,257 @@ from mcp_proxy_adapter.core.logging import logger
|
|
16
16
|
class ConfigValidator:
|
17
17
|
"""
|
18
18
|
Strict configuration validator.
|
19
|
-
|
19
|
+
|
20
20
|
Validates configuration before startup and prevents startup
|
21
21
|
with invalid or insecure configurations.
|
22
22
|
"""
|
23
|
-
|
23
|
+
|
24
24
|
def __init__(self, config: Dict[str, Any]):
|
25
25
|
"""
|
26
26
|
Initialize configuration validator.
|
27
|
-
|
27
|
+
|
28
28
|
Args:
|
29
29
|
config: Configuration dictionary to validate
|
30
30
|
"""
|
31
31
|
self.config = config
|
32
32
|
self.errors: List[str] = []
|
33
33
|
self.warnings: List[str] = []
|
34
|
-
|
34
|
+
|
35
35
|
def validate_all(self) -> bool:
|
36
36
|
"""
|
37
37
|
Validate all configuration sections.
|
38
|
-
|
38
|
+
|
39
39
|
Returns:
|
40
40
|
True if configuration is valid, False otherwise
|
41
41
|
"""
|
42
42
|
self.errors.clear()
|
43
43
|
self.warnings.clear()
|
44
|
-
|
44
|
+
|
45
45
|
# Validate basic structure
|
46
46
|
if not self._validate_basic_structure():
|
47
47
|
return False
|
48
|
-
|
48
|
+
|
49
|
+
# Validate UUID configuration (mandatory)
|
50
|
+
if not self._validate_uuid_config():
|
51
|
+
return False
|
52
|
+
|
49
53
|
# Validate server configuration
|
50
54
|
if not self._validate_server_config():
|
51
55
|
return False
|
52
|
-
|
56
|
+
|
53
57
|
# Validate security configuration
|
54
58
|
if not self._validate_security_config():
|
55
59
|
return False
|
56
|
-
|
60
|
+
|
57
61
|
# Validate commands configuration
|
58
62
|
if not self._validate_commands_config():
|
59
63
|
return False
|
60
|
-
|
64
|
+
|
61
65
|
# Validate SSL configuration
|
62
66
|
if not self._validate_ssl_config():
|
63
67
|
return False
|
64
|
-
|
68
|
+
|
65
69
|
# Validate roles configuration
|
66
70
|
if not self._validate_roles_config():
|
67
71
|
return False
|
68
|
-
|
72
|
+
|
69
73
|
# Log warnings if any
|
70
74
|
if self.warnings:
|
71
75
|
for warning in self.warnings:
|
72
76
|
logger.warning(f"Configuration warning: {warning}")
|
73
|
-
|
77
|
+
|
74
78
|
# Return success only if no errors
|
75
79
|
return len(self.errors) == 0
|
76
|
-
|
80
|
+
|
77
81
|
def _validate_basic_structure(self) -> bool:
|
78
82
|
"""Validate basic configuration structure."""
|
79
83
|
required_sections = ["server", "logging", "commands"]
|
80
|
-
|
84
|
+
|
81
85
|
for section in required_sections:
|
82
86
|
if section not in self.config:
|
83
87
|
self.errors.append(f"Missing required configuration section: {section}")
|
84
|
-
|
88
|
+
|
85
89
|
return len(self.errors) == 0
|
86
|
-
|
90
|
+
|
91
|
+
def _validate_uuid_config(self) -> bool:
|
92
|
+
"""Validate UUID configuration (mandatory parameter)."""
|
93
|
+
# Check if UUID is present in root config
|
94
|
+
service_uuid = self.config.get("uuid")
|
95
|
+
|
96
|
+
if not service_uuid:
|
97
|
+
self.errors.append(
|
98
|
+
"UUID is required in configuration. Add 'uuid' field with a valid UUID4 value."
|
99
|
+
)
|
100
|
+
return False
|
101
|
+
|
102
|
+
# Validate UUID format
|
103
|
+
try:
|
104
|
+
# Try to parse as UUID
|
105
|
+
parsed_uuid = uuid.UUID(service_uuid)
|
106
|
+
|
107
|
+
# Check if it's UUID4 (version 4)
|
108
|
+
if parsed_uuid.version != 4:
|
109
|
+
self.errors.append(
|
110
|
+
f"UUID must be version 4 (UUID4). Current version: {parsed_uuid.version}"
|
111
|
+
)
|
112
|
+
return False
|
113
|
+
|
114
|
+
except (ValueError, TypeError) as e:
|
115
|
+
self.errors.append(f"Invalid UUID format: {service_uuid}. Error: {e}")
|
116
|
+
return False
|
117
|
+
|
118
|
+
return len(self.errors) == 0
|
119
|
+
|
87
120
|
def _validate_server_config(self) -> bool:
|
88
121
|
"""Validate server configuration."""
|
89
122
|
server_config = self.config.get("server", {})
|
90
|
-
|
123
|
+
|
91
124
|
# Validate host
|
92
125
|
host = server_config.get("host")
|
93
126
|
if not host:
|
94
127
|
self.errors.append("Server host is required")
|
95
|
-
|
128
|
+
|
96
129
|
# Validate port
|
97
130
|
port = server_config.get("port")
|
98
131
|
if not isinstance(port, int) or port < 1 or port > 65535:
|
99
132
|
self.errors.append("Server port must be an integer between 1 and 65535")
|
100
|
-
|
133
|
+
|
101
134
|
return len(self.errors) == 0
|
102
|
-
|
135
|
+
|
103
136
|
def _validate_security_config(self) -> bool:
|
104
137
|
"""Validate security configuration."""
|
105
138
|
security_config = self.config.get("security", {})
|
106
|
-
|
139
|
+
|
107
140
|
# Check if security is enabled
|
108
141
|
security_enabled = security_config.get("enabled", True)
|
109
142
|
auth_enabled = self.config.get("auth_enabled", False)
|
110
|
-
|
143
|
+
|
111
144
|
# Validate permissions configuration
|
112
145
|
permissions_config = security_config.get("permissions", {})
|
113
146
|
permissions_enabled = permissions_config.get("enabled", False)
|
114
|
-
|
147
|
+
|
115
148
|
if permissions_enabled:
|
116
149
|
# Permissions require authentication to identify users
|
117
150
|
auth_config = security_config.get("auth", {})
|
118
151
|
if not auth_config.get("enabled", False):
|
119
|
-
self.errors.append(
|
152
|
+
self.errors.append(
|
153
|
+
"Permissions are enabled but authentication is disabled. Permissions require authentication to identify users."
|
154
|
+
)
|
120
155
|
return False
|
121
|
-
|
156
|
+
|
122
157
|
# Check if there are any authentication methods available
|
123
158
|
auth_methods = auth_config.get("methods", [])
|
124
159
|
if not auth_methods:
|
125
|
-
self.errors.append(
|
160
|
+
self.errors.append(
|
161
|
+
"Permissions are enabled but no authentication methods are configured. At least one authentication method is required."
|
162
|
+
)
|
126
163
|
return False
|
127
|
-
|
164
|
+
|
128
165
|
if security_enabled and auth_enabled:
|
129
166
|
# Validate auth configuration
|
130
167
|
auth_config = security_config.get("auth", {})
|
131
168
|
if not auth_config.get("enabled", False):
|
132
169
|
self.errors.append("Security is enabled but auth is disabled")
|
133
170
|
return False
|
134
|
-
|
171
|
+
|
135
172
|
# Validate API keys if auth is enabled
|
136
173
|
if auth_config.get("enabled", False):
|
137
174
|
api_keys = auth_config.get("api_keys", {})
|
138
175
|
if not api_keys:
|
139
176
|
self.errors.append("API keys are required when authentication is enabled")
|
140
177
|
return False
|
141
|
-
|
178
|
+
|
142
179
|
# Validate API key format
|
143
180
|
for key, value in api_keys.items():
|
144
181
|
if not key or not value:
|
145
182
|
self.errors.append("API keys must have non-empty key and value")
|
146
183
|
return False
|
147
|
-
|
184
|
+
|
148
185
|
return len(self.errors) == 0
|
149
|
-
|
186
|
+
|
150
187
|
def _validate_commands_config(self) -> bool:
|
151
188
|
"""Validate commands configuration."""
|
152
189
|
commands_config = self.config.get("commands", {})
|
153
|
-
|
190
|
+
|
154
191
|
# Validate commands directory if auto_discovery is enabled
|
155
192
|
if commands_config.get("auto_discovery", True):
|
156
193
|
commands_dir = commands_config.get("commands_directory", "./commands")
|
157
194
|
if not os.path.exists(commands_dir):
|
158
195
|
self.warnings.append(f"Commands directory does not exist: {commands_dir}")
|
159
|
-
|
196
|
+
|
160
197
|
return True
|
161
|
-
|
198
|
+
|
162
199
|
def _validate_ssl_config(self) -> bool:
|
163
200
|
"""Validate SSL configuration."""
|
164
201
|
ssl_config = self.config.get("ssl", {})
|
165
202
|
ssl_enabled = ssl_config.get("enabled", False)
|
166
|
-
|
203
|
+
|
167
204
|
if ssl_enabled:
|
168
205
|
# Validate certificate files
|
169
206
|
cert_file = ssl_config.get("cert_file")
|
170
207
|
key_file = ssl_config.get("key_file")
|
171
|
-
|
208
|
+
|
172
209
|
if not cert_file or not key_file:
|
173
210
|
self.errors.append("SSL certificate and key files are required when SSL is enabled")
|
174
211
|
return False
|
175
|
-
|
212
|
+
|
176
213
|
if not os.path.exists(cert_file):
|
177
214
|
self.errors.append(f"SSL certificate file not found: {cert_file}")
|
178
|
-
|
215
|
+
|
179
216
|
if not os.path.exists(key_file):
|
180
217
|
self.errors.append(f"SSL private key file not found: {key_file}")
|
181
|
-
|
218
|
+
|
182
219
|
return len(self.errors) == 0
|
183
|
-
|
220
|
+
|
184
221
|
def _validate_roles_config(self) -> bool:
|
185
222
|
"""Validate roles configuration."""
|
186
223
|
roles_config = self.config.get("roles", {})
|
187
224
|
roles_enabled = roles_config.get("enabled", False)
|
188
|
-
|
225
|
+
|
189
226
|
if roles_enabled:
|
190
227
|
config_file = roles_config.get("config_file")
|
191
228
|
if not config_file:
|
192
229
|
self.errors.append("Roles config file is required when roles are enabled")
|
193
230
|
return False
|
194
|
-
|
231
|
+
|
195
232
|
if not os.path.exists(config_file):
|
196
233
|
self.errors.append(f"Roles config file not found: {config_file}")
|
197
234
|
return False
|
198
|
-
|
235
|
+
|
199
236
|
# Validate roles schema file
|
200
237
|
try:
|
201
|
-
with open(config_file,
|
238
|
+
with open(config_file, "r") as f:
|
202
239
|
roles_schema = json.load(f)
|
203
|
-
|
240
|
+
|
204
241
|
if "roles" not in roles_schema:
|
205
242
|
self.errors.append("Roles config file must contain 'roles' section")
|
206
243
|
return False
|
207
|
-
|
244
|
+
|
208
245
|
except (json.JSONDecodeError, IOError) as e:
|
209
246
|
self.errors.append(f"Failed to read roles config file: {e}")
|
210
247
|
return False
|
211
|
-
|
248
|
+
|
212
249
|
return len(self.errors) == 0
|
213
|
-
|
250
|
+
|
214
251
|
def get_errors(self) -> List[str]:
|
215
252
|
"""Get validation errors."""
|
216
253
|
return self.errors.copy()
|
217
|
-
|
254
|
+
|
218
255
|
def get_warnings(self) -> List[str]:
|
219
256
|
"""Get validation warnings."""
|
220
257
|
return self.warnings.copy()
|
221
|
-
|
258
|
+
|
222
259
|
def print_validation_report(self):
|
223
260
|
"""Print validation report."""
|
224
261
|
if self.errors:
|
225
262
|
logger.error("Configuration validation failed:")
|
226
263
|
for error in self.errors:
|
227
264
|
logger.error(f" - {error}")
|
228
|
-
|
265
|
+
|
229
266
|
if self.warnings:
|
230
267
|
logger.warning("Configuration warnings:")
|
231
268
|
for warning in self.warnings:
|
232
269
|
logger.warning(f" - {warning}")
|
233
|
-
|
270
|
+
|
234
271
|
if not self.errors and not self.warnings:
|
235
272
|
logger.info("Configuration validation passed")
|
@@ -0,0 +1,348 @@
|
|
1
|
+
"""
|
2
|
+
CRL Utilities Module
|
3
|
+
|
4
|
+
This module provides utilities for working with Certificate Revocation Lists (CRL).
|
5
|
+
Supports both file-based and URL-based CRL sources.
|
6
|
+
|
7
|
+
Author: Vasiliy Zdanovskiy
|
8
|
+
email: vasilyvz@gmail.com
|
9
|
+
Version: 1.0.0
|
10
|
+
"""
|
11
|
+
|
12
|
+
import logging
|
13
|
+
import os
|
14
|
+
import tempfile
|
15
|
+
from pathlib import Path
|
16
|
+
from typing import Optional, Union, Dict, Any
|
17
|
+
import requests
|
18
|
+
from requests.adapters import HTTPAdapter
|
19
|
+
from urllib3.util.retry import Retry
|
20
|
+
|
21
|
+
# Import mcp_security_framework CRL utilities
|
22
|
+
try:
|
23
|
+
from mcp_security_framework.utils.cert_utils import (
|
24
|
+
is_certificate_revoked,
|
25
|
+
validate_certificate_against_crl,
|
26
|
+
is_crl_valid,
|
27
|
+
get_crl_info,
|
28
|
+
)
|
29
|
+
|
30
|
+
SECURITY_FRAMEWORK_AVAILABLE = True
|
31
|
+
except ImportError:
|
32
|
+
SECURITY_FRAMEWORK_AVAILABLE = False
|
33
|
+
|
34
|
+
logger = logging.getLogger(__name__)
|
35
|
+
|
36
|
+
|
37
|
+
class CRLManager:
|
38
|
+
"""
|
39
|
+
Manager for Certificate Revocation Lists (CRL).
|
40
|
+
|
41
|
+
Supports both file-based and URL-based CRL sources.
|
42
|
+
Automatically downloads CRL from URLs and caches them locally.
|
43
|
+
"""
|
44
|
+
|
45
|
+
def __init__(self, config: Dict[str, Any]):
|
46
|
+
"""
|
47
|
+
Initialize CRL manager.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
config: Configuration dictionary containing CRL settings
|
51
|
+
"""
|
52
|
+
self.config = config
|
53
|
+
self.crl_enabled = config.get("crl_enabled", False)
|
54
|
+
|
55
|
+
# Only analyze CRL paths if certificates are enabled
|
56
|
+
certificates_enabled = config.get("certificates_enabled", True)
|
57
|
+
if certificates_enabled and self.crl_enabled:
|
58
|
+
self.crl_path = config.get("crl_path")
|
59
|
+
self.crl_url = config.get("crl_url")
|
60
|
+
self.crl_validity_days = config.get("crl_validity_days", 30)
|
61
|
+
else:
|
62
|
+
# Don't analyze CRL paths if certificates are disabled
|
63
|
+
self.crl_path = None
|
64
|
+
self.crl_url = None
|
65
|
+
self.crl_validity_days = 30
|
66
|
+
|
67
|
+
# Cache for downloaded CRL files
|
68
|
+
self._crl_cache: Dict[str, str] = {}
|
69
|
+
|
70
|
+
# Setup HTTP session with retry strategy
|
71
|
+
self._setup_http_session()
|
72
|
+
|
73
|
+
logger.info(
|
74
|
+
f"CRL Manager initialized - enabled: {self.crl_enabled}, certificates_enabled: {certificates_enabled}"
|
75
|
+
)
|
76
|
+
|
77
|
+
def _setup_http_session(self):
|
78
|
+
"""Setup HTTP session with retry strategy for CRL downloads."""
|
79
|
+
self.session = requests.Session()
|
80
|
+
|
81
|
+
# Configure retry strategy
|
82
|
+
retry_strategy = Retry(
|
83
|
+
total=3,
|
84
|
+
backoff_factor=1,
|
85
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
86
|
+
)
|
87
|
+
|
88
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
89
|
+
self.session.mount("http://", adapter)
|
90
|
+
self.session.mount("https://", adapter)
|
91
|
+
|
92
|
+
# Set timeout
|
93
|
+
self.session.timeout = 30
|
94
|
+
|
95
|
+
def get_crl_data(self) -> Optional[Union[str, bytes, Path]]:
|
96
|
+
"""
|
97
|
+
Get CRL data from configured source.
|
98
|
+
|
99
|
+
Returns:
|
100
|
+
CRL data as string, bytes, or Path, or None if not available
|
101
|
+
|
102
|
+
Raises:
|
103
|
+
ValueError: If CRL is enabled but no source is configured
|
104
|
+
FileNotFoundError: If CRL file is not found
|
105
|
+
requests.RequestException: If CRL download fails
|
106
|
+
"""
|
107
|
+
if not self.crl_enabled:
|
108
|
+
logger.debug("CRL is disabled, skipping CRL check")
|
109
|
+
return None
|
110
|
+
|
111
|
+
# Check if CRL URL is configured
|
112
|
+
if self.crl_url:
|
113
|
+
return self._get_crl_from_url()
|
114
|
+
|
115
|
+
# Check if CRL file path is configured
|
116
|
+
if self.crl_path:
|
117
|
+
return self._get_crl_from_file()
|
118
|
+
|
119
|
+
# If CRL is enabled but no source is configured, this is an error
|
120
|
+
if self.crl_enabled:
|
121
|
+
raise ValueError("CRL is enabled but neither crl_path nor crl_url is configured")
|
122
|
+
|
123
|
+
return None
|
124
|
+
|
125
|
+
def _get_crl_from_url(self) -> str:
|
126
|
+
"""
|
127
|
+
Download CRL from URL.
|
128
|
+
|
129
|
+
Returns:
|
130
|
+
Path to downloaded CRL file
|
131
|
+
|
132
|
+
Raises:
|
133
|
+
requests.RequestException: If download fails
|
134
|
+
ValueError: If downloaded data is not valid CRL
|
135
|
+
"""
|
136
|
+
try:
|
137
|
+
logger.info(f"Downloading CRL from URL: {self.crl_url}")
|
138
|
+
|
139
|
+
# Download CRL
|
140
|
+
response = self.session.get(self.crl_url)
|
141
|
+
response.raise_for_status()
|
142
|
+
|
143
|
+
# Validate content type
|
144
|
+
content_type = response.headers.get("content-type", "").lower()
|
145
|
+
if (
|
146
|
+
"application/pkix-crl" not in content_type
|
147
|
+
and "application/x-pkcs7-crl" not in content_type
|
148
|
+
):
|
149
|
+
logger.warning(f"Unexpected content type for CRL: {content_type}")
|
150
|
+
|
151
|
+
# Save to temporary file
|
152
|
+
with tempfile.NamedTemporaryFile(mode="wb", suffix=".crl", delete=False) as temp_file:
|
153
|
+
temp_file.write(response.content)
|
154
|
+
temp_file_path = temp_file.name
|
155
|
+
|
156
|
+
# Validate CRL format
|
157
|
+
if SECURITY_FRAMEWORK_AVAILABLE:
|
158
|
+
try:
|
159
|
+
is_crl_valid(temp_file_path)
|
160
|
+
logger.info(f"CRL downloaded and validated successfully from {self.crl_url}")
|
161
|
+
except Exception as e:
|
162
|
+
os.unlink(temp_file_path)
|
163
|
+
raise ValueError(f"Downloaded CRL is not valid: {e}")
|
164
|
+
else:
|
165
|
+
logger.warning("mcp_security_framework not available, skipping CRL validation")
|
166
|
+
|
167
|
+
# Cache the file path
|
168
|
+
self._crl_cache[self.crl_url] = temp_file_path
|
169
|
+
|
170
|
+
return temp_file_path
|
171
|
+
|
172
|
+
except requests.RequestException as e:
|
173
|
+
logger.error(f"Failed to download CRL from {self.crl_url}: {e}")
|
174
|
+
raise
|
175
|
+
except Exception as e:
|
176
|
+
logger.error(f"CRL download failed: {e}")
|
177
|
+
raise
|
178
|
+
|
179
|
+
def _get_crl_from_file(self) -> str:
|
180
|
+
"""
|
181
|
+
Get CRL from file path.
|
182
|
+
|
183
|
+
Returns:
|
184
|
+
Path to CRL file
|
185
|
+
|
186
|
+
Raises:
|
187
|
+
FileNotFoundError: If CRL file is not found
|
188
|
+
ValueError: If CRL file is not valid
|
189
|
+
"""
|
190
|
+
if not os.path.exists(self.crl_path):
|
191
|
+
raise FileNotFoundError(f"CRL file not found: {self.crl_path}")
|
192
|
+
|
193
|
+
# Validate CRL format
|
194
|
+
if SECURITY_FRAMEWORK_AVAILABLE:
|
195
|
+
try:
|
196
|
+
is_crl_valid(self.crl_path)
|
197
|
+
logger.info(f"CRL file validated successfully: {self.crl_path}")
|
198
|
+
except Exception as e:
|
199
|
+
raise ValueError(f"CRL file is not valid: {e}")
|
200
|
+
else:
|
201
|
+
logger.warning("mcp_security_framework not available, skipping CRL validation")
|
202
|
+
|
203
|
+
return self.crl_path
|
204
|
+
|
205
|
+
def is_certificate_revoked(self, cert_path: str) -> bool:
|
206
|
+
"""
|
207
|
+
Check if certificate is revoked according to CRL.
|
208
|
+
|
209
|
+
Args:
|
210
|
+
cert_path: Path to certificate file
|
211
|
+
|
212
|
+
Returns:
|
213
|
+
True if certificate is revoked, False otherwise
|
214
|
+
|
215
|
+
Raises:
|
216
|
+
ValueError: If CRL is enabled but not available
|
217
|
+
FileNotFoundError: If certificate file is not found
|
218
|
+
"""
|
219
|
+
if not self.crl_enabled:
|
220
|
+
return False
|
221
|
+
|
222
|
+
if not SECURITY_FRAMEWORK_AVAILABLE:
|
223
|
+
logger.warning("mcp_security_framework not available, skipping CRL check")
|
224
|
+
return False
|
225
|
+
|
226
|
+
try:
|
227
|
+
crl_data = self.get_crl_data()
|
228
|
+
if not crl_data:
|
229
|
+
logger.warning("CRL is enabled but no CRL data is available")
|
230
|
+
return False
|
231
|
+
|
232
|
+
is_revoked = is_certificate_revoked(cert_path, crl_data)
|
233
|
+
|
234
|
+
if is_revoked:
|
235
|
+
logger.warning(f"Certificate is revoked according to CRL: {cert_path}")
|
236
|
+
else:
|
237
|
+
logger.debug(f"Certificate is not revoked according to CRL: {cert_path}")
|
238
|
+
|
239
|
+
return is_revoked
|
240
|
+
|
241
|
+
except Exception as e:
|
242
|
+
logger.error(f"CRL check failed for certificate {cert_path}: {e}")
|
243
|
+
# For security, consider certificate invalid if CRL check fails
|
244
|
+
return True
|
245
|
+
|
246
|
+
def validate_certificate_against_crl(self, cert_path: str) -> Dict[str, Any]:
|
247
|
+
"""
|
248
|
+
Validate certificate against CRL and return detailed status.
|
249
|
+
|
250
|
+
Args:
|
251
|
+
cert_path: Path to certificate file
|
252
|
+
|
253
|
+
Returns:
|
254
|
+
Dictionary containing validation results
|
255
|
+
|
256
|
+
Raises:
|
257
|
+
ValueError: If CRL is enabled but not available
|
258
|
+
FileNotFoundError: If certificate file is not found
|
259
|
+
"""
|
260
|
+
if not self.crl_enabled:
|
261
|
+
return {
|
262
|
+
"is_revoked": False,
|
263
|
+
"crl_checked": False,
|
264
|
+
"crl_source": None,
|
265
|
+
"message": "CRL check is disabled",
|
266
|
+
}
|
267
|
+
|
268
|
+
if not SECURITY_FRAMEWORK_AVAILABLE:
|
269
|
+
logger.warning("mcp_security_framework not available, skipping CRL validation")
|
270
|
+
return {
|
271
|
+
"is_revoked": False,
|
272
|
+
"crl_checked": False,
|
273
|
+
"crl_source": None,
|
274
|
+
"message": "mcp_security_framework not available",
|
275
|
+
}
|
276
|
+
|
277
|
+
try:
|
278
|
+
crl_data = self.get_crl_data()
|
279
|
+
if not crl_data:
|
280
|
+
logger.warning("CRL is enabled but no CRL data is available")
|
281
|
+
return {
|
282
|
+
"is_revoked": True, # For security, consider invalid if CRL unavailable
|
283
|
+
"crl_checked": False,
|
284
|
+
"crl_source": None,
|
285
|
+
"message": "CRL is enabled but not available",
|
286
|
+
}
|
287
|
+
|
288
|
+
# Get CRL source info
|
289
|
+
crl_source = self.crl_url if self.crl_url else self.crl_path
|
290
|
+
|
291
|
+
# Validate certificate against CRL
|
292
|
+
result = validate_certificate_against_crl(cert_path, crl_data)
|
293
|
+
|
294
|
+
result["crl_checked"] = True
|
295
|
+
result["crl_source"] = crl_source
|
296
|
+
|
297
|
+
return result
|
298
|
+
|
299
|
+
except Exception as e:
|
300
|
+
logger.error(f"CRL validation failed for certificate {cert_path}: {e}")
|
301
|
+
# For security, consider certificate invalid if CRL validation fails
|
302
|
+
return {
|
303
|
+
"is_revoked": True,
|
304
|
+
"crl_checked": False,
|
305
|
+
"crl_source": self.crl_url if self.crl_url else self.crl_path,
|
306
|
+
"message": f"CRL validation failed: {e}",
|
307
|
+
}
|
308
|
+
|
309
|
+
def get_crl_info(self) -> Optional[Dict[str, Any]]:
|
310
|
+
"""
|
311
|
+
Get information about the configured CRL.
|
312
|
+
|
313
|
+
Returns:
|
314
|
+
Dictionary containing CRL information, or None if CRL is not available
|
315
|
+
"""
|
316
|
+
if not self.crl_enabled:
|
317
|
+
return None
|
318
|
+
|
319
|
+
if not SECURITY_FRAMEWORK_AVAILABLE:
|
320
|
+
logger.warning("mcp_security_framework not available, cannot get CRL info")
|
321
|
+
return None
|
322
|
+
|
323
|
+
try:
|
324
|
+
crl_data = self.get_crl_data()
|
325
|
+
if not crl_data:
|
326
|
+
return None
|
327
|
+
|
328
|
+
return get_crl_info(crl_data)
|
329
|
+
|
330
|
+
except Exception as e:
|
331
|
+
logger.error(f"Failed to get CRL info: {e}")
|
332
|
+
return None
|
333
|
+
|
334
|
+
def cleanup_cache(self):
|
335
|
+
"""Clean up temporary CRL files."""
|
336
|
+
for url, temp_path in self._crl_cache.items():
|
337
|
+
try:
|
338
|
+
if os.path.exists(temp_path):
|
339
|
+
os.unlink(temp_path)
|
340
|
+
logger.debug(f"Cleaned up temporary CRL file: {temp_path}")
|
341
|
+
except Exception as e:
|
342
|
+
logger.warning(f"Failed to cleanup temporary CRL file {temp_path}: {e}")
|
343
|
+
|
344
|
+
self._crl_cache.clear()
|
345
|
+
|
346
|
+
def __del__(self):
|
347
|
+
"""Cleanup when object is destroyed."""
|
348
|
+
self.cleanup_cache()
|