mcp-security-framework 1.2.0__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.2.0 → mcp_security_framework-1.2.1}/PKG-INFO +1 -1
  2. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/__init__.py +1 -1
  3. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/core/auth_manager.py +29 -9
  4. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/core/cert_manager.py +20 -0
  5. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/schemas/config.py +31 -0
  6. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/schemas/models.py +46 -0
  7. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/utils/cert_utils.py +54 -0
  8. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework.egg-info/PKG-INFO +1 -1
  9. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework.egg-info/SOURCES.txt +1 -0
  10. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/pyproject.toml +1 -1
  11. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_core/test_auth_manager.py +6 -6
  12. mcp_security_framework-1.2.1/tests/test_utils/test_unitid_compat.py +550 -0
  13. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/README.md +0 -0
  14. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/cli/__init__.py +0 -0
  15. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/cli/cert_cli.py +0 -0
  16. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/cli/security_cli.py +0 -0
  17. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/constants.py +0 -0
  18. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/core/__init__.py +0 -0
  19. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/core/permission_manager.py +0 -0
  20. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/core/rate_limiter.py +0 -0
  21. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/core/security_manager.py +0 -0
  22. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/core/ssl_manager.py +0 -0
  23. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/__init__.py +0 -0
  24. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/comprehensive_example.py +0 -0
  25. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/django_example.py +0 -0
  26. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/fastapi_example.py +0 -0
  27. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/flask_example.py +0 -0
  28. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/gateway_example.py +0 -0
  29. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/microservice_example.py +0 -0
  30. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/standalone_example.py +0 -0
  31. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/test_all_examples.py +0 -0
  32. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/__init__.py +0 -0
  33. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/auth_middleware.py +0 -0
  34. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/fastapi_auth_middleware.py +0 -0
  35. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/fastapi_middleware.py +0 -0
  36. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/flask_auth_middleware.py +0 -0
  37. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/flask_middleware.py +0 -0
  38. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/mtls_middleware.py +0 -0
  39. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/rate_limit_middleware.py +0 -0
  40. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/security_middleware.py +0 -0
  41. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/schemas/__init__.py +0 -0
  42. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/schemas/responses.py +0 -0
  43. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/tests/__init__.py +0 -0
  44. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/utils/__init__.py +0 -0
  45. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/utils/crypto_utils.py +0 -0
  46. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/utils/datetime_compat.py +0 -0
  47. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/utils/validation_utils.py +0 -0
  48. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework.egg-info/dependency_links.txt +0 -0
  49. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework.egg-info/entry_points.txt +0 -0
  50. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework.egg-info/requires.txt +0 -0
  51. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework.egg-info/top_level.txt +0 -0
  52. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/setup.cfg +0 -0
  53. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/__init__.py +0 -0
  54. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/conftest.py +0 -0
  55. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_cli/__init__.py +0 -0
  56. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_cli/test_cert_cli.py +0 -0
  57. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_cli/test_security_cli.py +0 -0
  58. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_core/__init__.py +0 -0
  59. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_core/test_cert_manager.py +0 -0
  60. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_core/test_permission_manager.py +0 -0
  61. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_core/test_rate_limiter.py +0 -0
  62. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_core/test_security_manager.py +0 -0
  63. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_core/test_ssl_manager.py +0 -0
  64. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_examples/__init__.py +0 -0
  65. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_examples/test_comprehensive_example.py +0 -0
  66. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_examples/test_fastapi_example.py +0 -0
  67. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_examples/test_flask_example.py +0 -0
  68. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_examples/test_standalone_example.py +0 -0
  69. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_integration/__init__.py +0 -0
  70. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_integration/test_auth_flow.py +0 -0
  71. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_integration/test_certificate_flow.py +0 -0
  72. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_integration/test_fastapi_integration.py +0 -0
  73. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_integration/test_flask_integration.py +0 -0
  74. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_integration/test_standalone_integration.py +0 -0
  75. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_middleware/__init__.py +0 -0
  76. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_middleware/test_fastapi_auth_middleware.py +0 -0
  77. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_middleware/test_fastapi_middleware.py +0 -0
  78. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_middleware/test_flask_auth_middleware.py +0 -0
  79. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_middleware/test_flask_middleware.py +0 -0
  80. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_middleware/test_security_middleware.py +0 -0
  81. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_schemas/__init__.py +0 -0
  82. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_schemas/test_config.py +0 -0
  83. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_schemas/test_models.py +0 -0
  84. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_schemas/test_responses.py +0 -0
  85. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_schemas/test_serialization.py +0 -0
  86. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_utils/__init__.py +0 -0
  87. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_utils/test_cert_utils.py +0 -0
  88. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_utils/test_crypto_utils.py +0 -0
  89. {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_utils/test_datetime_compat.py +0 -0
  90. {mcp_security_framework-1.2.0 → 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.2.0
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"
@@ -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
@@ -599,6 +600,21 @@ class CAConfig(BaseModel):
599
600
  hash_algorithm: str = Field(
600
601
  default="sha256", description="Hash algorithm for signing"
601
602
  )
603
+ unitid: Optional[str] = Field(
604
+ default=None, description="Unique unit identifier (UUID4) for the certificate"
605
+ )
606
+
607
+ @field_validator("unitid")
608
+ @classmethod
609
+ def validate_unitid(cls, v):
610
+ """Validate unitid format."""
611
+ if v is not None:
612
+ try:
613
+ # Validate UUID4 format
614
+ uuid.UUID(v, version=4)
615
+ except ValueError:
616
+ raise ValueError("unitid must be a valid UUID4 string")
617
+ return v
602
618
 
603
619
 
604
620
  class IntermediateCAConfig(CAConfig):
@@ -668,6 +684,21 @@ class ClientCertConfig(BaseModel):
668
684
  )
669
685
  ca_cert_path: str = Field(..., description="Path to signing CA certificate")
670
686
  ca_key_path: str = Field(..., description="Path to signing CA private key")
687
+ unitid: Optional[str] = Field(
688
+ default=None, description="Unique unit identifier (UUID4) for the certificate"
689
+ )
690
+
691
+ @field_validator("unitid")
692
+ @classmethod
693
+ def validate_unitid(cls, v):
694
+ """Validate unitid format."""
695
+ if v is not None:
696
+ try:
697
+ # Validate UUID4 format
698
+ uuid.UUID(v, version=4)
699
+ except ValueError:
700
+ raise ValueError("unitid must be a valid UUID4 string")
701
+ return v
671
702
 
672
703
 
673
704
  class ServerCertConfig(ClientCertConfig):
@@ -38,6 +38,7 @@ License: MIT
38
38
  from datetime import datetime, timedelta, timezone
39
39
  from enum import Enum
40
40
  from typing import Any, Dict, List, Optional, Set, TypeAlias
41
+ import uuid
41
42
 
42
43
  from pydantic import BaseModel, Field, field_validator, model_validator
43
44
 
@@ -140,6 +141,9 @@ class AuthResult(BaseModel):
140
141
  metadata: Dict[str, Any] = Field(
141
142
  default_factory=dict, description="Additional authentication metadata"
142
143
  )
144
+ unitid: Optional[str] = Field(
145
+ default=None, description="Unique unit identifier (UUID4) from certificate"
146
+ )
143
147
 
144
148
  @field_validator("username")
145
149
  @classmethod
@@ -149,6 +153,18 @@ class AuthResult(BaseModel):
149
153
  raise ValueError("Username cannot be empty")
150
154
  return v
151
155
 
156
+ @field_validator("unitid")
157
+ @classmethod
158
+ def validate_unitid(cls, v):
159
+ """Validate unitid format."""
160
+ if v is not None:
161
+ try:
162
+ # Validate UUID4 format
163
+ uuid.UUID(v, version=4)
164
+ except ValueError:
165
+ raise ValueError("unitid must be a valid UUID4 string")
166
+ return v
167
+
152
168
  @model_validator(mode="after")
153
169
  def validate_auth_result(self):
154
170
  """Validate authentication result consistency."""
@@ -309,6 +325,9 @@ class CertificateInfo(BaseModel):
309
325
  fingerprint_sha256: Optional[str] = Field(
310
326
  default=None, description="SHA256 fingerprint"
311
327
  )
328
+ unitid: Optional[str] = Field(
329
+ default=None, description="Unique unit identifier (UUID4) for the certificate"
330
+ )
312
331
 
313
332
  @field_validator("key_size")
314
333
  @classmethod
@@ -318,6 +337,18 @@ class CertificateInfo(BaseModel):
318
337
  raise ValueError("Key size must be between 512 and 8192 bits")
319
338
  return v
320
339
 
340
+ @field_validator("unitid")
341
+ @classmethod
342
+ def validate_unitid(cls, v):
343
+ """Validate unitid format."""
344
+ if v is not None:
345
+ try:
346
+ # Validate UUID4 format
347
+ uuid.UUID(v, version=4)
348
+ except ValueError:
349
+ raise ValueError("unitid must be a valid UUID4 string")
350
+ return v
351
+
321
352
  @property
322
353
  def is_expired(self) -> bool:
323
354
  """Check if certificate is expired."""
@@ -424,6 +455,9 @@ class CertificatePair(BaseModel):
424
455
  metadata: Dict[str, Any] = Field(
425
456
  default_factory=dict, description="Additional certificate metadata"
426
457
  )
458
+ unitid: Optional[str] = Field(
459
+ default=None, description="Unique unit identifier (UUID4) for the certificate"
460
+ )
427
461
 
428
462
  @field_validator("certificate_pem")
429
463
  @classmethod
@@ -449,6 +483,18 @@ class CertificatePair(BaseModel):
449
483
  raise ValueError("Invalid private key PEM format")
450
484
  return v
451
485
 
486
+ @field_validator("unitid")
487
+ @classmethod
488
+ def validate_unitid(cls, v):
489
+ """Validate unitid format."""
490
+ if v is not None:
491
+ try:
492
+ # Validate UUID4 format
493
+ uuid.UUID(v, version=4)
494
+ except ValueError:
495
+ raise ValueError("unitid must be a valid UUID4 string")
496
+ return v
497
+
452
498
  @property
453
499
  def is_expired(self) -> bool:
454
500
  """Check if certificate is expired."""
@@ -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.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>
@@ -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.1"
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."""