mcp-security-framework 1.1.2__py3-none-any.whl → 1.2.1__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 +1 -1
- mcp_security_framework/cli/cert_cli.py +167 -3
- mcp_security_framework/core/auth_manager.py +32 -10
- mcp_security_framework/core/cert_manager.py +261 -6
- mcp_security_framework/core/ssl_manager.py +41 -9
- mcp_security_framework/middleware/mtls_middleware.py +10 -2
- mcp_security_framework/schemas/config.py +31 -0
- mcp_security_framework/schemas/models.py +46 -0
- mcp_security_framework/utils/cert_utils.py +309 -8
- {mcp_security_framework-1.1.2.dist-info → mcp_security_framework-1.2.1.dist-info}/METADATA +1 -1
- {mcp_security_framework-1.1.2.dist-info → mcp_security_framework-1.2.1.dist-info}/RECORD +17 -16
- tests/test_core/test_auth_manager.py +6 -6
- tests/test_utils/test_cert_utils.py +168 -0
- tests/test_utils/test_unitid_compat.py +550 -0
- {mcp_security_framework-1.1.2.dist-info → mcp_security_framework-1.2.1.dist-info}/WHEEL +0 -0
- {mcp_security_framework-1.1.2.dist-info → mcp_security_framework-1.2.1.dist-info}/entry_points.txt +0 -0
- {mcp_security_framework-1.1.2.dist-info → mcp_security_framework-1.2.1.dist-info}/top_level.txt +0 -0
@@ -37,6 +37,7 @@ from ..schemas.models import CertificateInfo
|
|
37
37
|
from ..utils.cert_utils import (
|
38
38
|
extract_certificate_info,
|
39
39
|
get_certificate_expiry,
|
40
|
+
is_certificate_revoked,
|
40
41
|
is_certificate_self_signed,
|
41
42
|
parse_certificate,
|
42
43
|
validate_certificate_chain,
|
@@ -349,21 +350,24 @@ class SSLManager:
|
|
349
350
|
f"Failed to create client SSL context: {str(e)}"
|
350
351
|
)
|
351
352
|
|
352
|
-
def validate_certificate(self, cert_path: str) -> bool:
|
353
|
+
def validate_certificate(self, cert_path: str, crl_path: Optional[str] = None) -> bool:
|
353
354
|
"""
|
354
|
-
Validate certificate file.
|
355
|
+
Validate certificate file with optional CRL check.
|
355
356
|
|
356
357
|
This method validates a certificate file by checking its format,
|
357
|
-
parsing it, and verifying basic certificate properties.
|
358
|
+
parsing it, and verifying basic certificate properties. If CRL
|
359
|
+
is provided, it also checks if the certificate is revoked.
|
358
360
|
|
359
361
|
Args:
|
360
362
|
cert_path (str): Path to certificate file to validate.
|
361
363
|
Must be a valid PEM or DER certificate file path.
|
364
|
+
crl_path (Optional[str]): Path to CRL file. If None, CRL check
|
365
|
+
is skipped. If provided, certificate revocation is checked.
|
362
366
|
|
363
367
|
Returns:
|
364
|
-
bool: True if certificate is valid, False otherwise.
|
365
|
-
Returns True when certificate can be parsed
|
366
|
-
|
368
|
+
bool: True if certificate is valid and not revoked, False otherwise.
|
369
|
+
Returns True when certificate can be parsed, has valid basic
|
370
|
+
properties, and is not revoked (if CRL is provided).
|
367
371
|
|
368
372
|
Raises:
|
369
373
|
FileNotFoundError: If certificate file is not found.
|
@@ -374,8 +378,9 @@ class SSLManager:
|
|
374
378
|
>>> is_valid = ssl_manager.validate_certificate("server.crt")
|
375
379
|
>>> if is_valid:
|
376
380
|
... print("Certificate is valid")
|
377
|
-
>>>
|
378
|
-
|
381
|
+
>>>
|
382
|
+
>>> # With CRL check
|
383
|
+
>>> is_valid = ssl_manager.validate_certificate("server.crt", "crl.pem")
|
379
384
|
"""
|
380
385
|
try:
|
381
386
|
# Check if file exists
|
@@ -402,8 +407,35 @@ class SSLManager:
|
|
402
407
|
)
|
403
408
|
return False
|
404
409
|
|
410
|
+
# Check CRL if provided
|
411
|
+
if crl_path:
|
412
|
+
try:
|
413
|
+
if is_certificate_revoked(cert_path, crl_path):
|
414
|
+
self.logger.warning(
|
415
|
+
"Certificate is revoked",
|
416
|
+
extra={
|
417
|
+
"cert_path": cert_path,
|
418
|
+
"crl_path": crl_path,
|
419
|
+
},
|
420
|
+
)
|
421
|
+
return False
|
422
|
+
except Exception as e:
|
423
|
+
self.logger.error(
|
424
|
+
"CRL validation failed",
|
425
|
+
extra={
|
426
|
+
"cert_path": cert_path,
|
427
|
+
"crl_path": crl_path,
|
428
|
+
"error": str(e),
|
429
|
+
},
|
430
|
+
)
|
431
|
+
return False
|
432
|
+
|
405
433
|
self.logger.info(
|
406
|
-
"Certificate validation successful",
|
434
|
+
"Certificate validation successful",
|
435
|
+
extra={
|
436
|
+
"cert_path": cert_path,
|
437
|
+
"crl_checked": crl_path is not None,
|
438
|
+
}
|
407
439
|
)
|
408
440
|
|
409
441
|
return True
|
@@ -223,9 +223,17 @@ class MTLSMiddleware(SecurityMiddleware):
|
|
223
223
|
Dict[str, Any]: Validation result with status and details
|
224
224
|
"""
|
225
225
|
try:
|
226
|
-
#
|
226
|
+
# Get CA certificate file
|
227
|
+
ca_cert_file = self.config.ssl.ca_cert_file if self.config.ssl else None
|
228
|
+
|
229
|
+
# Get CRL file if configured
|
230
|
+
crl_file = None
|
231
|
+
if self.config.ssl and hasattr(self.config.ssl, 'crl_file'):
|
232
|
+
crl_file = self.config.ssl.crl_file
|
233
|
+
|
234
|
+
# Use security manager's certificate validation with optional CRL check
|
227
235
|
is_valid = self.security_manager.cert_manager.validate_certificate_chain(
|
228
|
-
cert_pem,
|
236
|
+
cert_pem, ca_cert_file, crl_file
|
229
237
|
)
|
230
238
|
|
231
239
|
if is_valid:
|
@@ -33,6 +33,7 @@ License: MIT
|
|
33
33
|
from enum import Enum
|
34
34
|
from pathlib import Path
|
35
35
|
from typing import Any, Dict, List, Optional, Union
|
36
|
+
import uuid
|
36
37
|
|
37
38
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
38
39
|
from pydantic.types import SecretStr
|
@@ -599,6 +600,21 @@ class CAConfig(BaseModel):
|
|
599
600
|
hash_algorithm: str = Field(
|
600
601
|
default="sha256", description="Hash algorithm for signing"
|
601
602
|
)
|
603
|
+
unitid: Optional[str] = Field(
|
604
|
+
default=None, description="Unique unit identifier (UUID4) for the certificate"
|
605
|
+
)
|
606
|
+
|
607
|
+
@field_validator("unitid")
|
608
|
+
@classmethod
|
609
|
+
def validate_unitid(cls, v):
|
610
|
+
"""Validate unitid format."""
|
611
|
+
if v is not None:
|
612
|
+
try:
|
613
|
+
# Validate UUID4 format
|
614
|
+
uuid.UUID(v, version=4)
|
615
|
+
except ValueError:
|
616
|
+
raise ValueError("unitid must be a valid UUID4 string")
|
617
|
+
return v
|
602
618
|
|
603
619
|
|
604
620
|
class IntermediateCAConfig(CAConfig):
|
@@ -668,6 +684,21 @@ class ClientCertConfig(BaseModel):
|
|
668
684
|
)
|
669
685
|
ca_cert_path: str = Field(..., description="Path to signing CA certificate")
|
670
686
|
ca_key_path: str = Field(..., description="Path to signing CA private key")
|
687
|
+
unitid: Optional[str] = Field(
|
688
|
+
default=None, description="Unique unit identifier (UUID4) for the certificate"
|
689
|
+
)
|
690
|
+
|
691
|
+
@field_validator("unitid")
|
692
|
+
@classmethod
|
693
|
+
def validate_unitid(cls, v):
|
694
|
+
"""Validate unitid format."""
|
695
|
+
if v is not None:
|
696
|
+
try:
|
697
|
+
# Validate UUID4 format
|
698
|
+
uuid.UUID(v, version=4)
|
699
|
+
except ValueError:
|
700
|
+
raise ValueError("unitid must be a valid UUID4 string")
|
701
|
+
return v
|
671
702
|
|
672
703
|
|
673
704
|
class ServerCertConfig(ClientCertConfig):
|
@@ -38,6 +38,7 @@ License: MIT
|
|
38
38
|
from datetime import datetime, timedelta, timezone
|
39
39
|
from enum import Enum
|
40
40
|
from typing import Any, Dict, List, Optional, Set, TypeAlias
|
41
|
+
import uuid
|
41
42
|
|
42
43
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
43
44
|
|
@@ -140,6 +141,9 @@ class AuthResult(BaseModel):
|
|
140
141
|
metadata: Dict[str, Any] = Field(
|
141
142
|
default_factory=dict, description="Additional authentication metadata"
|
142
143
|
)
|
144
|
+
unitid: Optional[str] = Field(
|
145
|
+
default=None, description="Unique unit identifier (UUID4) from certificate"
|
146
|
+
)
|
143
147
|
|
144
148
|
@field_validator("username")
|
145
149
|
@classmethod
|
@@ -149,6 +153,18 @@ class AuthResult(BaseModel):
|
|
149
153
|
raise ValueError("Username cannot be empty")
|
150
154
|
return v
|
151
155
|
|
156
|
+
@field_validator("unitid")
|
157
|
+
@classmethod
|
158
|
+
def validate_unitid(cls, v):
|
159
|
+
"""Validate unitid format."""
|
160
|
+
if v is not None:
|
161
|
+
try:
|
162
|
+
# Validate UUID4 format
|
163
|
+
uuid.UUID(v, version=4)
|
164
|
+
except ValueError:
|
165
|
+
raise ValueError("unitid must be a valid UUID4 string")
|
166
|
+
return v
|
167
|
+
|
152
168
|
@model_validator(mode="after")
|
153
169
|
def validate_auth_result(self):
|
154
170
|
"""Validate authentication result consistency."""
|
@@ -309,6 +325,9 @@ class CertificateInfo(BaseModel):
|
|
309
325
|
fingerprint_sha256: Optional[str] = Field(
|
310
326
|
default=None, description="SHA256 fingerprint"
|
311
327
|
)
|
328
|
+
unitid: Optional[str] = Field(
|
329
|
+
default=None, description="Unique unit identifier (UUID4) for the certificate"
|
330
|
+
)
|
312
331
|
|
313
332
|
@field_validator("key_size")
|
314
333
|
@classmethod
|
@@ -318,6 +337,18 @@ class CertificateInfo(BaseModel):
|
|
318
337
|
raise ValueError("Key size must be between 512 and 8192 bits")
|
319
338
|
return v
|
320
339
|
|
340
|
+
@field_validator("unitid")
|
341
|
+
@classmethod
|
342
|
+
def validate_unitid(cls, v):
|
343
|
+
"""Validate unitid format."""
|
344
|
+
if v is not None:
|
345
|
+
try:
|
346
|
+
# Validate UUID4 format
|
347
|
+
uuid.UUID(v, version=4)
|
348
|
+
except ValueError:
|
349
|
+
raise ValueError("unitid must be a valid UUID4 string")
|
350
|
+
return v
|
351
|
+
|
321
352
|
@property
|
322
353
|
def is_expired(self) -> bool:
|
323
354
|
"""Check if certificate is expired."""
|
@@ -424,6 +455,9 @@ class CertificatePair(BaseModel):
|
|
424
455
|
metadata: Dict[str, Any] = Field(
|
425
456
|
default_factory=dict, description="Additional certificate metadata"
|
426
457
|
)
|
458
|
+
unitid: Optional[str] = Field(
|
459
|
+
default=None, description="Unique unit identifier (UUID4) for the certificate"
|
460
|
+
)
|
427
461
|
|
428
462
|
@field_validator("certificate_pem")
|
429
463
|
@classmethod
|
@@ -449,6 +483,18 @@ class CertificatePair(BaseModel):
|
|
449
483
|
raise ValueError("Invalid private key PEM format")
|
450
484
|
return v
|
451
485
|
|
486
|
+
@field_validator("unitid")
|
487
|
+
@classmethod
|
488
|
+
def validate_unitid(cls, v):
|
489
|
+
"""Validate unitid format."""
|
490
|
+
if v is not None:
|
491
|
+
try:
|
492
|
+
# Validate UUID4 format
|
493
|
+
uuid.UUID(v, version=4)
|
494
|
+
except ValueError:
|
495
|
+
raise ValueError("unitid must be a valid UUID4 string")
|
496
|
+
return v
|
497
|
+
|
452
498
|
@property
|
453
499
|
def is_expired(self) -> bool:
|
454
500
|
"""Check if certificate is expired."""
|
@@ -20,10 +20,15 @@ Functions:
|
|
20
20
|
validate_certificate_format: Validate certificate format
|
21
21
|
extract_roles_from_certificate: Extract roles from certificate
|
22
22
|
extract_permissions_from_certificate: Extract permissions from certificate
|
23
|
-
validate_certificate_chain: Validate certificate chain
|
23
|
+
validate_certificate_chain: Validate certificate chain with optional CRL check
|
24
24
|
get_certificate_expiry: Get certificate expiry information
|
25
25
|
convert_certificate_format: Convert between certificate formats
|
26
26
|
extract_public_key: Extract public key from certificate
|
27
|
+
parse_crl: Parse Certificate Revocation List from PEM/DER format
|
28
|
+
is_certificate_revoked: Check if certificate is revoked according to CRL
|
29
|
+
validate_certificate_against_crl: Validate certificate against CRL with details
|
30
|
+
is_crl_valid: Check if CRL is valid and not expired
|
31
|
+
get_crl_info: Get detailed information from CRL
|
27
32
|
|
28
33
|
Author: MCP Security Team
|
29
34
|
Version: 1.0.0
|
@@ -33,12 +38,12 @@ License: MIT
|
|
33
38
|
import base64
|
34
39
|
from datetime import datetime, timezone
|
35
40
|
from pathlib import Path
|
36
|
-
from typing import Dict, List, Union
|
41
|
+
from typing import Dict, List, Union, Optional
|
37
42
|
|
38
43
|
from cryptography import x509
|
39
44
|
from cryptography.hazmat.primitives import hashes, serialization
|
40
45
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
41
|
-
from cryptography.x509.oid import ExtensionOID, NameOID
|
46
|
+
from cryptography.x509.oid import ExtensionOID, NameOID, CRLEntryExtensionOID
|
42
47
|
|
43
48
|
from mcp_security_framework.utils.datetime_compat import (
|
44
49
|
get_not_valid_after_utc,
|
@@ -155,6 +160,15 @@ def extract_certificate_info(cert_data: Union[str, bytes, Path]) -> Dict:
|
|
155
160
|
if country:
|
156
161
|
info["country"] = country[0].value
|
157
162
|
|
163
|
+
# Extract unitid
|
164
|
+
try:
|
165
|
+
unitid = extract_unitid_from_certificate(cert_data)
|
166
|
+
if unitid:
|
167
|
+
info["unitid"] = unitid
|
168
|
+
except Exception:
|
169
|
+
# If unitid extraction fails, continue without it
|
170
|
+
pass
|
171
|
+
|
158
172
|
return info
|
159
173
|
except Exception as e:
|
160
174
|
raise CertificateError(f"Certificate information extraction failed: {str(e)}")
|
@@ -270,19 +284,66 @@ def extract_permissions_from_certificate(
|
|
270
284
|
raise CertificateError(f"Permission extraction failed: {str(e)}")
|
271
285
|
|
272
286
|
|
287
|
+
def extract_unitid_from_certificate(
|
288
|
+
cert_data: Union[str, bytes, Path],
|
289
|
+
) -> Optional[str]:
|
290
|
+
"""
|
291
|
+
Extract unitid from certificate extensions.
|
292
|
+
|
293
|
+
Args:
|
294
|
+
cert_data: Certificate data as string, bytes, or file path
|
295
|
+
|
296
|
+
Returns:
|
297
|
+
Unit ID (UUID4) found in certificate, or None if not found
|
298
|
+
|
299
|
+
Raises:
|
300
|
+
CertificateError: If unitid extraction fails
|
301
|
+
"""
|
302
|
+
try:
|
303
|
+
cert = parse_certificate(cert_data)
|
304
|
+
unitid = None
|
305
|
+
|
306
|
+
# Check for custom extension with unitid
|
307
|
+
try:
|
308
|
+
unitid_extension = cert.extensions.get_extension_for_oid(
|
309
|
+
x509.ObjectIdentifier("1.3.6.1.4.1.99999.1.3") # Custom unitid OID
|
310
|
+
)
|
311
|
+
if unitid_extension:
|
312
|
+
unitid_data = unitid_extension.value.value
|
313
|
+
if isinstance(unitid_data, bytes):
|
314
|
+
unitid_str = unitid_data.decode("utf-8")
|
315
|
+
unitid = unitid_str.strip()
|
316
|
+
|
317
|
+
# Validate UUID4 format
|
318
|
+
try:
|
319
|
+
import uuid
|
320
|
+
uuid.UUID(unitid, version=4)
|
321
|
+
except ValueError:
|
322
|
+
# Invalid UUID4 format, return None
|
323
|
+
unitid = None
|
324
|
+
except x509.extensions.ExtensionNotFound:
|
325
|
+
pass
|
326
|
+
|
327
|
+
return unitid
|
328
|
+
except Exception as e:
|
329
|
+
raise CertificateError(f"Unitid extraction failed: {str(e)}")
|
330
|
+
|
331
|
+
|
273
332
|
def validate_certificate_chain(
|
274
333
|
cert_data: Union[str, bytes, Path],
|
275
334
|
ca_cert_data: Union[str, bytes, Path, List[Union[str, bytes, Path]]],
|
335
|
+
crl_data: Optional[Union[str, bytes, Path]] = None,
|
276
336
|
) -> bool:
|
277
337
|
"""
|
278
|
-
Validate certificate chain against CA certificate(s).
|
338
|
+
Validate certificate chain against CA certificate(s) and optionally check CRL.
|
279
339
|
|
280
340
|
Args:
|
281
341
|
cert_data: Certificate data to validate
|
282
342
|
ca_cert_data: CA certificate data or list of CA certificates
|
343
|
+
crl_data: Optional CRL data to check for certificate revocation
|
283
344
|
|
284
345
|
Returns:
|
285
|
-
True if chain is valid, False otherwise
|
346
|
+
True if chain is valid and not revoked, False otherwise
|
286
347
|
|
287
348
|
Raises:
|
288
349
|
CertificateError: If validation fails
|
@@ -296,13 +357,28 @@ def validate_certificate_chain(
|
|
296
357
|
else:
|
297
358
|
ca_certs = [parse_certificate(ca_cert_data)]
|
298
359
|
|
299
|
-
#
|
360
|
+
# Check that the certificate was issued by one of the CA certificates
|
300
361
|
# This is a simplified validation - in a real scenario, you would use OpenSSL or similar
|
362
|
+
chain_valid = False
|
301
363
|
for ca_cert in ca_certs:
|
302
364
|
if cert.issuer == ca_cert.subject:
|
303
|
-
|
365
|
+
chain_valid = True
|
366
|
+
break
|
367
|
+
|
368
|
+
if not chain_valid:
|
369
|
+
return False
|
370
|
+
|
371
|
+
# If CRL is provided, check if certificate is revoked
|
372
|
+
if crl_data:
|
373
|
+
try:
|
374
|
+
if is_certificate_revoked(cert_data, crl_data):
|
375
|
+
return False
|
376
|
+
except Exception as e:
|
377
|
+
# If CRL check fails, we can choose to fail validation or continue
|
378
|
+
# For security, we'll fail validation if CRL check fails
|
379
|
+
raise CertificateError(f"CRL validation failed: {str(e)}")
|
304
380
|
|
305
|
-
return
|
381
|
+
return True
|
306
382
|
except Exception as e:
|
307
383
|
return False
|
308
384
|
|
@@ -529,3 +605,228 @@ def is_certificate_self_signed(cert_data: Union[str, bytes, Path]) -> bool:
|
|
529
605
|
return cert.subject == cert.issuer
|
530
606
|
except Exception as e:
|
531
607
|
raise CertificateError(f"Self-signed check failed: {str(e)}")
|
608
|
+
|
609
|
+
|
610
|
+
def parse_crl(crl_data: Union[str, bytes, Path]) -> x509.CertificateRevocationList:
|
611
|
+
"""
|
612
|
+
Parse Certificate Revocation List (CRL) from PEM or DER format.
|
613
|
+
|
614
|
+
Args:
|
615
|
+
crl_data: CRL data as string, bytes, or file path
|
616
|
+
|
617
|
+
Returns:
|
618
|
+
Parsed X.509 CRL object
|
619
|
+
|
620
|
+
Raises:
|
621
|
+
CertificateError: If CRL parsing fails
|
622
|
+
"""
|
623
|
+
try:
|
624
|
+
# Handle string input first (check if it's PEM data)
|
625
|
+
if isinstance(crl_data, str):
|
626
|
+
# Check if it looks like PEM data
|
627
|
+
if "-----BEGIN X509 CRL-----" in crl_data:
|
628
|
+
lines = crl_data.strip().split("\n")
|
629
|
+
crl_data = "".join(
|
630
|
+
line for line in lines if not line.startswith("-----")
|
631
|
+
)
|
632
|
+
crl_data = base64.b64decode(crl_data)
|
633
|
+
else:
|
634
|
+
# Try to treat as file path
|
635
|
+
try:
|
636
|
+
if Path(crl_data).exists():
|
637
|
+
with open(crl_data, "rb") as f:
|
638
|
+
crl_data = f.read()
|
639
|
+
else:
|
640
|
+
# Try to decode as base64
|
641
|
+
crl_data = base64.b64decode(crl_data)
|
642
|
+
except (OSError, ValueError):
|
643
|
+
# If file doesn't exist and not base64, try to decode anyway
|
644
|
+
crl_data = base64.b64decode(crl_data)
|
645
|
+
|
646
|
+
# Handle Path object
|
647
|
+
elif isinstance(crl_data, Path):
|
648
|
+
if crl_data.exists():
|
649
|
+
with open(crl_data, "rb") as f:
|
650
|
+
crl_data = f.read()
|
651
|
+
else:
|
652
|
+
raise CertificateError(f"CRL file not found: {crl_data}")
|
653
|
+
|
654
|
+
# Try to parse as PEM first, then as DER
|
655
|
+
try:
|
656
|
+
return x509.load_pem_x509_crl(crl_data)
|
657
|
+
except Exception:
|
658
|
+
return x509.load_der_x509_crl(crl_data)
|
659
|
+
except Exception as e:
|
660
|
+
raise CertificateError(f"CRL parsing failed: {str(e)}")
|
661
|
+
|
662
|
+
|
663
|
+
def is_certificate_revoked(
|
664
|
+
cert_data: Union[str, bytes, Path],
|
665
|
+
crl_data: Union[str, bytes, Path]
|
666
|
+
) -> bool:
|
667
|
+
"""
|
668
|
+
Check if certificate is revoked according to CRL.
|
669
|
+
|
670
|
+
Args:
|
671
|
+
cert_data: Certificate data to check
|
672
|
+
crl_data: CRL data to check against
|
673
|
+
|
674
|
+
Returns:
|
675
|
+
True if certificate is revoked, False otherwise
|
676
|
+
|
677
|
+
Raises:
|
678
|
+
CertificateError: If revocation check fails
|
679
|
+
"""
|
680
|
+
try:
|
681
|
+
cert = parse_certificate(cert_data)
|
682
|
+
crl = parse_crl(crl_data)
|
683
|
+
|
684
|
+
# Get certificate serial number
|
685
|
+
cert_serial = cert.serial_number
|
686
|
+
|
687
|
+
# Check if certificate serial number is in CRL
|
688
|
+
for revoked_cert in crl:
|
689
|
+
if revoked_cert.serial_number == cert_serial:
|
690
|
+
return True
|
691
|
+
|
692
|
+
return False
|
693
|
+
except Exception as e:
|
694
|
+
raise CertificateError(f"Certificate revocation check failed: {str(e)}")
|
695
|
+
|
696
|
+
|
697
|
+
def validate_certificate_against_crl(
|
698
|
+
cert_data: Union[str, bytes, Path],
|
699
|
+
crl_data: Union[str, bytes, Path]
|
700
|
+
) -> Dict[str, any]:
|
701
|
+
"""
|
702
|
+
Validate certificate against CRL and return detailed revocation status.
|
703
|
+
|
704
|
+
Args:
|
705
|
+
cert_data: Certificate data to validate
|
706
|
+
crl_data: CRL data to validate against
|
707
|
+
|
708
|
+
Returns:
|
709
|
+
Dictionary containing revocation status and details
|
710
|
+
|
711
|
+
Raises:
|
712
|
+
CertificateError: If validation fails
|
713
|
+
"""
|
714
|
+
try:
|
715
|
+
cert = parse_certificate(cert_data)
|
716
|
+
crl = parse_crl(crl_data)
|
717
|
+
|
718
|
+
# Get certificate serial number
|
719
|
+
cert_serial = cert.serial_number
|
720
|
+
|
721
|
+
# Check if certificate serial number is in CRL
|
722
|
+
for revoked_cert in crl:
|
723
|
+
if revoked_cert.serial_number == cert_serial:
|
724
|
+
# Extract revocation reason if available
|
725
|
+
revocation_reason = "unspecified"
|
726
|
+
try:
|
727
|
+
reason_ext = revoked_cert.extensions.get_extension_for_oid(
|
728
|
+
CRLEntryExtensionOID.CRL_REASON
|
729
|
+
)
|
730
|
+
if reason_ext:
|
731
|
+
revocation_reason = reason_ext.value.reason.name
|
732
|
+
except x509.extensions.ExtensionNotFound:
|
733
|
+
pass
|
734
|
+
|
735
|
+
return {
|
736
|
+
"is_revoked": True,
|
737
|
+
"serial_number": str(cert_serial),
|
738
|
+
"revocation_date": revoked_cert.revocation_date_utc,
|
739
|
+
"revocation_reason": revocation_reason,
|
740
|
+
"crl_issuer": str(crl.issuer),
|
741
|
+
"crl_last_update": crl.last_update_utc,
|
742
|
+
"crl_next_update": crl.next_update_utc
|
743
|
+
}
|
744
|
+
|
745
|
+
return {
|
746
|
+
"is_revoked": False,
|
747
|
+
"serial_number": str(cert_serial),
|
748
|
+
"crl_issuer": str(crl.issuer),
|
749
|
+
"crl_last_update": crl.last_update_utc,
|
750
|
+
"crl_next_update": crl.next_update_utc
|
751
|
+
}
|
752
|
+
except Exception as e:
|
753
|
+
raise CertificateError(f"Certificate CRL validation failed: {str(e)}")
|
754
|
+
|
755
|
+
|
756
|
+
def is_crl_valid(crl_data: Union[str, bytes, Path]) -> bool:
|
757
|
+
"""
|
758
|
+
Check if CRL is valid (not expired and properly formatted).
|
759
|
+
|
760
|
+
Args:
|
761
|
+
crl_data: CRL data to validate
|
762
|
+
|
763
|
+
Returns:
|
764
|
+
True if CRL is valid, False otherwise
|
765
|
+
|
766
|
+
Raises:
|
767
|
+
CertificateError: If CRL validation fails
|
768
|
+
"""
|
769
|
+
try:
|
770
|
+
crl = parse_crl(crl_data)
|
771
|
+
now = datetime.now(timezone.utc)
|
772
|
+
|
773
|
+
# Check if CRL is expired
|
774
|
+
if crl.next_update_utc and crl.next_update_utc < now:
|
775
|
+
return False
|
776
|
+
|
777
|
+
# Check if CRL is not yet valid
|
778
|
+
if crl.last_update_utc and crl.last_update_utc > now:
|
779
|
+
return False
|
780
|
+
|
781
|
+
return True
|
782
|
+
except Exception as e:
|
783
|
+
raise CertificateError(f"CRL validation failed: {str(e)}")
|
784
|
+
|
785
|
+
|
786
|
+
def get_crl_info(crl_data: Union[str, bytes, Path]) -> Dict:
|
787
|
+
"""
|
788
|
+
Get detailed information from CRL.
|
789
|
+
|
790
|
+
Args:
|
791
|
+
crl_data: CRL data to analyze
|
792
|
+
|
793
|
+
Returns:
|
794
|
+
Dictionary containing CRL information
|
795
|
+
|
796
|
+
Raises:
|
797
|
+
CertificateError: If CRL information extraction fails
|
798
|
+
"""
|
799
|
+
try:
|
800
|
+
crl = parse_crl(crl_data)
|
801
|
+
now = datetime.now(timezone.utc)
|
802
|
+
|
803
|
+
# Calculate time until CRL expiry
|
804
|
+
time_until_expiry = crl.next_update_utc - now if crl.next_update_utc else None
|
805
|
+
days_until_expiry = time_until_expiry.days if time_until_expiry else None
|
806
|
+
|
807
|
+
# Determine CRL status
|
808
|
+
if crl.next_update_utc and crl.next_update_utc < now:
|
809
|
+
status = "expired"
|
810
|
+
elif days_until_expiry and days_until_expiry <= 7:
|
811
|
+
status = "expires_soon"
|
812
|
+
else:
|
813
|
+
status = "valid"
|
814
|
+
|
815
|
+
# Count revoked certificates
|
816
|
+
revoked_count = len(list(crl))
|
817
|
+
|
818
|
+
return {
|
819
|
+
"issuer": str(crl.issuer),
|
820
|
+
"last_update": crl.last_update_utc,
|
821
|
+
"next_update": crl.next_update_utc,
|
822
|
+
"revoked_certificates_count": revoked_count,
|
823
|
+
"days_until_expiry": days_until_expiry,
|
824
|
+
"is_expired": crl.next_update_utc < now if crl.next_update_utc else False,
|
825
|
+
"expires_soon": days_until_expiry <= 7 if days_until_expiry else False,
|
826
|
+
"status": status,
|
827
|
+
"version": "v2", # CRL version is typically v2
|
828
|
+
"signature_algorithm": crl.signature_algorithm_oid._name,
|
829
|
+
"signature": crl.signature.hex(),
|
830
|
+
}
|
831
|
+
except Exception as e:
|
832
|
+
raise CertificateError(f"CRL information extraction failed: {str(e)}")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: mcp-security-framework
|
3
|
-
Version: 1.1
|
3
|
+
Version: 1.2.1
|
4
4
|
Summary: Universal security framework for microservices with SSL/TLS, authentication, authorization, and rate limiting. Requires cryptography>=42.0.0 for certificate operations.
|
5
5
|
Author-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
|
6
6
|
Maintainer-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
|