mcp-security-framework 2.1.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/PKG-INFO +1 -1
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/__init__.py +1 -1
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/core/cert_manager.py +109 -18
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/core/ssl_manager.py +249 -43
- mcp_security_framework-2.2.0/mcp_security_framework/examples/cert_inspection_example.py +391 -0
- mcp_security_framework-2.2.0/mcp_security_framework/examples/crl_retrieval_example.py +385 -0
- mcp_security_framework-2.2.0/mcp_security_framework/examples/protected_keys_example.py +297 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/schemas/config.py +22 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/utils/__init__.py +27 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/utils/cert_utils.py +206 -7
- mcp_security_framework-2.2.0/mcp_security_framework/utils/crl_client.py +408 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework.egg-info/PKG-INFO +1 -1
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework.egg-info/SOURCES.txt +4 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/pyproject.toml +1 -1
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/LICENSE +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/README.md +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/cli/__init__.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/cli/cert_cli.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/cli/security_cli.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/constants.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/core/__init__.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/core/adapter_wrapper.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/core/audit_logger.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/core/auth_manager.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/core/permission_manager.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/core/rate_limiter.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/core/security_adapter.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/core/security_manager.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/examples/__init__.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/examples/adapter_example.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/examples/comprehensive_example.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/examples/fastapi_example.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/examples/flask_example.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/examples/gateway_example.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/examples/microservice_example.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/examples/standalone_example.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/middleware/__init__.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/middleware/auth_middleware.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/middleware/fastapi_auth_middleware.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/middleware/fastapi_middleware.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/middleware/flask_auth_middleware.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/middleware/flask_middleware.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/middleware/mtls_middleware.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/middleware/rate_limit_middleware.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/middleware/security_middleware.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/schemas/__init__.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/schemas/models.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/schemas/operation_context.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/schemas/responses.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/utils/crypto_utils.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/utils/datetime_compat.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework/utils/validation_utils.py +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework.egg-info/dependency_links.txt +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework.egg-info/entry_points.txt +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework.egg-info/requires.txt +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/mcp_security_framework.egg-info/top_level.txt +0 -0
- {mcp_security_framework-2.1.0 → mcp_security_framework-2.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-security-framework
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Universal security framework for microservices with SSL/TLS, authentication, authorization, and rate limiting. Added extensible security adapter system for custom operation validation, structured audit logging, and operation context support. Comprehensive test coverage with 1104+ tests. Fully backward compatible. 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>
|
|
@@ -36,6 +36,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
|
|
36
36
|
from cryptography import x509
|
|
37
37
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
38
38
|
from cryptography.hazmat.primitives.asymmetric import ec, ed448, ed25519, rsa
|
|
39
|
+
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
|
|
39
40
|
from cryptography.x509.oid import ExtendedKeyUsageOID, ExtensionOID, NameOID
|
|
40
41
|
|
|
41
42
|
from mcp_security_framework.schemas.config import (
|
|
@@ -53,6 +54,7 @@ from mcp_security_framework.schemas.models import (
|
|
|
53
54
|
UnknownRoleError,
|
|
54
55
|
)
|
|
55
56
|
from mcp_security_framework.utils.cert_utils import (
|
|
57
|
+
CertificateError,
|
|
56
58
|
extract_permissions_from_certificate,
|
|
57
59
|
extract_roles_from_certificate,
|
|
58
60
|
get_certificate_expiry,
|
|
@@ -61,6 +63,7 @@ from mcp_security_framework.utils.cert_utils import (
|
|
|
61
63
|
is_certificate_revoked,
|
|
62
64
|
is_crl_valid,
|
|
63
65
|
parse_certificate,
|
|
66
|
+
parse_private_key,
|
|
64
67
|
validate_certificate_against_crl,
|
|
65
68
|
validate_certificate_chain,
|
|
66
69
|
)
|
|
@@ -123,6 +126,39 @@ def _get_signing_algorithm(private_key):
|
|
|
123
126
|
return hashes.SHA256()
|
|
124
127
|
|
|
125
128
|
|
|
129
|
+
def _load_private_key(
|
|
130
|
+
key_bytes: bytes,
|
|
131
|
+
password: Optional[bytes],
|
|
132
|
+
source: str,
|
|
133
|
+
) -> PrivateKeyTypes:
|
|
134
|
+
"""
|
|
135
|
+
Load a private key from PEM or DER bytes with optional passphrase.
|
|
136
|
+
|
|
137
|
+
This helper wraps parse_private_key to attach a source label and convert
|
|
138
|
+
from CertificateError (raised by parse_private_key) to CertificateValidationError
|
|
139
|
+
(expected by callers in this module). The classification of whether the key
|
|
140
|
+
is encrypted, whether a password was supplied, and whether the password is
|
|
141
|
+
correct all lives in parse_private_key; this wrapper's only job is to add
|
|
142
|
+
the source context and bridge the exception type.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
key_bytes: The PEM or DER-encoded private key
|
|
146
|
+
password: Optional passphrase as bytes, or None
|
|
147
|
+
source: Human-readable label for error messages (e.g. "CA private key",
|
|
148
|
+
"Parent CA private key"). Included in all error messages.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Loaded private key object
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
CertificateValidationError: When the key cannot be loaded or decrypted
|
|
155
|
+
"""
|
|
156
|
+
try:
|
|
157
|
+
return parse_private_key(key_bytes, password=password)
|
|
158
|
+
except CertificateError as exc:
|
|
159
|
+
raise CertificateValidationError(f"{source}: {exc}", error_code=-32003) from exc
|
|
160
|
+
|
|
161
|
+
|
|
126
162
|
def _subject_attribute(certificate, oid) -> str:
|
|
127
163
|
"""
|
|
128
164
|
Return one subject attribute of a certificate, or "" when it carries none.
|
|
@@ -616,8 +652,15 @@ class CertificateManager:
|
|
|
616
652
|
parent_ca_cert = x509.load_pem_x509_certificate(f.read())
|
|
617
653
|
|
|
618
654
|
with open(intermediate_config.parent_ca_key, "rb") as f:
|
|
619
|
-
|
|
620
|
-
|
|
655
|
+
password = (
|
|
656
|
+
intermediate_config.parent_ca_key_password.encode()
|
|
657
|
+
if intermediate_config.parent_ca_key_password
|
|
658
|
+
else None
|
|
659
|
+
)
|
|
660
|
+
parent_ca_key = _load_private_key(
|
|
661
|
+
f.read(),
|
|
662
|
+
password=password,
|
|
663
|
+
source="Parent CA private key",
|
|
621
664
|
)
|
|
622
665
|
|
|
623
666
|
# Generate intermediate CA private key
|
|
@@ -878,7 +921,16 @@ class CertificateManager:
|
|
|
878
921
|
ca_cert = x509.load_pem_x509_certificate(f.read())
|
|
879
922
|
|
|
880
923
|
with open(client_config.ca_key_path, "rb") as f:
|
|
881
|
-
|
|
924
|
+
password = (
|
|
925
|
+
client_config.ca_key_password.encode()
|
|
926
|
+
if client_config.ca_key_password
|
|
927
|
+
else None
|
|
928
|
+
)
|
|
929
|
+
ca_key = _load_private_key(
|
|
930
|
+
f.read(),
|
|
931
|
+
password=password,
|
|
932
|
+
source="CA private key",
|
|
933
|
+
)
|
|
882
934
|
|
|
883
935
|
# Verify that CA private key matches CA certificate
|
|
884
936
|
_verify_ca_key_matches_cert(
|
|
@@ -1104,8 +1156,8 @@ class CertificateManager:
|
|
|
1104
1156
|
)
|
|
1105
1157
|
except (FileNotFoundError, PermissionError, ValueError):
|
|
1106
1158
|
raise
|
|
1107
|
-
except CertificateGenerationError:
|
|
1108
|
-
# Re-raise
|
|
1159
|
+
except (CertificateGenerationError, CertificateValidationError):
|
|
1160
|
+
# Re-raise these without wrapping
|
|
1109
1161
|
raise
|
|
1110
1162
|
except Exception as e:
|
|
1111
1163
|
self.logger.error(
|
|
@@ -1189,7 +1241,16 @@ class CertificateManager:
|
|
|
1189
1241
|
ca_cert = x509.load_pem_x509_certificate(f.read())
|
|
1190
1242
|
|
|
1191
1243
|
with open(server_config.ca_key_path, "rb") as f:
|
|
1192
|
-
|
|
1244
|
+
password = (
|
|
1245
|
+
server_config.ca_key_password.encode()
|
|
1246
|
+
if server_config.ca_key_password
|
|
1247
|
+
else None
|
|
1248
|
+
)
|
|
1249
|
+
ca_key = _load_private_key(
|
|
1250
|
+
f.read(),
|
|
1251
|
+
password=password,
|
|
1252
|
+
source="CA private key",
|
|
1253
|
+
)
|
|
1193
1254
|
|
|
1194
1255
|
# Verify that CA private key matches CA certificate
|
|
1195
1256
|
_verify_ca_key_matches_cert(
|
|
@@ -1406,8 +1467,8 @@ class CertificateManager:
|
|
|
1406
1467
|
)
|
|
1407
1468
|
except (FileNotFoundError, PermissionError, ValueError):
|
|
1408
1469
|
raise
|
|
1409
|
-
except CertificateGenerationError:
|
|
1410
|
-
# Re-raise
|
|
1470
|
+
except (CertificateGenerationError, CertificateValidationError):
|
|
1471
|
+
# Re-raise these without wrapping
|
|
1411
1472
|
raise
|
|
1412
1473
|
except Exception as e:
|
|
1413
1474
|
self.logger.error(
|
|
@@ -1493,7 +1554,16 @@ class CertificateManager:
|
|
|
1493
1554
|
ca_cert = x509.load_pem_x509_certificate(f.read())
|
|
1494
1555
|
|
|
1495
1556
|
with open(ca_key_file, "rb") as f:
|
|
1496
|
-
|
|
1557
|
+
password = (
|
|
1558
|
+
self.config.ca_key_password.encode()
|
|
1559
|
+
if self.config.ca_key_password
|
|
1560
|
+
else None
|
|
1561
|
+
)
|
|
1562
|
+
ca_key = _load_private_key(
|
|
1563
|
+
f.read(),
|
|
1564
|
+
password=password,
|
|
1565
|
+
source="CA private key",
|
|
1566
|
+
)
|
|
1497
1567
|
|
|
1498
1568
|
# Create new certificate with extended validity
|
|
1499
1569
|
builder = x509.CertificateBuilder()
|
|
@@ -1651,7 +1721,16 @@ class CertificateManager:
|
|
|
1651
1721
|
ca_cert = x509.load_pem_x509_certificate(f.read())
|
|
1652
1722
|
|
|
1653
1723
|
with open(ca_key_file, "rb") as f:
|
|
1654
|
-
|
|
1724
|
+
password = (
|
|
1725
|
+
self.config.ca_key_password.encode()
|
|
1726
|
+
if self.config.ca_key_password
|
|
1727
|
+
else None
|
|
1728
|
+
)
|
|
1729
|
+
ca_key = _load_private_key(
|
|
1730
|
+
f.read(),
|
|
1731
|
+
password=password,
|
|
1732
|
+
source="CA private key",
|
|
1733
|
+
)
|
|
1655
1734
|
|
|
1656
1735
|
# Create CRL builder
|
|
1657
1736
|
builder = x509.CertificateRevocationListBuilder()
|
|
@@ -2165,8 +2244,15 @@ class CertificateManager:
|
|
|
2165
2244
|
# Load CA private key
|
|
2166
2245
|
with open(ca_key_path, "rb") as f:
|
|
2167
2246
|
ca_key_data = f.read()
|
|
2168
|
-
|
|
2169
|
-
|
|
2247
|
+
password = (
|
|
2248
|
+
self.config.ca_key_password.encode()
|
|
2249
|
+
if self.config.ca_key_password
|
|
2250
|
+
else None
|
|
2251
|
+
)
|
|
2252
|
+
ca_private_key = _load_private_key(
|
|
2253
|
+
ca_key_data,
|
|
2254
|
+
password=password,
|
|
2255
|
+
source="CA private key",
|
|
2170
2256
|
)
|
|
2171
2257
|
|
|
2172
2258
|
# Verify that CA private key matches CA certificate
|
|
@@ -2249,8 +2335,8 @@ class CertificateManager:
|
|
|
2249
2335
|
|
|
2250
2336
|
except (FileNotFoundError, PermissionError, ValueError):
|
|
2251
2337
|
raise
|
|
2252
|
-
except CertificateGenerationError:
|
|
2253
|
-
# Re-raise
|
|
2338
|
+
except (CertificateGenerationError, CertificateValidationError):
|
|
2339
|
+
# Re-raise these without wrapping
|
|
2254
2340
|
raise
|
|
2255
2341
|
except Exception as e:
|
|
2256
2342
|
self.logger.error(
|
|
@@ -2495,11 +2581,16 @@ class CertificateManager:
|
|
|
2495
2581
|
return key_data.decode("utf-8")
|
|
2496
2582
|
elif format.lower() == "der":
|
|
2497
2583
|
# Convert PEM to DER
|
|
2498
|
-
|
|
2499
|
-
|
|
2584
|
+
password = (
|
|
2585
|
+
self.config.ca_key_password.encode()
|
|
2586
|
+
if self.config.ca_key_password
|
|
2587
|
+
else None
|
|
2588
|
+
)
|
|
2589
|
+
key = _load_private_key(
|
|
2590
|
+
key_data,
|
|
2591
|
+
password=password,
|
|
2592
|
+
source="Private key",
|
|
2500
2593
|
)
|
|
2501
|
-
|
|
2502
|
-
key = load_pem_private_key(key_data, password=None)
|
|
2503
2594
|
return key.private_bytes(
|
|
2504
2595
|
encoding=serialization.Encoding.DER,
|
|
2505
2596
|
format=serialization.PrivateFormat.PKCS8,
|
|
@@ -24,7 +24,9 @@ License: MIT
|
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
26
|
import logging
|
|
27
|
+
import os
|
|
27
28
|
import ssl
|
|
29
|
+
import tempfile
|
|
28
30
|
from pathlib import Path
|
|
29
31
|
from typing import Dict, List, Optional
|
|
30
32
|
|
|
@@ -42,6 +44,39 @@ from ..utils.cert_utils import (
|
|
|
42
44
|
from .cert_manager import CertificateValidationError, _certificate_type_of
|
|
43
45
|
|
|
44
46
|
|
|
47
|
+
def _stage_pem_material(data: bytes, label: str) -> str:
|
|
48
|
+
"""
|
|
49
|
+
Write PEM bytes to a private temporary file and return its path.
|
|
50
|
+
|
|
51
|
+
The file descriptor from tempfile.mkstemp is taken ownership of by os.fdopen,
|
|
52
|
+
which ensures it is closed exactly once when the stream closes. This avoids
|
|
53
|
+
the double-close bug that occurs when a descriptor is closed manually and then
|
|
54
|
+
closed again in a finally block, which can close unrelated files opened in
|
|
55
|
+
between.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
data: PEM-encoded bytes to write
|
|
59
|
+
label: Prefix for the temporary file (for debugging)
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Path to the temporary file
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
OSError: If file creation or writing fails
|
|
66
|
+
"""
|
|
67
|
+
# Note: the chmod call once done here would have been 0600, but
|
|
68
|
+
# tempfile.mkstemp already creates files with mode 0600, so an
|
|
69
|
+
# explicit chmod was redundant; removed to avoid reader confusion.
|
|
70
|
+
handle, path = tempfile.mkstemp(prefix="mcp-ssl-", suffix=".pem")
|
|
71
|
+
try:
|
|
72
|
+
with os.fdopen(handle, "wb") as stream:
|
|
73
|
+
stream.write(data)
|
|
74
|
+
except BaseException:
|
|
75
|
+
os.unlink(path)
|
|
76
|
+
raise
|
|
77
|
+
return path
|
|
78
|
+
|
|
79
|
+
|
|
45
80
|
class SSLManager:
|
|
46
81
|
"""
|
|
47
82
|
SSL/TLS Management Class
|
|
@@ -112,6 +147,8 @@ class SSLManager:
|
|
|
112
147
|
self,
|
|
113
148
|
cert_file: Optional[str] = None,
|
|
114
149
|
key_file: Optional[str] = None,
|
|
150
|
+
cert_data: Optional[bytes] = None,
|
|
151
|
+
key_data: Optional[bytes] = None,
|
|
115
152
|
ca_cert_file: Optional[str] = None,
|
|
116
153
|
verify_mode: Optional[str] = None,
|
|
117
154
|
min_version: Optional[str] = None,
|
|
@@ -126,10 +163,20 @@ class SSLManager:
|
|
|
126
163
|
Args:
|
|
127
164
|
cert_file (Optional[str]): Path to server certificate file.
|
|
128
165
|
If None, uses certificate from config. Must be a valid PEM
|
|
129
|
-
or DER certificate file path.
|
|
166
|
+
or DER certificate file path. Incompatible with cert_data.
|
|
130
167
|
key_file (Optional[str]): Path to server private key file.
|
|
131
168
|
If None, uses key from config. Must be a valid PEM or DER
|
|
132
|
-
private key file path.
|
|
169
|
+
private key file path. Incompatible with key_data.
|
|
170
|
+
cert_data (Optional[bytes]): Server certificate as PEM bytes.
|
|
171
|
+
If provided, used instead of cert_file. Incompatible with cert_file.
|
|
172
|
+
When supplied, bytes are staged to a temporary file (mode 0600)
|
|
173
|
+
for loading, then deleted. This is a stdlib constraint:
|
|
174
|
+
ssl.SSLContext.load_cert_chain requires real files.
|
|
175
|
+
key_data (Optional[bytes]): Server private key as PEM bytes.
|
|
176
|
+
If provided, used instead of key_file. Incompatible with key_file.
|
|
177
|
+
When supplied, bytes are staged to a temporary file (mode 0600)
|
|
178
|
+
for loading, then deleted. This is a stdlib constraint:
|
|
179
|
+
ssl.SSLContext.load_cert_chain requires real files.
|
|
133
180
|
ca_cert_file (Optional[str]): Path to CA certificate file for
|
|
134
181
|
client certificate verification. If None, uses CA from config.
|
|
135
182
|
Must be a valid PEM certificate file path.
|
|
@@ -167,26 +214,44 @@ class SSLManager:
|
|
|
167
214
|
>>> # Use context for HTTPS server
|
|
168
215
|
"""
|
|
169
216
|
try:
|
|
217
|
+
# Validate cert_data/key_data compatibility
|
|
218
|
+
if cert_data is not None and key_data is None:
|
|
219
|
+
raise SSLConfigurationError(
|
|
220
|
+
"cert_data supplied without key_data; both must be present"
|
|
221
|
+
)
|
|
222
|
+
if key_data is not None and cert_data is None:
|
|
223
|
+
raise SSLConfigurationError(
|
|
224
|
+
"key_data supplied without cert_data; both must be present"
|
|
225
|
+
)
|
|
226
|
+
|
|
170
227
|
# Use config values if not provided
|
|
171
|
-
|
|
172
|
-
|
|
228
|
+
if cert_data is None:
|
|
229
|
+
cert_file = cert_file or self.config.cert_file
|
|
230
|
+
if key_data is None:
|
|
231
|
+
key_file = key_file or self.config.key_file
|
|
173
232
|
ca_cert_file = ca_cert_file or self.config.ca_cert_file
|
|
174
233
|
verify_mode = verify_mode or self.config.verify_mode
|
|
175
234
|
min_version = min_version or self.config.min_tls_version
|
|
176
235
|
|
|
177
|
-
# Validate required files
|
|
178
|
-
if not cert_file:
|
|
179
|
-
raise SSLConfigurationError(
|
|
180
|
-
|
|
181
|
-
|
|
236
|
+
# Validate required files or data
|
|
237
|
+
if cert_data is None and not cert_file:
|
|
238
|
+
raise SSLConfigurationError(
|
|
239
|
+
"Server certificate file or data is required"
|
|
240
|
+
)
|
|
241
|
+
if key_data is None and not key_file:
|
|
242
|
+
raise SSLConfigurationError(
|
|
243
|
+
"Server private key file or data is required"
|
|
244
|
+
)
|
|
182
245
|
|
|
183
|
-
# Check cache
|
|
184
|
-
cache_key =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
246
|
+
# Check cache (only for file-based contexts)
|
|
247
|
+
cache_key = None
|
|
248
|
+
if cert_data is None and key_data is None:
|
|
249
|
+
cache_key = (
|
|
250
|
+
f"server_{cert_file}_{key_file}_{ca_cert_file}"
|
|
251
|
+
f"_{verify_mode}_{min_version}"
|
|
252
|
+
)
|
|
253
|
+
if cache_key in self._contexts:
|
|
254
|
+
return self._contexts[cache_key]
|
|
190
255
|
|
|
191
256
|
# Create SSL context
|
|
192
257
|
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
|
@@ -197,8 +262,43 @@ class SSLManager:
|
|
|
197
262
|
# Set minimum TLS version
|
|
198
263
|
context.minimum_version = self._get_tls_version(min_version)
|
|
199
264
|
|
|
200
|
-
# Load certificate and key
|
|
201
|
-
|
|
265
|
+
# Load certificate and key from data or files
|
|
266
|
+
if cert_data is not None and key_data is not None:
|
|
267
|
+
# Stage in-memory data to temporary files for loading
|
|
268
|
+
cert_temp = _stage_pem_material(cert_data, "cert")
|
|
269
|
+
key_temp = _stage_pem_material(key_data, "key")
|
|
270
|
+
try:
|
|
271
|
+
# Load from temporary files with password from config
|
|
272
|
+
password = (
|
|
273
|
+
self.config.key_password.encode()
|
|
274
|
+
if self.config.key_password
|
|
275
|
+
else None
|
|
276
|
+
)
|
|
277
|
+
if password is not None:
|
|
278
|
+
context.load_cert_chain(cert_temp, key_temp, password=password)
|
|
279
|
+
else:
|
|
280
|
+
context.load_cert_chain(cert_temp, key_temp)
|
|
281
|
+
finally:
|
|
282
|
+
# Clean up temporary files
|
|
283
|
+
try:
|
|
284
|
+
os.unlink(cert_temp)
|
|
285
|
+
except OSError:
|
|
286
|
+
pass
|
|
287
|
+
try:
|
|
288
|
+
os.unlink(key_temp)
|
|
289
|
+
except OSError:
|
|
290
|
+
pass
|
|
291
|
+
else:
|
|
292
|
+
# Load from files with password from config
|
|
293
|
+
password = (
|
|
294
|
+
self.config.key_password.encode()
|
|
295
|
+
if self.config.key_password
|
|
296
|
+
else None
|
|
297
|
+
)
|
|
298
|
+
if password is not None:
|
|
299
|
+
context.load_cert_chain(cert_file, key_file, password=password)
|
|
300
|
+
else:
|
|
301
|
+
context.load_cert_chain(cert_file, key_file)
|
|
202
302
|
|
|
203
303
|
# Load CA certificate if provided
|
|
204
304
|
if ca_cert_file:
|
|
@@ -208,14 +308,16 @@ class SSLManager:
|
|
|
208
308
|
if self.config.cipher_suite:
|
|
209
309
|
context.set_ciphers(self.config.cipher_suite)
|
|
210
310
|
|
|
211
|
-
# Cache context
|
|
212
|
-
|
|
311
|
+
# Cache context (only for file-based contexts)
|
|
312
|
+
if cache_key is not None:
|
|
313
|
+
self._contexts[cache_key] = context
|
|
213
314
|
|
|
214
315
|
self.logger.info(
|
|
215
316
|
"Server SSL context created successfully",
|
|
216
317
|
extra={
|
|
217
318
|
"cert_file": cert_file,
|
|
218
319
|
"key_file": key_file,
|
|
320
|
+
"cert_data_provided": cert_data is not None,
|
|
219
321
|
"verify_mode": verify_mode,
|
|
220
322
|
"min_version": min_version,
|
|
221
323
|
},
|
|
@@ -226,7 +328,7 @@ class SSLManager:
|
|
|
226
328
|
except Exception as e:
|
|
227
329
|
self.logger.error(
|
|
228
330
|
"Failed to create server SSL context",
|
|
229
|
-
extra={"
|
|
331
|
+
extra={"error": str(e)},
|
|
230
332
|
)
|
|
231
333
|
raise SSLConfigurationError(
|
|
232
334
|
f"Failed to create server SSL context: {str(e)}"
|
|
@@ -237,6 +339,8 @@ class SSLManager:
|
|
|
237
339
|
ca_cert_file: Optional[str] = None,
|
|
238
340
|
client_cert_file: Optional[str] = None,
|
|
239
341
|
client_key_file: Optional[str] = None,
|
|
342
|
+
cert_data: Optional[bytes] = None,
|
|
343
|
+
key_data: Optional[bytes] = None,
|
|
240
344
|
verify_mode: Optional[str] = None,
|
|
241
345
|
min_version: Optional[str] = None,
|
|
242
346
|
) -> ssl.SSLContext:
|
|
@@ -253,10 +357,20 @@ class SSLManager:
|
|
|
253
357
|
Must be a valid PEM certificate file path.
|
|
254
358
|
client_cert_file (Optional[str]): Path to client certificate file
|
|
255
359
|
for client authentication. If None, uses client cert from config.
|
|
256
|
-
Must be a valid PEM certificate file path.
|
|
360
|
+
Must be a valid PEM certificate file path. Incompatible with cert_data.
|
|
257
361
|
client_key_file (Optional[str]): Path to client private key file.
|
|
258
362
|
If None, uses client key from config. Must be a valid PEM
|
|
259
|
-
private key file path.
|
|
363
|
+
private key file path. Incompatible with key_data.
|
|
364
|
+
cert_data (Optional[bytes]): Client certificate as PEM bytes.
|
|
365
|
+
If provided, used instead of client_cert_file. Incompatible with
|
|
366
|
+
client_cert_file. When supplied, bytes are staged to a temporary
|
|
367
|
+
file (mode 0600) for loading, then deleted. This is a stdlib
|
|
368
|
+
constraint: ssl.SSLContext.load_cert_chain requires real files.
|
|
369
|
+
key_data (Optional[bytes]): Client private key as PEM bytes.
|
|
370
|
+
If provided, used instead of client_key_file. Incompatible with
|
|
371
|
+
client_key_file. When supplied, bytes are staged to a temporary
|
|
372
|
+
file (mode 0600) for loading, then deleted. This is a stdlib
|
|
373
|
+
constraint: ssl.SSLContext.load_cert_chain requires real files.
|
|
260
374
|
verify_mode (Optional[str]): SSL verification mode. Valid values:
|
|
261
375
|
- "CERT_NONE": No certificate verification
|
|
262
376
|
- "CERT_OPTIONAL": Certificate verification optional
|
|
@@ -290,20 +404,34 @@ class SSLManager:
|
|
|
290
404
|
>>> # Use context for HTTPS client
|
|
291
405
|
"""
|
|
292
406
|
try:
|
|
407
|
+
# Validate cert_data/key_data compatibility
|
|
408
|
+
if cert_data is not None and key_data is None:
|
|
409
|
+
raise SSLConfigurationError(
|
|
410
|
+
"cert_data supplied without key_data; both must be present"
|
|
411
|
+
)
|
|
412
|
+
if key_data is not None and cert_data is None:
|
|
413
|
+
raise SSLConfigurationError(
|
|
414
|
+
"key_data supplied without cert_data; both must be present"
|
|
415
|
+
)
|
|
416
|
+
|
|
293
417
|
# Use config values if not provided
|
|
418
|
+
if cert_data is None:
|
|
419
|
+
client_cert_file = client_cert_file or self.config.client_cert_file
|
|
420
|
+
if key_data is None:
|
|
421
|
+
client_key_file = client_key_file or self.config.client_key_file
|
|
294
422
|
ca_cert_file = ca_cert_file or self.config.ca_cert_file
|
|
295
|
-
client_cert_file = client_cert_file or self.config.client_cert_file
|
|
296
|
-
client_key_file = client_key_file or self.config.client_key_file
|
|
297
423
|
verify_mode = verify_mode or self.config.verify_mode
|
|
298
424
|
min_version = min_version or self.config.min_tls_version
|
|
299
425
|
|
|
300
|
-
# Check cache
|
|
301
|
-
cache_key =
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
426
|
+
# Check cache (only for file-based contexts)
|
|
427
|
+
cache_key = None
|
|
428
|
+
if cert_data is None and key_data is None:
|
|
429
|
+
cache_key = (
|
|
430
|
+
f"client_{ca_cert_file}_{client_cert_file}_{client_key_file}"
|
|
431
|
+
f"_{verify_mode}_{min_version}"
|
|
432
|
+
)
|
|
433
|
+
if cache_key in self._contexts:
|
|
434
|
+
return self._contexts[cache_key]
|
|
307
435
|
|
|
308
436
|
# Determine actual verify mode (explicit parameter takes precedence)
|
|
309
437
|
actual_verify_mode = (
|
|
@@ -324,8 +452,45 @@ class SSLManager:
|
|
|
324
452
|
|
|
325
453
|
# Load client certificates for mTLS even when verification is
|
|
326
454
|
# disabled
|
|
327
|
-
if
|
|
328
|
-
|
|
455
|
+
if cert_data is not None and key_data is not None:
|
|
456
|
+
# Stage in-memory data to temporary files for loading
|
|
457
|
+
cert_temp = _stage_pem_material(cert_data, "cert")
|
|
458
|
+
key_temp = _stage_pem_material(key_data, "key")
|
|
459
|
+
try:
|
|
460
|
+
# Load from temporary files with password from config
|
|
461
|
+
password = (
|
|
462
|
+
self.config.client_key_password.encode()
|
|
463
|
+
if self.config.client_key_password
|
|
464
|
+
else None
|
|
465
|
+
)
|
|
466
|
+
if password is not None:
|
|
467
|
+
context.load_cert_chain(
|
|
468
|
+
cert_temp, key_temp, password=password
|
|
469
|
+
)
|
|
470
|
+
else:
|
|
471
|
+
context.load_cert_chain(cert_temp, key_temp)
|
|
472
|
+
finally:
|
|
473
|
+
# Clean up temporary files
|
|
474
|
+
try:
|
|
475
|
+
os.unlink(cert_temp)
|
|
476
|
+
except OSError:
|
|
477
|
+
pass
|
|
478
|
+
try:
|
|
479
|
+
os.unlink(key_temp)
|
|
480
|
+
except OSError:
|
|
481
|
+
pass
|
|
482
|
+
elif client_cert_file and client_key_file:
|
|
483
|
+
password = (
|
|
484
|
+
self.config.client_key_password.encode()
|
|
485
|
+
if self.config.client_key_password
|
|
486
|
+
else None
|
|
487
|
+
)
|
|
488
|
+
if password is not None:
|
|
489
|
+
context.load_cert_chain(
|
|
490
|
+
client_cert_file, client_key_file, password=password
|
|
491
|
+
)
|
|
492
|
+
else:
|
|
493
|
+
context.load_cert_chain(client_cert_file, client_key_file)
|
|
329
494
|
else:
|
|
330
495
|
# For other modes, use default context with verification
|
|
331
496
|
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
|
@@ -339,21 +504,60 @@ class SSLManager:
|
|
|
339
504
|
context.load_verify_locations(ca_cert_file)
|
|
340
505
|
|
|
341
506
|
# Load client certificate and key if provided (for mTLS)
|
|
342
|
-
if
|
|
343
|
-
|
|
507
|
+
if cert_data is not None and key_data is not None:
|
|
508
|
+
# Stage in-memory data to temporary files for loading
|
|
509
|
+
cert_temp = _stage_pem_material(cert_data, "cert")
|
|
510
|
+
key_temp = _stage_pem_material(key_data, "key")
|
|
511
|
+
try:
|
|
512
|
+
# Load from temporary files with password from config
|
|
513
|
+
password = (
|
|
514
|
+
self.config.client_key_password.encode()
|
|
515
|
+
if self.config.client_key_password
|
|
516
|
+
else None
|
|
517
|
+
)
|
|
518
|
+
if password is not None:
|
|
519
|
+
context.load_cert_chain(
|
|
520
|
+
cert_temp, key_temp, password=password
|
|
521
|
+
)
|
|
522
|
+
else:
|
|
523
|
+
context.load_cert_chain(cert_temp, key_temp)
|
|
524
|
+
finally:
|
|
525
|
+
# Clean up temporary files
|
|
526
|
+
try:
|
|
527
|
+
os.unlink(cert_temp)
|
|
528
|
+
except OSError:
|
|
529
|
+
pass
|
|
530
|
+
try:
|
|
531
|
+
os.unlink(key_temp)
|
|
532
|
+
except OSError:
|
|
533
|
+
pass
|
|
534
|
+
elif client_cert_file and client_key_file:
|
|
535
|
+
password = (
|
|
536
|
+
self.config.client_key_password.encode()
|
|
537
|
+
if self.config.client_key_password
|
|
538
|
+
else None
|
|
539
|
+
)
|
|
540
|
+
if password is not None:
|
|
541
|
+
context.load_cert_chain(
|
|
542
|
+
client_cert_file, client_key_file, password=password
|
|
543
|
+
)
|
|
544
|
+
else:
|
|
545
|
+
context.load_cert_chain(client_cert_file, client_key_file)
|
|
344
546
|
|
|
345
547
|
# Configure cipher suites
|
|
346
548
|
if self.config.cipher_suite:
|
|
347
549
|
context.set_ciphers(self.config.cipher_suite)
|
|
348
550
|
|
|
349
|
-
# Cache context
|
|
350
|
-
|
|
551
|
+
# Cache context (only for file-based contexts)
|
|
552
|
+
if cache_key is not None:
|
|
553
|
+
self._contexts[cache_key] = context
|
|
351
554
|
|
|
352
555
|
self.logger.info(
|
|
353
556
|
"Client SSL context created successfully",
|
|
354
557
|
extra={
|
|
355
558
|
"ca_cert_file": ca_cert_file,
|
|
356
559
|
"client_cert_file": client_cert_file,
|
|
560
|
+
"cert_data_provided": cert_data is not None,
|
|
357
561
|
"verify_mode": verify_mode,
|
|
358
562
|
"min_version": min_version,
|
|
359
563
|
},
|
|
@@ -365,8 +569,6 @@ class SSLManager:
|
|
|
365
569
|
self.logger.error(
|
|
366
570
|
"Failed to create client SSL context",
|
|
367
571
|
extra={
|
|
368
|
-
"ca_cert_file": ca_cert_file,
|
|
369
|
-
"client_cert_file": client_cert_file,
|
|
370
572
|
"error": str(e),
|
|
371
573
|
},
|
|
372
574
|
)
|
|
@@ -709,7 +911,8 @@ class SSLManager:
|
|
|
709
911
|
Check certificate expiry information.
|
|
710
912
|
|
|
711
913
|
This method provides detailed information about certificate
|
|
712
|
-
expiry including days until expiry and expiry status.
|
|
914
|
+
expiry including days until expiry and expiry status. The window
|
|
915
|
+
for flagging "expires_soon" is controlled by SSLConfig.expiry_warning_days.
|
|
713
916
|
|
|
714
917
|
Args:
|
|
715
918
|
cert_path (str): Path to certificate file. Must be a valid
|
|
@@ -721,7 +924,8 @@ class SSLManager:
|
|
|
721
924
|
- not_before: Certificate validity start date
|
|
722
925
|
- days_until_expiry: Days until certificate expires
|
|
723
926
|
- is_expired: Whether certificate is expired
|
|
724
|
-
- expires_soon: Whether certificate expires within
|
|
927
|
+
- expires_soon: Whether certificate expires within the configured
|
|
928
|
+
warning window (SSLConfig.expiry_warning_days)
|
|
725
929
|
- status: Expiry status (valid, expires_soon, expired)
|
|
726
930
|
- total_seconds_until_expiry: Seconds until expiry
|
|
727
931
|
|
|
@@ -741,7 +945,9 @@ class SSLManager:
|
|
|
741
945
|
... print(f"Certificate expires in {days} days")
|
|
742
946
|
"""
|
|
743
947
|
try:
|
|
744
|
-
expiry_info = get_certificate_expiry(
|
|
948
|
+
expiry_info = get_certificate_expiry(
|
|
949
|
+
cert_path, warn_days=self.config.expiry_warning_days
|
|
950
|
+
)
|
|
745
951
|
|
|
746
952
|
self.logger.info(
|
|
747
953
|
"Certificate expiry check completed",
|