mcp-security-framework 1.2.0__py3-none-any.whl → 1.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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>
@@ -1,11 +1,11 @@
1
- mcp_security_framework/__init__.py,sha256=TM8y71Navd_6woEab2cO07MefRsL0tRBItEYfVO7DS8,3172
1
+ mcp_security_framework/__init__.py,sha256=wRlT28hRsnSZCUVNGtU-KggRk66KzSQl4agKOxE5ZCA,3172
2
2
  mcp_security_framework/constants.py,sha256=k7NMSrgc83Cci8aoilybQxdC7jir7J-mVFE_EpqVrDk,5307
3
3
  mcp_security_framework/cli/__init__.py,sha256=plpWdiWMp2dcLvUuGwXynRg5CDjz8YKnNTBn7lcta08,369
4
4
  mcp_security_framework/cli/cert_cli.py,sha256=LdZ3SYKM3e3dP5LsVR5Y0OENtlG0ENu64aHefHjuiN8,23818
5
5
  mcp_security_framework/cli/security_cli.py,sha256=Thine_Zzfesz7j29y2k_XZFYUK5YSrhCc6w2FilgEiE,28486
6
6
  mcp_security_framework/core/__init__.py,sha256=LiX8_M5qWiTXccJFjSLxup9emhklp-poq57SvznsKEg,1729
7
- mcp_security_framework/core/auth_manager.py,sha256=6k-Bv7-P7K6TV1KDIzoJGTKYbJmrEUX8uCoZXrNT8Q4,39065
8
- mcp_security_framework/core/cert_manager.py,sha256=FHwcUAKkTuCRcDANiEBSC3ggHYplDpKR_26qoZKWAdw,88001
7
+ mcp_security_framework/core/auth_manager.py,sha256=GqGAW83Qg1_z2HJ0-FEVTmlli_DBSOPOap2jJMEU1_k,39882
8
+ mcp_security_framework/core/cert_manager.py,sha256=RJgZxElPgMV15Lj_N2uCyO7Ofb9z-MclM1hf8wzFwSc,88882
9
9
  mcp_security_framework/core/permission_manager.py,sha256=SADS_oXpwp9MhXHKJMCsvjEq8KWcz7vPYL05Yr-zfio,26478
10
10
  mcp_security_framework/core/rate_limiter.py,sha256=6qjVBxK2YHouSxQuCcbr0PBpRqA5toQss_Ce178RElY,20682
11
11
  mcp_security_framework/core/security_manager.py,sha256=mAF-5znqxin-MSSgXISB7t1kTkqHltEqGzzmlLAhRGs,37766
@@ -29,12 +29,12 @@ mcp_security_framework/middleware/mtls_middleware.py,sha256=WSyWIk1fCN96hkofODKj
29
29
  mcp_security_framework/middleware/rate_limit_middleware.py,sha256=deCwwigI0Pt7pBUnk2jDurI9ZyjujWTsexEWWndXm3g,13177
30
30
  mcp_security_framework/middleware/security_middleware.py,sha256=PQ251Fr2UrYVPgGfhXq6QJyqK2tRk0WCIg9_FBvfVkg,16844
31
31
  mcp_security_framework/schemas/__init__.py,sha256=lefkbRlbj2ICfasSj51MQ04o3z1YycnbnknSJCFfXbU,2590
32
- mcp_security_framework/schemas/config.py,sha256=kpJJBupR4ZpQyouZyxiZqm7U7AHfLoGncuRltpo_QEM,25825
33
- mcp_security_framework/schemas/models.py,sha256=n-Ug8O1cpMeA0mIdOd9h1i3kkBZOJsCQMxj3IcNpBFY,26957
32
+ mcp_security_framework/schemas/config.py,sha256=SUUrpOz9ZTIhZG9Fu2Gsz86yxoGbUt00Qk-Qk_GaTus,26819
33
+ mcp_security_framework/schemas/models.py,sha256=Izjy3I55zjMVLsVZpXZ0M4aK3SCks9sC2U1cbxrXYeI,28439
34
34
  mcp_security_framework/schemas/responses.py,sha256=nVXaqF5GTSprXTa_wiUEu38nvSw9WAXtKViAJNbO-Xg,23206
35
35
  mcp_security_framework/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
36
  mcp_security_framework/utils/__init__.py,sha256=wwwdmQYHTSz0Puvs9FD6aIKmWp3NFARe3JPWNH-b_wk,3098
37
- mcp_security_framework/utils/cert_utils.py,sha256=roxpUa2CqFHMLvkm8618Epac-U6_xUlNuV8bhEZoU9E,25865
37
+ mcp_security_framework/utils/cert_utils.py,sha256=4AHNrfL0Oqp354aEq5H2_0XLT7v3mNOnTVDV-DLaJyI,27585
38
38
  mcp_security_framework/utils/crypto_utils.py,sha256=OH2V7_C3FjStxFTIXMUPfNXZuWG2-QjgoBrIH4Lv4p0,12392
39
39
  mcp_security_framework/utils/datetime_compat.py,sha256=ool-xs-EevhuYygdzhiAenLAacLuZwGwjPkF43i-9gg,3859
40
40
  mcp_security_framework/utils/validation_utils.py,sha256=e9BX3kw9gdXSmFsc7lmG-qnzSlK0-Ynn7Xs4uKHquF4,16279
@@ -44,7 +44,7 @@ tests/test_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
44
  tests/test_cli/test_cert_cli.py,sha256=Rm7z-20VAvnmYKY3sgxS-qVNks1vbniQJSpSxjsx_wo,14677
45
45
  tests/test_cli/test_security_cli.py,sha256=Bpd31IPJSUl_V1Xzy74ZCOvQpwlbj8Da83C46T8Jewg,25569
46
46
  tests/test_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
- tests/test_core/test_auth_manager.py,sha256=tyJUe3yBP95SFwbhSr_IloKhEQw0BuEdvjAC1Hnwu4s,22540
47
+ tests/test_core/test_auth_manager.py,sha256=7Z2DLfJLqKtiwX5Q-lR85hN6NxHbE2Q_FT7IsoyKPQk,22568
48
48
  tests/test_core/test_cert_manager.py,sha256=4YMAkRedkAZW3PEZYEbo1PyrzMntUrKfl7arPsHXDCE,36356
49
49
  tests/test_core/test_permission_manager.py,sha256=0XeghWXZqVpKyyRuhuDu1dkLUSwuZaFWkRQxQhkkFVI,14966
50
50
  tests/test_core/test_rate_limiter.py,sha256=YzzlhlxZm-A7YGMiIV8LXDA0zmb_6uRF9GRx9s21Q0U,22544
@@ -76,9 +76,10 @@ tests/test_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
76
76
  tests/test_utils/test_cert_utils.py,sha256=yZGHPuJcjgSHFeT7gnMdsw6UYXmlGUiuHkErukOm8II,28238
77
77
  tests/test_utils/test_crypto_utils.py,sha256=yEb4hzG6-irj2DPoXY0DUboJfbeR87ussgTuBpxLGz4,20737
78
78
  tests/test_utils/test_datetime_compat.py,sha256=n8S4X5HN-_ejSNpgymDXRyZkmxhnyxwwjxFPdX23I40,5656
79
+ tests/test_utils/test_unitid_compat.py,sha256=MWh03A4FwzQyZa20PKHEWz4W03YtARwBOd_1JbABznQ,25544
79
80
  tests/test_utils/test_validation_utils.py,sha256=lus_wHJ2WyVnBGQ28S7dSv78uWcCIuLhn5uflJw-uGw,18569
80
- mcp_security_framework-1.2.0.dist-info/METADATA,sha256=_mkuF75Qp_BjlrBlamvGIcP-7ejSCdJWHwVsvjZw6eQ,11771
81
- mcp_security_framework-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
82
- mcp_security_framework-1.2.0.dist-info/entry_points.txt,sha256=qBh92fVDmd1m2f3xeW0hTu3Ksg8QfGJyV8UEkdA2itg,142
83
- mcp_security_framework-1.2.0.dist-info/top_level.txt,sha256=ifUiGrTDcD574MXSOoAN2rp2wpUvWlb4jD9LTUgDWCA,29
84
- mcp_security_framework-1.2.0.dist-info/RECORD,,
81
+ mcp_security_framework-1.2.1.dist-info/METADATA,sha256=3HiQhhOc1aqdstjAhfUnT2gRmxg61pYutP_lVkn6cxw,11771
82
+ mcp_security_framework-1.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
83
+ mcp_security_framework-1.2.1.dist-info/entry_points.txt,sha256=qBh92fVDmd1m2f3xeW0hTu3Ksg8QfGJyV8UEkdA2itg,142
84
+ mcp_security_framework-1.2.1.dist-info/top_level.txt,sha256=ifUiGrTDcD574MXSOoAN2rp2wpUvWlb4jD9LTUgDWCA,29
85
+ mcp_security_framework-1.2.1.dist-info/RECORD,,
@@ -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."""
@@ -0,0 +1,550 @@
1
+ """
2
+ Tests for UnitID Compatibility Module
3
+
4
+ This module contains tests for the unitid functionality that
5
+ handles unique unit identifiers in certificates.
6
+
7
+ Test Coverage:
8
+ - UnitID extraction from certificates
9
+ - UnitID validation
10
+ - UnitID integration with certificate creation
11
+ - UnitID integration with authentication
12
+
13
+ Author: Vasiliy Zdanovskiy
14
+ email: vasilyvz@gmail.com
15
+ """
16
+
17
+ import pytest
18
+ import uuid
19
+ from unittest.mock import Mock, patch
20
+ from cryptography import x509
21
+ from cryptography.hazmat.primitives import hashes, serialization
22
+ from cryptography.hazmat.primitives.asymmetric import rsa
23
+
24
+ from mcp_security_framework.utils.cert_utils import extract_unitid_from_certificate
25
+
26
+
27
+ class TestUnitIDCompatibility:
28
+ """Test suite for unitid compatibility functions."""
29
+
30
+ def test_extract_unitid_from_certificate_with_unitid_extension(self):
31
+ """Test extracting unitid from certificate with unitid extension."""
32
+ # Create a mock certificate with unitid extension
33
+ mock_cert = Mock()
34
+ mock_extension = Mock()
35
+ mock_extension.value.value = b"550e8400-e29b-41d4-a716-446655440000"
36
+ mock_cert.extensions.get_extension_for_oid.return_value = mock_extension
37
+
38
+ # Mock the parse_certificate function to return our mock certificate
39
+ with patch('mcp_security_framework.utils.cert_utils.parse_certificate') as mock_parse:
40
+ mock_parse.return_value = mock_cert
41
+
42
+ result = extract_unitid_from_certificate("mock_cert_data")
43
+
44
+ assert result == "550e8400-e29b-41d4-a716-446655440000"
45
+
46
+ def test_extract_unitid_from_certificate_without_unitid_extension(self):
47
+ """Test extracting unitid from certificate without unitid extension."""
48
+ # Create a mock certificate without unitid extension
49
+ mock_cert = Mock()
50
+ mock_cert.extensions.get_extension_for_oid.side_effect = x509.ExtensionNotFound(
51
+ "Extension not found", "1.3.6.1.4.1.99999.1.3"
52
+ )
53
+
54
+ # Mock the parse_certificate function to return our mock certificate
55
+ with patch('mcp_security_framework.utils.cert_utils.parse_certificate') as mock_parse:
56
+ mock_parse.return_value = mock_cert
57
+
58
+ result = extract_unitid_from_certificate("mock_cert_data")
59
+
60
+ assert result is None
61
+
62
+ def test_extract_unitid_from_certificate_with_invalid_unitid(self):
63
+ """Test extracting unitid from certificate with invalid unitid format."""
64
+ # Create a mock certificate with invalid unitid
65
+ mock_cert = Mock()
66
+ mock_extension = Mock()
67
+ mock_extension.value.value = b"invalid-unitid"
68
+ mock_cert.extensions.get_extension_for_oid.return_value = mock_extension
69
+
70
+ # Mock the parse_certificate function to return our mock certificate
71
+ with patch('mcp_security_framework.utils.cert_utils.parse_certificate') as mock_parse:
72
+ mock_parse.return_value = mock_cert
73
+
74
+ result = extract_unitid_from_certificate("mock_cert_data")
75
+
76
+ # Should return None for invalid unitid
77
+ assert result is None
78
+
79
+ def test_extract_unitid_from_certificate_with_real_certificate(self):
80
+ """Test extracting unitid from a real certificate."""
81
+ # Generate a valid UUID4
82
+ test_unitid = str(uuid.uuid4())
83
+
84
+ # Create a real certificate with unitid extension
85
+ private_key = rsa.generate_private_key(
86
+ public_exponent=65537,
87
+ key_size=2048,
88
+ )
89
+
90
+ subject = issuer = x509.Name([
91
+ x509.NameAttribute(x509.NameOID.COMMON_NAME, 'Test UnitID'),
92
+ ])
93
+
94
+ # Create certificate builder
95
+ from datetime import datetime, timezone, timedelta
96
+ builder = x509.CertificateBuilder()
97
+ builder = builder.subject_name(subject)
98
+ builder = builder.issuer_name(issuer)
99
+ builder = builder.public_key(private_key.public_key())
100
+ builder = builder.serial_number(x509.random_serial_number())
101
+ builder = builder.not_valid_before(datetime.now(timezone.utc))
102
+ builder = builder.not_valid_after(
103
+ datetime.now(timezone.utc) + timedelta(days=10)
104
+ )
105
+
106
+ # Add unitid extension
107
+ unitid_extension = x509.UnrecognizedExtension(
108
+ oid=x509.ObjectIdentifier("1.3.6.1.4.1.99999.1.3"),
109
+ value=test_unitid.encode(),
110
+ )
111
+ builder = builder.add_extension(unitid_extension, critical=False)
112
+
113
+ # Sign the certificate
114
+ certificate = builder.sign(private_key, hashes.SHA256())
115
+
116
+ # Test extraction
117
+ cert_pem = certificate.public_bytes(serialization.Encoding.PEM)
118
+ result = extract_unitid_from_certificate(cert_pem)
119
+
120
+ assert result == test_unitid
121
+
122
+ def test_unitid_validation_in_certificate_info(self):
123
+ """Test unitid validation in CertificateInfo model."""
124
+ from mcp_security_framework.schemas.models import CertificateInfo, CertificateType
125
+ from datetime import datetime, timezone
126
+
127
+ # Test with valid UUID4
128
+ valid_unitid = str(uuid.uuid4())
129
+ cert_info = CertificateInfo(
130
+ subject={"CN": "Test"},
131
+ issuer={"CN": "Test CA"},
132
+ serial_number="123456789",
133
+ not_before=datetime.now(timezone.utc),
134
+ not_after=datetime.now(timezone.utc),
135
+ certificate_type=CertificateType.CLIENT,
136
+ key_size=2048,
137
+ signature_algorithm="sha256",
138
+ unitid=valid_unitid
139
+ )
140
+
141
+ assert cert_info.unitid == valid_unitid
142
+
143
+ # Test with invalid UUID4
144
+ with pytest.raises(ValueError, match="unitid must be a valid UUID4 string"):
145
+ CertificateInfo(
146
+ subject={"CN": "Test"},
147
+ issuer={"CN": "Test CA"},
148
+ serial_number="123456789",
149
+ not_before=datetime.now(timezone.utc),
150
+ not_after=datetime.now(timezone.utc),
151
+ certificate_type=CertificateType.CLIENT,
152
+ key_size=2048,
153
+ signature_algorithm="sha256",
154
+ unitid="invalid-unitid"
155
+ )
156
+
157
+ def test_unitid_validation_in_certificate_pair(self):
158
+ """Test unitid validation in CertificatePair model."""
159
+ from mcp_security_framework.schemas.models import CertificatePair, CertificateType
160
+ from datetime import datetime, timezone
161
+
162
+ # Test with valid UUID4
163
+ valid_unitid = str(uuid.uuid4())
164
+ cert_pair = CertificatePair(
165
+ certificate_path="/path/to/cert.crt",
166
+ private_key_path="/path/to/key.key",
167
+ certificate_pem="-----BEGIN CERTIFICATE-----\nMOCK\n-----END CERTIFICATE-----",
168
+ private_key_pem="-----BEGIN PRIVATE KEY-----\nMOCK\n-----END PRIVATE KEY-----",
169
+ serial_number="123456789",
170
+ common_name="Test",
171
+ organization="Test Org",
172
+ not_before=datetime.now(timezone.utc),
173
+ not_after=datetime.now(timezone.utc),
174
+ certificate_type=CertificateType.CLIENT,
175
+ key_size=2048,
176
+ unitid=valid_unitid
177
+ )
178
+
179
+ assert cert_pair.unitid == valid_unitid
180
+
181
+ # Test with invalid UUID4
182
+ with pytest.raises(ValueError, match="unitid must be a valid UUID4 string"):
183
+ CertificatePair(
184
+ certificate_path="/path/to/cert.crt",
185
+ private_key_path="/path/to/key.key",
186
+ certificate_pem="-----BEGIN CERTIFICATE-----\nMOCK\n-----END CERTIFICATE-----",
187
+ private_key_pem="-----BEGIN PRIVATE KEY-----\nMOCK\n-----END PRIVATE KEY-----",
188
+ serial_number="123456789",
189
+ common_name="Test",
190
+ organization="Test Org",
191
+ not_before=datetime.now(timezone.utc),
192
+ not_after=datetime.now(timezone.utc),
193
+ certificate_type=CertificateType.CLIENT,
194
+ key_size=2048,
195
+ unitid="invalid-unitid"
196
+ )
197
+
198
+ def test_unitid_validation_in_auth_result(self):
199
+ """Test unitid validation in AuthResult model."""
200
+ from mcp_security_framework.schemas.models import AuthResult, AuthStatus
201
+ from datetime import datetime, timezone
202
+
203
+ # Test with valid UUID4
204
+ valid_unitid = str(uuid.uuid4())
205
+ auth_result = AuthResult(
206
+ is_valid=True,
207
+ status=AuthStatus.SUCCESS,
208
+ username="test_user",
209
+ unitid=valid_unitid
210
+ )
211
+
212
+ assert auth_result.unitid == valid_unitid
213
+
214
+ # Test with invalid UUID4
215
+ with pytest.raises(ValueError, match="unitid must be a valid UUID4 string"):
216
+ AuthResult(
217
+ is_valid=True,
218
+ status=AuthStatus.SUCCESS,
219
+ username="test_user",
220
+ unitid="invalid-unitid"
221
+ )
222
+
223
+ def test_unitid_in_certificate_creation_config(self):
224
+ """Test unitid in certificate creation configuration."""
225
+ from mcp_security_framework.schemas.config import CAConfig, ClientCertConfig
226
+
227
+ # Test with valid UUID4
228
+ valid_unitid = str(uuid.uuid4())
229
+
230
+ ca_config = CAConfig(
231
+ common_name="Test CA",
232
+ organization="Test Org",
233
+ country="US",
234
+ unitid=valid_unitid
235
+ )
236
+ assert ca_config.unitid == valid_unitid
237
+
238
+ client_config = ClientCertConfig(
239
+ common_name="test.example.com",
240
+ organization="Test Org",
241
+ country="US",
242
+ ca_cert_path="/path/to/ca.crt",
243
+ ca_key_path="/path/to/ca.key",
244
+ unitid=valid_unitid
245
+ )
246
+ assert client_config.unitid == valid_unitid
247
+
248
+ # Test with invalid UUID4
249
+ with pytest.raises(ValueError, match="unitid must be a valid UUID4 string"):
250
+ CAConfig(
251
+ common_name="Test CA",
252
+ organization="Test Org",
253
+ country="US",
254
+ unitid="invalid-unitid"
255
+ )
256
+
257
+ def test_unitid_integration_with_certificate_creation(self):
258
+ """Test unitid integration with certificate creation."""
259
+ from mcp_security_framework.core.cert_manager import CertificateManager
260
+ from mcp_security_framework.schemas.config import CertificateConfig, CAConfig
261
+ from datetime import datetime, timezone
262
+ import tempfile
263
+ import os
264
+
265
+ # Create temporary directory for certificates
266
+ with tempfile.TemporaryDirectory() as temp_dir:
267
+ cert_config = CertificateConfig(
268
+ enabled=True,
269
+ ca_cert_path=os.path.join(temp_dir, "ca.crt"),
270
+ ca_key_path=os.path.join(temp_dir, "ca.key"),
271
+ cert_storage_path=temp_dir,
272
+ key_storage_path=temp_dir
273
+ )
274
+
275
+ # Mock the configuration validation to avoid file system checks
276
+ with patch.object(CertificateManager, '_validate_configuration'):
277
+ cert_manager = CertificateManager(cert_config)
278
+
279
+ # Test unitid in CA creation
280
+ valid_unitid = str(uuid.uuid4())
281
+ ca_config = CAConfig(
282
+ common_name="Test CA",
283
+ organization="Test Org",
284
+ country="US",
285
+ unitid=valid_unitid
286
+ )
287
+
288
+ # Mock the certificate creation to avoid actual file operations
289
+ with patch.object(cert_manager, '_validate_configuration'):
290
+ with patch('builtins.open', create=True):
291
+ with patch('os.makedirs'):
292
+ with patch('os.chmod'):
293
+ # Mock the certificate building process
294
+ mock_cert = Mock()
295
+ mock_cert.serial_number = 123456789
296
+ mock_cert.not_valid_before = datetime.now(timezone.utc)
297
+ mock_cert.not_valid_after = datetime.now(timezone.utc)
298
+ mock_cert.public_bytes.return_value = b"-----BEGIN CERTIFICATE-----\nMOCK\n-----END CERTIFICATE-----"
299
+
300
+ with patch('mcp_security_framework.core.cert_manager.rsa.generate_private_key') as mock_key:
301
+ mock_private_key = Mock()
302
+ mock_private_key.public_key.return_value = Mock()
303
+ mock_private_key.private_bytes.return_value = b"-----BEGIN PRIVATE KEY-----\nMOCK\n-----END PRIVATE KEY-----"
304
+ mock_key.return_value = mock_private_key
305
+
306
+ with patch('mcp_security_framework.core.cert_manager.x509.CertificateBuilder') as mock_builder:
307
+ mock_builder_instance = Mock()
308
+ mock_builder_instance.subject_name.return_value = mock_builder_instance
309
+ mock_builder_instance.issuer_name.return_value = mock_builder_instance
310
+ mock_builder_instance.public_key.return_value = mock_builder_instance
311
+ mock_builder_instance.serial_number.return_value = mock_builder_instance
312
+ mock_builder_instance.not_valid_before.return_value = mock_builder_instance
313
+ mock_builder_instance.not_valid_after.return_value = mock_builder_instance
314
+ mock_builder_instance.add_extension.return_value = mock_builder_instance
315
+ mock_builder_instance.sign.return_value = mock_cert
316
+ mock_builder.return_value = mock_builder_instance
317
+
318
+ # Mock the datetime compatibility functions
319
+ with patch('mcp_security_framework.core.cert_manager.get_not_valid_before_utc') as mock_before:
320
+ with patch('mcp_security_framework.core.cert_manager.get_not_valid_after_utc') as mock_after:
321
+ mock_before.return_value = datetime.now(timezone.utc)
322
+ mock_after.return_value = datetime.now(timezone.utc)
323
+
324
+ result = cert_manager.create_root_ca(ca_config)
325
+
326
+ # Verify unitid was added to the certificate pair
327
+ assert result.unitid == valid_unitid
328
+
329
+ def test_unitid_integration_with_authentication(self):
330
+ """Test unitid integration with authentication."""
331
+ from mcp_security_framework.core.auth_manager import AuthManager
332
+ from mcp_security_framework.schemas.config import AuthConfig
333
+ from mcp_security_framework.core.permission_manager import PermissionManager
334
+ from mcp_security_framework.schemas.config import PermissionConfig
335
+ from unittest.mock import Mock
336
+ import tempfile
337
+ import json
338
+ import os
339
+
340
+ # Create test configuration
341
+ auth_config = AuthConfig(
342
+ enabled=True,
343
+ api_keys={"test_key": "test_user"},
344
+ jwt_secret="test_secret_key_for_jwt_signing_12345"
345
+ )
346
+
347
+ # Create temporary roles file
348
+ with tempfile.TemporaryDirectory() as temp_dir:
349
+ roles_file = os.path.join(temp_dir, "test_roles.json")
350
+ with open(roles_file, "w") as f:
351
+ json.dump({"roles": {}}, f)
352
+
353
+ perm_config = PermissionConfig(
354
+ enabled=True,
355
+ roles_file=roles_file
356
+ )
357
+
358
+ permission_manager = PermissionManager(perm_config)
359
+ auth_manager = AuthManager(auth_config, permission_manager)
360
+
361
+ # Test unitid in certificate authentication
362
+ valid_unitid = str(uuid.uuid4())
363
+
364
+ # Mock certificate with unitid
365
+ mock_cert_pem = "-----BEGIN CERTIFICATE-----\nMOCK\n-----END CERTIFICATE-----"
366
+
367
+ # Mock the authenticate_certificate method to return a successful result with unitid
368
+ from mcp_security_framework.schemas.models import AuthResult, AuthStatus
369
+ from datetime import datetime, timezone, timedelta
370
+
371
+ expected_result = AuthResult(
372
+ is_valid=True,
373
+ status=AuthStatus.SUCCESS,
374
+ username="test_user",
375
+ roles=[],
376
+ auth_method="certificate",
377
+ auth_timestamp=datetime.now(timezone.utc),
378
+ token_expiry=datetime.now(timezone.utc) + timedelta(days=30),
379
+ unitid=valid_unitid,
380
+ )
381
+
382
+ with patch.object(auth_manager, 'authenticate_certificate', return_value=expected_result):
383
+ result = auth_manager.authenticate_certificate(mock_cert_pem)
384
+
385
+ # Verify unitid was included in authentication result
386
+ assert result.unitid == valid_unitid
387
+
388
+ def test_unitid_optional_in_certificate_creation(self):
389
+ """Test that unitid is optional in certificate creation."""
390
+ from mcp_security_framework.schemas.config import CAConfig, ClientCertConfig
391
+
392
+ # Test CA config without unitid
393
+ ca_config = CAConfig(
394
+ common_name="Test CA",
395
+ organization="Test Org",
396
+ country="US"
397
+ # unitid not specified
398
+ )
399
+ assert ca_config.unitid is None
400
+
401
+ # Test client config without unitid
402
+ client_config = ClientCertConfig(
403
+ common_name="test.example.com",
404
+ organization="Test Org",
405
+ country="US",
406
+ ca_cert_path="/path/to/ca.crt",
407
+ ca_key_path="/path/to/ca.key"
408
+ # unitid not specified
409
+ )
410
+ assert client_config.unitid is None
411
+
412
+ def test_unitid_optional_in_certificate_info(self):
413
+ """Test that unitid is optional in CertificateInfo."""
414
+ from mcp_security_framework.schemas.models import CertificateInfo, CertificateType
415
+ from datetime import datetime, timezone
416
+
417
+ # Test CertificateInfo without unitid
418
+ cert_info = CertificateInfo(
419
+ subject={"CN": "Test"},
420
+ issuer={"CN": "Test CA"},
421
+ serial_number="123456789",
422
+ not_before=datetime.now(timezone.utc),
423
+ not_after=datetime.now(timezone.utc),
424
+ certificate_type=CertificateType.CLIENT,
425
+ key_size=2048,
426
+ signature_algorithm="sha256"
427
+ # unitid not specified
428
+ )
429
+ assert cert_info.unitid is None
430
+
431
+ def test_unitid_optional_in_certificate_pair(self):
432
+ """Test that unitid is optional in CertificatePair."""
433
+ from mcp_security_framework.schemas.models import CertificatePair, CertificateType
434
+ from datetime import datetime, timezone
435
+
436
+ # Test CertificatePair without unitid
437
+ cert_pair = CertificatePair(
438
+ certificate_path="/path/to/cert.crt",
439
+ private_key_path="/path/to/key.key",
440
+ certificate_pem="-----BEGIN CERTIFICATE-----\nMOCK\n-----END CERTIFICATE-----",
441
+ private_key_pem="-----BEGIN PRIVATE KEY-----\nMOCK\n-----END PRIVATE KEY-----",
442
+ serial_number="123456789",
443
+ common_name="Test",
444
+ organization="Test Org",
445
+ not_before=datetime.now(timezone.utc),
446
+ not_after=datetime.now(timezone.utc),
447
+ certificate_type=CertificateType.CLIENT,
448
+ key_size=2048
449
+ # unitid not specified
450
+ )
451
+ assert cert_pair.unitid is None
452
+
453
+ def test_unitid_optional_in_auth_result(self):
454
+ """Test that unitid is optional in AuthResult."""
455
+ from mcp_security_framework.schemas.models import AuthResult, AuthStatus
456
+
457
+ # Test AuthResult without unitid
458
+ auth_result = AuthResult(
459
+ is_valid=True,
460
+ status=AuthStatus.SUCCESS,
461
+ username="test_user"
462
+ # unitid not specified
463
+ )
464
+ assert auth_result.unitid is None
465
+
466
+ def test_extract_unitid_from_certificate_without_unitid_returns_none(self):
467
+ """Test that extract_unitid_from_certificate returns None when unitid is not present."""
468
+ # Create a mock certificate without unitid extension
469
+ mock_cert = Mock()
470
+ mock_cert.extensions.get_extension_for_oid.side_effect = x509.ExtensionNotFound(
471
+ "Extension not found", "1.3.6.1.4.1.99999.1.3"
472
+ )
473
+
474
+ # Mock the parse_certificate function to return our mock certificate
475
+ with patch('mcp_security_framework.utils.cert_utils.parse_certificate') as mock_parse:
476
+ mock_parse.return_value = mock_cert
477
+
478
+ result = extract_unitid_from_certificate("mock_cert_data")
479
+
480
+ # Should return None when unitid is not present
481
+ assert result is None
482
+
483
+ def test_certificate_creation_without_unitid(self):
484
+ """Test certificate creation when unitid is not specified."""
485
+ from mcp_security_framework.core.cert_manager import CertificateManager
486
+ from mcp_security_framework.schemas.config import CertificateConfig, CAConfig
487
+ from datetime import datetime, timezone
488
+ import tempfile
489
+ import os
490
+
491
+ # Create temporary directory for certificates
492
+ with tempfile.TemporaryDirectory() as temp_dir:
493
+ cert_config = CertificateConfig(
494
+ enabled=True,
495
+ ca_cert_path=os.path.join(temp_dir, "ca.crt"),
496
+ ca_key_path=os.path.join(temp_dir, "ca.key"),
497
+ cert_storage_path=temp_dir,
498
+ key_storage_path=temp_dir
499
+ )
500
+
501
+ # Mock the configuration validation to avoid file system checks
502
+ with patch.object(CertificateManager, '_validate_configuration'):
503
+ cert_manager = CertificateManager(cert_config)
504
+
505
+ # Test CA creation without unitid
506
+ ca_config = CAConfig(
507
+ common_name="Test CA",
508
+ organization="Test Org",
509
+ country="US"
510
+ # unitid not specified
511
+ )
512
+
513
+ # Mock the certificate building process
514
+ with patch('builtins.open', create=True):
515
+ with patch('os.makedirs'):
516
+ with patch('os.chmod'):
517
+ mock_cert = Mock()
518
+ mock_cert.serial_number = 123456789
519
+ mock_cert.not_valid_before = datetime.now(timezone.utc)
520
+ mock_cert.not_valid_after = datetime.now(timezone.utc)
521
+ mock_cert.public_bytes.return_value = b"-----BEGIN CERTIFICATE-----\nMOCK\n-----END CERTIFICATE-----"
522
+
523
+ with patch('mcp_security_framework.core.cert_manager.rsa.generate_private_key') as mock_key:
524
+ mock_private_key = Mock()
525
+ mock_private_key.public_key.return_value = Mock()
526
+ mock_private_key.private_bytes.return_value = b"-----BEGIN PRIVATE KEY-----\nMOCK\n-----END PRIVATE KEY-----"
527
+ mock_key.return_value = mock_private_key
528
+
529
+ with patch('mcp_security_framework.core.cert_manager.x509.CertificateBuilder') as mock_builder:
530
+ mock_builder_instance = Mock()
531
+ mock_builder_instance.subject_name.return_value = mock_builder_instance
532
+ mock_builder_instance.issuer_name.return_value = mock_builder_instance
533
+ mock_builder_instance.public_key.return_value = mock_builder_instance
534
+ mock_builder_instance.serial_number.return_value = mock_builder_instance
535
+ mock_builder_instance.not_valid_before.return_value = mock_builder_instance
536
+ mock_builder_instance.not_valid_after.return_value = mock_builder_instance
537
+ mock_builder_instance.add_extension.return_value = mock_builder_instance
538
+ mock_builder_instance.sign.return_value = mock_cert
539
+ mock_builder.return_value = mock_builder_instance
540
+
541
+ # Mock the datetime compatibility functions
542
+ with patch('mcp_security_framework.core.cert_manager.get_not_valid_before_utc') as mock_before:
543
+ with patch('mcp_security_framework.core.cert_manager.get_not_valid_after_utc') as mock_after:
544
+ mock_before.return_value = datetime.now(timezone.utc)
545
+ mock_after.return_value = datetime.now(timezone.utc)
546
+
547
+ result = cert_manager.create_root_ca(ca_config)
548
+
549
+ # Verify unitid is None when not specified
550
+ assert result.unitid is None