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,475 @@
|
|
1
|
+
"""
|
2
|
+
Cryptographic Utilities Module
|
3
|
+
|
4
|
+
This module provides comprehensive cryptographic utilities for the
|
5
|
+
MCP Security Framework. It includes functions for hashing, key generation,
|
6
|
+
signing, verification, and JWT operations.
|
7
|
+
|
8
|
+
Key Features:
|
9
|
+
- Secure hash functions (SHA-256, SHA-512)
|
10
|
+
- Key generation utilities
|
11
|
+
- Digital signature creation and verification
|
12
|
+
- JWT token creation and validation
|
13
|
+
- Password hashing and verification
|
14
|
+
- Random data generation
|
15
|
+
|
16
|
+
Functions:
|
17
|
+
hash_password: Hash password with salt
|
18
|
+
verify_password: Verify password against hash
|
19
|
+
generate_random_bytes: Generate random bytes
|
20
|
+
generate_api_key: Generate secure API key
|
21
|
+
create_jwt_token: Create JWT token
|
22
|
+
verify_jwt_token: Verify JWT token
|
23
|
+
hash_data: Hash data with specified algorithm
|
24
|
+
sign_data: Sign data with private key
|
25
|
+
verify_signature: Verify signature with public key
|
26
|
+
|
27
|
+
Author: MCP Security Team
|
28
|
+
Version: 1.0.0
|
29
|
+
License: MIT
|
30
|
+
"""
|
31
|
+
|
32
|
+
import base64
|
33
|
+
import hashlib
|
34
|
+
import hmac
|
35
|
+
import secrets
|
36
|
+
from datetime import datetime, timedelta, timezone
|
37
|
+
from typing import Dict, Optional, Union
|
38
|
+
|
39
|
+
import jwt
|
40
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
41
|
+
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
42
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
43
|
+
|
44
|
+
|
45
|
+
class CryptoError(Exception):
|
46
|
+
"""Raised when cryptographic operations fail."""
|
47
|
+
|
48
|
+
def __init__(self, message: str, error_code: int = -32001):
|
49
|
+
self.message = message
|
50
|
+
self.error_code = error_code
|
51
|
+
super().__init__(self.message)
|
52
|
+
|
53
|
+
|
54
|
+
def hash_password(password: str, salt: Optional[str] = None) -> Dict[str, str]:
|
55
|
+
"""
|
56
|
+
Hash password with salt using PBKDF2.
|
57
|
+
|
58
|
+
Args:
|
59
|
+
password: Plain text password to hash
|
60
|
+
salt: Optional salt. If None, generates random salt
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
Dictionary containing hash and salt
|
64
|
+
|
65
|
+
Raises:
|
66
|
+
CryptoError: If password is empty or hashing fails
|
67
|
+
"""
|
68
|
+
if not password or not password.strip():
|
69
|
+
raise CryptoError("Password cannot be empty")
|
70
|
+
|
71
|
+
try:
|
72
|
+
# Generate salt if not provided
|
73
|
+
if salt is None:
|
74
|
+
salt = secrets.token_hex(16)
|
75
|
+
|
76
|
+
# Convert password and salt to bytes
|
77
|
+
password_bytes = password.encode("utf-8")
|
78
|
+
salt_bytes = salt.encode("utf-8")
|
79
|
+
|
80
|
+
# Use PBKDF2 for key derivation
|
81
|
+
kdf = PBKDF2HMAC(
|
82
|
+
algorithm=hashes.SHA256(),
|
83
|
+
length=32,
|
84
|
+
salt=salt_bytes,
|
85
|
+
iterations=100000,
|
86
|
+
)
|
87
|
+
|
88
|
+
# Generate hash
|
89
|
+
key = kdf.derive(password_bytes)
|
90
|
+
hash_hex = key.hex()
|
91
|
+
|
92
|
+
return {"hash": hash_hex, "salt": salt}
|
93
|
+
except Exception as e:
|
94
|
+
raise CryptoError(f"Password hashing failed: {str(e)}")
|
95
|
+
|
96
|
+
|
97
|
+
def verify_password(password: str, hash_value: str, salt: str) -> bool:
|
98
|
+
"""
|
99
|
+
Verify password against stored hash and salt.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
password: Plain text password to verify
|
103
|
+
hash_value: Stored hash value
|
104
|
+
salt: Stored salt value
|
105
|
+
|
106
|
+
Returns:
|
107
|
+
True if password matches, False otherwise
|
108
|
+
|
109
|
+
Raises:
|
110
|
+
CryptoError: If verification fails
|
111
|
+
"""
|
112
|
+
try:
|
113
|
+
# Hash the provided password with the stored salt
|
114
|
+
result = hash_password(password, salt)
|
115
|
+
return result["hash"] == hash_value
|
116
|
+
except Exception as e:
|
117
|
+
raise CryptoError(f"Password verification failed: {str(e)}")
|
118
|
+
|
119
|
+
|
120
|
+
def generate_random_bytes(length: int = 32) -> bytes:
|
121
|
+
"""
|
122
|
+
Generate cryptographically secure random bytes.
|
123
|
+
|
124
|
+
Args:
|
125
|
+
length: Number of bytes to generate
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
Random bytes
|
129
|
+
|
130
|
+
Raises:
|
131
|
+
CryptoError: If length is invalid or generation fails
|
132
|
+
"""
|
133
|
+
if length <= 0:
|
134
|
+
raise CryptoError("Length must be positive")
|
135
|
+
|
136
|
+
try:
|
137
|
+
return secrets.token_bytes(length)
|
138
|
+
except Exception as e:
|
139
|
+
raise CryptoError(f"Random bytes generation failed: {str(e)}")
|
140
|
+
|
141
|
+
|
142
|
+
def generate_api_key(length: int = 32) -> str:
|
143
|
+
"""
|
144
|
+
Generate secure API key.
|
145
|
+
|
146
|
+
Args:
|
147
|
+
length: Length of API key in bytes
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
Base64 encoded API key
|
151
|
+
|
152
|
+
Raises:
|
153
|
+
CryptoError: If generation fails
|
154
|
+
"""
|
155
|
+
try:
|
156
|
+
random_bytes = generate_random_bytes(length)
|
157
|
+
return base64.urlsafe_b64encode(random_bytes).decode("utf-8").rstrip("=")
|
158
|
+
except Exception as e:
|
159
|
+
raise CryptoError(f"API key generation failed: {str(e)}")
|
160
|
+
|
161
|
+
|
162
|
+
def hash_data(data: Union[str, bytes], algorithm: str = "sha256") -> str:
|
163
|
+
"""
|
164
|
+
Hash data with specified algorithm.
|
165
|
+
|
166
|
+
Args:
|
167
|
+
data: Data to hash
|
168
|
+
algorithm: Hash algorithm (sha256, sha512, md5)
|
169
|
+
|
170
|
+
Returns:
|
171
|
+
Hexadecimal hash string
|
172
|
+
|
173
|
+
Raises:
|
174
|
+
CryptoError: If algorithm is unsupported or hashing fails
|
175
|
+
"""
|
176
|
+
if isinstance(data, str):
|
177
|
+
data = data.encode("utf-8")
|
178
|
+
|
179
|
+
try:
|
180
|
+
if algorithm.lower() == "sha256":
|
181
|
+
hash_obj = hashlib.sha256(data)
|
182
|
+
elif algorithm.lower() == "sha512":
|
183
|
+
hash_obj = hashlib.sha512(data)
|
184
|
+
elif algorithm.lower() == "md5":
|
185
|
+
hash_obj = hashlib.md5(data)
|
186
|
+
else:
|
187
|
+
raise CryptoError(f"Unsupported hash algorithm: {algorithm}")
|
188
|
+
|
189
|
+
return hash_obj.hexdigest()
|
190
|
+
except Exception as e:
|
191
|
+
raise CryptoError(f"Data hashing failed: {str(e)}")
|
192
|
+
|
193
|
+
|
194
|
+
def create_jwt_token(
|
195
|
+
payload: Dict,
|
196
|
+
secret: str,
|
197
|
+
algorithm: str = "HS256",
|
198
|
+
expires_in: Optional[int] = None,
|
199
|
+
) -> str:
|
200
|
+
"""
|
201
|
+
Create JWT token.
|
202
|
+
|
203
|
+
Args:
|
204
|
+
payload: Token payload data
|
205
|
+
secret: Secret key for signing
|
206
|
+
algorithm: JWT algorithm (HS256, HS512, RS256)
|
207
|
+
expires_in: Token expiration time in seconds
|
208
|
+
|
209
|
+
Returns:
|
210
|
+
JWT token string
|
211
|
+
|
212
|
+
Raises:
|
213
|
+
CryptoError: If token creation fails
|
214
|
+
"""
|
215
|
+
try:
|
216
|
+
# Add expiration if specified
|
217
|
+
if expires_in:
|
218
|
+
payload["exp"] = datetime.now(timezone.utc) + timedelta(seconds=expires_in)
|
219
|
+
|
220
|
+
# Add issued at time
|
221
|
+
payload["iat"] = datetime.now(timezone.utc)
|
222
|
+
|
223
|
+
return jwt.encode(payload, secret, algorithm=algorithm)
|
224
|
+
except Exception as e:
|
225
|
+
raise CryptoError(f"JWT token creation failed: {str(e)}")
|
226
|
+
|
227
|
+
|
228
|
+
def verify_jwt_token(
|
229
|
+
token: str, secret: str, algorithms: Optional[list] = None
|
230
|
+
) -> Dict:
|
231
|
+
"""
|
232
|
+
Verify JWT token.
|
233
|
+
|
234
|
+
Args:
|
235
|
+
token: JWT token to verify
|
236
|
+
secret: Secret key for verification
|
237
|
+
algorithms: List of allowed algorithms
|
238
|
+
|
239
|
+
Returns:
|
240
|
+
Decoded token payload
|
241
|
+
|
242
|
+
Raises:
|
243
|
+
CryptoError: If token verification fails
|
244
|
+
"""
|
245
|
+
if algorithms is None:
|
246
|
+
algorithms = ["HS256", "HS512"]
|
247
|
+
|
248
|
+
try:
|
249
|
+
payload = jwt.decode(token, secret, algorithms=algorithms)
|
250
|
+
return payload
|
251
|
+
except jwt.ExpiredSignatureError:
|
252
|
+
raise CryptoError("JWT token has expired")
|
253
|
+
except jwt.InvalidTokenError as e:
|
254
|
+
raise CryptoError(f"Invalid JWT token: {str(e)}")
|
255
|
+
except Exception as e:
|
256
|
+
raise CryptoError(f"JWT token verification failed: {str(e)}")
|
257
|
+
|
258
|
+
|
259
|
+
def generate_rsa_key_pair(key_size: int = 2048) -> Dict[str, str]:
|
260
|
+
"""
|
261
|
+
Generate RSA key pair.
|
262
|
+
|
263
|
+
Args:
|
264
|
+
key_size: Key size in bits (2048, 4096)
|
265
|
+
|
266
|
+
Returns:
|
267
|
+
Dictionary containing private and public keys in PEM format
|
268
|
+
|
269
|
+
Raises:
|
270
|
+
CryptoError: If key generation fails
|
271
|
+
"""
|
272
|
+
if key_size not in [2048, 4096]:
|
273
|
+
raise CryptoError("Key size must be 2048 or 4096 bits")
|
274
|
+
|
275
|
+
try:
|
276
|
+
# Generate private key
|
277
|
+
private_key = rsa.generate_private_key(public_exponent=65537, key_size=key_size)
|
278
|
+
|
279
|
+
# Get public key
|
280
|
+
public_key = private_key.public_key()
|
281
|
+
|
282
|
+
# Serialize keys to PEM format
|
283
|
+
private_pem = private_key.private_bytes(
|
284
|
+
encoding=serialization.Encoding.PEM,
|
285
|
+
format=serialization.PrivateFormat.PKCS8,
|
286
|
+
encryption_algorithm=serialization.NoEncryption(),
|
287
|
+
)
|
288
|
+
|
289
|
+
public_pem = public_key.public_bytes(
|
290
|
+
encoding=serialization.Encoding.PEM,
|
291
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
292
|
+
)
|
293
|
+
|
294
|
+
return {
|
295
|
+
"private_key": private_pem.decode("utf-8"),
|
296
|
+
"public_key": public_pem.decode("utf-8"),
|
297
|
+
}
|
298
|
+
except Exception as e:
|
299
|
+
raise CryptoError(f"RSA key pair generation failed: {str(e)}")
|
300
|
+
|
301
|
+
|
302
|
+
def sign_data(data: Union[str, bytes], private_key_pem: str) -> str:
|
303
|
+
"""
|
304
|
+
Sign data with private key.
|
305
|
+
|
306
|
+
Args:
|
307
|
+
data: Data to sign
|
308
|
+
private_key_pem: Private key in PEM format
|
309
|
+
|
310
|
+
Returns:
|
311
|
+
Base64 encoded signature
|
312
|
+
|
313
|
+
Raises:
|
314
|
+
CryptoError: If signing fails
|
315
|
+
"""
|
316
|
+
if isinstance(data, str):
|
317
|
+
data = data.encode("utf-8")
|
318
|
+
|
319
|
+
try:
|
320
|
+
# Load private key
|
321
|
+
private_key = serialization.load_pem_private_key(
|
322
|
+
private_key_pem.encode("utf-8"), password=None
|
323
|
+
)
|
324
|
+
|
325
|
+
# Sign data
|
326
|
+
signature = private_key.sign(
|
327
|
+
data,
|
328
|
+
padding.PSS(
|
329
|
+
mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
|
330
|
+
),
|
331
|
+
hashes.SHA256(),
|
332
|
+
)
|
333
|
+
|
334
|
+
return base64.b64encode(signature).decode("utf-8")
|
335
|
+
except Exception as e:
|
336
|
+
raise CryptoError(f"Data signing failed: {str(e)}")
|
337
|
+
|
338
|
+
|
339
|
+
def verify_signature(
|
340
|
+
data: Union[str, bytes], signature: str, public_key_pem: str
|
341
|
+
) -> bool:
|
342
|
+
"""
|
343
|
+
Verify signature with public key.
|
344
|
+
|
345
|
+
Args:
|
346
|
+
data: Original data
|
347
|
+
signature: Base64 encoded signature
|
348
|
+
public_key_pem: Public key in PEM format
|
349
|
+
|
350
|
+
Returns:
|
351
|
+
True if signature is valid, False otherwise
|
352
|
+
|
353
|
+
Raises:
|
354
|
+
CryptoError: If verification fails
|
355
|
+
"""
|
356
|
+
if isinstance(data, str):
|
357
|
+
data = data.encode("utf-8")
|
358
|
+
|
359
|
+
try:
|
360
|
+
# Load public key
|
361
|
+
public_key = serialization.load_pem_public_key(public_key_pem.encode("utf-8"))
|
362
|
+
|
363
|
+
# Decode signature
|
364
|
+
signature_bytes = base64.b64decode(signature)
|
365
|
+
|
366
|
+
# Verify signature
|
367
|
+
public_key.verify(
|
368
|
+
signature_bytes,
|
369
|
+
data,
|
370
|
+
padding.PSS(
|
371
|
+
mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
|
372
|
+
),
|
373
|
+
hashes.SHA256(),
|
374
|
+
)
|
375
|
+
|
376
|
+
return True
|
377
|
+
except Exception:
|
378
|
+
return False
|
379
|
+
|
380
|
+
|
381
|
+
def generate_hmac(data: Union[str, bytes], key: Union[str, bytes]) -> str:
|
382
|
+
"""
|
383
|
+
Generate HMAC for data.
|
384
|
+
|
385
|
+
Args:
|
386
|
+
data: Data to hash
|
387
|
+
key: HMAC key
|
388
|
+
|
389
|
+
Returns:
|
390
|
+
Hexadecimal HMAC string
|
391
|
+
|
392
|
+
Raises:
|
393
|
+
CryptoError: If HMAC generation fails
|
394
|
+
"""
|
395
|
+
if isinstance(data, str):
|
396
|
+
data = data.encode("utf-8")
|
397
|
+
if isinstance(key, str):
|
398
|
+
key = key.encode("utf-8")
|
399
|
+
|
400
|
+
try:
|
401
|
+
hmac_obj = hmac.new(key, data, hashlib.sha256)
|
402
|
+
return hmac_obj.hexdigest()
|
403
|
+
except Exception as e:
|
404
|
+
raise CryptoError(f"HMAC generation failed: {str(e)}")
|
405
|
+
|
406
|
+
|
407
|
+
def verify_hmac(
|
408
|
+
data: Union[str, bytes], key: Union[str, bytes], expected_hmac: str
|
409
|
+
) -> bool:
|
410
|
+
"""
|
411
|
+
Verify HMAC for data.
|
412
|
+
|
413
|
+
Args:
|
414
|
+
data: Original data
|
415
|
+
key: HMAC key
|
416
|
+
expected_hmac: Expected HMAC value
|
417
|
+
|
418
|
+
Returns:
|
419
|
+
True if HMAC matches, False otherwise
|
420
|
+
"""
|
421
|
+
try:
|
422
|
+
actual_hmac = generate_hmac(data, key)
|
423
|
+
return hmac.compare_digest(actual_hmac, expected_hmac)
|
424
|
+
except Exception:
|
425
|
+
return False
|
426
|
+
|
427
|
+
|
428
|
+
def generate_secure_token(length: int = 32) -> str:
|
429
|
+
"""
|
430
|
+
Generate secure random token.
|
431
|
+
|
432
|
+
Args:
|
433
|
+
length: Length of token in bytes
|
434
|
+
|
435
|
+
Returns:
|
436
|
+
Hexadecimal token string
|
437
|
+
|
438
|
+
Raises:
|
439
|
+
CryptoError: If generation fails
|
440
|
+
"""
|
441
|
+
try:
|
442
|
+
random_bytes = generate_random_bytes(length)
|
443
|
+
return random_bytes.hex()
|
444
|
+
except Exception as e:
|
445
|
+
raise CryptoError(f"Secure token generation failed: {str(e)}")
|
446
|
+
|
447
|
+
|
448
|
+
def validate_api_key_format(api_key: str) -> bool:
|
449
|
+
"""
|
450
|
+
Validate API key format.
|
451
|
+
|
452
|
+
Args:
|
453
|
+
api_key: API key to validate
|
454
|
+
|
455
|
+
Returns:
|
456
|
+
True if format is valid, False otherwise
|
457
|
+
"""
|
458
|
+
if not api_key or not isinstance(api_key, str):
|
459
|
+
return False
|
460
|
+
|
461
|
+
# Check minimum length (at least 8 characters for flexibility)
|
462
|
+
if len(api_key) < 8:
|
463
|
+
return False
|
464
|
+
|
465
|
+
# Check maximum length (reasonable limit)
|
466
|
+
if len(api_key) > 256:
|
467
|
+
return False
|
468
|
+
|
469
|
+
# Check for valid characters (alphanumeric, hyphens, underscores)
|
470
|
+
import re
|
471
|
+
|
472
|
+
if not re.match(r"^[a-zA-Z0-9_-]+$", api_key):
|
473
|
+
return False
|
474
|
+
|
475
|
+
return True
|