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.
- mcp_security_framework/__init__.py +26 -15
- mcp_security_framework/cli/__init__.py +1 -1
- mcp_security_framework/cli/cert_cli.py +233 -197
- mcp_security_framework/cli/security_cli.py +324 -234
- mcp_security_framework/constants.py +21 -27
- mcp_security_framework/core/auth_manager.py +41 -22
- mcp_security_framework/core/cert_manager.py +210 -147
- mcp_security_framework/core/permission_manager.py +9 -9
- mcp_security_framework/core/rate_limiter.py +2 -2
- mcp_security_framework/core/security_manager.py +284 -229
- mcp_security_framework/examples/__init__.py +6 -0
- mcp_security_framework/examples/comprehensive_example.py +349 -279
- mcp_security_framework/examples/django_example.py +247 -206
- mcp_security_framework/examples/fastapi_example.py +315 -283
- mcp_security_framework/examples/flask_example.py +274 -203
- mcp_security_framework/examples/gateway_example.py +304 -237
- mcp_security_framework/examples/microservice_example.py +258 -189
- mcp_security_framework/examples/standalone_example.py +255 -230
- mcp_security_framework/examples/test_all_examples.py +151 -135
- mcp_security_framework/middleware/__init__.py +46 -55
- mcp_security_framework/middleware/auth_middleware.py +62 -63
- mcp_security_framework/middleware/fastapi_auth_middleware.py +119 -118
- mcp_security_framework/middleware/fastapi_middleware.py +156 -148
- mcp_security_framework/middleware/flask_auth_middleware.py +160 -147
- mcp_security_framework/middleware/flask_middleware.py +183 -157
- mcp_security_framework/middleware/mtls_middleware.py +106 -117
- mcp_security_framework/middleware/rate_limit_middleware.py +105 -101
- mcp_security_framework/middleware/security_middleware.py +109 -124
- mcp_security_framework/schemas/config.py +2 -1
- mcp_security_framework/schemas/models.py +18 -6
- mcp_security_framework/utils/cert_utils.py +14 -8
- mcp_security_framework/utils/datetime_compat.py +116 -0
- {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/METADATA +2 -1
- mcp_security_framework-1.1.1.dist-info/RECORD +84 -0
- tests/conftest.py +63 -66
- tests/test_cli/test_cert_cli.py +184 -146
- tests/test_cli/test_security_cli.py +274 -247
- tests/test_core/test_cert_manager.py +24 -10
- tests/test_core/test_security_manager.py +2 -2
- tests/test_examples/test_comprehensive_example.py +190 -137
- tests/test_examples/test_fastapi_example.py +124 -101
- tests/test_examples/test_flask_example.py +124 -101
- tests/test_examples/test_standalone_example.py +73 -80
- tests/test_integration/test_auth_flow.py +213 -197
- tests/test_integration/test_certificate_flow.py +180 -149
- tests/test_integration/test_fastapi_integration.py +108 -111
- tests/test_integration/test_flask_integration.py +141 -140
- tests/test_integration/test_standalone_integration.py +290 -259
- tests/test_middleware/test_fastapi_auth_middleware.py +195 -174
- tests/test_middleware/test_fastapi_middleware.py +147 -132
- tests/test_middleware/test_flask_auth_middleware.py +260 -202
- tests/test_middleware/test_flask_middleware.py +201 -179
- tests/test_middleware/test_security_middleware.py +145 -130
- tests/test_utils/test_datetime_compat.py +147 -0
- mcp_security_framework-1.1.0.dist-info/RECORD +0 -82
- {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/WHEEL +0 -0
- {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/entry_points.txt +0 -0
- {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
|
317
|
-
not_after=certificate
|
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(
|
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(
|
409
|
+
raise ValueError(
|
410
|
+
"Common name is required for intermediate CA certificate"
|
411
|
+
)
|
404
412
|
|
405
|
-
if
|
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(
|
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(
|
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(
|
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,
|
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(
|
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(
|
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(
|
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)
|
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(
|
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(
|
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
|
538
|
-
not_after=certificate
|
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={
|
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
|
779
|
-
not_after=certificate
|
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
|
1013
|
-
not_after=certificate
|
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,
|
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(
|
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,
|
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,
|
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(
|
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,
|
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(
|
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,
|
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,
|
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(
|
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
|
1148
|
-
not_after=new_cert
|
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,
|
1173
|
-
|
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)[
|
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
|
1415
|
-
not_after=cert
|
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(
|
1450
|
-
|
1451
|
-
|
1452
|
-
|
1453
|
-
|
1454
|
-
|
1455
|
-
|
1456
|
-
|
1457
|
-
|
1458
|
-
|
1459
|
-
|
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(
|
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,
|
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,
|
1589
|
-
f.write(
|
1590
|
-
|
1591
|
-
|
1592
|
-
|
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(
|
1625
|
-
|
1626
|
-
|
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,
|
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,
|
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 =
|
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 =
|
1714
|
-
|
1715
|
-
|
1716
|
-
revocation_date
|
1717
|
-
|
1718
|
-
|
1719
|
-
)
|
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(
|
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,
|
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(
|
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,
|
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(
|
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(
|
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,
|
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(
|
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
|
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
|
|