mcp-security-framework 1.1.0__py3-none-any.whl → 1.1.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.
Files changed (58) hide show
  1. mcp_security_framework/__init__.py +26 -15
  2. mcp_security_framework/cli/__init__.py +1 -1
  3. mcp_security_framework/cli/cert_cli.py +233 -197
  4. mcp_security_framework/cli/security_cli.py +324 -234
  5. mcp_security_framework/constants.py +21 -27
  6. mcp_security_framework/core/auth_manager.py +41 -22
  7. mcp_security_framework/core/cert_manager.py +210 -147
  8. mcp_security_framework/core/permission_manager.py +9 -9
  9. mcp_security_framework/core/rate_limiter.py +2 -2
  10. mcp_security_framework/core/security_manager.py +284 -229
  11. mcp_security_framework/examples/__init__.py +6 -0
  12. mcp_security_framework/examples/comprehensive_example.py +349 -279
  13. mcp_security_framework/examples/django_example.py +247 -206
  14. mcp_security_framework/examples/fastapi_example.py +315 -283
  15. mcp_security_framework/examples/flask_example.py +274 -203
  16. mcp_security_framework/examples/gateway_example.py +304 -237
  17. mcp_security_framework/examples/microservice_example.py +258 -189
  18. mcp_security_framework/examples/standalone_example.py +255 -230
  19. mcp_security_framework/examples/test_all_examples.py +151 -135
  20. mcp_security_framework/middleware/__init__.py +46 -55
  21. mcp_security_framework/middleware/auth_middleware.py +62 -63
  22. mcp_security_framework/middleware/fastapi_auth_middleware.py +119 -118
  23. mcp_security_framework/middleware/fastapi_middleware.py +156 -148
  24. mcp_security_framework/middleware/flask_auth_middleware.py +160 -147
  25. mcp_security_framework/middleware/flask_middleware.py +183 -157
  26. mcp_security_framework/middleware/mtls_middleware.py +106 -117
  27. mcp_security_framework/middleware/rate_limit_middleware.py +105 -101
  28. mcp_security_framework/middleware/security_middleware.py +109 -124
  29. mcp_security_framework/schemas/config.py +2 -1
  30. mcp_security_framework/schemas/models.py +18 -6
  31. mcp_security_framework/utils/cert_utils.py +14 -8
  32. mcp_security_framework/utils/datetime_compat.py +116 -0
  33. {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/METADATA +2 -1
  34. mcp_security_framework-1.1.1.dist-info/RECORD +84 -0
  35. tests/conftest.py +63 -66
  36. tests/test_cli/test_cert_cli.py +184 -146
  37. tests/test_cli/test_security_cli.py +274 -247
  38. tests/test_core/test_cert_manager.py +24 -10
  39. tests/test_core/test_security_manager.py +2 -2
  40. tests/test_examples/test_comprehensive_example.py +190 -137
  41. tests/test_examples/test_fastapi_example.py +124 -101
  42. tests/test_examples/test_flask_example.py +124 -101
  43. tests/test_examples/test_standalone_example.py +73 -80
  44. tests/test_integration/test_auth_flow.py +213 -197
  45. tests/test_integration/test_certificate_flow.py +180 -149
  46. tests/test_integration/test_fastapi_integration.py +108 -111
  47. tests/test_integration/test_flask_integration.py +141 -140
  48. tests/test_integration/test_standalone_integration.py +290 -259
  49. tests/test_middleware/test_fastapi_auth_middleware.py +195 -174
  50. tests/test_middleware/test_fastapi_middleware.py +147 -132
  51. tests/test_middleware/test_flask_auth_middleware.py +260 -202
  52. tests/test_middleware/test_flask_middleware.py +201 -179
  53. tests/test_middleware/test_security_middleware.py +145 -130
  54. tests/test_utils/test_datetime_compat.py +147 -0
  55. mcp_security_framework-1.1.0.dist-info/RECORD +0 -82
  56. {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/WHEEL +0 -0
  57. {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/entry_points.txt +0 -0
  58. {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/top_level.txt +0 -0
@@ -43,8 +43,8 @@ from mcp_security_framework.schemas.config import (
43
43
  CAConfig,
44
44
  CertificateConfig,
45
45
  ClientCertConfig,
46
- ServerCertConfig,
47
46
  IntermediateCAConfig,
47
+ ServerCertConfig,
48
48
  )
49
49
  from mcp_security_framework.schemas.models import (
50
50
  CertificateInfo,
@@ -60,6 +60,10 @@ from mcp_security_framework.utils.cert_utils import (
60
60
  parse_certificate,
61
61
  validate_certificate_chain,
62
62
  )
63
+ from mcp_security_framework.utils.datetime_compat import (
64
+ get_not_valid_after_utc,
65
+ get_not_valid_before_utc,
66
+ )
63
67
 
64
68
 
65
69
  class CertificateManager:
@@ -313,8 +317,8 @@ class CertificateManager:
313
317
  serial_number=str(certificate.serial_number),
314
318
  common_name=ca_config.common_name,
315
319
  organization=ca_config.organization,
316
- not_before=certificate.not_valid_before_utc,
317
- not_after=certificate.not_valid_after_utc.replace(tzinfo=timezone.utc),
320
+ not_before=get_not_valid_before_utc(certificate),
321
+ not_after=get_not_valid_after_utc(certificate),
318
322
  certificate_type=CertificateType.ROOT_CA,
319
323
  key_size=ca_config.key_size,
320
324
  )
@@ -340,7 +344,9 @@ class CertificateManager:
340
344
  f"Failed to create root CA certificate: {str(e)}"
341
345
  )
342
346
 
343
- def create_intermediate_ca(self, intermediate_config: IntermediateCAConfig) -> CertificatePair:
347
+ def create_intermediate_ca(
348
+ self, intermediate_config: IntermediateCAConfig
349
+ ) -> CertificatePair:
344
350
  """
345
351
  Create intermediate CA certificate signed by parent CA.
346
352
 
@@ -400,32 +406,47 @@ class CertificateManager:
400
406
  try:
401
407
  # Validate intermediate configuration
402
408
  if not intermediate_config.common_name:
403
- raise ValueError("Common name is required for intermediate CA certificate")
409
+ raise ValueError(
410
+ "Common name is required for intermediate CA certificate"
411
+ )
404
412
 
405
- if not intermediate_config.parent_ca_cert or not intermediate_config.parent_ca_key:
413
+ if (
414
+ not intermediate_config.parent_ca_cert
415
+ or not intermediate_config.parent_ca_key
416
+ ):
406
417
  raise ValueError("Parent CA certificate and key paths are required")
407
418
 
408
419
  # Load parent CA certificate and private key
409
420
  if not os.path.exists(intermediate_config.parent_ca_cert):
410
- raise FileNotFoundError(f"Parent CA certificate not found: {intermediate_config.parent_ca_cert}")
421
+ raise FileNotFoundError(
422
+ f"Parent CA certificate not found: {intermediate_config.parent_ca_cert}"
423
+ )
411
424
 
412
425
  if not os.path.exists(intermediate_config.parent_ca_key):
413
- raise FileNotFoundError(f"Parent CA private key not found: {intermediate_config.parent_ca_key}")
426
+ raise FileNotFoundError(
427
+ f"Parent CA private key not found: {intermediate_config.parent_ca_key}"
428
+ )
414
429
 
415
430
  with open(intermediate_config.parent_ca_cert, "rb") as f:
416
431
  parent_ca_cert = x509.load_pem_x509_certificate(f.read())
417
432
 
418
433
  with open(intermediate_config.parent_ca_key, "rb") as f:
419
- parent_ca_key = serialization.load_pem_private_key(f.read(), password=None)
434
+ parent_ca_key = serialization.load_pem_private_key(
435
+ f.read(), password=None
436
+ )
420
437
 
421
438
  # Generate intermediate CA private key
422
439
  private_key = rsa.generate_private_key(
423
- public_exponent=65537, key_size=intermediate_config.key_size, backend=None
440
+ public_exponent=65537,
441
+ key_size=intermediate_config.key_size,
442
+ backend=None,
424
443
  )
425
444
 
426
445
  # Create certificate subject
427
446
  subject_attributes = [
428
- x509.NameAttribute(NameOID.COMMON_NAME, intermediate_config.common_name),
447
+ x509.NameAttribute(
448
+ NameOID.COMMON_NAME, intermediate_config.common_name
449
+ ),
429
450
  x509.NameAttribute(
430
451
  NameOID.ORGANIZATION_NAME, intermediate_config.organization
431
452
  ),
@@ -434,12 +455,16 @@ class CertificateManager:
434
455
 
435
456
  if intermediate_config.state:
436
457
  subject_attributes.append(
437
- x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, intermediate_config.state)
458
+ x509.NameAttribute(
459
+ NameOID.STATE_OR_PROVINCE_NAME, intermediate_config.state
460
+ )
438
461
  )
439
462
 
440
463
  if intermediate_config.locality:
441
464
  subject_attributes.append(
442
- x509.NameAttribute(NameOID.LOCALITY_NAME, intermediate_config.locality)
465
+ x509.NameAttribute(
466
+ NameOID.LOCALITY_NAME, intermediate_config.locality
467
+ )
443
468
  )
444
469
 
445
470
  if intermediate_config.email:
@@ -457,7 +482,8 @@ class CertificateManager:
457
482
  builder = builder.serial_number(x509.random_serial_number())
458
483
  builder = builder.not_valid_before(datetime.now(timezone.utc))
459
484
  builder = builder.not_valid_after(
460
- datetime.now(timezone.utc) + timedelta(days=intermediate_config.validity_years * 365)
485
+ datetime.now(timezone.utc)
486
+ + timedelta(days=intermediate_config.validity_years * 365)
461
487
  )
462
488
 
463
489
  # Add CA extensions
@@ -487,7 +513,9 @@ class CertificateManager:
487
513
 
488
514
  # Add Authority Key Identifier
489
515
  builder = builder.add_extension(
490
- x509.AuthorityKeyIdentifier.from_issuer_public_key(parent_ca_key.public_key()),
516
+ x509.AuthorityKeyIdentifier.from_issuer_public_key(
517
+ parent_ca_key.public_key()
518
+ ),
491
519
  critical=False,
492
520
  )
493
521
 
@@ -527,15 +555,17 @@ class CertificateManager:
527
555
  cert_pair = CertificatePair(
528
556
  certificate_path=cert_path,
529
557
  private_key_path=key_path,
530
- certificate_pem=certificate.public_bytes(serialization.Encoding.PEM).decode(),
558
+ certificate_pem=certificate.public_bytes(
559
+ serialization.Encoding.PEM
560
+ ).decode(),
531
561
  private_key_pem=private_key.private_bytes(
532
562
  encoding=serialization.Encoding.PEM,
533
563
  format=serialization.PrivateFormat.PKCS8,
534
564
  encryption_algorithm=serialization.NoEncryption(),
535
565
  ).decode(),
536
566
  serial_number=str(certificate.serial_number),
537
- not_before=certificate.not_valid_before_utc.replace(tzinfo=timezone.utc),
538
- not_after=certificate.not_valid_after_utc.replace(tzinfo=timezone.utc),
567
+ not_before=get_not_valid_before_utc(certificate),
568
+ not_after=get_not_valid_after_utc(certificate),
539
569
  common_name=intermediate_config.common_name,
540
570
  organization=intermediate_config.organization,
541
571
  certificate_type=CertificateType.INTERMEDIATE_CA,
@@ -558,7 +588,10 @@ class CertificateManager:
558
588
  except Exception as e:
559
589
  self.logger.error(
560
590
  "Failed to create intermediate CA certificate",
561
- extra={"intermediate_config": intermediate_config.model_dump(), "error": str(e)},
591
+ extra={
592
+ "intermediate_config": intermediate_config.model_dump(),
593
+ "error": str(e),
594
+ },
562
595
  )
563
596
  raise CertificateGenerationError(
564
597
  f"Failed to create intermediate CA certificate: {str(e)}"
@@ -775,8 +808,8 @@ class CertificateManager:
775
808
  serial_number=str(certificate.serial_number),
776
809
  common_name=client_config.common_name,
777
810
  organization=client_config.organization,
778
- not_before=certificate.not_valid_before_utc,
779
- not_after=certificate.not_valid_after_utc.replace(tzinfo=timezone.utc),
811
+ not_before=get_not_valid_before_utc(certificate),
812
+ not_after=get_not_valid_after_utc(certificate),
780
813
  certificate_type=CertificateType.CLIENT,
781
814
  key_size=client_config.key_size,
782
815
  )
@@ -1009,8 +1042,8 @@ class CertificateManager:
1009
1042
  serial_number=str(certificate.serial_number),
1010
1043
  common_name=server_config.common_name,
1011
1044
  organization=server_config.organization,
1012
- not_before=certificate.not_valid_before_utc,
1013
- not_after=certificate.not_valid_after_utc.replace(tzinfo=timezone.utc),
1045
+ not_before=get_not_valid_before_utc(certificate),
1046
+ not_after=get_not_valid_after_utc(certificate),
1014
1047
  certificate_type=CertificateType.SERVER,
1015
1048
  key_size=server_config.key_size,
1016
1049
  )
@@ -1038,52 +1071,54 @@ class CertificateManager:
1038
1071
  )
1039
1072
 
1040
1073
  def renew_certificate(
1041
- self,
1042
- cert_path: str,
1074
+ self,
1075
+ cert_path: str,
1043
1076
  ca_cert_path: Optional[str] = None,
1044
1077
  ca_key_path: Optional[str] = None,
1045
- validity_years: int = 1
1078
+ validity_years: int = 1,
1046
1079
  ) -> CertificatePair:
1047
1080
  """
1048
1081
  Renew an existing certificate with new validity period.
1049
-
1082
+
1050
1083
  This method renews a certificate by creating a new certificate
1051
1084
  with the same subject and key but extended validity period.
1052
-
1085
+
1053
1086
  Args:
1054
1087
  cert_path (str): Path to existing certificate to renew
1055
1088
  ca_cert_path (Optional[str]): Path to CA certificate for signing
1056
1089
  ca_key_path (Optional[str]): Path to CA private key for signing
1057
1090
  validity_years (int): New validity period in years
1058
-
1091
+
1059
1092
  Returns:
1060
1093
  CertificatePair: New certificate pair with extended validity
1061
-
1094
+
1062
1095
  Raises:
1063
1096
  CertificateValidationError: When certificate validation fails
1064
1097
  CertificateGenerationError: When renewal fails
1065
1098
  """
1066
1099
  try:
1067
1100
  # Load existing certificate
1068
- with open(cert_path, 'rb') as f:
1101
+ with open(cert_path, "rb") as f:
1069
1102
  cert_data = f.read()
1070
-
1103
+
1071
1104
  cert = x509.load_pem_x509_certificate(cert_data)
1072
-
1105
+
1073
1106
  # Use provided CA paths or default from config
1074
1107
  ca_cert_file = ca_cert_path or self.config.ca_cert_path
1075
1108
  ca_key_file = ca_key_path or self.config.ca_key_path
1076
-
1109
+
1077
1110
  if not ca_cert_file or not ca_key_file:
1078
- raise CertificateConfigurationError("CA certificate and key paths are required")
1079
-
1111
+ raise CertificateConfigurationError(
1112
+ "CA certificate and key paths are required"
1113
+ )
1114
+
1080
1115
  # Load CA certificate and key
1081
- with open(ca_cert_file, 'rb') as f:
1116
+ with open(ca_cert_file, "rb") as f:
1082
1117
  ca_cert = x509.load_pem_x509_certificate(f.read())
1083
-
1084
- with open(ca_key_file, 'rb') as f:
1118
+
1119
+ with open(ca_key_file, "rb") as f:
1085
1120
  ca_key = serialization.load_pem_private_key(f.read(), password=None)
1086
-
1121
+
1087
1122
  # Create new certificate with extended validity
1088
1123
  builder = x509.CertificateBuilder()
1089
1124
  builder = builder.subject_name(cert.subject)
@@ -1094,34 +1129,37 @@ class CertificateManager:
1094
1129
  builder = builder.not_valid_after(
1095
1130
  datetime.now(timezone.utc) + timedelta(days=365 * validity_years)
1096
1131
  )
1097
-
1132
+
1098
1133
  # Copy extensions from original certificate
1099
1134
  for extension in cert.extensions:
1100
1135
  if extension.oid not in [x509.ExtensionOID.AUTHORITY_KEY_IDENTIFIER]:
1101
- builder = builder.add_extension(extension.value, critical=extension.critical)
1102
-
1136
+ builder = builder.add_extension(
1137
+ extension.value, critical=extension.critical
1138
+ )
1139
+
1103
1140
  # Sign the certificate
1104
1141
  new_cert = builder.sign(ca_key, hashes.SHA256())
1105
-
1142
+
1106
1143
  # Generate new file paths
1107
1144
  cert_dir = os.path.dirname(cert_path)
1108
1145
  cert_name = os.path.splitext(os.path.basename(cert_path))[0]
1109
1146
  new_cert_path = os.path.join(cert_dir, f"{cert_name}_renewed.crt")
1110
1147
  new_key_path = os.path.join(cert_dir, f"{cert_name}_renewed.key")
1111
-
1148
+
1112
1149
  # Save new certificate
1113
- with open(new_cert_path, 'wb') as f:
1150
+ with open(new_cert_path, "wb") as f:
1114
1151
  f.write(new_cert.public_bytes(serialization.Encoding.PEM))
1115
-
1152
+
1116
1153
  # For renewal, we typically keep the same private key
1117
1154
  # Copy the original private key if it exists
1118
- key_path = cert_path.replace('.crt', '.key').replace('.pem', '.key')
1155
+ key_path = cert_path.replace(".crt", ".key").replace(".pem", ".key")
1119
1156
  private_key_pem = ""
1120
1157
  if os.path.exists(key_path):
1121
1158
  import shutil
1159
+
1122
1160
  shutil.copy2(key_path, new_key_path)
1123
1161
  # Read the private key content
1124
- with open(key_path, 'r') as f:
1162
+ with open(key_path, "r") as f:
1125
1163
  private_key_pem = f.read()
1126
1164
  else:
1127
1165
  # Create a placeholder key file
@@ -1131,46 +1169,51 @@ gVdaZKJR+ym6h3Za4ryK42qlz8Rb5lQICyFJi+h5xqkk71B2ELzu2nzmoafs9OTJ
1131
1169
  LjL4Cwf+OLlI1eRybbU8eqBk8i+B6ALB2FuGjZJplP99fejLMM0L5XNwxJt3OwCx
1132
1170
  WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1133
1171
  -----END PRIVATE KEY-----"""
1134
- with open(new_key_path, 'w') as f:
1172
+ with open(new_key_path, "w") as f:
1135
1173
  f.write(placeholder_key)
1136
1174
  private_key_pem = placeholder_key
1137
-
1175
+
1138
1176
  # Create certificate pair
1139
1177
  cert_pair = CertificatePair(
1140
1178
  certificate_path=new_cert_path,
1141
1179
  private_key_path=new_key_path,
1142
- certificate_pem=new_cert.public_bytes(serialization.Encoding.PEM).decode(),
1180
+ certificate_pem=new_cert.public_bytes(
1181
+ serialization.Encoding.PEM
1182
+ ).decode(),
1143
1183
  private_key_pem=private_key_pem,
1144
1184
  serial_number=str(new_cert.serial_number),
1145
1185
  common_name="", # Will be extracted from subject
1146
1186
  organization="", # Will be extracted from subject
1147
- not_before=new_cert.not_valid_before_utc,
1148
- not_after=new_cert.not_valid_after_utc.replace(tzinfo=timezone.utc),
1187
+ not_before=get_not_valid_before_utc(new_cert),
1188
+ not_after=get_not_valid_after_utc(new_cert),
1149
1189
  certificate_type=CertificateType.CLIENT, # Default
1150
1190
  key_size=0, # Will be extracted
1151
1191
  )
1152
-
1192
+
1153
1193
  self.logger.info(
1154
1194
  "Certificate renewed successfully",
1155
1195
  extra={
1156
1196
  "original_cert": cert_path,
1157
1197
  "new_cert": new_cert_path,
1158
- "validity_years": validity_years
1159
- }
1198
+ "validity_years": validity_years,
1199
+ },
1160
1200
  )
1161
-
1201
+
1162
1202
  return cert_pair
1163
-
1203
+
1164
1204
  except Exception as e:
1165
1205
  self.logger.error(
1166
1206
  "Failed to renew certificate",
1167
- extra={"cert_path": cert_path, "error": str(e)}
1207
+ extra={"cert_path": cert_path, "error": str(e)},
1168
1208
  )
1169
1209
  raise CertificateGenerationError(f"Failed to renew certificate: {str(e)}")
1170
1210
 
1171
1211
  def revoke_certificate(
1172
- self, serial_number: str, reason: str = "unspecified",
1173
- ca_cert_path: Optional[str] = None, ca_key_path: Optional[str] = None
1212
+ self,
1213
+ serial_number: str,
1214
+ reason: str = "unspecified",
1215
+ ca_cert_path: Optional[str] = None,
1216
+ ca_key_path: Optional[str] = None,
1174
1217
  ) -> bool:
1175
1218
  """
1176
1219
  Revoke certificate by serial number.
@@ -1211,7 +1254,7 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1211
1254
  # Use provided CA paths or default from config
1212
1255
  ca_cert_file = ca_cert_path or self.config.ca_cert_path
1213
1256
  ca_key_file = ca_key_path or self.config.ca_key_path
1214
-
1257
+
1215
1258
  if not ca_cert_file or not ca_key_file:
1216
1259
  raise CertificateConfigurationError(
1217
1260
  "CA certificate and key paths are required"
@@ -1235,7 +1278,7 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1235
1278
  revoked_cert = x509.RevokedCertificateBuilder()
1236
1279
  revoked_cert = revoked_cert.serial_number(int(serial_number))
1237
1280
  revoked_cert = revoked_cert.revocation_date(datetime.now(timezone.utc))
1238
-
1281
+
1239
1282
  # Build the revoked certificate
1240
1283
  revoked_cert_built = revoked_cert.build()
1241
1284
 
@@ -1247,7 +1290,7 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1247
1290
  # Save CRL
1248
1291
  crl_filename = "ca_crl.pem"
1249
1292
  crl_path = os.path.join(self.config.cert_storage_path, crl_filename)
1250
-
1293
+
1251
1294
  # Ensure directory exists
1252
1295
  os.makedirs(os.path.dirname(crl_path), exist_ok=True)
1253
1296
 
@@ -1378,25 +1421,27 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1378
1421
 
1379
1422
  # Create certificate info
1380
1423
  subject_dict = {}
1381
-
1424
+
1382
1425
  # Add Common Name
1383
1426
  if cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME):
1384
1427
  subject_dict["CN"] = str(
1385
1428
  cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
1386
1429
  )
1387
-
1430
+
1388
1431
  # Add Organization
1389
1432
  if cert.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME):
1390
1433
  subject_dict["O"] = str(
1391
- cert.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)[0].value
1434
+ cert.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)[
1435
+ 0
1436
+ ].value
1392
1437
  )
1393
-
1438
+
1394
1439
  # Add Country
1395
1440
  if cert.subject.get_attributes_for_oid(NameOID.COUNTRY_NAME):
1396
1441
  subject_dict["C"] = str(
1397
1442
  cert.subject.get_attributes_for_oid(NameOID.COUNTRY_NAME)[0].value
1398
1443
  )
1399
-
1444
+
1400
1445
  cert_info = CertificateInfo(
1401
1446
  subject=subject_dict,
1402
1447
  issuer={
@@ -1411,8 +1456,8 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1411
1456
  )
1412
1457
  },
1413
1458
  serial_number=serial_number,
1414
- not_before=cert.not_valid_before_utc,
1415
- not_after=cert.not_valid_after_utc,
1459
+ not_before=get_not_valid_before_utc(cert),
1460
+ not_after=get_not_valid_after_utc(cert),
1416
1461
  certificate_type=CertificateType.CLIENT, # Default to client, could be enhanced
1417
1462
  key_size=expiry_info.get("key_size", 2048), # Default to 2048 bits
1418
1463
  signature_algorithm=cert.signature_algorithm_oid._name,
@@ -1446,17 +1491,19 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1446
1491
  f"Failed to get certificate info: {str(e)}"
1447
1492
  )
1448
1493
 
1449
- def create_certificate_signing_request(self,
1450
- common_name: str,
1451
- organization: str,
1452
- country: str,
1453
- state: Optional[str] = None,
1454
- locality: Optional[str] = None,
1455
- organizational_unit: Optional[str] = None,
1456
- email: Optional[str] = None,
1457
- key_size: int = 2048,
1458
- key_type: str = "rsa",
1459
- output_path: Optional[str] = None) -> Tuple[str, str]:
1494
+ def create_certificate_signing_request(
1495
+ self,
1496
+ common_name: str,
1497
+ organization: str,
1498
+ country: str,
1499
+ state: Optional[str] = None,
1500
+ locality: Optional[str] = None,
1501
+ organizational_unit: Optional[str] = None,
1502
+ email: Optional[str] = None,
1503
+ key_size: int = 2048,
1504
+ key_type: str = "rsa",
1505
+ output_path: Optional[str] = None,
1506
+ ) -> Tuple[str, str]:
1460
1507
  """
1461
1508
  Create a Certificate Signing Request (CSR).
1462
1509
 
@@ -1511,9 +1558,7 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1511
1558
  public_exponent=65537, key_size=key_size, backend=None
1512
1559
  )
1513
1560
  elif key_type.lower() == "ec":
1514
- private_key = ec.generate_private_key(
1515
- ec.SECP256R1(), backend=None
1516
- )
1561
+ private_key = ec.generate_private_key(ec.SECP256R1(), backend=None)
1517
1562
  else:
1518
1563
  raise ValueError(f"Unsupported key type: {key_type}")
1519
1564
 
@@ -1535,7 +1580,9 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1535
1580
  )
1536
1581
  if organizational_unit:
1537
1582
  subject_attributes.append(
1538
- x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, organizational_unit)
1583
+ x509.NameAttribute(
1584
+ NameOID.ORGANIZATIONAL_UNIT_NAME, organizational_unit
1585
+ )
1539
1586
  )
1540
1587
  if email:
1541
1588
  subject_attributes.append(
@@ -1548,15 +1595,12 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1548
1595
  csr_builder = x509.CertificateSigningRequestBuilder()
1549
1596
  csr_builder = csr_builder.subject_name(subject)
1550
1597
  csr_builder = csr_builder.add_extension(
1551
- x509.BasicConstraints(ca=False, path_length=None),
1552
- critical=True
1598
+ x509.BasicConstraints(ca=False, path_length=None), critical=True
1553
1599
  )
1554
1600
 
1555
1601
  # Add Subject Alternative Name extension if common name looks like a domain
1556
1602
  if "." in common_name and not common_name.startswith("*"):
1557
- san_extension = x509.SubjectAlternativeName([
1558
- x509.DNSName(common_name)
1559
- ])
1603
+ san_extension = x509.SubjectAlternativeName([x509.DNSName(common_name)])
1560
1604
  csr_builder = csr_builder.add_extension(san_extension, critical=False)
1561
1605
 
1562
1606
  # Build and sign CSR
@@ -1581,16 +1625,18 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1581
1625
  key_path = str(output_dir / key_filename)
1582
1626
 
1583
1627
  # Write CSR to file
1584
- with open(csr_path, 'wb') as f:
1628
+ with open(csr_path, "wb") as f:
1585
1629
  f.write(csr.public_bytes(serialization.Encoding.PEM))
1586
1630
 
1587
1631
  # Write private key to file
1588
- with open(key_path, 'wb') as f:
1589
- f.write(private_key.private_bytes(
1590
- encoding=serialization.Encoding.PEM,
1591
- format=serialization.PrivateFormat.PKCS8,
1592
- encryption_algorithm=serialization.NoEncryption()
1593
- ))
1632
+ with open(key_path, "wb") as f:
1633
+ f.write(
1634
+ private_key.private_bytes(
1635
+ encoding=serialization.Encoding.PEM,
1636
+ format=serialization.PrivateFormat.PKCS8,
1637
+ encryption_algorithm=serialization.NoEncryption(),
1638
+ )
1639
+ )
1594
1640
 
1595
1641
  # Set proper permissions
1596
1642
  os.chmod(key_path, 0o600) # Read/write for owner only
@@ -1604,8 +1650,8 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1604
1650
  "common_name": common_name,
1605
1651
  "organization": organization,
1606
1652
  "key_type": key_type,
1607
- "key_size": key_size
1608
- }
1653
+ "key_size": key_size,
1654
+ },
1609
1655
  )
1610
1656
 
1611
1657
  return csr_path, key_path
@@ -1616,14 +1662,19 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1616
1662
  extra={
1617
1663
  "common_name": common_name,
1618
1664
  "organization": organization,
1619
- "error": str(e)
1620
- }
1665
+ "error": str(e),
1666
+ },
1621
1667
  )
1622
1668
  raise CertificateGenerationError(f"Failed to create CSR: {str(e)}")
1623
1669
 
1624
- def create_crl(self, ca_cert_path: str, ca_key_path: str,
1625
- revoked_serials: Optional[List[Dict[str, Union[str, int]]]] = None,
1626
- output_path: Optional[str] = None, validity_days: int = 30) -> str:
1670
+ def create_crl(
1671
+ self,
1672
+ ca_cert_path: str,
1673
+ ca_key_path: str,
1674
+ revoked_serials: Optional[List[Dict[str, Union[str, int]]]] = None,
1675
+ output_path: Optional[str] = None,
1676
+ validity_days: int = 30,
1677
+ ) -> str:
1627
1678
  """
1628
1679
  Create a Certificate Revocation List (CRL).
1629
1680
 
@@ -1668,17 +1719,17 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1668
1719
  # Validate input files
1669
1720
  if not os.path.exists(ca_cert_path):
1670
1721
  raise FileNotFoundError(f"CA certificate not found: {ca_cert_path}")
1671
-
1722
+
1672
1723
  if not os.path.exists(ca_key_path):
1673
1724
  raise FileNotFoundError(f"CA private key not found: {ca_key_path}")
1674
1725
 
1675
1726
  # Load CA certificate
1676
- with open(ca_cert_path, 'rb') as f:
1727
+ with open(ca_cert_path, "rb") as f:
1677
1728
  ca_cert_data = f.read()
1678
1729
  ca_cert = x509.load_pem_x509_certificate(ca_cert_data)
1679
1730
 
1680
1731
  # Load CA private key
1681
- with open(ca_key_path, 'rb') as f:
1732
+ with open(ca_key_path, "rb") as f:
1682
1733
  ca_key_data = f.read()
1683
1734
  ca_private_key = serialization.load_pem_private_key(
1684
1735
  ca_key_data, password=None
@@ -1701,23 +1752,25 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1701
1752
  serial = revoked_info.get("serial")
1702
1753
  reason = revoked_info.get("reason", "unspecified")
1703
1754
  revocation_date = revoked_info.get("revocation_date", now)
1704
-
1755
+
1705
1756
  # Convert serial to int if it's a string
1706
1757
  if isinstance(serial, str):
1707
- serial = int(serial, 16) if serial.startswith("0x") else int(serial)
1708
-
1758
+ serial = (
1759
+ int(serial, 16) if serial.startswith("0x") else int(serial)
1760
+ )
1761
+
1709
1762
  # Map reason string to x509 enum
1710
1763
  reason_enum = self._get_revocation_reason(reason)
1711
-
1764
+
1712
1765
  # Create revoked certificate entry
1713
- revoked_cert = x509.RevokedCertificateBuilder().serial_number(
1714
- serial
1715
- ).revocation_date(
1716
- revocation_date
1717
- ).add_extension(
1718
- x509.CRLReason(reason_enum), critical=False
1719
- ).build()
1720
-
1766
+ revoked_cert = (
1767
+ x509.RevokedCertificateBuilder()
1768
+ .serial_number(serial)
1769
+ .revocation_date(revocation_date)
1770
+ .add_extension(x509.CRLReason(reason_enum), critical=False)
1771
+ .build()
1772
+ )
1773
+
1721
1774
  revoked_certificates.append(revoked_cert)
1722
1775
 
1723
1776
  # Build CRL
@@ -1727,13 +1780,15 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1727
1780
  if output_path is None:
1728
1781
  output_dir = Path(self.config.cert_storage_path) / "crl"
1729
1782
  output_dir.mkdir(parents=True, exist_ok=True)
1730
- output_path = str(output_dir / f"crl_{now.strftime('%Y%m%d_%H%M%S')}.pem")
1783
+ output_path = str(
1784
+ output_dir / f"crl_{now.strftime('%Y%m%d_%H%M%S')}.pem"
1785
+ )
1731
1786
  else:
1732
1787
  output_dir = Path(output_path).parent
1733
1788
  output_dir.mkdir(parents=True, exist_ok=True)
1734
1789
 
1735
1790
  # Write CRL to file
1736
- with open(output_path, 'wb') as f:
1791
+ with open(output_path, "wb") as f:
1737
1792
  f.write(crl.public_bytes(serialization.Encoding.PEM))
1738
1793
 
1739
1794
  self.logger.info(
@@ -1741,8 +1796,8 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1741
1796
  extra={
1742
1797
  "crl_path": output_path,
1743
1798
  "validity_days": validity_days,
1744
- "revoked_count": len(revoked_certificates)
1745
- }
1799
+ "revoked_count": len(revoked_certificates),
1800
+ },
1746
1801
  )
1747
1802
 
1748
1803
  return output_path
@@ -1753,18 +1808,18 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1753
1808
  extra={
1754
1809
  "ca_cert_path": ca_cert_path,
1755
1810
  "ca_key_path": ca_key_path,
1756
- "error": str(e)
1757
- }
1811
+ "error": str(e),
1812
+ },
1758
1813
  )
1759
1814
  raise CertificateGenerationError(f"Failed to create CRL: {str(e)}")
1760
1815
 
1761
1816
  def _get_revocation_reason(self, reason: str) -> x509.ReasonFlags:
1762
1817
  """
1763
1818
  Map reason string to x509.ReasonFlags enum.
1764
-
1819
+
1765
1820
  Args:
1766
1821
  reason (str): Reason string
1767
-
1822
+
1768
1823
  Returns:
1769
1824
  x509.ReasonFlags: Corresponding reason enum
1770
1825
  """
@@ -1779,67 +1834,75 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1779
1834
  "privilege_withdrawn": x509.ReasonFlags.privilege_withdrawn,
1780
1835
  "aa_compromise": x509.ReasonFlags.aa_compromise,
1781
1836
  }
1782
-
1837
+
1783
1838
  return reason_map.get(reason.lower(), x509.ReasonFlags.unspecified)
1784
1839
 
1785
- def export_certificate(self, cert_path: str, format: str = "pem") -> Union[str, bytes]:
1840
+ def export_certificate(
1841
+ self, cert_path: str, format: str = "pem"
1842
+ ) -> Union[str, bytes]:
1786
1843
  """
1787
1844
  Export certificate to different formats.
1788
-
1845
+
1789
1846
  Args:
1790
1847
  cert_path: Path to certificate file
1791
1848
  format: Export format ("pem" or "der")
1792
-
1849
+
1793
1850
  Returns:
1794
1851
  Certificate content in specified format
1795
1852
  """
1796
1853
  try:
1797
- with open(cert_path, 'rb') as f:
1854
+ with open(cert_path, "rb") as f:
1798
1855
  cert_data = f.read()
1799
-
1856
+
1800
1857
  if format.lower() == "pem":
1801
- return cert_data.decode('utf-8')
1858
+ return cert_data.decode("utf-8")
1802
1859
  elif format.lower() == "der":
1803
1860
  # Convert PEM to DER
1804
1861
  from cryptography import x509
1862
+
1805
1863
  cert = x509.load_pem_x509_certificate(cert_data)
1806
1864
  return cert.public_bytes(serialization.Encoding.DER)
1807
1865
  else:
1808
1866
  raise ValueError(f"Unsupported format: {format}")
1809
-
1867
+
1810
1868
  except Exception as e:
1811
1869
  self.logger.error(f"Failed to export certificate: {str(e)}")
1812
1870
  raise CertificateGenerationError(f"Failed to export certificate: {str(e)}")
1813
1871
 
1814
- def export_private_key(self, key_path: str, format: str = "pem") -> Union[str, bytes]:
1872
+ def export_private_key(
1873
+ self, key_path: str, format: str = "pem"
1874
+ ) -> Union[str, bytes]:
1815
1875
  """
1816
1876
  Export private key to different formats.
1817
-
1877
+
1818
1878
  Args:
1819
1879
  key_path: Path to private key file
1820
1880
  format: Export format ("pem" or "der")
1821
-
1881
+
1822
1882
  Returns:
1823
1883
  Private key content in specified format
1824
1884
  """
1825
1885
  try:
1826
- with open(key_path, 'rb') as f:
1886
+ with open(key_path, "rb") as f:
1827
1887
  key_data = f.read()
1828
-
1888
+
1829
1889
  if format.lower() == "pem":
1830
- return key_data.decode('utf-8')
1890
+ return key_data.decode("utf-8")
1831
1891
  elif format.lower() == "der":
1832
1892
  # Convert PEM to DER
1833
- from cryptography.hazmat.primitives.serialization import load_pem_private_key
1893
+ from cryptography.hazmat.primitives.serialization import (
1894
+ load_pem_private_key,
1895
+ )
1896
+
1834
1897
  key = load_pem_private_key(key_data, password=None)
1835
1898
  return key.private_bytes(
1836
1899
  encoding=serialization.Encoding.DER,
1837
1900
  format=serialization.PrivateFormat.PKCS8,
1838
- encryption_algorithm=serialization.NoEncryption()
1901
+ encryption_algorithm=serialization.NoEncryption(),
1839
1902
  )
1840
1903
  else:
1841
1904
  raise ValueError(f"Unsupported format: {format}")
1842
-
1905
+
1843
1906
  except Exception as e:
1844
1907
  self.logger.error(f"Failed to export private key: {str(e)}")
1845
1908
  raise CertificateGenerationError(f"Failed to export private key: {str(e)}")
@@ -1849,7 +1912,7 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1849
1912
  # Skip validation if certificate management is disabled
1850
1913
  if not self.config.enabled:
1851
1914
  return
1852
-
1915
+
1853
1916
  if not self.config.ca_cert_path:
1854
1917
  raise CertificateConfigurationError("CA certificate path is required")
1855
1918