mcp-security-framework 0.1.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_security_framework/__init__.py +96 -0
- mcp_security_framework/cli/__init__.py +18 -0
- mcp_security_framework/cli/cert_cli.py +511 -0
- mcp_security_framework/cli/security_cli.py +791 -0
- mcp_security_framework/constants.py +209 -0
- mcp_security_framework/core/__init__.py +61 -0
- mcp_security_framework/core/auth_manager.py +1011 -0
- mcp_security_framework/core/cert_manager.py +1663 -0
- mcp_security_framework/core/permission_manager.py +735 -0
- mcp_security_framework/core/rate_limiter.py +602 -0
- mcp_security_framework/core/security_manager.py +943 -0
- mcp_security_framework/core/ssl_manager.py +735 -0
- mcp_security_framework/examples/__init__.py +75 -0
- mcp_security_framework/examples/django_example.py +615 -0
- mcp_security_framework/examples/fastapi_example.py +472 -0
- mcp_security_framework/examples/flask_example.py +506 -0
- mcp_security_framework/examples/gateway_example.py +803 -0
- mcp_security_framework/examples/microservice_example.py +690 -0
- mcp_security_framework/examples/standalone_example.py +576 -0
- mcp_security_framework/middleware/__init__.py +250 -0
- mcp_security_framework/middleware/auth_middleware.py +292 -0
- mcp_security_framework/middleware/fastapi_auth_middleware.py +447 -0
- mcp_security_framework/middleware/fastapi_middleware.py +757 -0
- mcp_security_framework/middleware/flask_auth_middleware.py +465 -0
- mcp_security_framework/middleware/flask_middleware.py +591 -0
- mcp_security_framework/middleware/mtls_middleware.py +439 -0
- mcp_security_framework/middleware/rate_limit_middleware.py +403 -0
- mcp_security_framework/middleware/security_middleware.py +507 -0
- mcp_security_framework/schemas/__init__.py +109 -0
- mcp_security_framework/schemas/config.py +694 -0
- mcp_security_framework/schemas/models.py +709 -0
- mcp_security_framework/schemas/responses.py +686 -0
- mcp_security_framework/tests/__init__.py +0 -0
- mcp_security_framework/utils/__init__.py +121 -0
- mcp_security_framework/utils/cert_utils.py +525 -0
- mcp_security_framework/utils/crypto_utils.py +475 -0
- mcp_security_framework/utils/validation_utils.py +571 -0
- mcp_security_framework-0.1.0.dist-info/METADATA +411 -0
- mcp_security_framework-0.1.0.dist-info/RECORD +76 -0
- mcp_security_framework-0.1.0.dist-info/WHEEL +5 -0
- mcp_security_framework-0.1.0.dist-info/entry_points.txt +3 -0
- mcp_security_framework-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_cli/__init__.py +0 -0
- tests/test_cli/test_cert_cli.py +379 -0
- tests/test_cli/test_security_cli.py +657 -0
- tests/test_core/__init__.py +0 -0
- tests/test_core/test_auth_manager.py +582 -0
- tests/test_core/test_cert_manager.py +795 -0
- tests/test_core/test_permission_manager.py +395 -0
- tests/test_core/test_rate_limiter.py +626 -0
- tests/test_core/test_security_manager.py +841 -0
- tests/test_core/test_ssl_manager.py +532 -0
- tests/test_examples/__init__.py +8 -0
- tests/test_examples/test_fastapi_example.py +264 -0
- tests/test_examples/test_flask_example.py +238 -0
- tests/test_examples/test_standalone_example.py +292 -0
- tests/test_integration/__init__.py +0 -0
- tests/test_integration/test_auth_flow.py +502 -0
- tests/test_integration/test_certificate_flow.py +527 -0
- tests/test_integration/test_fastapi_integration.py +341 -0
- tests/test_integration/test_flask_integration.py +398 -0
- tests/test_integration/test_standalone_integration.py +493 -0
- tests/test_middleware/__init__.py +0 -0
- tests/test_middleware/test_fastapi_middleware.py +523 -0
- tests/test_middleware/test_flask_middleware.py +582 -0
- tests/test_middleware/test_security_middleware.py +493 -0
- tests/test_schemas/__init__.py +0 -0
- tests/test_schemas/test_config.py +811 -0
- tests/test_schemas/test_models.py +879 -0
- tests/test_schemas/test_responses.py +1054 -0
- tests/test_schemas/test_serialization.py +493 -0
- tests/test_utils/__init__.py +0 -0
- tests/test_utils/test_cert_utils.py +510 -0
- tests/test_utils/test_crypto_utils.py +603 -0
- tests/test_utils/test_validation_utils.py +477 -0
@@ -0,0 +1,795 @@
|
|
1
|
+
"""
|
2
|
+
Tests for Certificate Manager Module
|
3
|
+
|
4
|
+
This module contains comprehensive tests for the CertificateManager class,
|
5
|
+
covering all certificate creation and management functionality.
|
6
|
+
|
7
|
+
Test Coverage:
|
8
|
+
- Root CA certificate creation
|
9
|
+
- Client certificate creation
|
10
|
+
- Server certificate creation
|
11
|
+
- Certificate revocation
|
12
|
+
- Certificate chain validation
|
13
|
+
- Certificate information extraction
|
14
|
+
- Error handling and edge cases
|
15
|
+
- Configuration validation
|
16
|
+
|
17
|
+
Author: MCP Security Team
|
18
|
+
Version: 1.0.0
|
19
|
+
License: MIT
|
20
|
+
"""
|
21
|
+
|
22
|
+
import os
|
23
|
+
import tempfile
|
24
|
+
from datetime import datetime, timedelta, timezone
|
25
|
+
from unittest.mock import MagicMock, Mock, patch
|
26
|
+
|
27
|
+
import pytest
|
28
|
+
from pydantic import ValidationError
|
29
|
+
|
30
|
+
from mcp_security_framework.core.cert_manager import (
|
31
|
+
CertificateConfigurationError,
|
32
|
+
CertificateGenerationError,
|
33
|
+
CertificateManager,
|
34
|
+
CertificateValidationError,
|
35
|
+
)
|
36
|
+
from mcp_security_framework.schemas.config import (
|
37
|
+
CAConfig,
|
38
|
+
CertificateConfig,
|
39
|
+
ClientCertConfig,
|
40
|
+
ServerCertConfig,
|
41
|
+
)
|
42
|
+
from mcp_security_framework.schemas.models import (
|
43
|
+
CertificateInfo,
|
44
|
+
CertificatePair,
|
45
|
+
CertificateType,
|
46
|
+
)
|
47
|
+
|
48
|
+
|
49
|
+
class TestCertificateManager:
|
50
|
+
"""Test suite for CertificateManager class."""
|
51
|
+
|
52
|
+
def setup_method(self):
|
53
|
+
"""Set up test fixtures before each test method."""
|
54
|
+
# Create temporary directory for test certificates
|
55
|
+
self.temp_dir = tempfile.mkdtemp()
|
56
|
+
|
57
|
+
# Create test CA certificate and key files
|
58
|
+
self.ca_cert_path = os.path.join(self.temp_dir, "test_ca.crt")
|
59
|
+
self.ca_key_path = os.path.join(self.temp_dir, "test_ca.key")
|
60
|
+
|
61
|
+
# Create dummy CA files for testing
|
62
|
+
with open(self.ca_cert_path, "w") as f:
|
63
|
+
f.write(
|
64
|
+
"-----BEGIN CERTIFICATE-----\nDUMMY CA CERT\n-----END CERTIFICATE-----"
|
65
|
+
)
|
66
|
+
|
67
|
+
with open(self.ca_key_path, "w") as f:
|
68
|
+
f.write(
|
69
|
+
"-----BEGIN PRIVATE KEY-----\nDUMMY CA KEY\n-----END PRIVATE KEY-----"
|
70
|
+
)
|
71
|
+
|
72
|
+
# Create test configuration
|
73
|
+
self.cert_config = CertificateConfig(
|
74
|
+
enabled=True,
|
75
|
+
ca_cert_path=self.ca_cert_path,
|
76
|
+
ca_key_path=self.ca_key_path,
|
77
|
+
cert_storage_path=self.temp_dir,
|
78
|
+
key_storage_path=self.temp_dir,
|
79
|
+
)
|
80
|
+
|
81
|
+
# Create certificate manager
|
82
|
+
self.cert_manager = CertificateManager(self.cert_config)
|
83
|
+
|
84
|
+
def teardown_method(self):
|
85
|
+
"""Clean up after each test method."""
|
86
|
+
# Remove temporary directory
|
87
|
+
import shutil
|
88
|
+
|
89
|
+
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
90
|
+
|
91
|
+
def test_init_success(self):
|
92
|
+
"""Test successful CertificateManager initialization."""
|
93
|
+
assert self.cert_manager.config == self.cert_config
|
94
|
+
assert self.cert_manager._certificate_cache == {}
|
95
|
+
assert self.cert_manager._crl_cache == {}
|
96
|
+
|
97
|
+
def test_init_missing_ca_cert(self):
|
98
|
+
"""Test initialization with missing CA certificate."""
|
99
|
+
config = CertificateConfig(
|
100
|
+
enabled=True,
|
101
|
+
ca_cert_path="/nonexistent/ca.crt", ca_key_path=self.ca_key_path
|
102
|
+
)
|
103
|
+
|
104
|
+
with pytest.raises(CertificateConfigurationError) as exc_info:
|
105
|
+
CertificateManager(config)
|
106
|
+
|
107
|
+
assert "CA certificate file not found" in str(exc_info.value)
|
108
|
+
|
109
|
+
def test_init_missing_ca_key(self):
|
110
|
+
"""Test initialization with missing CA key."""
|
111
|
+
config = CertificateConfig(
|
112
|
+
enabled=True,
|
113
|
+
ca_cert_path=self.ca_cert_path, ca_key_path="/nonexistent/ca.key"
|
114
|
+
)
|
115
|
+
|
116
|
+
with pytest.raises(CertificateConfigurationError) as exc_info:
|
117
|
+
CertificateManager(config)
|
118
|
+
|
119
|
+
assert "CA private key file not found" in str(exc_info.value)
|
120
|
+
|
121
|
+
def test_create_root_ca_success(self):
|
122
|
+
"""Test successful root CA certificate creation."""
|
123
|
+
ca_config = CAConfig(
|
124
|
+
common_name="Test Root CA",
|
125
|
+
organization="Test Organization",
|
126
|
+
country="US",
|
127
|
+
state="California",
|
128
|
+
locality="San Francisco",
|
129
|
+
validity_years=10,
|
130
|
+
key_size=2048,
|
131
|
+
)
|
132
|
+
|
133
|
+
with patch(
|
134
|
+
"cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key"
|
135
|
+
) as mock_rsa:
|
136
|
+
with patch("cryptography.x509.CertificateBuilder") as mock_builder:
|
137
|
+
with patch("builtins.open", create=True) as mock_open:
|
138
|
+
# Mock the certificate building process
|
139
|
+
mock_cert = Mock()
|
140
|
+
mock_cert.serial_number = 123456789
|
141
|
+
mock_cert.not_valid_before = datetime.now(timezone.utc)
|
142
|
+
mock_cert.not_valid_after = datetime.now(timezone.utc) + timedelta(
|
143
|
+
days=3650
|
144
|
+
)
|
145
|
+
mock_cert.public_bytes.return_value = b"-----BEGIN CERTIFICATE-----\nMOCK CERT\n-----END CERTIFICATE-----"
|
146
|
+
|
147
|
+
mock_private_key = Mock()
|
148
|
+
mock_public_key = Mock()
|
149
|
+
mock_public_key.public_bytes.return_value = b"mock_public_key_data"
|
150
|
+
mock_private_key.public_key.return_value = mock_public_key
|
151
|
+
mock_private_key.private_bytes.return_value = b"-----BEGIN PRIVATE KEY-----\nMOCK KEY\n-----END PRIVATE KEY-----"
|
152
|
+
|
153
|
+
mock_rsa.return_value = mock_private_key
|
154
|
+
|
155
|
+
# Configure the builder mock to return the certificate
|
156
|
+
mock_builder_instance = Mock()
|
157
|
+
mock_builder_instance.subject_name.return_value = (
|
158
|
+
mock_builder_instance
|
159
|
+
)
|
160
|
+
mock_builder_instance.issuer_name.return_value = (
|
161
|
+
mock_builder_instance
|
162
|
+
)
|
163
|
+
mock_builder_instance.public_key.return_value = (
|
164
|
+
mock_builder_instance
|
165
|
+
)
|
166
|
+
mock_builder_instance.serial_number.return_value = (
|
167
|
+
mock_builder_instance
|
168
|
+
)
|
169
|
+
mock_builder_instance.not_valid_before.return_value = (
|
170
|
+
mock_builder_instance
|
171
|
+
)
|
172
|
+
mock_builder_instance.not_valid_after.return_value = (
|
173
|
+
mock_builder_instance
|
174
|
+
)
|
175
|
+
mock_builder_instance.add_extension.return_value = (
|
176
|
+
mock_builder_instance
|
177
|
+
)
|
178
|
+
mock_builder_instance.sign.return_value = mock_cert
|
179
|
+
mock_builder.return_value = mock_builder_instance
|
180
|
+
|
181
|
+
cert_pair = self.cert_manager.create_root_ca(ca_config)
|
182
|
+
|
183
|
+
assert isinstance(cert_pair, CertificatePair)
|
184
|
+
assert cert_pair.certificate_path.endswith("test_root_ca_ca.crt")
|
185
|
+
assert cert_pair.private_key_path.endswith("test_root_ca_ca.key")
|
186
|
+
assert cert_pair.serial_number == "123456789"
|
187
|
+
|
188
|
+
def test_create_root_ca_missing_common_name(self):
|
189
|
+
"""Test root CA creation with missing common name."""
|
190
|
+
ca_config = CAConfig(
|
191
|
+
common_name="",
|
192
|
+
organization="Test Organization",
|
193
|
+
country="US",
|
194
|
+
validity_years=10,
|
195
|
+
)
|
196
|
+
|
197
|
+
with pytest.raises(CertificateGenerationError) as exc_info:
|
198
|
+
self.cert_manager.create_root_ca(ca_config)
|
199
|
+
|
200
|
+
assert "Common name is required for CA certificate" in str(exc_info.value)
|
201
|
+
|
202
|
+
def test_create_client_certificate_success(self):
|
203
|
+
"""Test successful client certificate creation."""
|
204
|
+
client_config = ClientCertConfig(
|
205
|
+
common_name="test.client.com",
|
206
|
+
organization="Test Organization",
|
207
|
+
country="US",
|
208
|
+
validity_days=365,
|
209
|
+
ca_cert_path=self.ca_cert_path,
|
210
|
+
ca_key_path=self.ca_key_path,
|
211
|
+
)
|
212
|
+
|
213
|
+
with patch(
|
214
|
+
"cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key"
|
215
|
+
) as mock_rsa:
|
216
|
+
with patch("cryptography.x509.CertificateBuilder") as mock_builder_class:
|
217
|
+
with patch(
|
218
|
+
"cryptography.x509.load_pem_x509_certificate"
|
219
|
+
) as mock_load_cert:
|
220
|
+
with patch(
|
221
|
+
"cryptography.hazmat.primitives.serialization.load_pem_private_key"
|
222
|
+
) as mock_load_key:
|
223
|
+
with patch("builtins.open", create=True) as mock_open:
|
224
|
+
with patch("os.chmod") as mock_chmod:
|
225
|
+
# Mock file operations
|
226
|
+
mock_file = Mock()
|
227
|
+
mock_open.return_value.__enter__.return_value = (
|
228
|
+
mock_file
|
229
|
+
)
|
230
|
+
|
231
|
+
# Mock CA certificate and key
|
232
|
+
mock_ca_cert = Mock()
|
233
|
+
mock_ca_cert.subject = Mock()
|
234
|
+
mock_load_cert.return_value = mock_ca_cert
|
235
|
+
|
236
|
+
mock_ca_key = Mock()
|
237
|
+
mock_load_key.return_value = mock_ca_key
|
238
|
+
|
239
|
+
# Mock the certificate building process
|
240
|
+
mock_cert = Mock()
|
241
|
+
mock_cert.serial_number = 987654321
|
242
|
+
mock_cert.not_valid_before = datetime.now(timezone.utc)
|
243
|
+
mock_cert.not_valid_after = datetime.now(
|
244
|
+
timezone.utc
|
245
|
+
) + timedelta(days=365)
|
246
|
+
mock_cert.public_bytes.return_value = b"-----BEGIN CERTIFICATE-----\nMOCK CLIENT CERT\n-----END CERTIFICATE-----"
|
247
|
+
|
248
|
+
mock_private_key = Mock()
|
249
|
+
mock_public_key = Mock()
|
250
|
+
mock_public_key.public_bytes.return_value = (
|
251
|
+
b"mock_public_key_data"
|
252
|
+
)
|
253
|
+
mock_private_key.public_key.return_value = (
|
254
|
+
mock_public_key
|
255
|
+
)
|
256
|
+
mock_private_key.private_bytes.return_value = b"-----BEGIN PRIVATE KEY-----\nMOCK CLIENT KEY\n-----END PRIVATE KEY-----"
|
257
|
+
|
258
|
+
# Mock the builder chain
|
259
|
+
mock_builder = Mock()
|
260
|
+
mock_builder.subject_name.return_value = mock_builder
|
261
|
+
mock_builder.issuer_name.return_value = mock_builder
|
262
|
+
mock_builder.public_key.return_value = mock_builder
|
263
|
+
mock_builder.serial_number.return_value = mock_builder
|
264
|
+
mock_builder.not_valid_before.return_value = (
|
265
|
+
mock_builder
|
266
|
+
)
|
267
|
+
mock_builder.not_valid_after.return_value = mock_builder
|
268
|
+
mock_builder.add_extension.return_value = mock_builder
|
269
|
+
mock_builder.sign.return_value = mock_cert
|
270
|
+
|
271
|
+
mock_builder_class.return_value = mock_builder
|
272
|
+
mock_rsa.return_value = mock_private_key
|
273
|
+
|
274
|
+
cert_pair = self.cert_manager.create_client_certificate(
|
275
|
+
client_config
|
276
|
+
)
|
277
|
+
|
278
|
+
assert isinstance(cert_pair, CertificatePair)
|
279
|
+
assert cert_pair.certificate_path.endswith("test.client.com_client.crt")
|
280
|
+
assert cert_pair.private_key_path.endswith("test.client.com_client.key")
|
281
|
+
assert cert_pair.serial_number == "987654321"
|
282
|
+
|
283
|
+
def test_create_client_certificate_missing_common_name(self):
|
284
|
+
"""Test client certificate creation with missing common name."""
|
285
|
+
client_config = ClientCertConfig(
|
286
|
+
common_name="",
|
287
|
+
organization="Test Organization",
|
288
|
+
country="US",
|
289
|
+
validity_days=365,
|
290
|
+
ca_cert_path=self.ca_cert_path,
|
291
|
+
ca_key_path=self.ca_key_path,
|
292
|
+
)
|
293
|
+
|
294
|
+
with pytest.raises(CertificateGenerationError) as exc_info:
|
295
|
+
self.cert_manager.create_client_certificate(client_config)
|
296
|
+
|
297
|
+
assert "Common name is required for client certificate" in str(exc_info.value)
|
298
|
+
|
299
|
+
def test_create_server_certificate_success(self):
|
300
|
+
"""Test successful server certificate creation."""
|
301
|
+
server_config = ServerCertConfig(
|
302
|
+
common_name="api.test.com",
|
303
|
+
organization="Test Organization",
|
304
|
+
country="US",
|
305
|
+
state="California",
|
306
|
+
locality="San Francisco",
|
307
|
+
validity_days=365,
|
308
|
+
key_size=2048,
|
309
|
+
subject_alt_names=["api.test.com", "www.test.com"],
|
310
|
+
ca_cert_path=self.ca_cert_path,
|
311
|
+
ca_key_path=self.ca_key_path,
|
312
|
+
)
|
313
|
+
|
314
|
+
with patch(
|
315
|
+
"cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key"
|
316
|
+
) as mock_rsa:
|
317
|
+
with patch("cryptography.x509.CertificateBuilder") as mock_builder_class:
|
318
|
+
with patch(
|
319
|
+
"cryptography.x509.load_pem_x509_certificate"
|
320
|
+
) as mock_load_cert:
|
321
|
+
with patch(
|
322
|
+
"cryptography.hazmat.primitives.serialization.load_pem_private_key"
|
323
|
+
) as mock_load_key:
|
324
|
+
with patch("builtins.open", create=True) as mock_open:
|
325
|
+
with patch("os.chmod") as mock_chmod:
|
326
|
+
# Mock file operations
|
327
|
+
mock_file = Mock()
|
328
|
+
mock_open.return_value.__enter__.return_value = (
|
329
|
+
mock_file
|
330
|
+
)
|
331
|
+
|
332
|
+
# Mock CA certificate and key
|
333
|
+
mock_ca_cert = Mock()
|
334
|
+
mock_ca_cert.subject = Mock()
|
335
|
+
mock_load_cert.return_value = mock_ca_cert
|
336
|
+
|
337
|
+
mock_ca_key = Mock()
|
338
|
+
mock_load_key.return_value = mock_ca_key
|
339
|
+
|
340
|
+
# Mock the certificate building process
|
341
|
+
mock_cert = Mock()
|
342
|
+
mock_cert.serial_number = 555666777
|
343
|
+
mock_cert.not_valid_before = datetime.now(timezone.utc)
|
344
|
+
mock_cert.not_valid_after = datetime.now(
|
345
|
+
timezone.utc
|
346
|
+
) + timedelta(days=365)
|
347
|
+
mock_cert.public_bytes.return_value = b"-----BEGIN CERTIFICATE-----\nMOCK SERVER CERT\n-----END CERTIFICATE-----"
|
348
|
+
|
349
|
+
mock_private_key = Mock()
|
350
|
+
mock_public_key = Mock()
|
351
|
+
mock_public_key.public_bytes.return_value = (
|
352
|
+
b"mock_public_key_data"
|
353
|
+
)
|
354
|
+
mock_private_key.public_key.return_value = (
|
355
|
+
mock_public_key
|
356
|
+
)
|
357
|
+
mock_private_key.private_bytes.return_value = b"-----BEGIN PRIVATE KEY-----\nMOCK SERVER KEY\n-----END PRIVATE KEY-----"
|
358
|
+
|
359
|
+
# Mock the builder chain
|
360
|
+
mock_builder = Mock()
|
361
|
+
mock_builder.subject_name.return_value = mock_builder
|
362
|
+
mock_builder.issuer_name.return_value = mock_builder
|
363
|
+
mock_builder.public_key.return_value = mock_builder
|
364
|
+
mock_builder.serial_number.return_value = mock_builder
|
365
|
+
mock_builder.not_valid_before.return_value = (
|
366
|
+
mock_builder
|
367
|
+
)
|
368
|
+
mock_builder.not_valid_after.return_value = mock_builder
|
369
|
+
mock_builder.add_extension.return_value = mock_builder
|
370
|
+
mock_builder.sign.return_value = mock_cert
|
371
|
+
|
372
|
+
mock_builder_class.return_value = mock_builder
|
373
|
+
mock_rsa.return_value = mock_private_key
|
374
|
+
|
375
|
+
cert_pair = self.cert_manager.create_server_certificate(
|
376
|
+
server_config
|
377
|
+
)
|
378
|
+
|
379
|
+
assert isinstance(cert_pair, CertificatePair)
|
380
|
+
assert cert_pair.certificate_path.endswith("api.test.com_server.crt")
|
381
|
+
assert cert_pair.private_key_path.endswith("api.test.com_server.key")
|
382
|
+
assert cert_pair.serial_number == "555666777"
|
383
|
+
|
384
|
+
def test_create_server_certificate_missing_common_name(self):
|
385
|
+
"""Test server certificate creation with missing common name."""
|
386
|
+
server_config = ServerCertConfig(
|
387
|
+
common_name="",
|
388
|
+
organization="Test Organization",
|
389
|
+
country="US",
|
390
|
+
validity_days=365,
|
391
|
+
ca_cert_path=self.ca_cert_path,
|
392
|
+
ca_key_path=self.ca_key_path,
|
393
|
+
)
|
394
|
+
|
395
|
+
with pytest.raises(CertificateGenerationError) as exc_info:
|
396
|
+
self.cert_manager.create_server_certificate(server_config)
|
397
|
+
|
398
|
+
assert "Common name is required for server certificate" in str(exc_info.value)
|
399
|
+
|
400
|
+
def test_revoke_certificate_success(self):
|
401
|
+
"""Test successful certificate revocation."""
|
402
|
+
serial_number = "123456789"
|
403
|
+
reason = "key_compromise"
|
404
|
+
|
405
|
+
with patch("builtins.open", create=True) as mock_open:
|
406
|
+
with patch(
|
407
|
+
"cryptography.x509.CertificateRevocationListBuilder"
|
408
|
+
) as mock_crl_builder_class:
|
409
|
+
with patch(
|
410
|
+
"cryptography.x509.RevokedCertificateBuilder"
|
411
|
+
) as mock_revoked_builder_class:
|
412
|
+
with patch("cryptography.x509.ReasonFlags") as mock_reason_flags:
|
413
|
+
with patch(
|
414
|
+
"cryptography.x509.load_pem_x509_certificate"
|
415
|
+
) as mock_load_cert:
|
416
|
+
with patch(
|
417
|
+
"cryptography.hazmat.primitives.serialization.load_pem_private_key"
|
418
|
+
) as mock_load_key:
|
419
|
+
with patch("os.chmod") as mock_chmod:
|
420
|
+
# Mock file operations
|
421
|
+
mock_file = Mock()
|
422
|
+
mock_open.return_value.__enter__.return_value = (
|
423
|
+
mock_file
|
424
|
+
)
|
425
|
+
|
426
|
+
# Mock CA certificate and key
|
427
|
+
mock_ca_cert = Mock()
|
428
|
+
mock_ca_cert.subject = Mock()
|
429
|
+
mock_load_cert.return_value = mock_ca_cert
|
430
|
+
|
431
|
+
mock_ca_key = Mock()
|
432
|
+
mock_load_key.return_value = mock_ca_key
|
433
|
+
|
434
|
+
# Mock CRL building process
|
435
|
+
mock_crl = Mock()
|
436
|
+
mock_crl.public_bytes.return_value = b"-----BEGIN X509 CRL-----\nMOCK CRL\n-----END X509 CRL-----"
|
437
|
+
|
438
|
+
# Mock the builder chain
|
439
|
+
mock_crl_builder = Mock()
|
440
|
+
mock_crl_builder.last_update.return_value = (
|
441
|
+
mock_crl_builder
|
442
|
+
)
|
443
|
+
mock_crl_builder.next_update.return_value = (
|
444
|
+
mock_crl_builder
|
445
|
+
)
|
446
|
+
mock_crl_builder.add_revoked_certificate.return_value = (
|
447
|
+
mock_crl_builder
|
448
|
+
)
|
449
|
+
mock_crl_builder.issuer_name.return_value = (
|
450
|
+
mock_crl_builder
|
451
|
+
)
|
452
|
+
mock_crl_builder.sign.return_value = mock_crl
|
453
|
+
|
454
|
+
# Mock revoked certificate builder
|
455
|
+
mock_revoked_cert = Mock()
|
456
|
+
mock_revoked_builder = Mock()
|
457
|
+
mock_revoked_builder.serial_number.return_value = (
|
458
|
+
mock_revoked_builder
|
459
|
+
)
|
460
|
+
mock_revoked_builder.revocation_date.return_value = (
|
461
|
+
mock_revoked_builder
|
462
|
+
)
|
463
|
+
mock_revoked_builder.revocation_reason.return_value = (
|
464
|
+
mock_revoked_builder
|
465
|
+
)
|
466
|
+
mock_revoked_builder.build.return_value = (
|
467
|
+
mock_revoked_cert
|
468
|
+
)
|
469
|
+
|
470
|
+
# Mock ReasonFlags
|
471
|
+
mock_reason_flags.__getitem__.return_value = (
|
472
|
+
"KEY_COMPROMISE"
|
473
|
+
)
|
474
|
+
|
475
|
+
mock_crl_builder_class.return_value = (
|
476
|
+
mock_crl_builder
|
477
|
+
)
|
478
|
+
mock_revoked_builder_class.return_value = (
|
479
|
+
mock_revoked_builder
|
480
|
+
)
|
481
|
+
|
482
|
+
success = self.cert_manager.revoke_certificate(
|
483
|
+
serial_number, reason
|
484
|
+
)
|
485
|
+
|
486
|
+
assert success is True
|
487
|
+
|
488
|
+
def test_revoke_certificate_missing_serial_number(self):
|
489
|
+
"""Test certificate revocation with missing serial number."""
|
490
|
+
with pytest.raises(ValueError):
|
491
|
+
self.cert_manager.revoke_certificate("", "key_compromise")
|
492
|
+
|
493
|
+
def test_validate_certificate_chain_success(self):
|
494
|
+
"""Test successful certificate chain validation."""
|
495
|
+
cert_path = "/path/to/cert.crt"
|
496
|
+
|
497
|
+
with patch(
|
498
|
+
"mcp_security_framework.core.cert_manager.validate_certificate_chain",
|
499
|
+
return_value=True,
|
500
|
+
):
|
501
|
+
is_valid = self.cert_manager.validate_certificate_chain(cert_path)
|
502
|
+
|
503
|
+
assert is_valid is True
|
504
|
+
|
505
|
+
def test_validate_certificate_chain_failure(self):
|
506
|
+
"""Test certificate chain validation failure."""
|
507
|
+
cert_path = "/path/to/cert.crt"
|
508
|
+
|
509
|
+
with patch(
|
510
|
+
"mcp_security_framework.core.cert_manager.validate_certificate_chain",
|
511
|
+
return_value=False,
|
512
|
+
):
|
513
|
+
is_valid = self.cert_manager.validate_certificate_chain(cert_path)
|
514
|
+
|
515
|
+
assert is_valid is False
|
516
|
+
|
517
|
+
def test_validate_certificate_chain_with_custom_ca(self):
|
518
|
+
"""Test certificate chain validation with custom CA certificate."""
|
519
|
+
cert_path = "/path/to/cert.crt"
|
520
|
+
ca_cert_path = "/path/to/custom_ca.crt"
|
521
|
+
|
522
|
+
with patch(
|
523
|
+
"mcp_security_framework.core.cert_manager.validate_certificate_chain",
|
524
|
+
return_value=True,
|
525
|
+
):
|
526
|
+
is_valid = self.cert_manager.validate_certificate_chain(
|
527
|
+
cert_path, ca_cert_path
|
528
|
+
)
|
529
|
+
|
530
|
+
assert is_valid is True
|
531
|
+
|
532
|
+
def test_get_certificate_info_success(self):
|
533
|
+
"""Test successful certificate information extraction."""
|
534
|
+
cert_path = "/path/to/cert.crt"
|
535
|
+
|
536
|
+
# Mock certificate data with proper structure
|
537
|
+
mock_cert = Mock()
|
538
|
+
|
539
|
+
# Mock subject and issuer as x509.Name objects
|
540
|
+
mock_subject = Mock()
|
541
|
+
mock_subject.get_attributes_for_oid.return_value = [
|
542
|
+
Mock(value="test.client.com")
|
543
|
+
]
|
544
|
+
mock_cert.subject = mock_subject
|
545
|
+
|
546
|
+
mock_issuer = Mock()
|
547
|
+
mock_issuer.get_attributes_for_oid.return_value = [Mock(value="Test Root CA")]
|
548
|
+
mock_cert.issuer = mock_issuer
|
549
|
+
|
550
|
+
mock_cert.serial_number = 123456789
|
551
|
+
mock_cert.version.name = "v3"
|
552
|
+
mock_cert.not_valid_before = datetime.now(timezone.utc)
|
553
|
+
mock_cert.not_valid_after = datetime.now(timezone.utc) + timedelta(days=365)
|
554
|
+
|
555
|
+
# Mock signature algorithm
|
556
|
+
mock_sig_alg = Mock()
|
557
|
+
mock_sig_alg._name = "sha256WithRSAEncryption"
|
558
|
+
mock_cert.signature_algorithm_oid = mock_sig_alg
|
559
|
+
|
560
|
+
# Mock public key algorithm
|
561
|
+
mock_pub_alg = Mock()
|
562
|
+
mock_pub_alg._name = "rsaEncryption"
|
563
|
+
mock_cert.public_key_algorithm_oid = mock_pub_alg
|
564
|
+
|
565
|
+
# Mock fingerprint methods
|
566
|
+
mock_cert.fingerprint.side_effect = lambda hash_alg: b"mock_fingerprint"
|
567
|
+
|
568
|
+
# Mock extensions
|
569
|
+
mock_extension = Mock()
|
570
|
+
mock_extension.value.ca = False
|
571
|
+
mock_cert.extensions.get_extension_for_oid.return_value = mock_extension
|
572
|
+
|
573
|
+
# Mock all utility functions
|
574
|
+
with patch(
|
575
|
+
"mcp_security_framework.core.cert_manager.parse_certificate",
|
576
|
+
return_value=mock_cert,
|
577
|
+
):
|
578
|
+
with patch(
|
579
|
+
"mcp_security_framework.core.cert_manager.extract_roles_from_certificate",
|
580
|
+
return_value=["user"],
|
581
|
+
):
|
582
|
+
with patch(
|
583
|
+
"mcp_security_framework.core.cert_manager.extract_permissions_from_certificate",
|
584
|
+
return_value=["read:users"],
|
585
|
+
):
|
586
|
+
with patch(
|
587
|
+
"mcp_security_framework.core.cert_manager.get_certificate_expiry",
|
588
|
+
return_value={"key_size": 2048},
|
589
|
+
):
|
590
|
+
with patch(
|
591
|
+
"mcp_security_framework.core.cert_manager.get_certificate_serial_number",
|
592
|
+
return_value="123456789",
|
593
|
+
):
|
594
|
+
with patch(
|
595
|
+
"mcp_security_framework.core.cert_manager.is_certificate_self_signed",
|
596
|
+
return_value=False,
|
597
|
+
):
|
598
|
+
cert_info = self.cert_manager.get_certificate_info(
|
599
|
+
cert_path
|
600
|
+
)
|
601
|
+
|
602
|
+
assert isinstance(cert_info, CertificateInfo)
|
603
|
+
assert cert_info.subject == {"CN": "test.client.com", "C": "test.client.com", "O": "test.client.com"}
|
604
|
+
assert cert_info.issuer == {"CN": "Test Root CA"}
|
605
|
+
assert cert_info.serial_number == "123456789"
|
606
|
+
assert cert_info.roles == ["user"]
|
607
|
+
assert cert_info.permissions == ["read:users"]
|
608
|
+
assert cert_info.certificate_path == cert_path
|
609
|
+
|
610
|
+
def test_get_certificate_info_cached(self):
|
611
|
+
"""Test certificate information retrieval from cache."""
|
612
|
+
cert_path = "/path/to/cert.crt"
|
613
|
+
|
614
|
+
# Create cached certificate info
|
615
|
+
cached_info = CertificateInfo(
|
616
|
+
subject={"CN": "cached.client.com"},
|
617
|
+
issuer={"CN": "Test Root CA"},
|
618
|
+
serial_number="987654321",
|
619
|
+
not_before=datetime.now(timezone.utc),
|
620
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=365),
|
621
|
+
certificate_type=CertificateType.CLIENT,
|
622
|
+
key_size=2048,
|
623
|
+
signature_algorithm="sha256WithRSAEncryption",
|
624
|
+
fingerprint_sha1="mock_sha1",
|
625
|
+
fingerprint_sha256="mock_sha256",
|
626
|
+
is_ca=False,
|
627
|
+
roles=["user"],
|
628
|
+
permissions=["read:users"],
|
629
|
+
certificate_path=cert_path,
|
630
|
+
)
|
631
|
+
|
632
|
+
self.cert_manager._certificate_cache[cert_path] = cached_info
|
633
|
+
|
634
|
+
cert_info = self.cert_manager.get_certificate_info(cert_path)
|
635
|
+
|
636
|
+
assert cert_info == cached_info
|
637
|
+
|
638
|
+
def test_get_certificate_info_parsing_error(self):
|
639
|
+
"""Test certificate information extraction with parsing error."""
|
640
|
+
cert_path = "/path/to/invalid_cert.crt"
|
641
|
+
|
642
|
+
with patch(
|
643
|
+
"mcp_security_framework.utils.cert_utils.parse_certificate",
|
644
|
+
side_effect=Exception("Parsing failed"),
|
645
|
+
):
|
646
|
+
with pytest.raises(CertificateValidationError) as exc_info:
|
647
|
+
self.cert_manager.get_certificate_info(cert_path)
|
648
|
+
|
649
|
+
assert "Failed to get certificate info" in str(exc_info.value)
|
650
|
+
|
651
|
+
def test_validate_configuration_missing_ca_cert_path(self):
|
652
|
+
"""Test configuration validation with missing CA certificate path."""
|
653
|
+
with pytest.raises(ValidationError) as exc_info:
|
654
|
+
CertificateConfig(enabled=True, ca_cert_path="", ca_key_path=self.ca_key_path)
|
655
|
+
|
656
|
+
assert "CA certificate and key paths are required" in str(exc_info.value)
|
657
|
+
|
658
|
+
def test_validate_configuration_missing_ca_key_path(self):
|
659
|
+
"""Test configuration validation with missing CA key path."""
|
660
|
+
with pytest.raises(ValidationError) as exc_info:
|
661
|
+
CertificateConfig(enabled=True, ca_cert_path=self.ca_cert_path, ca_key_path="")
|
662
|
+
|
663
|
+
assert "CA certificate and key paths are required" in str(exc_info.value)
|
664
|
+
|
665
|
+
def test_create_output_directory(self):
|
666
|
+
"""Test automatic output directory creation."""
|
667
|
+
# Create new temp directory
|
668
|
+
new_temp_dir = os.path.join(self.temp_dir, "new_output")
|
669
|
+
|
670
|
+
config = CertificateConfig(
|
671
|
+
enabled=True,
|
672
|
+
ca_cert_path=self.ca_cert_path,
|
673
|
+
ca_key_path=self.ca_key_path,
|
674
|
+
cert_storage_path=new_temp_dir,
|
675
|
+
key_storage_path=new_temp_dir,
|
676
|
+
)
|
677
|
+
|
678
|
+
cert_manager = CertificateManager(config)
|
679
|
+
|
680
|
+
assert os.path.exists(new_temp_dir)
|
681
|
+
assert cert_manager.config.cert_storage_path == new_temp_dir
|
682
|
+
|
683
|
+
def test_ec_key_generation(self):
|
684
|
+
"""Test EC key generation for certificates."""
|
685
|
+
ca_config = CAConfig(
|
686
|
+
common_name="Test EC CA",
|
687
|
+
organization="Test Organization",
|
688
|
+
country="US",
|
689
|
+
validity_years=10,
|
690
|
+
)
|
691
|
+
|
692
|
+
with patch(
|
693
|
+
"cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key"
|
694
|
+
) as mock_rsa:
|
695
|
+
with patch("cryptography.x509.CertificateBuilder") as mock_builder_class:
|
696
|
+
with patch("builtins.open", create=True) as mock_open:
|
697
|
+
with patch("os.chmod") as mock_chmod:
|
698
|
+
# Mock the certificate building process
|
699
|
+
mock_cert = Mock()
|
700
|
+
mock_cert.serial_number = 111222333
|
701
|
+
mock_cert.not_valid_before = datetime.now(timezone.utc)
|
702
|
+
mock_cert.not_valid_after = datetime.now(
|
703
|
+
timezone.utc
|
704
|
+
) + timedelta(days=3650)
|
705
|
+
mock_cert.public_bytes.return_value = b"-----BEGIN CERTIFICATE-----\nMOCK EC CERT\n-----END CERTIFICATE-----"
|
706
|
+
|
707
|
+
mock_private_key = Mock()
|
708
|
+
mock_public_key = Mock()
|
709
|
+
mock_public_key.public_bytes.return_value = (
|
710
|
+
b"mock_public_key_data"
|
711
|
+
)
|
712
|
+
mock_private_key.public_key.return_value = mock_public_key
|
713
|
+
mock_private_key.private_bytes.return_value = b"-----BEGIN PRIVATE KEY-----\nMOCK EC KEY\n-----END PRIVATE KEY-----"
|
714
|
+
|
715
|
+
# Mock the builder chain
|
716
|
+
mock_builder = Mock()
|
717
|
+
mock_builder.subject_name.return_value = mock_builder
|
718
|
+
mock_builder.issuer_name.return_value = mock_builder
|
719
|
+
mock_builder.public_key.return_value = mock_builder
|
720
|
+
mock_builder.serial_number.return_value = mock_builder
|
721
|
+
mock_builder.not_valid_before.return_value = mock_builder
|
722
|
+
mock_builder.not_valid_after.return_value = mock_builder
|
723
|
+
mock_builder.add_extension.return_value = mock_builder
|
724
|
+
mock_builder.sign.return_value = mock_cert
|
725
|
+
|
726
|
+
mock_builder_class.return_value = mock_builder
|
727
|
+
mock_rsa.return_value = mock_private_key
|
728
|
+
|
729
|
+
# Mock file operations
|
730
|
+
mock_file = Mock()
|
731
|
+
mock_open.return_value.__enter__.return_value = mock_file
|
732
|
+
|
733
|
+
cert_pair = self.cert_manager.create_root_ca(ca_config)
|
734
|
+
|
735
|
+
assert isinstance(cert_pair, CertificatePair)
|
736
|
+
assert cert_pair.serial_number == "111222333"
|
737
|
+
assert cert_pair.common_name == "Test EC CA"
|
738
|
+
assert cert_pair.organization == "Test Organization"
|
739
|
+
|
740
|
+
def test_certificate_permissions(self):
|
741
|
+
"""Test that generated certificate files have correct permissions."""
|
742
|
+
ca_config = CAConfig(
|
743
|
+
common_name="Test Permissions CA",
|
744
|
+
organization="Test Organization",
|
745
|
+
country="US",
|
746
|
+
validity_years=10,
|
747
|
+
)
|
748
|
+
|
749
|
+
with patch(
|
750
|
+
"cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key"
|
751
|
+
) as mock_rsa:
|
752
|
+
with patch("cryptography.x509.CertificateBuilder") as mock_builder_class:
|
753
|
+
with patch("builtins.open", create=True) as mock_open:
|
754
|
+
with patch("os.chmod") as mock_chmod:
|
755
|
+
# Mock the certificate building process
|
756
|
+
mock_cert = Mock()
|
757
|
+
mock_cert.serial_number = 444555666
|
758
|
+
mock_cert.not_valid_before = datetime.now(timezone.utc)
|
759
|
+
mock_cert.not_valid_after = datetime.now(
|
760
|
+
timezone.utc
|
761
|
+
) + timedelta(days=3650)
|
762
|
+
mock_cert.public_bytes.return_value = b"-----BEGIN CERTIFICATE-----\nMOCK CERT\n-----END CERTIFICATE-----"
|
763
|
+
|
764
|
+
mock_private_key = Mock()
|
765
|
+
mock_public_key = Mock()
|
766
|
+
mock_public_key.public_bytes.return_value = (
|
767
|
+
b"mock_public_key_data"
|
768
|
+
)
|
769
|
+
mock_private_key.public_key.return_value = mock_public_key
|
770
|
+
mock_private_key.private_bytes.return_value = b"-----BEGIN PRIVATE KEY-----\nMOCK KEY\n-----END PRIVATE KEY-----"
|
771
|
+
|
772
|
+
# Mock the builder chain
|
773
|
+
mock_builder = Mock()
|
774
|
+
mock_builder.subject_name.return_value = mock_builder
|
775
|
+
mock_builder.issuer_name.return_value = mock_builder
|
776
|
+
mock_builder.public_key.return_value = mock_builder
|
777
|
+
mock_builder.serial_number.return_value = mock_builder
|
778
|
+
mock_builder.not_valid_before.return_value = mock_builder
|
779
|
+
mock_builder.not_valid_after.return_value = mock_builder
|
780
|
+
mock_builder.add_extension.return_value = mock_builder
|
781
|
+
mock_builder.sign.return_value = mock_cert
|
782
|
+
|
783
|
+
mock_builder_class.return_value = mock_builder
|
784
|
+
mock_rsa.return_value = mock_private_key
|
785
|
+
|
786
|
+
# Mock file operations
|
787
|
+
mock_file = Mock()
|
788
|
+
mock_open.return_value.__enter__.return_value = mock_file
|
789
|
+
|
790
|
+
cert_pair = self.cert_manager.create_root_ca(ca_config)
|
791
|
+
|
792
|
+
# Verify that chmod was called with correct permissions
|
793
|
+
assert mock_chmod.call_count >= 2 # At least for cert and key files
|
794
|
+
# Note: In a real test, we would check actual file permissions
|
795
|
+
# but since we're using mocks, we just verify the chmod calls were made
|