mcp-security-framework 1.1.2__tar.gz → 1.2.1__tar.gz

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.
Files changed (90) hide show
  1. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/PKG-INFO +1 -1
  2. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/__init__.py +1 -1
  3. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/cli/cert_cli.py +167 -3
  4. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/core/auth_manager.py +32 -10
  5. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/core/cert_manager.py +261 -6
  6. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/core/ssl_manager.py +41 -9
  7. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/mtls_middleware.py +10 -2
  8. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/schemas/config.py +31 -0
  9. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/schemas/models.py +46 -0
  10. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/utils/cert_utils.py +309 -8
  11. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework.egg-info/PKG-INFO +1 -1
  12. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework.egg-info/SOURCES.txt +1 -0
  13. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework.egg-info/top_level.txt +0 -1
  14. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/pyproject.toml +1 -1
  15. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_core/test_auth_manager.py +6 -6
  16. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_utils/test_cert_utils.py +168 -0
  17. mcp_security_framework-1.2.1/tests/test_utils/test_unitid_compat.py +550 -0
  18. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/README.md +0 -0
  19. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/cli/__init__.py +0 -0
  20. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/cli/security_cli.py +0 -0
  21. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/constants.py +0 -0
  22. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/core/__init__.py +0 -0
  23. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/core/permission_manager.py +0 -0
  24. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/core/rate_limiter.py +0 -0
  25. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/core/security_manager.py +0 -0
  26. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/__init__.py +0 -0
  27. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/comprehensive_example.py +0 -0
  28. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/django_example.py +0 -0
  29. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/fastapi_example.py +0 -0
  30. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/flask_example.py +0 -0
  31. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/gateway_example.py +0 -0
  32. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/microservice_example.py +0 -0
  33. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/standalone_example.py +0 -0
  34. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/test_all_examples.py +0 -0
  35. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/__init__.py +0 -0
  36. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/auth_middleware.py +0 -0
  37. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/fastapi_auth_middleware.py +0 -0
  38. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/fastapi_middleware.py +0 -0
  39. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/flask_auth_middleware.py +0 -0
  40. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/flask_middleware.py +0 -0
  41. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/rate_limit_middleware.py +0 -0
  42. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/security_middleware.py +0 -0
  43. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/schemas/__init__.py +0 -0
  44. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/schemas/responses.py +0 -0
  45. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/tests/__init__.py +0 -0
  46. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/utils/__init__.py +0 -0
  47. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/utils/crypto_utils.py +0 -0
  48. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/utils/datetime_compat.py +0 -0
  49. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework/utils/validation_utils.py +0 -0
  50. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework.egg-info/dependency_links.txt +0 -0
  51. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework.egg-info/entry_points.txt +0 -0
  52. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/mcp_security_framework.egg-info/requires.txt +0 -0
  53. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/setup.cfg +0 -0
  54. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/__init__.py +0 -0
  55. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/conftest.py +0 -0
  56. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_cli/__init__.py +0 -0
  57. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_cli/test_cert_cli.py +0 -0
  58. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_cli/test_security_cli.py +0 -0
  59. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_core/__init__.py +0 -0
  60. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_core/test_cert_manager.py +0 -0
  61. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_core/test_permission_manager.py +0 -0
  62. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_core/test_rate_limiter.py +0 -0
  63. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_core/test_security_manager.py +0 -0
  64. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_core/test_ssl_manager.py +0 -0
  65. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_examples/__init__.py +0 -0
  66. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_examples/test_comprehensive_example.py +0 -0
  67. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_examples/test_fastapi_example.py +0 -0
  68. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_examples/test_flask_example.py +0 -0
  69. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_examples/test_standalone_example.py +0 -0
  70. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_integration/__init__.py +0 -0
  71. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_integration/test_auth_flow.py +0 -0
  72. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_integration/test_certificate_flow.py +0 -0
  73. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_integration/test_fastapi_integration.py +0 -0
  74. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_integration/test_flask_integration.py +0 -0
  75. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_integration/test_standalone_integration.py +0 -0
  76. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_middleware/__init__.py +0 -0
  77. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_middleware/test_fastapi_auth_middleware.py +0 -0
  78. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_middleware/test_fastapi_middleware.py +0 -0
  79. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_middleware/test_flask_auth_middleware.py +0 -0
  80. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_middleware/test_flask_middleware.py +0 -0
  81. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_middleware/test_security_middleware.py +0 -0
  82. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_schemas/__init__.py +0 -0
  83. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_schemas/test_config.py +0 -0
  84. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_schemas/test_models.py +0 -0
  85. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_schemas/test_responses.py +0 -0
  86. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_schemas/test_serialization.py +0 -0
  87. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_utils/__init__.py +0 -0
  88. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_utils/test_crypto_utils.py +0 -0
  89. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_utils/test_datetime_compat.py +0 -0
  90. {mcp_security_framework-1.1.2 → mcp_security_framework-1.2.1}/tests/test_utils/test_validation_utils.py +0 -0
@@ -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.1
4
4
  Summary: Universal security framework for microservices with SSL/TLS, authentication, authorization, and rate limiting. Requires cryptography>=42.0.0 for certificate operations.
5
5
  Author-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
6
6
  Maintainer-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
@@ -71,7 +71,7 @@ from mcp_security_framework.schemas.responses import (
71
71
  )
72
72
 
73
73
  # Version information
74
- __version__ = "0.1.0"
74
+ __version__ = "1.2.1"
75
75
  __author__ = "Vasiliy Zdanovskiy"
76
76
  __email__ = "vasilyvz@gmail.com"
77
77
  __license__ = "MIT"
@@ -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()
@@ -38,6 +38,7 @@ from ..schemas.models import AuthResult, AuthStatus, ValidationResult
38
38
  from ..utils.cert_utils import (
39
39
  extract_permissions_from_certificate,
40
40
  extract_roles_from_certificate,
41
+ extract_unitid_from_certificate,
41
42
  parse_certificate,
42
43
  validate_certificate_chain,
43
44
  )
@@ -124,20 +125,21 @@ class AuthManager:
124
125
  self.logger = logging.getLogger(__name__)
125
126
 
126
127
  # Initialize storage
127
- # Convert API keys from "key": "user" format to "user": "key" format
128
+ # Store API keys in format: {"api_key": "username"}
128
129
  # or handle new format "key": {"username": "user", "roles": ["role1", "role2"]}
129
130
  if config.api_keys:
130
131
  self._api_keys = {}
131
132
  self._api_key_metadata = {}
132
133
  for key, value in config.api_keys.items():
133
134
  if isinstance(value, str):
134
- # Old format: "key": "user"
135
+ # Old format: "username": "api_key" -> store as {"api_key": "username"}
135
136
  self._api_keys[value] = key
136
137
  elif isinstance(value, dict):
137
- # New format: "key": {"username": "user", "roles": ["role1", "role2"]}
138
+ # New format: "username": {"api_key": "key", "roles": ["role1", "role2"]}
139
+ api_key = value.get("api_key", key)
138
140
  username = value.get("username", key)
139
- self._api_keys[username] = key
140
- self._api_key_metadata[key] = value
141
+ self._api_keys[api_key] = username
142
+ self._api_key_metadata[api_key] = value
141
143
  else:
142
144
  self.logger.warning(
143
145
  f"Invalid API key format for key {key}: {value}"
@@ -253,7 +255,7 @@ class AuthManager:
253
255
  # Find user by API key
254
256
  username = None
255
257
  user_roles = []
256
- for user, api_key_in_config in self._api_keys.items():
258
+ for api_key_in_config, user in self._api_keys.items():
257
259
  if api_key_in_config == api_key:
258
260
  username = user
259
261
  # Check if we have metadata for this API key
@@ -625,11 +627,23 @@ class AuthManager:
625
627
  )
626
628
  roles = []
627
629
 
630
+ # Extract unitid from certificate
631
+ unitid = None
632
+ try:
633
+ unitid = extract_unitid_from_certificate(cert_pem)
634
+ except Exception as e:
635
+ self.logger.warning(
636
+ "Failed to extract unitid from certificate",
637
+ extra={"username": username, "error": str(e)},
638
+ )
639
+
628
640
  # Validate certificate chain if CA is configured
629
641
  if self.config.ca_cert_file:
630
642
  try:
643
+ # Check if CRL is configured for certificate validation
644
+ crl_file = getattr(self.config, 'crl_file', None)
631
645
  is_valid_chain = validate_certificate_chain(
632
- cert_pem, self.config.ca_cert_file
646
+ cert_pem, self.config.ca_cert_file, crl_file
633
647
  )
634
648
  if not is_valid_chain:
635
649
  return AuthResult(
@@ -654,6 +668,7 @@ class AuthManager:
654
668
  auth_method="certificate",
655
669
  auth_timestamp=datetime.now(timezone.utc),
656
670
  token_expiry=get_not_valid_after_utc(cert),
671
+ unitid=unitid,
657
672
  )
658
673
 
659
674
  self.logger.info(
@@ -859,7 +874,7 @@ class AuthManager:
859
874
  if not validate_api_key_format(api_key):
860
875
  return False
861
876
 
862
- self._api_keys[username] = api_key
877
+ self._api_keys[api_key] = username
863
878
 
864
879
  self.logger.info("API key added for user", extra={"username": username})
865
880
 
@@ -882,8 +897,15 @@ class AuthManager:
882
897
  bool: True if API key was removed successfully, False otherwise
883
898
  """
884
899
  try:
885
- if username in self._api_keys:
886
- del self._api_keys[username]
900
+ # Find API key for the username and remove it
901
+ api_key_to_remove = None
902
+ for api_key, user in self._api_keys.items():
903
+ if user == username:
904
+ api_key_to_remove = api_key
905
+ break
906
+
907
+ if api_key_to_remove:
908
+ del self._api_keys[api_key_to_remove]
887
909
 
888
910
  self.logger.info(
889
911
  "API key removed for user", extra={"username": username}
@@ -29,6 +29,7 @@ License: MIT
29
29
 
30
30
  import logging
31
31
  import os
32
+ import uuid
32
33
  from datetime import datetime, timedelta, timezone
33
34
  from pathlib import Path
34
35
  from typing import Dict, List, Optional, Tuple, Union
@@ -54,10 +55,15 @@ from mcp_security_framework.schemas.models import (
54
55
  from mcp_security_framework.utils.cert_utils import (
55
56
  extract_permissions_from_certificate,
56
57
  extract_roles_from_certificate,
58
+ extract_unitid_from_certificate,
57
59
  get_certificate_expiry,
58
60
  get_certificate_serial_number,
61
+ get_crl_info,
62
+ is_certificate_revoked,
59
63
  is_certificate_self_signed,
64
+ is_crl_valid,
60
65
  parse_certificate,
66
+ validate_certificate_against_crl,
61
67
  validate_certificate_chain,
62
68
  )
63
69
  from mcp_security_framework.utils.datetime_compat import (
@@ -250,6 +256,14 @@ class CertificateManager:
250
256
  x509.BasicConstraints(ca=True, path_length=None), critical=True
251
257
  )
252
258
 
259
+ # Add unitid extension if provided
260
+ if ca_config.unitid:
261
+ unitid_extension = x509.UnrecognizedExtension(
262
+ oid=x509.ObjectIdentifier("1.3.6.1.4.1.99999.1.3"),
263
+ value=ca_config.unitid.encode(),
264
+ )
265
+ builder = builder.add_extension(unitid_extension, critical=False)
266
+
253
267
  builder = builder.add_extension(
254
268
  x509.KeyUsage(
255
269
  digital_signature=True,
@@ -321,6 +335,7 @@ class CertificateManager:
321
335
  not_after=get_not_valid_after_utc(certificate),
322
336
  certificate_type=CertificateType.ROOT_CA,
323
337
  key_size=ca_config.key_size,
338
+ unitid=ca_config.unitid,
324
339
  )
325
340
 
326
341
  self.logger.info(
@@ -762,6 +777,14 @@ class CertificateManager:
762
777
  )
763
778
  builder = builder.add_extension(permissions_extension, critical=False)
764
779
 
780
+ # Add unitid extension if provided
781
+ if client_config.unitid:
782
+ unitid_extension = x509.UnrecognizedExtension(
783
+ oid=x509.ObjectIdentifier("1.3.6.1.4.1.99999.1.3"),
784
+ value=client_config.unitid.encode(),
785
+ )
786
+ builder = builder.add_extension(unitid_extension, critical=False)
787
+
765
788
  # Create certificate
766
789
  certificate = builder.sign(ca_key, hashes.SHA256())
767
790
 
@@ -812,6 +835,7 @@ class CertificateManager:
812
835
  not_after=get_not_valid_after_utc(certificate),
813
836
  certificate_type=CertificateType.CLIENT,
814
837
  key_size=client_config.key_size,
838
+ unitid=client_config.unitid,
815
839
  )
816
840
 
817
841
  self.logger.info(
@@ -1327,21 +1351,27 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1327
1351
  return False
1328
1352
 
1329
1353
  def validate_certificate_chain(
1330
- self, cert_path: str, ca_cert_path: Optional[str] = None
1354
+ self,
1355
+ cert_path: str,
1356
+ ca_cert_path: Optional[str] = None,
1357
+ crl_path: Optional[str] = None
1331
1358
  ) -> bool:
1332
1359
  """
1333
- Validate certificate chain against CA.
1360
+ Validate certificate chain against CA and optionally check CRL.
1334
1361
 
1335
1362
  This method validates a certificate chain by checking the certificate
1336
- against the CA certificate and verifying the chain of trust.
1363
+ against the CA certificate and verifying the chain of trust. If CRL
1364
+ is provided, it also checks if the certificate is revoked.
1337
1365
 
1338
1366
  Args:
1339
1367
  cert_path (str): Path to certificate to validate
1340
1368
  ca_cert_path (Optional[str]): Path to CA certificate. If None,
1341
1369
  uses CA certificate from configuration.
1370
+ crl_path (Optional[str]): Path to CRL file. If None, CRL check
1371
+ is skipped. If provided, certificate revocation is checked.
1342
1372
 
1343
1373
  Returns:
1344
- bool: True if certificate chain is valid, False otherwise
1374
+ bool: True if certificate chain is valid and not revoked, False otherwise
1345
1375
 
1346
1376
  Raises:
1347
1377
  FileNotFoundError: When certificate files are not found
@@ -1352,6 +1382,11 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1352
1382
  >>> is_valid = cert_manager.validate_certificate_chain("client.crt")
1353
1383
  >>> if is_valid:
1354
1384
  ... print("Certificate chain is valid")
1385
+ >>>
1386
+ >>> # With CRL check
1387
+ >>> is_valid = cert_manager.validate_certificate_chain(
1388
+ ... "client.crt", crl_path="crl.pem"
1389
+ ... )
1355
1390
  """
1356
1391
  try:
1357
1392
  # Use configured CA certificate if not provided
@@ -1361,8 +1396,12 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1361
1396
  if not ca_cert_path:
1362
1397
  raise CertificateConfigurationError("CA certificate path is required")
1363
1398
 
1364
- # Validate certificate chain
1365
- return validate_certificate_chain(cert_path, ca_cert_path)
1399
+ # Use configured CRL path if not provided
1400
+ if not crl_path and self.config.crl_enabled:
1401
+ crl_path = self.config.crl_path
1402
+
1403
+ # Validate certificate chain with optional CRL check
1404
+ return validate_certificate_chain(cert_path, ca_cert_path, crl_path)
1366
1405
 
1367
1406
  except Exception as e:
1368
1407
  self.logger.error(
@@ -1370,6 +1409,7 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1370
1409
  extra={
1371
1410
  "cert_path": cert_path,
1372
1411
  "ca_cert_path": ca_cert_path,
1412
+ "crl_path": crl_path,
1373
1413
  "error": str(e),
1374
1414
  },
1375
1415
  )
@@ -1929,6 +1969,221 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1929
1969
  f"CA private key file not found: {self.config.ca_key_path}"
1930
1970
  )
1931
1971
 
1972
+ def validate_certificate_against_crl(
1973
+ self,
1974
+ cert_path: str,
1975
+ crl_path: Optional[str] = None
1976
+ ) -> Dict[str, any]:
1977
+ """
1978
+ Validate certificate against CRL and return detailed revocation status.
1979
+
1980
+ This method checks if a certificate is revoked according to the
1981
+ provided CRL and returns detailed revocation information.
1982
+
1983
+ Args:
1984
+ cert_path (str): Path to certificate to validate
1985
+ crl_path (Optional[str]): Path to CRL file. If None, uses CRL
1986
+ from configuration if CRL is enabled.
1987
+
1988
+ Returns:
1989
+ Dict[str, any]: Dictionary containing revocation status and details:
1990
+ - is_revoked (bool): True if certificate is revoked
1991
+ - serial_number (str): Certificate serial number
1992
+ - revocation_date (datetime): Date of revocation (if revoked)
1993
+ - revocation_reason (str): Reason for revocation (if revoked)
1994
+ - crl_issuer (str): CRL issuer information
1995
+ - crl_last_update (datetime): CRL last update time
1996
+ - crl_next_update (datetime): CRL next update time
1997
+
1998
+ Raises:
1999
+ CertificateConfigurationError: When CRL configuration is invalid
2000
+ CertificateValidationError: When CRL validation fails
2001
+
2002
+ Example:
2003
+ >>> cert_manager = CertificateManager(config)
2004
+ >>> result = cert_manager.validate_certificate_against_crl("client.crt")
2005
+ >>> if result["is_revoked"]:
2006
+ ... print(f"Certificate revoked: {result['revocation_reason']}")
2007
+ """
2008
+ try:
2009
+ # Use configured CRL path if not provided
2010
+ if not crl_path:
2011
+ if not self.config.crl_enabled:
2012
+ raise CertificateConfigurationError("CRL is not enabled in configuration")
2013
+ crl_path = self.config.crl_path
2014
+
2015
+ if not crl_path:
2016
+ raise CertificateConfigurationError("CRL path is required")
2017
+
2018
+ # Validate certificate against CRL
2019
+ return validate_certificate_against_crl(cert_path, crl_path)
2020
+
2021
+ except Exception as e:
2022
+ self.logger.error(
2023
+ "Certificate CRL validation failed",
2024
+ extra={
2025
+ "cert_path": cert_path,
2026
+ "crl_path": crl_path,
2027
+ "error": str(e),
2028
+ },
2029
+ )
2030
+ raise CertificateValidationError(f"CRL validation failed: {str(e)}")
2031
+
2032
+ def is_certificate_revoked(
2033
+ self,
2034
+ cert_path: str,
2035
+ crl_path: Optional[str] = None
2036
+ ) -> bool:
2037
+ """
2038
+ Check if certificate is revoked according to CRL.
2039
+
2040
+ This method provides a simple boolean check for certificate revocation
2041
+ without detailed revocation information.
2042
+
2043
+ Args:
2044
+ cert_path (str): Path to certificate to check
2045
+ crl_path (Optional[str]): Path to CRL file. If None, uses CRL
2046
+ from configuration if CRL is enabled.
2047
+
2048
+ Returns:
2049
+ bool: True if certificate is revoked, False otherwise
2050
+
2051
+ Raises:
2052
+ CertificateConfigurationError: When CRL configuration is invalid
2053
+ CertificateValidationError: When CRL validation fails
2054
+
2055
+ Example:
2056
+ >>> cert_manager = CertificateManager(config)
2057
+ >>> if cert_manager.is_certificate_revoked("client.crt"):
2058
+ ... print("Certificate is revoked")
2059
+ """
2060
+ try:
2061
+ # Use configured CRL path if not provided
2062
+ if not crl_path:
2063
+ if not self.config.crl_enabled:
2064
+ raise CertificateConfigurationError("CRL is not enabled in configuration")
2065
+ crl_path = self.config.crl_path
2066
+
2067
+ if not crl_path:
2068
+ raise CertificateConfigurationError("CRL path is required")
2069
+
2070
+ # Check if certificate is revoked
2071
+ return is_certificate_revoked(cert_path, crl_path)
2072
+
2073
+ except Exception as e:
2074
+ self.logger.error(
2075
+ "Certificate revocation check failed",
2076
+ extra={
2077
+ "cert_path": cert_path,
2078
+ "crl_path": crl_path,
2079
+ "error": str(e),
2080
+ },
2081
+ )
2082
+ raise CertificateValidationError(f"Revocation check failed: {str(e)}")
2083
+
2084
+ def get_crl_info(self, crl_path: Optional[str] = None) -> Dict:
2085
+ """
2086
+ Get detailed information from CRL.
2087
+
2088
+ This method extracts comprehensive information from a CRL including
2089
+ issuer details, validity period, and revoked certificate count.
2090
+
2091
+ Args:
2092
+ crl_path (Optional[str]): Path to CRL file. If None, uses CRL
2093
+ from configuration if CRL is enabled.
2094
+
2095
+ Returns:
2096
+ Dict: Dictionary containing CRL information:
2097
+ - issuer (str): CRL issuer information
2098
+ - last_update (datetime): CRL last update time
2099
+ - next_update (datetime): CRL next update time
2100
+ - revoked_certificates_count (int): Number of revoked certificates
2101
+ - days_until_expiry (int): Days until CRL expires
2102
+ - is_expired (bool): True if CRL is expired
2103
+ - expires_soon (bool): True if CRL expires within 7 days
2104
+ - status (str): CRL status (valid, expires_soon, expired)
2105
+ - version (str): CRL version
2106
+ - signature_algorithm (str): Signature algorithm used
2107
+ - signature (str): CRL signature in hex format
2108
+
2109
+ Raises:
2110
+ CertificateConfigurationError: When CRL configuration is invalid
2111
+ CertificateValidationError: When CRL information extraction fails
2112
+
2113
+ Example:
2114
+ >>> cert_manager = CertificateManager(config)
2115
+ >>> crl_info = cert_manager.get_crl_info()
2116
+ >>> print(f"CRL has {crl_info['revoked_certificates_count']} revoked certificates")
2117
+ """
2118
+ try:
2119
+ # Use configured CRL path if not provided
2120
+ if not crl_path:
2121
+ if not self.config.crl_enabled:
2122
+ raise CertificateConfigurationError("CRL is not enabled in configuration")
2123
+ crl_path = self.config.crl_path
2124
+
2125
+ if not crl_path:
2126
+ raise CertificateConfigurationError("CRL path is required")
2127
+
2128
+ # Get CRL information
2129
+ return get_crl_info(crl_path)
2130
+
2131
+ except Exception as e:
2132
+ self.logger.error(
2133
+ "CRL information extraction failed",
2134
+ extra={
2135
+ "crl_path": crl_path,
2136
+ "error": str(e),
2137
+ },
2138
+ )
2139
+ raise CertificateValidationError(f"CRL information extraction failed: {str(e)}")
2140
+
2141
+ def is_crl_valid(self, crl_path: Optional[str] = None) -> bool:
2142
+ """
2143
+ Check if CRL is valid (not expired and properly formatted).
2144
+
2145
+ This method validates CRL format and checks if it's within its
2146
+ validity period.
2147
+
2148
+ Args:
2149
+ crl_path (Optional[str]): Path to CRL file. If None, uses CRL
2150
+ from configuration if CRL is enabled.
2151
+
2152
+ Returns:
2153
+ bool: True if CRL is valid, False otherwise
2154
+
2155
+ Raises:
2156
+ CertificateConfigurationError: When CRL configuration is invalid
2157
+ CertificateValidationError: When CRL validation fails
2158
+
2159
+ Example:
2160
+ >>> cert_manager = CertificateManager(config)
2161
+ >>> if cert_manager.is_crl_valid():
2162
+ ... print("CRL is valid")
2163
+ """
2164
+ try:
2165
+ # Use configured CRL path if not provided
2166
+ if not crl_path:
2167
+ if not self.config.crl_enabled:
2168
+ raise CertificateConfigurationError("CRL is not enabled in configuration")
2169
+ crl_path = self.config.crl_path
2170
+
2171
+ if not crl_path:
2172
+ raise CertificateConfigurationError("CRL path is required")
2173
+
2174
+ # Check if CRL is valid
2175
+ return is_crl_valid(crl_path)
2176
+
2177
+ except Exception as e:
2178
+ self.logger.error(
2179
+ "CRL validation failed",
2180
+ extra={
2181
+ "crl_path": crl_path,
2182
+ "error": str(e),
2183
+ },
2184
+ )
2185
+ raise CertificateValidationError(f"CRL validation failed: {str(e)}")
2186
+
1932
2187
 
1933
2188
  class CertificateConfigurationError(Exception):
1934
2189
  """Raised when certificate configuration is invalid."""