mcp-security-framework 1.1.2__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.
@@ -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, cert_path: str, ca_cert_path: Optional[str] = None
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
- # Validate certificate chain
1365
- return validate_certificate_chain(cert_path, ca_cert_path)
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 and has
366
- valid basic properties.
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
- >>> else:
378
- ... print("Certificate is invalid")
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", extra={"cert_path": cert_path}
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
- # Use security manager's certificate validation
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, self.config.ssl.ca_cert_file if self.config.ssl else None
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
- # For now, just check that the certificate was issued by one of the CA certificates
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
- return True
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 False
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-security-framework
3
- Version: 1.1.2
3
+ Version: 1.2.0
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>
@@ -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=ME8RgTBrjNvOAMPVnXGLr3cbTpOZ2NN4Ei1SouGRPQs,18297
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=vCa6YBlz7P_vmif-KGWrCCUE54bHO5F3jN-bDJF2o9Y,38909
8
- mcp_security_framework/core/cert_manager.py,sha256=s625nyMsmrglT7QaqRZtX4oAJVKla5zu0sptHmXJnh4,78750
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=nCDrukaELONQs_uAfb30g69HDxRp2u4k0Bpq8eJH6Zo,27718
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=iCOX3PVro49GMlTSUtYQFaWLrqtqoWPgI5DEa6Cnst4,13881
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=DNzCqFKbFgYvQxxUUJPgeWe1XCnM4DpUBDqQYRYtlOQ,17266
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=2k1kUCJy0T2HPBwOcU5USGfEBQZ_rGzumAGkDdywLak,21232
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.1.2.dist-info/METADATA,sha256=xuXJme6tsfv0HEsky6X-Nd-S7sULfScqr9y6aU6w-ZM,11771
81
- mcp_security_framework-1.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
82
- mcp_security_framework-1.1.2.dist-info/entry_points.txt,sha256=qBh92fVDmd1m2f3xeW0hTu3Ksg8QfGJyV8UEkdA2itg,142
83
- mcp_security_framework-1.1.2.dist-info/top_level.txt,sha256=ifUiGrTDcD574MXSOoAN2rp2wpUvWlb4jD9LTUgDWCA,29
84
- mcp_security_framework-1.1.2.dist-info/RECORD,,
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