mcp-security-framework 1.2.0__tar.gz → 1.2.2__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.2.0 → mcp_security_framework-1.2.2}/PKG-INFO +1 -1
  2. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/__init__.py +1 -1
  3. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/core/auth_manager.py +29 -9
  4. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/core/cert_manager.py +20 -0
  5. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/schemas/config.py +45 -4
  6. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/schemas/models.py +46 -0
  7. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/utils/cert_utils.py +54 -0
  8. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework.egg-info/PKG-INFO +1 -1
  9. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework.egg-info/SOURCES.txt +1 -0
  10. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/pyproject.toml +1 -1
  11. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_core/test_auth_manager.py +6 -6
  12. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_schemas/test_config.py +50 -0
  13. mcp_security_framework-1.2.2/tests/test_utils/test_unitid_compat.py +550 -0
  14. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/README.md +0 -0
  15. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/cli/__init__.py +0 -0
  16. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/cli/cert_cli.py +0 -0
  17. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/cli/security_cli.py +0 -0
  18. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/constants.py +0 -0
  19. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/core/__init__.py +0 -0
  20. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/core/permission_manager.py +0 -0
  21. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/core/rate_limiter.py +0 -0
  22. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/core/security_manager.py +0 -0
  23. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/core/ssl_manager.py +0 -0
  24. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/examples/__init__.py +0 -0
  25. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/examples/comprehensive_example.py +0 -0
  26. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/examples/django_example.py +0 -0
  27. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/examples/fastapi_example.py +0 -0
  28. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/examples/flask_example.py +0 -0
  29. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/examples/gateway_example.py +0 -0
  30. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/examples/microservice_example.py +0 -0
  31. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/examples/standalone_example.py +0 -0
  32. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/examples/test_all_examples.py +0 -0
  33. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/middleware/__init__.py +0 -0
  34. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/middleware/auth_middleware.py +0 -0
  35. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/middleware/fastapi_auth_middleware.py +0 -0
  36. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/middleware/fastapi_middleware.py +0 -0
  37. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/middleware/flask_auth_middleware.py +0 -0
  38. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/middleware/flask_middleware.py +0 -0
  39. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/middleware/mtls_middleware.py +0 -0
  40. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/middleware/rate_limit_middleware.py +0 -0
  41. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/middleware/security_middleware.py +0 -0
  42. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/schemas/__init__.py +0 -0
  43. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/schemas/responses.py +0 -0
  44. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/tests/__init__.py +0 -0
  45. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/utils/__init__.py +0 -0
  46. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/utils/crypto_utils.py +0 -0
  47. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/utils/datetime_compat.py +0 -0
  48. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework/utils/validation_utils.py +0 -0
  49. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework.egg-info/dependency_links.txt +0 -0
  50. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework.egg-info/entry_points.txt +0 -0
  51. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework.egg-info/requires.txt +0 -0
  52. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/mcp_security_framework.egg-info/top_level.txt +0 -0
  53. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/setup.cfg +0 -0
  54. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/__init__.py +0 -0
  55. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/conftest.py +0 -0
  56. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_cli/__init__.py +0 -0
  57. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_cli/test_cert_cli.py +0 -0
  58. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_cli/test_security_cli.py +0 -0
  59. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_core/__init__.py +0 -0
  60. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_core/test_cert_manager.py +0 -0
  61. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_core/test_permission_manager.py +0 -0
  62. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_core/test_rate_limiter.py +0 -0
  63. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_core/test_security_manager.py +0 -0
  64. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_core/test_ssl_manager.py +0 -0
  65. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_examples/__init__.py +0 -0
  66. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_examples/test_comprehensive_example.py +0 -0
  67. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_examples/test_fastapi_example.py +0 -0
  68. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_examples/test_flask_example.py +0 -0
  69. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_examples/test_standalone_example.py +0 -0
  70. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_integration/__init__.py +0 -0
  71. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_integration/test_auth_flow.py +0 -0
  72. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_integration/test_certificate_flow.py +0 -0
  73. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_integration/test_fastapi_integration.py +0 -0
  74. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_integration/test_flask_integration.py +0 -0
  75. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_integration/test_standalone_integration.py +0 -0
  76. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_middleware/__init__.py +0 -0
  77. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_middleware/test_fastapi_auth_middleware.py +0 -0
  78. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_middleware/test_fastapi_middleware.py +0 -0
  79. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_middleware/test_flask_auth_middleware.py +0 -0
  80. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_middleware/test_flask_middleware.py +0 -0
  81. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_middleware/test_security_middleware.py +0 -0
  82. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_schemas/__init__.py +0 -0
  83. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_schemas/test_models.py +0 -0
  84. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_schemas/test_responses.py +0 -0
  85. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_schemas/test_serialization.py +0 -0
  86. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_utils/__init__.py +0 -0
  87. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_utils/test_cert_utils.py +0 -0
  88. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_utils/test_crypto_utils.py +0 -0
  89. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/tests/test_utils/test_datetime_compat.py +0 -0
  90. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.2}/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.2.0
3
+ Version: 1.2.2
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.2"
75
75
  __author__ = "Vasiliy Zdanovskiy"
76
76
  __email__ = "vasilyvz@gmail.com"
77
77
  __license__ = "MIT"
@@ -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,6 +627,16 @@ 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:
@@ -656,6 +668,7 @@ class AuthManager:
656
668
  auth_method="certificate",
657
669
  auth_timestamp=datetime.now(timezone.utc),
658
670
  token_expiry=get_not_valid_after_utc(cert),
671
+ unitid=unitid,
659
672
  )
660
673
 
661
674
  self.logger.info(
@@ -861,7 +874,7 @@ class AuthManager:
861
874
  if not validate_api_key_format(api_key):
862
875
  return False
863
876
 
864
- self._api_keys[username] = api_key
877
+ self._api_keys[api_key] = username
865
878
 
866
879
  self.logger.info("API key added for user", extra={"username": username})
867
880
 
@@ -884,8 +897,15 @@ class AuthManager:
884
897
  bool: True if API key was removed successfully, False otherwise
885
898
  """
886
899
  try:
887
- if username in self._api_keys:
888
- 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]
889
909
 
890
910
  self.logger.info(
891
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,6 +55,7 @@ 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,
59
61
  get_crl_info,
@@ -254,6 +256,14 @@ class CertificateManager:
254
256
  x509.BasicConstraints(ca=True, path_length=None), critical=True
255
257
  )
256
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
+
257
267
  builder = builder.add_extension(
258
268
  x509.KeyUsage(
259
269
  digital_signature=True,
@@ -325,6 +335,7 @@ class CertificateManager:
325
335
  not_after=get_not_valid_after_utc(certificate),
326
336
  certificate_type=CertificateType.ROOT_CA,
327
337
  key_size=ca_config.key_size,
338
+ unitid=ca_config.unitid,
328
339
  )
329
340
 
330
341
  self.logger.info(
@@ -766,6 +777,14 @@ class CertificateManager:
766
777
  )
767
778
  builder = builder.add_extension(permissions_extension, critical=False)
768
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
+
769
788
  # Create certificate
770
789
  certificate = builder.sign(ca_key, hashes.SHA256())
771
790
 
@@ -816,6 +835,7 @@ class CertificateManager:
816
835
  not_after=get_not_valid_after_utc(certificate),
817
836
  certificate_type=CertificateType.CLIENT,
818
837
  key_size=client_config.key_size,
838
+ unitid=client_config.unitid,
819
839
  )
820
840
 
821
841
  self.logger.info(
@@ -33,6 +33,7 @@ License: MIT
33
33
  from enum import Enum
34
34
  from pathlib import Path
35
35
  from typing import Any, Dict, List, Optional, Union
36
+ import uuid
36
37
 
37
38
  from pydantic import BaseModel, Field, field_validator, model_validator
38
39
  from pydantic.types import SecretStr
@@ -246,8 +247,12 @@ class CertificateConfig(BaseModel):
246
247
  This model defines certificate management configuration settings
247
248
  including CA settings, certificate storage, and validation options.
248
249
 
250
+ BUGFIX: Added ca_creation_mode to allow CA certificate creation
251
+ without requiring existing CA paths.
252
+
249
253
  Attributes:
250
254
  enabled: Whether certificate management is enabled
255
+ ca_creation_mode: Whether we are in CA creation mode (bypasses CA path validation)
251
256
  ca_cert_path: Path to CA certificate
252
257
  ca_key_path: Path to CA private key
253
258
  cert_storage_path: Path for certificate storage
@@ -265,6 +270,9 @@ class CertificateConfig(BaseModel):
265
270
  enabled: bool = Field(
266
271
  default=False, description="Whether certificate management is enabled"
267
272
  )
273
+ ca_creation_mode: bool = Field(
274
+ default=False, description="Whether we are in CA creation mode (bypasses CA path validation)"
275
+ )
268
276
  ca_cert_path: Optional[str] = Field(
269
277
  default=None, description="Path to CA certificate"
270
278
  )
@@ -316,10 +324,13 @@ class CertificateConfig(BaseModel):
316
324
  def validate_certificate_configuration(self):
317
325
  """Validate certificate configuration consistency."""
318
326
  if self.enabled:
319
- if not self.ca_cert_path or not self.ca_key_path:
320
- raise ValueError(
321
- "Certificate management enabled but CA certificate and key paths are required"
322
- )
327
+ # BUGFIX: Only require CA paths if not in CA creation mode
328
+ if not self.ca_creation_mode:
329
+ if not self.ca_cert_path or not self.ca_key_path:
330
+ raise ValueError(
331
+ "Certificate management enabled but CA certificate and key paths are required. "
332
+ "Set ca_creation_mode=True if you are creating a CA certificate."
333
+ )
323
334
 
324
335
  if self.crl_enabled and not self.crl_path:
325
336
  raise ValueError("CRL enabled but CRL path is required")
@@ -599,6 +610,21 @@ class CAConfig(BaseModel):
599
610
  hash_algorithm: str = Field(
600
611
  default="sha256", description="Hash algorithm for signing"
601
612
  )
613
+ unitid: Optional[str] = Field(
614
+ default=None, description="Unique unit identifier (UUID4) for the certificate"
615
+ )
616
+
617
+ @field_validator("unitid")
618
+ @classmethod
619
+ def validate_unitid(cls, v):
620
+ """Validate unitid format."""
621
+ if v is not None:
622
+ try:
623
+ # Validate UUID4 format
624
+ uuid.UUID(v, version=4)
625
+ except ValueError:
626
+ raise ValueError("unitid must be a valid UUID4 string")
627
+ return v
602
628
 
603
629
 
604
630
  class IntermediateCAConfig(CAConfig):
@@ -668,6 +694,21 @@ class ClientCertConfig(BaseModel):
668
694
  )
669
695
  ca_cert_path: str = Field(..., description="Path to signing CA certificate")
670
696
  ca_key_path: str = Field(..., description="Path to signing CA private key")
697
+ unitid: Optional[str] = Field(
698
+ default=None, description="Unique unit identifier (UUID4) for the certificate"
699
+ )
700
+
701
+ @field_validator("unitid")
702
+ @classmethod
703
+ def validate_unitid(cls, v):
704
+ """Validate unitid format."""
705
+ if v is not None:
706
+ try:
707
+ # Validate UUID4 format
708
+ uuid.UUID(v, version=4)
709
+ except ValueError:
710
+ raise ValueError("unitid must be a valid UUID4 string")
711
+ return v
671
712
 
672
713
 
673
714
  class ServerCertConfig(ClientCertConfig):
@@ -38,6 +38,7 @@ License: MIT
38
38
  from datetime import datetime, timedelta, timezone
39
39
  from enum import Enum
40
40
  from typing import Any, Dict, List, Optional, Set, TypeAlias
41
+ import uuid
41
42
 
42
43
  from pydantic import BaseModel, Field, field_validator, model_validator
43
44
 
@@ -140,6 +141,9 @@ class AuthResult(BaseModel):
140
141
  metadata: Dict[str, Any] = Field(
141
142
  default_factory=dict, description="Additional authentication metadata"
142
143
  )
144
+ unitid: Optional[str] = Field(
145
+ default=None, description="Unique unit identifier (UUID4) from certificate"
146
+ )
143
147
 
144
148
  @field_validator("username")
145
149
  @classmethod
@@ -149,6 +153,18 @@ class AuthResult(BaseModel):
149
153
  raise ValueError("Username cannot be empty")
150
154
  return v
151
155
 
156
+ @field_validator("unitid")
157
+ @classmethod
158
+ def validate_unitid(cls, v):
159
+ """Validate unitid format."""
160
+ if v is not None:
161
+ try:
162
+ # Validate UUID4 format
163
+ uuid.UUID(v, version=4)
164
+ except ValueError:
165
+ raise ValueError("unitid must be a valid UUID4 string")
166
+ return v
167
+
152
168
  @model_validator(mode="after")
153
169
  def validate_auth_result(self):
154
170
  """Validate authentication result consistency."""
@@ -309,6 +325,9 @@ class CertificateInfo(BaseModel):
309
325
  fingerprint_sha256: Optional[str] = Field(
310
326
  default=None, description="SHA256 fingerprint"
311
327
  )
328
+ unitid: Optional[str] = Field(
329
+ default=None, description="Unique unit identifier (UUID4) for the certificate"
330
+ )
312
331
 
313
332
  @field_validator("key_size")
314
333
  @classmethod
@@ -318,6 +337,18 @@ class CertificateInfo(BaseModel):
318
337
  raise ValueError("Key size must be between 512 and 8192 bits")
319
338
  return v
320
339
 
340
+ @field_validator("unitid")
341
+ @classmethod
342
+ def validate_unitid(cls, v):
343
+ """Validate unitid format."""
344
+ if v is not None:
345
+ try:
346
+ # Validate UUID4 format
347
+ uuid.UUID(v, version=4)
348
+ except ValueError:
349
+ raise ValueError("unitid must be a valid UUID4 string")
350
+ return v
351
+
321
352
  @property
322
353
  def is_expired(self) -> bool:
323
354
  """Check if certificate is expired."""
@@ -424,6 +455,9 @@ class CertificatePair(BaseModel):
424
455
  metadata: Dict[str, Any] = Field(
425
456
  default_factory=dict, description="Additional certificate metadata"
426
457
  )
458
+ unitid: Optional[str] = Field(
459
+ default=None, description="Unique unit identifier (UUID4) for the certificate"
460
+ )
427
461
 
428
462
  @field_validator("certificate_pem")
429
463
  @classmethod
@@ -449,6 +483,18 @@ class CertificatePair(BaseModel):
449
483
  raise ValueError("Invalid private key PEM format")
450
484
  return v
451
485
 
486
+ @field_validator("unitid")
487
+ @classmethod
488
+ def validate_unitid(cls, v):
489
+ """Validate unitid format."""
490
+ if v is not None:
491
+ try:
492
+ # Validate UUID4 format
493
+ uuid.UUID(v, version=4)
494
+ except ValueError:
495
+ raise ValueError("unitid must be a valid UUID4 string")
496
+ return v
497
+
452
498
  @property
453
499
  def is_expired(self) -> bool:
454
500
  """Check if certificate is expired."""
@@ -160,6 +160,15 @@ def extract_certificate_info(cert_data: Union[str, bytes, Path]) -> Dict:
160
160
  if country:
161
161
  info["country"] = country[0].value
162
162
 
163
+ # Extract unitid
164
+ try:
165
+ unitid = extract_unitid_from_certificate(cert_data)
166
+ if unitid:
167
+ info["unitid"] = unitid
168
+ except Exception:
169
+ # If unitid extraction fails, continue without it
170
+ pass
171
+
163
172
  return info
164
173
  except Exception as e:
165
174
  raise CertificateError(f"Certificate information extraction failed: {str(e)}")
@@ -275,6 +284,51 @@ def extract_permissions_from_certificate(
275
284
  raise CertificateError(f"Permission extraction failed: {str(e)}")
276
285
 
277
286
 
287
+ def extract_unitid_from_certificate(
288
+ cert_data: Union[str, bytes, Path],
289
+ ) -> Optional[str]:
290
+ """
291
+ Extract unitid from certificate extensions.
292
+
293
+ Args:
294
+ cert_data: Certificate data as string, bytes, or file path
295
+
296
+ Returns:
297
+ Unit ID (UUID4) found in certificate, or None if not found
298
+
299
+ Raises:
300
+ CertificateError: If unitid extraction fails
301
+ """
302
+ try:
303
+ cert = parse_certificate(cert_data)
304
+ unitid = None
305
+
306
+ # Check for custom extension with unitid
307
+ try:
308
+ unitid_extension = cert.extensions.get_extension_for_oid(
309
+ x509.ObjectIdentifier("1.3.6.1.4.1.99999.1.3") # Custom unitid OID
310
+ )
311
+ if unitid_extension:
312
+ unitid_data = unitid_extension.value.value
313
+ if isinstance(unitid_data, bytes):
314
+ unitid_str = unitid_data.decode("utf-8")
315
+ unitid = unitid_str.strip()
316
+
317
+ # Validate UUID4 format
318
+ try:
319
+ import uuid
320
+ uuid.UUID(unitid, version=4)
321
+ except ValueError:
322
+ # Invalid UUID4 format, return None
323
+ unitid = None
324
+ except x509.extensions.ExtensionNotFound:
325
+ pass
326
+
327
+ return unitid
328
+ except Exception as e:
329
+ raise CertificateError(f"Unitid extraction failed: {str(e)}")
330
+
331
+
278
332
  def validate_certificate_chain(
279
333
  cert_data: Union[str, bytes, Path],
280
334
  ca_cert_data: Union[str, bytes, Path, List[Union[str, bytes, Path]]],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-security-framework
3
- Version: 1.2.0
3
+ Version: 1.2.2
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>
@@ -84,4 +84,5 @@ tests/test_utils/__init__.py
84
84
  tests/test_utils/test_cert_utils.py
85
85
  tests/test_utils/test_crypto_utils.py
86
86
  tests/test_utils/test_datetime_compat.py
87
+ tests/test_utils/test_unitid_compat.py
87
88
  tests/test_utils/test_validation_utils.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mcp-security-framework"
7
- version = "1.2.0"
7
+ version = "1.2.2"
8
8
  description = "Universal security framework for microservices with SSL/TLS, authentication, authorization, and rate limiting. Requires cryptography>=42.0.0 for certificate operations."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -46,7 +46,7 @@ class TestAuthManager:
46
46
 
47
47
  # Create test configuration
48
48
  self.auth_config = AuthConfig(
49
- api_keys={"test_api_key_123": "test_user"},
49
+ api_keys={"test_user": "test_api_key_123"},
50
50
  jwt_secret="test_jwt_secret_key_32_chars_long",
51
51
  jwt_expiry_hours=1,
52
52
  ca_cert_file=None,
@@ -65,7 +65,7 @@ class TestAuthManager:
65
65
  """Test successful AuthManager initialization."""
66
66
  assert self.auth_manager.config == self.auth_config
67
67
  assert self.auth_manager.permission_manager == self.mock_permission_manager
68
- assert self.auth_manager._api_keys == {"test_user": "test_api_key_123"}
68
+ assert self.auth_manager._api_keys == {"test_api_key_123": "test_user"}
69
69
  assert self.auth_manager._jwt_secret == "test_jwt_secret_key_32_chars_long"
70
70
  assert isinstance(self.auth_manager._token_cache, dict)
71
71
  assert isinstance(self.auth_manager._session_store, dict)
@@ -429,8 +429,8 @@ class TestAuthManager:
429
429
  success = self.auth_manager.add_api_key("new_user", "new_api_key_456789")
430
430
 
431
431
  assert success is True
432
- assert "new_user" in self.auth_manager._api_keys
433
- assert self.auth_manager._api_keys["new_user"] == "new_api_key_456789"
432
+ assert "new_api_key_456789" in self.auth_manager._api_keys
433
+ assert self.auth_manager._api_keys["new_api_key_456789"] == "new_user"
434
434
 
435
435
  def test_add_api_key_invalid_input(self):
436
436
  """Test API key addition with invalid input."""
@@ -456,12 +456,12 @@ class TestAuthManager:
456
456
  """Test successful API key removal."""
457
457
  # Add a key first
458
458
  self.auth_manager.add_api_key("temp_user", "temp_key_123456789")
459
- assert "temp_user" in self.auth_manager._api_keys
459
+ assert "temp_key_123456789" in self.auth_manager._api_keys
460
460
 
461
461
  # Remove the key
462
462
  success = self.auth_manager.remove_api_key("temp_user")
463
463
  assert success is True
464
- assert "temp_user" not in self.auth_manager._api_keys
464
+ assert "temp_key_123456789" not in self.auth_manager._api_keys
465
465
 
466
466
  def test_remove_api_key_not_found(self):
467
467
  """Test API key removal for non-existent user."""
@@ -195,6 +195,7 @@ class TestCertificateConfig:
195
195
  config = CertificateConfig()
196
196
 
197
197
  assert config.enabled is False
198
+ assert config.ca_creation_mode is False
198
199
  assert config.ca_cert_path is None
199
200
  assert config.ca_key_path is None
200
201
  assert config.cert_storage_path == "./certs"
@@ -217,6 +218,7 @@ class TestCertificateConfig:
217
218
  "Certificate management enabled but CA certificate and key paths are required"
218
219
  in str(exc_info.value)
219
220
  )
221
+ assert "ca_creation_mode=True" in str(exc_info.value)
220
222
 
221
223
  def test_certificate_config_crl_enabled_without_path(self):
222
224
  """Test CertificateConfig validation when CRL enabled without path."""
@@ -272,6 +274,54 @@ class TestCertificateConfig:
272
274
  with pytest.raises(ValidationError):
273
275
  CertificateConfig(default_validity_days=3651)
274
276
 
277
+ def test_certificate_config_ca_creation_mode(self):
278
+ """Test CertificateConfig with CA creation mode enabled."""
279
+ config = CertificateConfig(
280
+ enabled=True,
281
+ ca_creation_mode=True,
282
+ cert_storage_path="./certs",
283
+ key_storage_path="./keys"
284
+ )
285
+
286
+ assert config.enabled is True
287
+ assert config.ca_creation_mode is True
288
+ assert config.ca_cert_path is None
289
+ assert config.ca_key_path is None
290
+ assert config.cert_storage_path == "./certs"
291
+ assert config.key_storage_path == "./keys"
292
+
293
+ def test_certificate_config_ca_creation_mode_with_ca_paths(self):
294
+ """Test CertificateConfig with CA creation mode and CA paths (should work)."""
295
+ config = CertificateConfig(
296
+ enabled=True,
297
+ ca_creation_mode=True,
298
+ ca_cert_path="./certs/ca.crt",
299
+ ca_key_path="./keys/ca.key",
300
+ cert_storage_path="./certs",
301
+ key_storage_path="./keys"
302
+ )
303
+
304
+ assert config.enabled is True
305
+ assert config.ca_creation_mode is True
306
+ assert config.ca_cert_path == "./certs/ca.crt"
307
+ assert config.ca_key_path == "./keys/ca.key"
308
+
309
+ def test_certificate_config_normal_mode_with_ca_paths(self):
310
+ """Test CertificateConfig in normal mode with CA paths."""
311
+ config = CertificateConfig(
312
+ enabled=True,
313
+ ca_creation_mode=False,
314
+ ca_cert_path="./certs/ca.crt",
315
+ ca_key_path="./keys/ca.key",
316
+ cert_storage_path="./certs",
317
+ key_storage_path="./keys"
318
+ )
319
+
320
+ assert config.enabled is True
321
+ assert config.ca_creation_mode is False
322
+ assert config.ca_cert_path == "./certs/ca.crt"
323
+ assert config.ca_key_path == "./keys/ca.key"
324
+
275
325
 
276
326
  class TestPermissionConfig:
277
327
  """Test suite for PermissionConfig class."""