mcp-security-framework 1.1.1__py3-none-any.whl → 1.2.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/cli/cert_cli.py +167 -3
- mcp_security_framework/core/auth_manager.py +3 -1
- mcp_security_framework/core/cert_manager.py +241 -6
- mcp_security_framework/core/ssl_manager.py +41 -9
- mcp_security_framework/middleware/mtls_middleware.py +10 -2
- mcp_security_framework/utils/cert_utils.py +255 -8
- {mcp_security_framework-1.1.1.dist-info → mcp_security_framework-1.2.0.dist-info}/METADATA +3 -3
- {mcp_security_framework-1.1.1.dist-info → mcp_security_framework-1.2.0.dist-info}/RECORD +12 -12
- tests/test_utils/test_cert_utils.py +168 -0
- {mcp_security_framework-1.1.1.dist-info → mcp_security_framework-1.2.0.dist-info}/WHEEL +0 -0
- {mcp_security_framework-1.1.1.dist-info → mcp_security_framework-1.2.0.dist-info}/entry_points.txt +0 -0
- {mcp_security_framework-1.1.1.dist-info → mcp_security_framework-1.2.0.dist-info}/top_level.txt +0 -0
@@ -314,12 +314,18 @@ def create_client(
|
|
314
314
|
type=click.Path(exists=True),
|
315
315
|
help="Path to CA certificate for validation",
|
316
316
|
)
|
317
|
+
@click.option(
|
318
|
+
"--crl",
|
319
|
+
type=click.Path(exists=True),
|
320
|
+
help="Path to CRL file for revocation check",
|
321
|
+
)
|
317
322
|
@click.pass_context
|
318
|
-
def validate(ctx, cert_path: str, ca_cert: Optional[str]):
|
323
|
+
def validate(ctx, cert_path: str, ca_cert: Optional[str], crl: Optional[str]):
|
319
324
|
"""
|
320
325
|
Validate a certificate.
|
321
326
|
|
322
|
-
This command validates a certificate and optionally checks it against a CA
|
327
|
+
This command validates a certificate and optionally checks it against a CA
|
328
|
+
and CRL for revocation status.
|
323
329
|
"""
|
324
330
|
try:
|
325
331
|
config = ctx.obj["config"]
|
@@ -330,12 +336,16 @@ def validate(ctx, cert_path: str, ca_cert: Optional[str]):
|
|
330
336
|
click.echo(f"Validating certificate: {cert_path}")
|
331
337
|
if ca_cert:
|
332
338
|
click.echo(f"Using CA certificate: {ca_cert}")
|
339
|
+
if crl:
|
340
|
+
click.echo(f"Using CRL file: {crl}")
|
333
341
|
|
334
342
|
# Validate certificate
|
335
|
-
is_valid = cert_manager.validate_certificate_chain(cert_path, ca_cert)
|
343
|
+
is_valid = cert_manager.validate_certificate_chain(cert_path, ca_cert, crl)
|
336
344
|
|
337
345
|
if is_valid:
|
338
346
|
click.echo(f"✅ Certificate is valid!")
|
347
|
+
if crl:
|
348
|
+
click.echo(f"✅ Certificate is not revoked according to CRL")
|
339
349
|
else:
|
340
350
|
click.echo(f"❌ Certificate validation failed!", err=True)
|
341
351
|
raise click.Abort()
|
@@ -543,5 +553,159 @@ def revoke(ctx, serial_number: str, reason: str):
|
|
543
553
|
raise click.Abort()
|
544
554
|
|
545
555
|
|
556
|
+
@cert_cli.command()
|
557
|
+
@click.argument("cert_path", type=click.Path(exists=True))
|
558
|
+
@click.option(
|
559
|
+
"--crl",
|
560
|
+
type=click.Path(exists=True),
|
561
|
+
help="Path to CRL file for revocation check",
|
562
|
+
)
|
563
|
+
@click.pass_context
|
564
|
+
def check_revocation(ctx, cert_path: str, crl: Optional[str]):
|
565
|
+
"""
|
566
|
+
Check if certificate is revoked according to CRL.
|
567
|
+
|
568
|
+
This command checks if a certificate is revoked according to the provided CRL.
|
569
|
+
"""
|
570
|
+
try:
|
571
|
+
config = ctx.obj["config"]
|
572
|
+
cert_manager = ctx.obj["cert_manager"]
|
573
|
+
verbose = ctx.obj["verbose"]
|
574
|
+
|
575
|
+
if verbose:
|
576
|
+
click.echo(f"Checking revocation status for certificate: {cert_path}")
|
577
|
+
if crl:
|
578
|
+
click.echo(f"Using CRL file: {crl}")
|
579
|
+
|
580
|
+
# Check if certificate is revoked
|
581
|
+
is_revoked = cert_manager.is_certificate_revoked(cert_path, crl)
|
582
|
+
|
583
|
+
if is_revoked:
|
584
|
+
click.echo(f"❌ Certificate is REVOKED!", err=True)
|
585
|
+
else:
|
586
|
+
click.echo(f"✅ Certificate is NOT revoked")
|
587
|
+
|
588
|
+
except Exception as e:
|
589
|
+
click.echo(f"❌ Failed to check revocation status: {str(e)}", err=True)
|
590
|
+
raise click.Abort()
|
591
|
+
|
592
|
+
|
593
|
+
@cert_cli.command()
|
594
|
+
@click.argument("cert_path", type=click.Path(exists=True))
|
595
|
+
@click.option(
|
596
|
+
"--crl",
|
597
|
+
type=click.Path(exists=True),
|
598
|
+
help="Path to CRL file for detailed revocation check",
|
599
|
+
)
|
600
|
+
@click.pass_context
|
601
|
+
def revocation_info(ctx, cert_path: str, crl: Optional[str]):
|
602
|
+
"""
|
603
|
+
Get detailed revocation information for certificate.
|
604
|
+
|
605
|
+
This command provides detailed revocation information including
|
606
|
+
revocation date, reason, and CRL details.
|
607
|
+
"""
|
608
|
+
try:
|
609
|
+
config = ctx.obj["config"]
|
610
|
+
cert_manager = ctx.obj["cert_manager"]
|
611
|
+
verbose = ctx.obj["verbose"]
|
612
|
+
|
613
|
+
if verbose:
|
614
|
+
click.echo(f"Getting revocation information for certificate: {cert_path}")
|
615
|
+
if crl:
|
616
|
+
click.echo(f"Using CRL file: {crl}")
|
617
|
+
|
618
|
+
# Get detailed revocation information
|
619
|
+
revocation_info = cert_manager.validate_certificate_against_crl(cert_path, crl)
|
620
|
+
|
621
|
+
click.echo(f"Certificate Serial Number: {revocation_info['serial_number']}")
|
622
|
+
click.echo(f"CRL Issuer: {revocation_info['crl_issuer']}")
|
623
|
+
click.echo(f"CRL Last Update: {revocation_info['crl_last_update']}")
|
624
|
+
click.echo(f"CRL Next Update: {revocation_info['crl_next_update']}")
|
625
|
+
|
626
|
+
if revocation_info["is_revoked"]:
|
627
|
+
click.echo(f"❌ Certificate is REVOKED!", err=True)
|
628
|
+
click.echo(f"Revocation Date: {revocation_info['revocation_date']}")
|
629
|
+
click.echo(f"Revocation Reason: {revocation_info['revocation_reason']}")
|
630
|
+
else:
|
631
|
+
click.echo(f"✅ Certificate is NOT revoked")
|
632
|
+
|
633
|
+
except Exception as e:
|
634
|
+
click.echo(f"❌ Failed to get revocation information: {str(e)}", err=True)
|
635
|
+
raise click.Abort()
|
636
|
+
|
637
|
+
|
638
|
+
@cert_cli.command()
|
639
|
+
@click.argument("crl_path", type=click.Path(exists=True))
|
640
|
+
@click.pass_context
|
641
|
+
def crl_info(ctx, crl_path: str):
|
642
|
+
"""
|
643
|
+
Display CRL information.
|
644
|
+
|
645
|
+
This command displays detailed information about a CRL including
|
646
|
+
issuer, validity period, and revoked certificate count.
|
647
|
+
"""
|
648
|
+
try:
|
649
|
+
config = ctx.obj["config"]
|
650
|
+
cert_manager = ctx.obj["cert_manager"]
|
651
|
+
verbose = ctx.obj["verbose"]
|
652
|
+
|
653
|
+
if verbose:
|
654
|
+
click.echo(f"Getting CRL information: {crl_path}")
|
655
|
+
|
656
|
+
# Get CRL information
|
657
|
+
crl_info = cert_manager.get_crl_info(crl_path)
|
658
|
+
|
659
|
+
click.echo(f"CRL Issuer: {crl_info['issuer']}")
|
660
|
+
click.echo(f"Last Update: {crl_info['last_update']}")
|
661
|
+
click.echo(f"Next Update: {crl_info['next_update']}")
|
662
|
+
click.echo(f"Revoked Certificates: {crl_info['revoked_certificates_count']}")
|
663
|
+
click.echo(f"Status: {crl_info['status']}")
|
664
|
+
click.echo(f"Version: {crl_info['version']}")
|
665
|
+
click.echo(f"Signature Algorithm: {crl_info['signature_algorithm']}")
|
666
|
+
|
667
|
+
if crl_info["is_expired"]:
|
668
|
+
click.echo(f"❌ CRL is EXPIRED!", err=True)
|
669
|
+
elif crl_info["expires_soon"]:
|
670
|
+
click.echo(f"⚠️ CRL expires soon ({crl_info['days_until_expiry']} days)", err=True)
|
671
|
+
else:
|
672
|
+
click.echo(f"✅ CRL is valid")
|
673
|
+
|
674
|
+
except Exception as e:
|
675
|
+
click.echo(f"❌ Failed to get CRL information: {str(e)}", err=True)
|
676
|
+
raise click.Abort()
|
677
|
+
|
678
|
+
|
679
|
+
@cert_cli.command()
|
680
|
+
@click.argument("crl_path", type=click.Path(exists=True))
|
681
|
+
@click.pass_context
|
682
|
+
def validate_crl(ctx, crl_path: str):
|
683
|
+
"""
|
684
|
+
Validate CRL file.
|
685
|
+
|
686
|
+
This command validates a CRL file for format and validity period.
|
687
|
+
"""
|
688
|
+
try:
|
689
|
+
config = ctx.obj["config"]
|
690
|
+
cert_manager = ctx.obj["cert_manager"]
|
691
|
+
verbose = ctx.obj["verbose"]
|
692
|
+
|
693
|
+
if verbose:
|
694
|
+
click.echo(f"Validating CRL: {crl_path}")
|
695
|
+
|
696
|
+
# Validate CRL
|
697
|
+
is_valid = cert_manager.is_crl_valid(crl_path)
|
698
|
+
|
699
|
+
if is_valid:
|
700
|
+
click.echo(f"✅ CRL is valid!")
|
701
|
+
else:
|
702
|
+
click.echo(f"❌ CRL validation failed!", err=True)
|
703
|
+
raise click.Abort()
|
704
|
+
|
705
|
+
except Exception as e:
|
706
|
+
click.echo(f"❌ CRL validation failed: {str(e)}", err=True)
|
707
|
+
raise click.Abort()
|
708
|
+
|
709
|
+
|
546
710
|
if __name__ == "__main__":
|
547
711
|
cert_cli()
|
@@ -628,8 +628,10 @@ class AuthManager:
|
|
628
628
|
# Validate certificate chain if CA is configured
|
629
629
|
if self.config.ca_cert_file:
|
630
630
|
try:
|
631
|
+
# Check if CRL is configured for certificate validation
|
632
|
+
crl_file = getattr(self.config, 'crl_file', None)
|
631
633
|
is_valid_chain = validate_certificate_chain(
|
632
|
-
cert_pem, self.config.ca_cert_file
|
634
|
+
cert_pem, self.config.ca_cert_file, crl_file
|
633
635
|
)
|
634
636
|
if not is_valid_chain:
|
635
637
|
return AuthResult(
|
@@ -56,8 +56,12 @@ from mcp_security_framework.utils.cert_utils import (
|
|
56
56
|
extract_roles_from_certificate,
|
57
57
|
get_certificate_expiry,
|
58
58
|
get_certificate_serial_number,
|
59
|
+
get_crl_info,
|
60
|
+
is_certificate_revoked,
|
59
61
|
is_certificate_self_signed,
|
62
|
+
is_crl_valid,
|
60
63
|
parse_certificate,
|
64
|
+
validate_certificate_against_crl,
|
61
65
|
validate_certificate_chain,
|
62
66
|
)
|
63
67
|
from mcp_security_framework.utils.datetime_compat import (
|
@@ -1327,21 +1331,27 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
|
|
1327
1331
|
return False
|
1328
1332
|
|
1329
1333
|
def validate_certificate_chain(
|
1330
|
-
self,
|
1334
|
+
self,
|
1335
|
+
cert_path: str,
|
1336
|
+
ca_cert_path: Optional[str] = None,
|
1337
|
+
crl_path: Optional[str] = None
|
1331
1338
|
) -> bool:
|
1332
1339
|
"""
|
1333
|
-
Validate certificate chain against CA.
|
1340
|
+
Validate certificate chain against CA and optionally check CRL.
|
1334
1341
|
|
1335
1342
|
This method validates a certificate chain by checking the certificate
|
1336
|
-
against the CA certificate and verifying the chain of trust.
|
1343
|
+
against the CA certificate and verifying the chain of trust. If CRL
|
1344
|
+
is provided, it also checks if the certificate is revoked.
|
1337
1345
|
|
1338
1346
|
Args:
|
1339
1347
|
cert_path (str): Path to certificate to validate
|
1340
1348
|
ca_cert_path (Optional[str]): Path to CA certificate. If None,
|
1341
1349
|
uses CA certificate from configuration.
|
1350
|
+
crl_path (Optional[str]): Path to CRL file. If None, CRL check
|
1351
|
+
is skipped. If provided, certificate revocation is checked.
|
1342
1352
|
|
1343
1353
|
Returns:
|
1344
|
-
bool: True if certificate chain is valid, False otherwise
|
1354
|
+
bool: True if certificate chain is valid and not revoked, False otherwise
|
1345
1355
|
|
1346
1356
|
Raises:
|
1347
1357
|
FileNotFoundError: When certificate files are not found
|
@@ -1352,6 +1362,11 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
|
|
1352
1362
|
>>> is_valid = cert_manager.validate_certificate_chain("client.crt")
|
1353
1363
|
>>> if is_valid:
|
1354
1364
|
... print("Certificate chain is valid")
|
1365
|
+
>>>
|
1366
|
+
>>> # With CRL check
|
1367
|
+
>>> is_valid = cert_manager.validate_certificate_chain(
|
1368
|
+
... "client.crt", crl_path="crl.pem"
|
1369
|
+
... )
|
1355
1370
|
"""
|
1356
1371
|
try:
|
1357
1372
|
# Use configured CA certificate if not provided
|
@@ -1361,8 +1376,12 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
|
|
1361
1376
|
if not ca_cert_path:
|
1362
1377
|
raise CertificateConfigurationError("CA certificate path is required")
|
1363
1378
|
|
1364
|
-
#
|
1365
|
-
|
1379
|
+
# Use configured CRL path if not provided
|
1380
|
+
if not crl_path and self.config.crl_enabled:
|
1381
|
+
crl_path = self.config.crl_path
|
1382
|
+
|
1383
|
+
# Validate certificate chain with optional CRL check
|
1384
|
+
return validate_certificate_chain(cert_path, ca_cert_path, crl_path)
|
1366
1385
|
|
1367
1386
|
except Exception as e:
|
1368
1387
|
self.logger.error(
|
@@ -1370,6 +1389,7 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
|
|
1370
1389
|
extra={
|
1371
1390
|
"cert_path": cert_path,
|
1372
1391
|
"ca_cert_path": ca_cert_path,
|
1392
|
+
"crl_path": crl_path,
|
1373
1393
|
"error": str(e),
|
1374
1394
|
},
|
1375
1395
|
)
|
@@ -1929,6 +1949,221 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
|
|
1929
1949
|
f"CA private key file not found: {self.config.ca_key_path}"
|
1930
1950
|
)
|
1931
1951
|
|
1952
|
+
def validate_certificate_against_crl(
|
1953
|
+
self,
|
1954
|
+
cert_path: str,
|
1955
|
+
crl_path: Optional[str] = None
|
1956
|
+
) -> Dict[str, any]:
|
1957
|
+
"""
|
1958
|
+
Validate certificate against CRL and return detailed revocation status.
|
1959
|
+
|
1960
|
+
This method checks if a certificate is revoked according to the
|
1961
|
+
provided CRL and returns detailed revocation information.
|
1962
|
+
|
1963
|
+
Args:
|
1964
|
+
cert_path (str): Path to certificate to validate
|
1965
|
+
crl_path (Optional[str]): Path to CRL file. If None, uses CRL
|
1966
|
+
from configuration if CRL is enabled.
|
1967
|
+
|
1968
|
+
Returns:
|
1969
|
+
Dict[str, any]: Dictionary containing revocation status and details:
|
1970
|
+
- is_revoked (bool): True if certificate is revoked
|
1971
|
+
- serial_number (str): Certificate serial number
|
1972
|
+
- revocation_date (datetime): Date of revocation (if revoked)
|
1973
|
+
- revocation_reason (str): Reason for revocation (if revoked)
|
1974
|
+
- crl_issuer (str): CRL issuer information
|
1975
|
+
- crl_last_update (datetime): CRL last update time
|
1976
|
+
- crl_next_update (datetime): CRL next update time
|
1977
|
+
|
1978
|
+
Raises:
|
1979
|
+
CertificateConfigurationError: When CRL configuration is invalid
|
1980
|
+
CertificateValidationError: When CRL validation fails
|
1981
|
+
|
1982
|
+
Example:
|
1983
|
+
>>> cert_manager = CertificateManager(config)
|
1984
|
+
>>> result = cert_manager.validate_certificate_against_crl("client.crt")
|
1985
|
+
>>> if result["is_revoked"]:
|
1986
|
+
... print(f"Certificate revoked: {result['revocation_reason']}")
|
1987
|
+
"""
|
1988
|
+
try:
|
1989
|
+
# Use configured CRL path if not provided
|
1990
|
+
if not crl_path:
|
1991
|
+
if not self.config.crl_enabled:
|
1992
|
+
raise CertificateConfigurationError("CRL is not enabled in configuration")
|
1993
|
+
crl_path = self.config.crl_path
|
1994
|
+
|
1995
|
+
if not crl_path:
|
1996
|
+
raise CertificateConfigurationError("CRL path is required")
|
1997
|
+
|
1998
|
+
# Validate certificate against CRL
|
1999
|
+
return validate_certificate_against_crl(cert_path, crl_path)
|
2000
|
+
|
2001
|
+
except Exception as e:
|
2002
|
+
self.logger.error(
|
2003
|
+
"Certificate CRL validation failed",
|
2004
|
+
extra={
|
2005
|
+
"cert_path": cert_path,
|
2006
|
+
"crl_path": crl_path,
|
2007
|
+
"error": str(e),
|
2008
|
+
},
|
2009
|
+
)
|
2010
|
+
raise CertificateValidationError(f"CRL validation failed: {str(e)}")
|
2011
|
+
|
2012
|
+
def is_certificate_revoked(
|
2013
|
+
self,
|
2014
|
+
cert_path: str,
|
2015
|
+
crl_path: Optional[str] = None
|
2016
|
+
) -> bool:
|
2017
|
+
"""
|
2018
|
+
Check if certificate is revoked according to CRL.
|
2019
|
+
|
2020
|
+
This method provides a simple boolean check for certificate revocation
|
2021
|
+
without detailed revocation information.
|
2022
|
+
|
2023
|
+
Args:
|
2024
|
+
cert_path (str): Path to certificate to check
|
2025
|
+
crl_path (Optional[str]): Path to CRL file. If None, uses CRL
|
2026
|
+
from configuration if CRL is enabled.
|
2027
|
+
|
2028
|
+
Returns:
|
2029
|
+
bool: True if certificate is revoked, False otherwise
|
2030
|
+
|
2031
|
+
Raises:
|
2032
|
+
CertificateConfigurationError: When CRL configuration is invalid
|
2033
|
+
CertificateValidationError: When CRL validation fails
|
2034
|
+
|
2035
|
+
Example:
|
2036
|
+
>>> cert_manager = CertificateManager(config)
|
2037
|
+
>>> if cert_manager.is_certificate_revoked("client.crt"):
|
2038
|
+
... print("Certificate is revoked")
|
2039
|
+
"""
|
2040
|
+
try:
|
2041
|
+
# Use configured CRL path if not provided
|
2042
|
+
if not crl_path:
|
2043
|
+
if not self.config.crl_enabled:
|
2044
|
+
raise CertificateConfigurationError("CRL is not enabled in configuration")
|
2045
|
+
crl_path = self.config.crl_path
|
2046
|
+
|
2047
|
+
if not crl_path:
|
2048
|
+
raise CertificateConfigurationError("CRL path is required")
|
2049
|
+
|
2050
|
+
# Check if certificate is revoked
|
2051
|
+
return is_certificate_revoked(cert_path, crl_path)
|
2052
|
+
|
2053
|
+
except Exception as e:
|
2054
|
+
self.logger.error(
|
2055
|
+
"Certificate revocation check failed",
|
2056
|
+
extra={
|
2057
|
+
"cert_path": cert_path,
|
2058
|
+
"crl_path": crl_path,
|
2059
|
+
"error": str(e),
|
2060
|
+
},
|
2061
|
+
)
|
2062
|
+
raise CertificateValidationError(f"Revocation check failed: {str(e)}")
|
2063
|
+
|
2064
|
+
def get_crl_info(self, crl_path: Optional[str] = None) -> Dict:
|
2065
|
+
"""
|
2066
|
+
Get detailed information from CRL.
|
2067
|
+
|
2068
|
+
This method extracts comprehensive information from a CRL including
|
2069
|
+
issuer details, validity period, and revoked certificate count.
|
2070
|
+
|
2071
|
+
Args:
|
2072
|
+
crl_path (Optional[str]): Path to CRL file. If None, uses CRL
|
2073
|
+
from configuration if CRL is enabled.
|
2074
|
+
|
2075
|
+
Returns:
|
2076
|
+
Dict: Dictionary containing CRL information:
|
2077
|
+
- issuer (str): CRL issuer information
|
2078
|
+
- last_update (datetime): CRL last update time
|
2079
|
+
- next_update (datetime): CRL next update time
|
2080
|
+
- revoked_certificates_count (int): Number of revoked certificates
|
2081
|
+
- days_until_expiry (int): Days until CRL expires
|
2082
|
+
- is_expired (bool): True if CRL is expired
|
2083
|
+
- expires_soon (bool): True if CRL expires within 7 days
|
2084
|
+
- status (str): CRL status (valid, expires_soon, expired)
|
2085
|
+
- version (str): CRL version
|
2086
|
+
- signature_algorithm (str): Signature algorithm used
|
2087
|
+
- signature (str): CRL signature in hex format
|
2088
|
+
|
2089
|
+
Raises:
|
2090
|
+
CertificateConfigurationError: When CRL configuration is invalid
|
2091
|
+
CertificateValidationError: When CRL information extraction fails
|
2092
|
+
|
2093
|
+
Example:
|
2094
|
+
>>> cert_manager = CertificateManager(config)
|
2095
|
+
>>> crl_info = cert_manager.get_crl_info()
|
2096
|
+
>>> print(f"CRL has {crl_info['revoked_certificates_count']} revoked certificates")
|
2097
|
+
"""
|
2098
|
+
try:
|
2099
|
+
# Use configured CRL path if not provided
|
2100
|
+
if not crl_path:
|
2101
|
+
if not self.config.crl_enabled:
|
2102
|
+
raise CertificateConfigurationError("CRL is not enabled in configuration")
|
2103
|
+
crl_path = self.config.crl_path
|
2104
|
+
|
2105
|
+
if not crl_path:
|
2106
|
+
raise CertificateConfigurationError("CRL path is required")
|
2107
|
+
|
2108
|
+
# Get CRL information
|
2109
|
+
return get_crl_info(crl_path)
|
2110
|
+
|
2111
|
+
except Exception as e:
|
2112
|
+
self.logger.error(
|
2113
|
+
"CRL information extraction failed",
|
2114
|
+
extra={
|
2115
|
+
"crl_path": crl_path,
|
2116
|
+
"error": str(e),
|
2117
|
+
},
|
2118
|
+
)
|
2119
|
+
raise CertificateValidationError(f"CRL information extraction failed: {str(e)}")
|
2120
|
+
|
2121
|
+
def is_crl_valid(self, crl_path: Optional[str] = None) -> bool:
|
2122
|
+
"""
|
2123
|
+
Check if CRL is valid (not expired and properly formatted).
|
2124
|
+
|
2125
|
+
This method validates CRL format and checks if it's within its
|
2126
|
+
validity period.
|
2127
|
+
|
2128
|
+
Args:
|
2129
|
+
crl_path (Optional[str]): Path to CRL file. If None, uses CRL
|
2130
|
+
from configuration if CRL is enabled.
|
2131
|
+
|
2132
|
+
Returns:
|
2133
|
+
bool: True if CRL is valid, False otherwise
|
2134
|
+
|
2135
|
+
Raises:
|
2136
|
+
CertificateConfigurationError: When CRL configuration is invalid
|
2137
|
+
CertificateValidationError: When CRL validation fails
|
2138
|
+
|
2139
|
+
Example:
|
2140
|
+
>>> cert_manager = CertificateManager(config)
|
2141
|
+
>>> if cert_manager.is_crl_valid():
|
2142
|
+
... print("CRL is valid")
|
2143
|
+
"""
|
2144
|
+
try:
|
2145
|
+
# Use configured CRL path if not provided
|
2146
|
+
if not crl_path:
|
2147
|
+
if not self.config.crl_enabled:
|
2148
|
+
raise CertificateConfigurationError("CRL is not enabled in configuration")
|
2149
|
+
crl_path = self.config.crl_path
|
2150
|
+
|
2151
|
+
if not crl_path:
|
2152
|
+
raise CertificateConfigurationError("CRL path is required")
|
2153
|
+
|
2154
|
+
# Check if CRL is valid
|
2155
|
+
return is_crl_valid(crl_path)
|
2156
|
+
|
2157
|
+
except Exception as e:
|
2158
|
+
self.logger.error(
|
2159
|
+
"CRL validation failed",
|
2160
|
+
extra={
|
2161
|
+
"crl_path": crl_path,
|
2162
|
+
"error": str(e),
|
2163
|
+
},
|
2164
|
+
)
|
2165
|
+
raise CertificateValidationError(f"CRL validation failed: {str(e)}")
|
2166
|
+
|
1932
2167
|
|
1933
2168
|
class CertificateConfigurationError(Exception):
|
1934
2169
|
"""Raised when certificate configuration is invalid."""
|
@@ -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:
|
@@ -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,
|
@@ -273,16 +278,18 @@ def extract_permissions_from_certificate(
|
|
273
278
|
def validate_certificate_chain(
|
274
279
|
cert_data: Union[str, bytes, Path],
|
275
280
|
ca_cert_data: Union[str, bytes, Path, List[Union[str, bytes, Path]]],
|
281
|
+
crl_data: Optional[Union[str, bytes, Path]] = None,
|
276
282
|
) -> bool:
|
277
283
|
"""
|
278
|
-
Validate certificate chain against CA certificate(s).
|
284
|
+
Validate certificate chain against CA certificate(s) and optionally check CRL.
|
279
285
|
|
280
286
|
Args:
|
281
287
|
cert_data: Certificate data to validate
|
282
288
|
ca_cert_data: CA certificate data or list of CA certificates
|
289
|
+
crl_data: Optional CRL data to check for certificate revocation
|
283
290
|
|
284
291
|
Returns:
|
285
|
-
True if chain is valid, False otherwise
|
292
|
+
True if chain is valid and not revoked, False otherwise
|
286
293
|
|
287
294
|
Raises:
|
288
295
|
CertificateError: If validation fails
|
@@ -296,13 +303,28 @@ def validate_certificate_chain(
|
|
296
303
|
else:
|
297
304
|
ca_certs = [parse_certificate(ca_cert_data)]
|
298
305
|
|
299
|
-
#
|
306
|
+
# Check that the certificate was issued by one of the CA certificates
|
300
307
|
# This is a simplified validation - in a real scenario, you would use OpenSSL or similar
|
308
|
+
chain_valid = False
|
301
309
|
for ca_cert in ca_certs:
|
302
310
|
if cert.issuer == ca_cert.subject:
|
303
|
-
|
311
|
+
chain_valid = True
|
312
|
+
break
|
313
|
+
|
314
|
+
if not chain_valid:
|
315
|
+
return False
|
316
|
+
|
317
|
+
# If CRL is provided, check if certificate is revoked
|
318
|
+
if crl_data:
|
319
|
+
try:
|
320
|
+
if is_certificate_revoked(cert_data, crl_data):
|
321
|
+
return False
|
322
|
+
except Exception as e:
|
323
|
+
# If CRL check fails, we can choose to fail validation or continue
|
324
|
+
# For security, we'll fail validation if CRL check fails
|
325
|
+
raise CertificateError(f"CRL validation failed: {str(e)}")
|
304
326
|
|
305
|
-
return
|
327
|
+
return True
|
306
328
|
except Exception as e:
|
307
329
|
return False
|
308
330
|
|
@@ -529,3 +551,228 @@ def is_certificate_self_signed(cert_data: Union[str, bytes, Path]) -> bool:
|
|
529
551
|
return cert.subject == cert.issuer
|
530
552
|
except Exception as e:
|
531
553
|
raise CertificateError(f"Self-signed check failed: {str(e)}")
|
554
|
+
|
555
|
+
|
556
|
+
def parse_crl(crl_data: Union[str, bytes, Path]) -> x509.CertificateRevocationList:
|
557
|
+
"""
|
558
|
+
Parse Certificate Revocation List (CRL) from PEM or DER format.
|
559
|
+
|
560
|
+
Args:
|
561
|
+
crl_data: CRL data as string, bytes, or file path
|
562
|
+
|
563
|
+
Returns:
|
564
|
+
Parsed X.509 CRL object
|
565
|
+
|
566
|
+
Raises:
|
567
|
+
CertificateError: If CRL parsing fails
|
568
|
+
"""
|
569
|
+
try:
|
570
|
+
# Handle string input first (check if it's PEM data)
|
571
|
+
if isinstance(crl_data, str):
|
572
|
+
# Check if it looks like PEM data
|
573
|
+
if "-----BEGIN X509 CRL-----" in crl_data:
|
574
|
+
lines = crl_data.strip().split("\n")
|
575
|
+
crl_data = "".join(
|
576
|
+
line for line in lines if not line.startswith("-----")
|
577
|
+
)
|
578
|
+
crl_data = base64.b64decode(crl_data)
|
579
|
+
else:
|
580
|
+
# Try to treat as file path
|
581
|
+
try:
|
582
|
+
if Path(crl_data).exists():
|
583
|
+
with open(crl_data, "rb") as f:
|
584
|
+
crl_data = f.read()
|
585
|
+
else:
|
586
|
+
# Try to decode as base64
|
587
|
+
crl_data = base64.b64decode(crl_data)
|
588
|
+
except (OSError, ValueError):
|
589
|
+
# If file doesn't exist and not base64, try to decode anyway
|
590
|
+
crl_data = base64.b64decode(crl_data)
|
591
|
+
|
592
|
+
# Handle Path object
|
593
|
+
elif isinstance(crl_data, Path):
|
594
|
+
if crl_data.exists():
|
595
|
+
with open(crl_data, "rb") as f:
|
596
|
+
crl_data = f.read()
|
597
|
+
else:
|
598
|
+
raise CertificateError(f"CRL file not found: {crl_data}")
|
599
|
+
|
600
|
+
# Try to parse as PEM first, then as DER
|
601
|
+
try:
|
602
|
+
return x509.load_pem_x509_crl(crl_data)
|
603
|
+
except Exception:
|
604
|
+
return x509.load_der_x509_crl(crl_data)
|
605
|
+
except Exception as e:
|
606
|
+
raise CertificateError(f"CRL parsing failed: {str(e)}")
|
607
|
+
|
608
|
+
|
609
|
+
def is_certificate_revoked(
|
610
|
+
cert_data: Union[str, bytes, Path],
|
611
|
+
crl_data: Union[str, bytes, Path]
|
612
|
+
) -> bool:
|
613
|
+
"""
|
614
|
+
Check if certificate is revoked according to CRL.
|
615
|
+
|
616
|
+
Args:
|
617
|
+
cert_data: Certificate data to check
|
618
|
+
crl_data: CRL data to check against
|
619
|
+
|
620
|
+
Returns:
|
621
|
+
True if certificate is revoked, False otherwise
|
622
|
+
|
623
|
+
Raises:
|
624
|
+
CertificateError: If revocation check fails
|
625
|
+
"""
|
626
|
+
try:
|
627
|
+
cert = parse_certificate(cert_data)
|
628
|
+
crl = parse_crl(crl_data)
|
629
|
+
|
630
|
+
# Get certificate serial number
|
631
|
+
cert_serial = cert.serial_number
|
632
|
+
|
633
|
+
# Check if certificate serial number is in CRL
|
634
|
+
for revoked_cert in crl:
|
635
|
+
if revoked_cert.serial_number == cert_serial:
|
636
|
+
return True
|
637
|
+
|
638
|
+
return False
|
639
|
+
except Exception as e:
|
640
|
+
raise CertificateError(f"Certificate revocation check failed: {str(e)}")
|
641
|
+
|
642
|
+
|
643
|
+
def validate_certificate_against_crl(
|
644
|
+
cert_data: Union[str, bytes, Path],
|
645
|
+
crl_data: Union[str, bytes, Path]
|
646
|
+
) -> Dict[str, any]:
|
647
|
+
"""
|
648
|
+
Validate certificate against CRL and return detailed revocation status.
|
649
|
+
|
650
|
+
Args:
|
651
|
+
cert_data: Certificate data to validate
|
652
|
+
crl_data: CRL data to validate against
|
653
|
+
|
654
|
+
Returns:
|
655
|
+
Dictionary containing revocation status and details
|
656
|
+
|
657
|
+
Raises:
|
658
|
+
CertificateError: If validation fails
|
659
|
+
"""
|
660
|
+
try:
|
661
|
+
cert = parse_certificate(cert_data)
|
662
|
+
crl = parse_crl(crl_data)
|
663
|
+
|
664
|
+
# Get certificate serial number
|
665
|
+
cert_serial = cert.serial_number
|
666
|
+
|
667
|
+
# Check if certificate serial number is in CRL
|
668
|
+
for revoked_cert in crl:
|
669
|
+
if revoked_cert.serial_number == cert_serial:
|
670
|
+
# Extract revocation reason if available
|
671
|
+
revocation_reason = "unspecified"
|
672
|
+
try:
|
673
|
+
reason_ext = revoked_cert.extensions.get_extension_for_oid(
|
674
|
+
CRLEntryExtensionOID.CRL_REASON
|
675
|
+
)
|
676
|
+
if reason_ext:
|
677
|
+
revocation_reason = reason_ext.value.reason.name
|
678
|
+
except x509.extensions.ExtensionNotFound:
|
679
|
+
pass
|
680
|
+
|
681
|
+
return {
|
682
|
+
"is_revoked": True,
|
683
|
+
"serial_number": str(cert_serial),
|
684
|
+
"revocation_date": revoked_cert.revocation_date_utc,
|
685
|
+
"revocation_reason": revocation_reason,
|
686
|
+
"crl_issuer": str(crl.issuer),
|
687
|
+
"crl_last_update": crl.last_update_utc,
|
688
|
+
"crl_next_update": crl.next_update_utc
|
689
|
+
}
|
690
|
+
|
691
|
+
return {
|
692
|
+
"is_revoked": False,
|
693
|
+
"serial_number": str(cert_serial),
|
694
|
+
"crl_issuer": str(crl.issuer),
|
695
|
+
"crl_last_update": crl.last_update_utc,
|
696
|
+
"crl_next_update": crl.next_update_utc
|
697
|
+
}
|
698
|
+
except Exception as e:
|
699
|
+
raise CertificateError(f"Certificate CRL validation failed: {str(e)}")
|
700
|
+
|
701
|
+
|
702
|
+
def is_crl_valid(crl_data: Union[str, bytes, Path]) -> bool:
|
703
|
+
"""
|
704
|
+
Check if CRL is valid (not expired and properly formatted).
|
705
|
+
|
706
|
+
Args:
|
707
|
+
crl_data: CRL data to validate
|
708
|
+
|
709
|
+
Returns:
|
710
|
+
True if CRL is valid, False otherwise
|
711
|
+
|
712
|
+
Raises:
|
713
|
+
CertificateError: If CRL validation fails
|
714
|
+
"""
|
715
|
+
try:
|
716
|
+
crl = parse_crl(crl_data)
|
717
|
+
now = datetime.now(timezone.utc)
|
718
|
+
|
719
|
+
# Check if CRL is expired
|
720
|
+
if crl.next_update_utc and crl.next_update_utc < now:
|
721
|
+
return False
|
722
|
+
|
723
|
+
# Check if CRL is not yet valid
|
724
|
+
if crl.last_update_utc and crl.last_update_utc > now:
|
725
|
+
return False
|
726
|
+
|
727
|
+
return True
|
728
|
+
except Exception as e:
|
729
|
+
raise CertificateError(f"CRL validation failed: {str(e)}")
|
730
|
+
|
731
|
+
|
732
|
+
def get_crl_info(crl_data: Union[str, bytes, Path]) -> Dict:
|
733
|
+
"""
|
734
|
+
Get detailed information from CRL.
|
735
|
+
|
736
|
+
Args:
|
737
|
+
crl_data: CRL data to analyze
|
738
|
+
|
739
|
+
Returns:
|
740
|
+
Dictionary containing CRL information
|
741
|
+
|
742
|
+
Raises:
|
743
|
+
CertificateError: If CRL information extraction fails
|
744
|
+
"""
|
745
|
+
try:
|
746
|
+
crl = parse_crl(crl_data)
|
747
|
+
now = datetime.now(timezone.utc)
|
748
|
+
|
749
|
+
# Calculate time until CRL expiry
|
750
|
+
time_until_expiry = crl.next_update_utc - now if crl.next_update_utc else None
|
751
|
+
days_until_expiry = time_until_expiry.days if time_until_expiry else None
|
752
|
+
|
753
|
+
# Determine CRL status
|
754
|
+
if crl.next_update_utc and crl.next_update_utc < now:
|
755
|
+
status = "expired"
|
756
|
+
elif days_until_expiry and days_until_expiry <= 7:
|
757
|
+
status = "expires_soon"
|
758
|
+
else:
|
759
|
+
status = "valid"
|
760
|
+
|
761
|
+
# Count revoked certificates
|
762
|
+
revoked_count = len(list(crl))
|
763
|
+
|
764
|
+
return {
|
765
|
+
"issuer": str(crl.issuer),
|
766
|
+
"last_update": crl.last_update_utc,
|
767
|
+
"next_update": crl.next_update_utc,
|
768
|
+
"revoked_certificates_count": revoked_count,
|
769
|
+
"days_until_expiry": days_until_expiry,
|
770
|
+
"is_expired": crl.next_update_utc < now if crl.next_update_utc else False,
|
771
|
+
"expires_soon": days_until_expiry <= 7 if days_until_expiry else False,
|
772
|
+
"status": status,
|
773
|
+
"version": "v2", # CRL version is typically v2
|
774
|
+
"signature_algorithm": crl.signature_algorithm_oid._name,
|
775
|
+
"signature": crl.signature.hex(),
|
776
|
+
}
|
777
|
+
except Exception as e:
|
778
|
+
raise CertificateError(f"CRL information extraction failed: {str(e)}")
|
@@ -1,7 +1,7 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: mcp-security-framework
|
3
|
-
Version: 1.
|
4
|
-
Summary: Universal security framework for microservices with SSL/TLS, authentication, authorization, and rate limiting
|
3
|
+
Version: 1.2.0
|
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>
|
7
7
|
License: MIT
|
@@ -26,7 +26,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
26
|
Classifier: Topic :: System :: Systems Administration :: Authentication/Directory
|
27
27
|
Requires-Python: >=3.8
|
28
28
|
Description-Content-Type: text/markdown
|
29
|
-
Requires-Dist: cryptography>=
|
29
|
+
Requires-Dist: cryptography>=42.0.0
|
30
30
|
Requires-Dist: pydantic<3.0.0,>=1.8.0
|
31
31
|
Requires-Dist: PyJWT>=2.0.0
|
32
32
|
Requires-Dist: click>=8.0.0
|
@@ -1,15 +1,15 @@
|
|
1
1
|
mcp_security_framework/__init__.py,sha256=TM8y71Navd_6woEab2cO07MefRsL0tRBItEYfVO7DS8,3172
|
2
2
|
mcp_security_framework/constants.py,sha256=k7NMSrgc83Cci8aoilybQxdC7jir7J-mVFE_EpqVrDk,5307
|
3
3
|
mcp_security_framework/cli/__init__.py,sha256=plpWdiWMp2dcLvUuGwXynRg5CDjz8YKnNTBn7lcta08,369
|
4
|
-
mcp_security_framework/cli/cert_cli.py,sha256=
|
4
|
+
mcp_security_framework/cli/cert_cli.py,sha256=LdZ3SYKM3e3dP5LsVR5Y0OENtlG0ENu64aHefHjuiN8,23818
|
5
5
|
mcp_security_framework/cli/security_cli.py,sha256=Thine_Zzfesz7j29y2k_XZFYUK5YSrhCc6w2FilgEiE,28486
|
6
6
|
mcp_security_framework/core/__init__.py,sha256=LiX8_M5qWiTXccJFjSLxup9emhklp-poq57SvznsKEg,1729
|
7
|
-
mcp_security_framework/core/auth_manager.py,sha256=
|
8
|
-
mcp_security_framework/core/cert_manager.py,sha256=
|
7
|
+
mcp_security_framework/core/auth_manager.py,sha256=6k-Bv7-P7K6TV1KDIzoJGTKYbJmrEUX8uCoZXrNT8Q4,39065
|
8
|
+
mcp_security_framework/core/cert_manager.py,sha256=FHwcUAKkTuCRcDANiEBSC3ggHYplDpKR_26qoZKWAdw,88001
|
9
9
|
mcp_security_framework/core/permission_manager.py,sha256=SADS_oXpwp9MhXHKJMCsvjEq8KWcz7vPYL05Yr-zfio,26478
|
10
10
|
mcp_security_framework/core/rate_limiter.py,sha256=6qjVBxK2YHouSxQuCcbr0PBpRqA5toQss_Ce178RElY,20682
|
11
11
|
mcp_security_framework/core/security_manager.py,sha256=mAF-5znqxin-MSSgXISB7t1kTkqHltEqGzzmlLAhRGs,37766
|
12
|
-
mcp_security_framework/core/ssl_manager.py,sha256=
|
12
|
+
mcp_security_framework/core/ssl_manager.py,sha256=SXuN5PMTAnMNz04CEKzHbxRKjzF-VqvS-QCFhV-wFeo,29133
|
13
13
|
mcp_security_framework/examples/__init__.py,sha256=nfYPVvIQ5wHuDkQvyCYDd1VjCsZMw9HnvjeUORqKyuQ,1915
|
14
14
|
mcp_security_framework/examples/comprehensive_example.py,sha256=6CXkqLFjWIQ2rRMYPnDrBdBuWFKbeu2gd2GI_SFVjds,35421
|
15
15
|
mcp_security_framework/examples/django_example.py,sha256=IHk-aHsah-cEHjvsngUx91lup1aRC8W9XHzK6jfOMdA,24628
|
@@ -25,7 +25,7 @@ mcp_security_framework/middleware/fastapi_auth_middleware.py,sha256=LWVEn90I1XpV
|
|
25
25
|
mcp_security_framework/middleware/fastapi_middleware.py,sha256=Ye0qJsEMwgeUqVJpqXgbjJJbbf-ZU-6SysMQPmQba-s,26658
|
26
26
|
mcp_security_framework/middleware/flask_auth_middleware.py,sha256=ubBlKO0ponOV_KuxkUK4xGcSoslXTaikrdsIZQtGeV0,20228
|
27
27
|
mcp_security_framework/middleware/flask_middleware.py,sha256=Ag0zYDKwlvU78LBQ-7Za14IYOAlEVZaXJPD0Qc4MUvA,20666
|
28
|
-
mcp_security_framework/middleware/mtls_middleware.py,sha256=
|
28
|
+
mcp_security_framework/middleware/mtls_middleware.py,sha256=WSyWIk1fCN96hkofODKj_kzLG0d7A0NmDnTr3HHZ5xw,14213
|
29
29
|
mcp_security_framework/middleware/rate_limit_middleware.py,sha256=deCwwigI0Pt7pBUnk2jDurI9ZyjujWTsexEWWndXm3g,13177
|
30
30
|
mcp_security_framework/middleware/security_middleware.py,sha256=PQ251Fr2UrYVPgGfhXq6QJyqK2tRk0WCIg9_FBvfVkg,16844
|
31
31
|
mcp_security_framework/schemas/__init__.py,sha256=lefkbRlbj2ICfasSj51MQ04o3z1YycnbnknSJCFfXbU,2590
|
@@ -34,7 +34,7 @@ mcp_security_framework/schemas/models.py,sha256=n-Ug8O1cpMeA0mIdOd9h1i3kkBZOJsCQ
|
|
34
34
|
mcp_security_framework/schemas/responses.py,sha256=nVXaqF5GTSprXTa_wiUEu38nvSw9WAXtKViAJNbO-Xg,23206
|
35
35
|
mcp_security_framework/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
36
36
|
mcp_security_framework/utils/__init__.py,sha256=wwwdmQYHTSz0Puvs9FD6aIKmWp3NFARe3JPWNH-b_wk,3098
|
37
|
-
mcp_security_framework/utils/cert_utils.py,sha256=
|
37
|
+
mcp_security_framework/utils/cert_utils.py,sha256=roxpUa2CqFHMLvkm8618Epac-U6_xUlNuV8bhEZoU9E,25865
|
38
38
|
mcp_security_framework/utils/crypto_utils.py,sha256=OH2V7_C3FjStxFTIXMUPfNXZuWG2-QjgoBrIH4Lv4p0,12392
|
39
39
|
mcp_security_framework/utils/datetime_compat.py,sha256=ool-xs-EevhuYygdzhiAenLAacLuZwGwjPkF43i-9gg,3859
|
40
40
|
mcp_security_framework/utils/validation_utils.py,sha256=e9BX3kw9gdXSmFsc7lmG-qnzSlK0-Ynn7Xs4uKHquF4,16279
|
@@ -73,12 +73,12 @@ tests/test_schemas/test_models.py,sha256=bBeZOPqveuVJuEi_BTVWdVsdj08JXJTEFwvBM4e
|
|
73
73
|
tests/test_schemas/test_responses.py,sha256=ZSbO7A3ThPBovTXO8PFF-2ONWAjJx2dMOoV2lQIfd8s,40774
|
74
74
|
tests/test_schemas/test_serialization.py,sha256=jCugAyrdD6Mw1U7Kxni9oTukarZmMMl6KUcl6cq_NTk,18599
|
75
75
|
tests/test_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
76
|
-
tests/test_utils/test_cert_utils.py,sha256=
|
76
|
+
tests/test_utils/test_cert_utils.py,sha256=yZGHPuJcjgSHFeT7gnMdsw6UYXmlGUiuHkErukOm8II,28238
|
77
77
|
tests/test_utils/test_crypto_utils.py,sha256=yEb4hzG6-irj2DPoXY0DUboJfbeR87ussgTuBpxLGz4,20737
|
78
78
|
tests/test_utils/test_datetime_compat.py,sha256=n8S4X5HN-_ejSNpgymDXRyZkmxhnyxwwjxFPdX23I40,5656
|
79
79
|
tests/test_utils/test_validation_utils.py,sha256=lus_wHJ2WyVnBGQ28S7dSv78uWcCIuLhn5uflJw-uGw,18569
|
80
|
-
mcp_security_framework-1.
|
81
|
-
mcp_security_framework-1.
|
82
|
-
mcp_security_framework-1.
|
83
|
-
mcp_security_framework-1.
|
84
|
-
mcp_security_framework-1.
|
80
|
+
mcp_security_framework-1.2.0.dist-info/METADATA,sha256=_mkuF75Qp_BjlrBlamvGIcP-7ejSCdJWHwVsvjZw6eQ,11771
|
81
|
+
mcp_security_framework-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
82
|
+
mcp_security_framework-1.2.0.dist-info/entry_points.txt,sha256=qBh92fVDmd1m2f3xeW0hTu3Ksg8QfGJyV8UEkdA2itg,142
|
83
|
+
mcp_security_framework-1.2.0.dist-info/top_level.txt,sha256=ifUiGrTDcD574MXSOoAN2rp2wpUvWlb4jD9LTUgDWCA,29
|
84
|
+
mcp_security_framework-1.2.0.dist-info/RECORD,,
|
@@ -32,8 +32,13 @@ from mcp_security_framework.utils.cert_utils import (
|
|
32
32
|
extract_roles_from_certificate,
|
33
33
|
get_certificate_expiry,
|
34
34
|
get_certificate_serial_number,
|
35
|
+
get_crl_info,
|
36
|
+
is_certificate_revoked,
|
35
37
|
is_certificate_self_signed,
|
38
|
+
is_crl_valid,
|
36
39
|
parse_certificate,
|
40
|
+
parse_crl,
|
41
|
+
validate_certificate_against_crl,
|
37
42
|
validate_certificate_chain,
|
38
43
|
validate_certificate_format,
|
39
44
|
validate_certificate_purpose,
|
@@ -508,3 +513,166 @@ class TestCertificateFormatConversion:
|
|
508
513
|
extract_public_key("invalid_cert_data")
|
509
514
|
|
510
515
|
assert "Public key extraction failed" in str(exc_info.value)
|
516
|
+
|
517
|
+
|
518
|
+
class TestCRLFunctionality:
|
519
|
+
"""Test suite for CRL (Certificate Revocation List) functionality."""
|
520
|
+
|
521
|
+
@staticmethod
|
522
|
+
def create_test_crl():
|
523
|
+
"""Create a test CRL for testing."""
|
524
|
+
# Generate private key for CRL issuer
|
525
|
+
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
526
|
+
|
527
|
+
# Create issuer name
|
528
|
+
issuer = x509.Name([
|
529
|
+
x509.NameAttribute(NameOID.COMMON_NAME, "Test CA"),
|
530
|
+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Test Organization"),
|
531
|
+
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
|
532
|
+
])
|
533
|
+
|
534
|
+
# Create CRL
|
535
|
+
now = datetime.now(timezone.utc)
|
536
|
+
crl = x509.CertificateRevocationListBuilder().issuer_name(
|
537
|
+
issuer
|
538
|
+
).last_update(
|
539
|
+
now
|
540
|
+
).next_update(
|
541
|
+
now + timedelta(days=30)
|
542
|
+
).add_extension(
|
543
|
+
x509.CRLNumber(1),
|
544
|
+
critical=False,
|
545
|
+
).sign(private_key, hashes.SHA256())
|
546
|
+
|
547
|
+
return crl, private_key
|
548
|
+
|
549
|
+
def test_parse_crl_success(self):
|
550
|
+
"""Test successful CRL parsing."""
|
551
|
+
crl, _ = self.create_test_crl()
|
552
|
+
crl_pem = crl.public_bytes(serialization.Encoding.PEM).decode('utf-8')
|
553
|
+
|
554
|
+
parsed_crl = parse_crl(crl_pem)
|
555
|
+
|
556
|
+
assert parsed_crl is not None
|
557
|
+
assert isinstance(parsed_crl, x509.CertificateRevocationList)
|
558
|
+
assert "Test CA" in str(parsed_crl.issuer)
|
559
|
+
|
560
|
+
def test_parse_crl_invalid_data(self):
|
561
|
+
"""Test CRL parsing with invalid data."""
|
562
|
+
with pytest.raises(CertificateError) as exc_info:
|
563
|
+
parse_crl("invalid_crl_data")
|
564
|
+
|
565
|
+
assert "CRL parsing failed" in str(exc_info.value)
|
566
|
+
|
567
|
+
def test_is_certificate_revoked_not_revoked(self):
|
568
|
+
"""Test certificate revocation check when certificate is not revoked."""
|
569
|
+
# Create test certificate and CRL
|
570
|
+
cert, _ = TestCertificateCreation.create_test_certificate()
|
571
|
+
cert_pem = cert.public_bytes(serialization.Encoding.PEM).decode('utf-8')
|
572
|
+
crl, _ = self.create_test_crl()
|
573
|
+
crl_pem = crl.public_bytes(serialization.Encoding.PEM).decode('utf-8')
|
574
|
+
|
575
|
+
# Certificate should not be revoked since it's not in the CRL
|
576
|
+
is_revoked = is_certificate_revoked(cert_pem, crl_pem)
|
577
|
+
assert is_revoked is False
|
578
|
+
|
579
|
+
def test_is_certificate_revoked_invalid_cert(self):
|
580
|
+
"""Test certificate revocation check with invalid certificate."""
|
581
|
+
crl, _ = self.create_test_crl()
|
582
|
+
crl_pem = crl.public_bytes(serialization.Encoding.PEM).decode('utf-8')
|
583
|
+
|
584
|
+
with pytest.raises(CertificateError) as exc_info:
|
585
|
+
is_certificate_revoked("invalid_cert", crl_pem)
|
586
|
+
|
587
|
+
assert "Certificate revocation check failed" in str(exc_info.value)
|
588
|
+
|
589
|
+
def test_validate_certificate_against_crl_not_revoked(self):
|
590
|
+
"""Test certificate validation against CRL when not revoked."""
|
591
|
+
# Create test certificate and CRL
|
592
|
+
cert, _ = TestCertificateCreation.create_test_certificate()
|
593
|
+
cert_pem = cert.public_bytes(serialization.Encoding.PEM).decode('utf-8')
|
594
|
+
crl, _ = self.create_test_crl()
|
595
|
+
crl_pem = crl.public_bytes(serialization.Encoding.PEM).decode('utf-8')
|
596
|
+
|
597
|
+
result = validate_certificate_against_crl(cert_pem, crl_pem)
|
598
|
+
|
599
|
+
assert result["is_revoked"] is False
|
600
|
+
assert "serial_number" in result
|
601
|
+
assert "crl_issuer" in result
|
602
|
+
assert "crl_last_update" in result
|
603
|
+
assert "crl_next_update" in result
|
604
|
+
|
605
|
+
def test_validate_certificate_against_crl_invalid_data(self):
|
606
|
+
"""Test certificate validation against CRL with invalid data."""
|
607
|
+
with pytest.raises(CertificateError) as exc_info:
|
608
|
+
validate_certificate_against_crl("invalid_cert", "invalid_crl")
|
609
|
+
|
610
|
+
assert "Certificate CRL validation failed" in str(exc_info.value)
|
611
|
+
|
612
|
+
def test_is_crl_valid_success(self):
|
613
|
+
"""Test CRL validation success."""
|
614
|
+
crl, _ = self.create_test_crl()
|
615
|
+
crl_pem = crl.public_bytes(serialization.Encoding.PEM).decode('utf-8')
|
616
|
+
|
617
|
+
is_valid = is_crl_valid(crl_pem)
|
618
|
+
assert is_valid is True
|
619
|
+
|
620
|
+
def test_is_crl_valid_invalid_data(self):
|
621
|
+
"""Test CRL validation with invalid data."""
|
622
|
+
with pytest.raises(CertificateError) as exc_info:
|
623
|
+
is_crl_valid("invalid_crl_data")
|
624
|
+
|
625
|
+
assert "CRL validation failed" in str(exc_info.value)
|
626
|
+
|
627
|
+
def test_get_crl_info_success(self):
|
628
|
+
"""Test CRL information extraction success."""
|
629
|
+
crl, _ = self.create_test_crl()
|
630
|
+
crl_pem = crl.public_bytes(serialization.Encoding.PEM).decode('utf-8')
|
631
|
+
|
632
|
+
info = get_crl_info(crl_pem)
|
633
|
+
|
634
|
+
assert "issuer" in info
|
635
|
+
assert "last_update" in info
|
636
|
+
assert "next_update" in info
|
637
|
+
assert "revoked_certificates_count" in info
|
638
|
+
assert "status" in info
|
639
|
+
assert "version" in info
|
640
|
+
assert "signature_algorithm" in info
|
641
|
+
assert "signature" in info
|
642
|
+
assert info["revoked_certificates_count"] == 0
|
643
|
+
assert info["status"] == "valid"
|
644
|
+
|
645
|
+
def test_get_crl_info_invalid_data(self):
|
646
|
+
"""Test CRL information extraction with invalid data."""
|
647
|
+
with pytest.raises(CertificateError) as exc_info:
|
648
|
+
get_crl_info("invalid_crl_data")
|
649
|
+
|
650
|
+
assert "CRL information extraction failed" in str(exc_info.value)
|
651
|
+
|
652
|
+
def test_validate_certificate_chain_with_crl(self):
|
653
|
+
"""Test certificate chain validation with CRL."""
|
654
|
+
# Create test certificate and CA certificate
|
655
|
+
cert, _ = TestCertificateCreation.create_test_certificate()
|
656
|
+
cert_pem = cert.public_bytes(serialization.Encoding.PEM).decode('utf-8')
|
657
|
+
ca_cert, _ = TestCertificateCreation.create_test_certificate()
|
658
|
+
ca_cert_pem = ca_cert.public_bytes(serialization.Encoding.PEM).decode('utf-8')
|
659
|
+
|
660
|
+
# Create test CRL
|
661
|
+
crl, _ = self.create_test_crl()
|
662
|
+
crl_pem = crl.public_bytes(serialization.Encoding.PEM).decode('utf-8')
|
663
|
+
|
664
|
+
# Test validation with CRL (should pass since certificate is not revoked)
|
665
|
+
is_valid = validate_certificate_chain(cert_pem, ca_cert_pem, crl_pem)
|
666
|
+
assert is_valid is True
|
667
|
+
|
668
|
+
def test_validate_certificate_chain_without_crl(self):
|
669
|
+
"""Test certificate chain validation without CRL."""
|
670
|
+
# Create test certificate and CA certificate
|
671
|
+
cert, _ = TestCertificateCreation.create_test_certificate()
|
672
|
+
cert_pem = cert.public_bytes(serialization.Encoding.PEM).decode('utf-8')
|
673
|
+
ca_cert, _ = TestCertificateCreation.create_test_certificate()
|
674
|
+
ca_cert_pem = ca_cert.public_bytes(serialization.Encoding.PEM).decode('utf-8')
|
675
|
+
|
676
|
+
# Test validation without CRL
|
677
|
+
is_valid = validate_certificate_chain(cert_pem, ca_cert_pem)
|
678
|
+
assert is_valid is True
|
File without changes
|
{mcp_security_framework-1.1.1.dist-info → mcp_security_framework-1.2.0.dist-info}/entry_points.txt
RENAMED
File without changes
|
{mcp_security_framework-1.1.1.dist-info → mcp_security_framework-1.2.0.dist-info}/top_level.txt
RENAMED
File without changes
|