mcp-security-framework 0.1.0__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 +96 -0
- mcp_security_framework/cli/__init__.py +18 -0
- mcp_security_framework/cli/cert_cli.py +511 -0
- mcp_security_framework/cli/security_cli.py +791 -0
- mcp_security_framework/constants.py +209 -0
- mcp_security_framework/core/__init__.py +61 -0
- mcp_security_framework/core/auth_manager.py +1011 -0
- mcp_security_framework/core/cert_manager.py +1663 -0
- mcp_security_framework/core/permission_manager.py +735 -0
- mcp_security_framework/core/rate_limiter.py +602 -0
- mcp_security_framework/core/security_manager.py +943 -0
- mcp_security_framework/core/ssl_manager.py +735 -0
- mcp_security_framework/examples/__init__.py +75 -0
- mcp_security_framework/examples/django_example.py +615 -0
- mcp_security_framework/examples/fastapi_example.py +472 -0
- mcp_security_framework/examples/flask_example.py +506 -0
- mcp_security_framework/examples/gateway_example.py +803 -0
- mcp_security_framework/examples/microservice_example.py +690 -0
- mcp_security_framework/examples/standalone_example.py +576 -0
- mcp_security_framework/middleware/__init__.py +250 -0
- mcp_security_framework/middleware/auth_middleware.py +292 -0
- mcp_security_framework/middleware/fastapi_auth_middleware.py +447 -0
- mcp_security_framework/middleware/fastapi_middleware.py +757 -0
- mcp_security_framework/middleware/flask_auth_middleware.py +465 -0
- mcp_security_framework/middleware/flask_middleware.py +591 -0
- mcp_security_framework/middleware/mtls_middleware.py +439 -0
- mcp_security_framework/middleware/rate_limit_middleware.py +403 -0
- mcp_security_framework/middleware/security_middleware.py +507 -0
- mcp_security_framework/schemas/__init__.py +109 -0
- mcp_security_framework/schemas/config.py +694 -0
- mcp_security_framework/schemas/models.py +709 -0
- mcp_security_framework/schemas/responses.py +686 -0
- mcp_security_framework/tests/__init__.py +0 -0
- mcp_security_framework/utils/__init__.py +121 -0
- mcp_security_framework/utils/cert_utils.py +525 -0
- mcp_security_framework/utils/crypto_utils.py +475 -0
- mcp_security_framework/utils/validation_utils.py +571 -0
- mcp_security_framework-0.1.0.dist-info/METADATA +411 -0
- mcp_security_framework-0.1.0.dist-info/RECORD +76 -0
- mcp_security_framework-0.1.0.dist-info/WHEEL +5 -0
- mcp_security_framework-0.1.0.dist-info/entry_points.txt +3 -0
- mcp_security_framework-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_cli/__init__.py +0 -0
- tests/test_cli/test_cert_cli.py +379 -0
- tests/test_cli/test_security_cli.py +657 -0
- tests/test_core/__init__.py +0 -0
- tests/test_core/test_auth_manager.py +582 -0
- tests/test_core/test_cert_manager.py +795 -0
- tests/test_core/test_permission_manager.py +395 -0
- tests/test_core/test_rate_limiter.py +626 -0
- tests/test_core/test_security_manager.py +841 -0
- tests/test_core/test_ssl_manager.py +532 -0
- tests/test_examples/__init__.py +8 -0
- tests/test_examples/test_fastapi_example.py +264 -0
- tests/test_examples/test_flask_example.py +238 -0
- tests/test_examples/test_standalone_example.py +292 -0
- tests/test_integration/__init__.py +0 -0
- tests/test_integration/test_auth_flow.py +502 -0
- tests/test_integration/test_certificate_flow.py +527 -0
- tests/test_integration/test_fastapi_integration.py +341 -0
- tests/test_integration/test_flask_integration.py +398 -0
- tests/test_integration/test_standalone_integration.py +493 -0
- tests/test_middleware/__init__.py +0 -0
- tests/test_middleware/test_fastapi_middleware.py +523 -0
- tests/test_middleware/test_flask_middleware.py +582 -0
- tests/test_middleware/test_security_middleware.py +493 -0
- tests/test_schemas/__init__.py +0 -0
- tests/test_schemas/test_config.py +811 -0
- tests/test_schemas/test_models.py +879 -0
- tests/test_schemas/test_responses.py +1054 -0
- tests/test_schemas/test_serialization.py +493 -0
- tests/test_utils/__init__.py +0 -0
- tests/test_utils/test_cert_utils.py +510 -0
- tests/test_utils/test_crypto_utils.py +603 -0
- tests/test_utils/test_validation_utils.py +477 -0
@@ -0,0 +1,121 @@
|
|
1
|
+
"""
|
2
|
+
MCP Security Framework Utilities Module
|
3
|
+
|
4
|
+
This module provides comprehensive utilities for the MCP Security Framework
|
5
|
+
including cryptographic functions, certificate utilities, and validation
|
6
|
+
utilities.
|
7
|
+
|
8
|
+
Key Components:
|
9
|
+
- Cryptographic utilities for hashing, signing, and JWT operations
|
10
|
+
- Certificate utilities for parsing and validation
|
11
|
+
- Validation utilities for data and configuration validation
|
12
|
+
|
13
|
+
Functions:
|
14
|
+
crypto_utils: Cryptographic operations and utilities
|
15
|
+
cert_utils: Certificate parsing and validation utilities
|
16
|
+
validation_utils: Data validation and normalization utilities
|
17
|
+
|
18
|
+
Author: MCP Security Team
|
19
|
+
Version: 1.0.0
|
20
|
+
License: MIT
|
21
|
+
"""
|
22
|
+
|
23
|
+
# Certificate utilities
|
24
|
+
from .cert_utils import (
|
25
|
+
CertificateError,
|
26
|
+
convert_certificate_format,
|
27
|
+
extract_certificate_info,
|
28
|
+
extract_permissions_from_certificate,
|
29
|
+
extract_public_key,
|
30
|
+
extract_roles_from_certificate,
|
31
|
+
get_certificate_expiry,
|
32
|
+
get_certificate_serial_number,
|
33
|
+
is_certificate_self_signed,
|
34
|
+
parse_certificate,
|
35
|
+
validate_certificate_chain,
|
36
|
+
validate_certificate_format,
|
37
|
+
validate_certificate_purpose,
|
38
|
+
)
|
39
|
+
|
40
|
+
# Crypto utilities
|
41
|
+
from .crypto_utils import (
|
42
|
+
CryptoError,
|
43
|
+
create_jwt_token,
|
44
|
+
generate_api_key,
|
45
|
+
generate_hmac,
|
46
|
+
generate_random_bytes,
|
47
|
+
generate_rsa_key_pair,
|
48
|
+
hash_data,
|
49
|
+
hash_password,
|
50
|
+
sign_data,
|
51
|
+
verify_hmac,
|
52
|
+
verify_jwt_token,
|
53
|
+
verify_password,
|
54
|
+
verify_signature,
|
55
|
+
)
|
56
|
+
|
57
|
+
# Validation utilities
|
58
|
+
from .validation_utils import (
|
59
|
+
ValidationError,
|
60
|
+
normalize_data,
|
61
|
+
sanitize_string,
|
62
|
+
validate_configuration_file,
|
63
|
+
validate_directory_structure,
|
64
|
+
validate_email,
|
65
|
+
validate_file_extension,
|
66
|
+
validate_file_path,
|
67
|
+
validate_input_data,
|
68
|
+
validate_ip_address,
|
69
|
+
validate_json_schema,
|
70
|
+
validate_list_content,
|
71
|
+
validate_numeric_range,
|
72
|
+
validate_string_length,
|
73
|
+
validate_url,
|
74
|
+
)
|
75
|
+
|
76
|
+
__all__ = [
|
77
|
+
# Crypto utilities
|
78
|
+
"CryptoError",
|
79
|
+
"create_jwt_token",
|
80
|
+
"generate_api_key",
|
81
|
+
"generate_hmac",
|
82
|
+
"generate_random_bytes",
|
83
|
+
"generate_rsa_key_pair",
|
84
|
+
"hash_data",
|
85
|
+
"hash_password",
|
86
|
+
"sign_data",
|
87
|
+
"verify_hmac",
|
88
|
+
"verify_jwt_token",
|
89
|
+
"verify_password",
|
90
|
+
"verify_signature",
|
91
|
+
# Certificate utilities
|
92
|
+
"CertificateError",
|
93
|
+
"convert_certificate_format",
|
94
|
+
"extract_certificate_info",
|
95
|
+
"extract_permissions_from_certificate",
|
96
|
+
"extract_public_key",
|
97
|
+
"extract_roles_from_certificate",
|
98
|
+
"get_certificate_expiry",
|
99
|
+
"get_certificate_serial_number",
|
100
|
+
"is_certificate_self_signed",
|
101
|
+
"parse_certificate",
|
102
|
+
"validate_certificate_chain",
|
103
|
+
"validate_certificate_format",
|
104
|
+
"validate_certificate_purpose",
|
105
|
+
# Validation utilities
|
106
|
+
"ValidationError",
|
107
|
+
"normalize_data",
|
108
|
+
"sanitize_string",
|
109
|
+
"validate_configuration_file",
|
110
|
+
"validate_directory_structure",
|
111
|
+
"validate_email",
|
112
|
+
"validate_file_extension",
|
113
|
+
"validate_file_path",
|
114
|
+
"validate_input_data",
|
115
|
+
"validate_ip_address",
|
116
|
+
"validate_json_schema",
|
117
|
+
"validate_list_content",
|
118
|
+
"validate_numeric_range",
|
119
|
+
"validate_string_length",
|
120
|
+
"validate_url",
|
121
|
+
]
|
@@ -0,0 +1,525 @@
|
|
1
|
+
"""
|
2
|
+
Certificate Utilities Module
|
3
|
+
|
4
|
+
This module provides comprehensive utilities for working with X.509
|
5
|
+
certificates in the MCP Security Framework. It includes functions for
|
6
|
+
parsing certificates, extracting information, validating formats,
|
7
|
+
and working with OIDs.
|
8
|
+
|
9
|
+
Key Features:
|
10
|
+
- Certificate parsing and validation
|
11
|
+
- Certificate information extraction
|
12
|
+
- OID handling and extension parsing
|
13
|
+
- Certificate chain validation
|
14
|
+
- Certificate format conversion
|
15
|
+
- Certificate metadata extraction
|
16
|
+
|
17
|
+
Functions:
|
18
|
+
parse_certificate: Parse certificate from PEM/DER format
|
19
|
+
extract_certificate_info: Extract detailed certificate information
|
20
|
+
validate_certificate_format: Validate certificate format
|
21
|
+
extract_roles_from_certificate: Extract roles from certificate
|
22
|
+
extract_permissions_from_certificate: Extract permissions from certificate
|
23
|
+
validate_certificate_chain: Validate certificate chain
|
24
|
+
get_certificate_expiry: Get certificate expiry information
|
25
|
+
convert_certificate_format: Convert between certificate formats
|
26
|
+
extract_public_key: Extract public key from certificate
|
27
|
+
|
28
|
+
Author: MCP Security Team
|
29
|
+
Version: 1.0.0
|
30
|
+
License: MIT
|
31
|
+
"""
|
32
|
+
|
33
|
+
import base64
|
34
|
+
from datetime import datetime, timezone
|
35
|
+
from pathlib import Path
|
36
|
+
from typing import Dict, List, Union
|
37
|
+
|
38
|
+
from cryptography import x509
|
39
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
40
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
41
|
+
from cryptography.x509.oid import ExtensionOID, NameOID
|
42
|
+
|
43
|
+
|
44
|
+
class CertificateError(Exception):
|
45
|
+
"""Raised when certificate operations fail."""
|
46
|
+
|
47
|
+
def __init__(self, message: str, error_code: int = -32002):
|
48
|
+
self.message = message
|
49
|
+
self.error_code = error_code
|
50
|
+
super().__init__(self.message)
|
51
|
+
|
52
|
+
|
53
|
+
def parse_certificate(cert_data: Union[str, bytes, Path]) -> x509.Certificate:
|
54
|
+
"""
|
55
|
+
Parse certificate from PEM or DER format.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
cert_data: Certificate data as string, bytes, or file path
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
Parsed X.509 certificate object
|
62
|
+
|
63
|
+
Raises:
|
64
|
+
CertificateError: If certificate parsing fails
|
65
|
+
"""
|
66
|
+
try:
|
67
|
+
# Handle string input first (check if it's PEM data)
|
68
|
+
if isinstance(cert_data, str):
|
69
|
+
# Check if it looks like PEM data
|
70
|
+
if "-----BEGIN CERTIFICATE-----" in cert_data:
|
71
|
+
lines = cert_data.strip().split("\n")
|
72
|
+
cert_data = "".join(
|
73
|
+
line for line in lines if not line.startswith("-----")
|
74
|
+
)
|
75
|
+
cert_data = base64.b64decode(cert_data)
|
76
|
+
else:
|
77
|
+
# Try to treat as file path
|
78
|
+
try:
|
79
|
+
if Path(cert_data).exists():
|
80
|
+
with open(cert_data, "rb") as f:
|
81
|
+
cert_data = f.read()
|
82
|
+
else:
|
83
|
+
# Try to decode as base64
|
84
|
+
cert_data = base64.b64decode(cert_data)
|
85
|
+
except (OSError, ValueError):
|
86
|
+
# If file doesn't exist and not base64, try to decode anyway
|
87
|
+
cert_data = base64.b64decode(cert_data)
|
88
|
+
|
89
|
+
# Handle Path object
|
90
|
+
elif isinstance(cert_data, Path):
|
91
|
+
if cert_data.exists():
|
92
|
+
with open(cert_data, "rb") as f:
|
93
|
+
cert_data = f.read()
|
94
|
+
else:
|
95
|
+
raise CertificateError(f"Certificate file not found: {cert_data}")
|
96
|
+
|
97
|
+
# Try to parse as PEM first, then as DER
|
98
|
+
try:
|
99
|
+
return x509.load_pem_x509_certificate(cert_data)
|
100
|
+
except Exception:
|
101
|
+
return x509.load_der_x509_certificate(cert_data)
|
102
|
+
except Exception as e:
|
103
|
+
raise CertificateError(f"Certificate parsing failed: {str(e)}")
|
104
|
+
|
105
|
+
|
106
|
+
def extract_certificate_info(cert_data: Union[str, bytes, Path]) -> Dict:
|
107
|
+
"""
|
108
|
+
Extract detailed information from certificate.
|
109
|
+
|
110
|
+
Args:
|
111
|
+
cert_data: Certificate data as string, bytes, or file path
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
Dictionary containing certificate information
|
115
|
+
|
116
|
+
Raises:
|
117
|
+
CertificateError: If information extraction fails
|
118
|
+
"""
|
119
|
+
try:
|
120
|
+
cert = parse_certificate(cert_data)
|
121
|
+
|
122
|
+
# Extract basic information
|
123
|
+
info = {
|
124
|
+
"subject": str(cert.subject),
|
125
|
+
"issuer": str(cert.issuer),
|
126
|
+
"serial_number": str(cert.serial_number),
|
127
|
+
"version": cert.version.name,
|
128
|
+
"not_before": cert.not_valid_before,
|
129
|
+
"not_after": cert.not_valid_after,
|
130
|
+
"signature_algorithm": cert.signature_algorithm_oid._name,
|
131
|
+
"public_key_algorithm": cert.public_key_algorithm_oid._name,
|
132
|
+
"key_size": _get_key_size(cert.public_key()),
|
133
|
+
"extensions": _extract_extensions(cert),
|
134
|
+
"fingerprint_sha1": cert.fingerprint(hashes.SHA1()).hex(),
|
135
|
+
"fingerprint_sha256": cert.fingerprint(hashes.SHA256()).hex(),
|
136
|
+
}
|
137
|
+
|
138
|
+
# Extract common name
|
139
|
+
cn = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
|
140
|
+
if cn:
|
141
|
+
info["common_name"] = cn[0].value
|
142
|
+
|
143
|
+
# Extract organization
|
144
|
+
org = cert.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)
|
145
|
+
if org:
|
146
|
+
info["organization"] = org[0].value
|
147
|
+
|
148
|
+
# Extract country
|
149
|
+
country = cert.subject.get_attributes_for_oid(NameOID.COUNTRY_NAME)
|
150
|
+
if country:
|
151
|
+
info["country"] = country[0].value
|
152
|
+
|
153
|
+
return info
|
154
|
+
except Exception as e:
|
155
|
+
raise CertificateError(f"Certificate information extraction failed: {str(e)}")
|
156
|
+
|
157
|
+
|
158
|
+
def validate_certificate_format(cert_data: Union[str, bytes]) -> bool:
|
159
|
+
"""
|
160
|
+
Validate certificate format.
|
161
|
+
|
162
|
+
Args:
|
163
|
+
cert_data: Certificate data to validate
|
164
|
+
|
165
|
+
Returns:
|
166
|
+
True if format is valid, False otherwise
|
167
|
+
"""
|
168
|
+
try:
|
169
|
+
parse_certificate(cert_data)
|
170
|
+
return True
|
171
|
+
except Exception:
|
172
|
+
return False
|
173
|
+
|
174
|
+
|
175
|
+
def extract_roles_from_certificate(cert_data: Union[str, bytes, Path]) -> List[str]:
|
176
|
+
"""
|
177
|
+
Extract roles from certificate extensions.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
cert_data: Certificate data as string, bytes, or file path
|
181
|
+
|
182
|
+
Returns:
|
183
|
+
List of roles found in certificate
|
184
|
+
|
185
|
+
Raises:
|
186
|
+
CertificateError: If role extraction fails
|
187
|
+
"""
|
188
|
+
try:
|
189
|
+
cert = parse_certificate(cert_data)
|
190
|
+
roles = []
|
191
|
+
|
192
|
+
# Check for custom extension with roles
|
193
|
+
try:
|
194
|
+
roles_extension = cert.extensions.get_extension_for_oid(
|
195
|
+
x509.ObjectIdentifier("1.3.6.1.4.1.99999.1.1") # Custom roles OID
|
196
|
+
)
|
197
|
+
if roles_extension:
|
198
|
+
roles_data = roles_extension.value.value
|
199
|
+
if isinstance(roles_data, bytes):
|
200
|
+
roles_str = roles_data.decode("utf-8")
|
201
|
+
roles = [
|
202
|
+
role.strip() for role in roles_str.split(",") if role.strip()
|
203
|
+
]
|
204
|
+
except x509.extensions.ExtensionNotFound:
|
205
|
+
pass
|
206
|
+
|
207
|
+
# Check subject alternative names for roles
|
208
|
+
try:
|
209
|
+
san_extension = cert.extensions.get_extension_for_oid(
|
210
|
+
ExtensionOID.SUBJECT_ALTERNATIVE_NAME
|
211
|
+
)
|
212
|
+
if san_extension:
|
213
|
+
san = san_extension.value
|
214
|
+
for name in san:
|
215
|
+
if isinstance(name, x509.DNSName):
|
216
|
+
# Check if DNS name contains role information
|
217
|
+
if "role=" in name.value:
|
218
|
+
role = name.value.split("role=")[1].split(",")[0]
|
219
|
+
if role not in roles:
|
220
|
+
roles.append(role)
|
221
|
+
except x509.extensions.ExtensionNotFound:
|
222
|
+
pass
|
223
|
+
|
224
|
+
return roles
|
225
|
+
except Exception as e:
|
226
|
+
raise CertificateError(f"Role extraction failed: {str(e)}")
|
227
|
+
|
228
|
+
|
229
|
+
def extract_permissions_from_certificate(
|
230
|
+
cert_data: Union[str, bytes, Path],
|
231
|
+
) -> List[str]:
|
232
|
+
"""
|
233
|
+
Extract permissions from certificate extensions.
|
234
|
+
|
235
|
+
Args:
|
236
|
+
cert_data: Certificate data as string, bytes, or file path
|
237
|
+
|
238
|
+
Returns:
|
239
|
+
List of permissions found in certificate
|
240
|
+
|
241
|
+
Raises:
|
242
|
+
CertificateError: If permission extraction fails
|
243
|
+
"""
|
244
|
+
try:
|
245
|
+
cert = parse_certificate(cert_data)
|
246
|
+
permissions = []
|
247
|
+
|
248
|
+
# Check for custom extension with permissions
|
249
|
+
try:
|
250
|
+
perms_extension = cert.extensions.get_extension_for_oid(
|
251
|
+
x509.ObjectIdentifier("1.3.6.1.4.1.99999.1.2") # Custom permissions OID
|
252
|
+
)
|
253
|
+
if perms_extension:
|
254
|
+
perms_data = perms_extension.value.value
|
255
|
+
if isinstance(perms_data, bytes):
|
256
|
+
perms_str = perms_data.decode("utf-8")
|
257
|
+
permissions = [
|
258
|
+
perm.strip() for perm in perms_str.split(",") if perm.strip()
|
259
|
+
]
|
260
|
+
except x509.extensions.ExtensionNotFound:
|
261
|
+
pass
|
262
|
+
|
263
|
+
return permissions
|
264
|
+
except Exception as e:
|
265
|
+
raise CertificateError(f"Permission extraction failed: {str(e)}")
|
266
|
+
|
267
|
+
|
268
|
+
def validate_certificate_chain(
|
269
|
+
cert_data: Union[str, bytes, Path], ca_cert_data: Union[str, bytes, Path, List[Union[str, bytes, Path]]]
|
270
|
+
) -> bool:
|
271
|
+
"""
|
272
|
+
Validate certificate chain against CA certificate(s).
|
273
|
+
|
274
|
+
Args:
|
275
|
+
cert_data: Certificate data to validate
|
276
|
+
ca_cert_data: CA certificate data or list of CA certificates
|
277
|
+
|
278
|
+
Returns:
|
279
|
+
True if chain is valid, False otherwise
|
280
|
+
|
281
|
+
Raises:
|
282
|
+
CertificateError: If validation fails
|
283
|
+
"""
|
284
|
+
try:
|
285
|
+
cert = parse_certificate(cert_data)
|
286
|
+
|
287
|
+
# Handle single CA certificate or list of CA certificates
|
288
|
+
if isinstance(ca_cert_data, list):
|
289
|
+
ca_certs = [parse_certificate(ca_cert) for ca_cert in ca_cert_data]
|
290
|
+
else:
|
291
|
+
ca_certs = [parse_certificate(ca_cert_data)]
|
292
|
+
|
293
|
+
# For now, just check that the certificate was issued by one of the CA certificates
|
294
|
+
# This is a simplified validation - in a real scenario, you would use OpenSSL or similar
|
295
|
+
for ca_cert in ca_certs:
|
296
|
+
if cert.issuer == ca_cert.subject:
|
297
|
+
return True
|
298
|
+
|
299
|
+
return False
|
300
|
+
except Exception as e:
|
301
|
+
return False
|
302
|
+
|
303
|
+
|
304
|
+
def get_certificate_expiry(cert_data: Union[str, bytes, Path]) -> Dict:
|
305
|
+
"""
|
306
|
+
Get certificate expiry information.
|
307
|
+
|
308
|
+
Args:
|
309
|
+
cert_data: Certificate data as string, bytes, or file path
|
310
|
+
|
311
|
+
Returns:
|
312
|
+
Dictionary containing expiry information
|
313
|
+
|
314
|
+
Raises:
|
315
|
+
CertificateError: If expiry information extraction fails
|
316
|
+
"""
|
317
|
+
try:
|
318
|
+
cert = parse_certificate(cert_data)
|
319
|
+
now = datetime.now(timezone.utc)
|
320
|
+
|
321
|
+
# Calculate time until expiry
|
322
|
+
time_until_expiry = cert.not_valid_after.replace(tzinfo=timezone.utc) - now
|
323
|
+
days_until_expiry = time_until_expiry.days
|
324
|
+
|
325
|
+
# Determine expiry status
|
326
|
+
if time_until_expiry.total_seconds() < 0:
|
327
|
+
status = "expired"
|
328
|
+
elif days_until_expiry <= 30:
|
329
|
+
status = "expires_soon"
|
330
|
+
else:
|
331
|
+
status = "valid"
|
332
|
+
|
333
|
+
return {
|
334
|
+
"not_after": cert.not_valid_after,
|
335
|
+
"not_before": cert.not_valid_before,
|
336
|
+
"days_until_expiry": days_until_expiry,
|
337
|
+
"is_expired": time_until_expiry.total_seconds() < 0,
|
338
|
+
"expires_soon": days_until_expiry <= 30,
|
339
|
+
"status": status,
|
340
|
+
"total_seconds_until_expiry": time_until_expiry.total_seconds(),
|
341
|
+
}
|
342
|
+
except Exception as e:
|
343
|
+
raise CertificateError(
|
344
|
+
f"Certificate expiry information extraction failed: {str(e)}"
|
345
|
+
)
|
346
|
+
|
347
|
+
|
348
|
+
def convert_certificate_format(
|
349
|
+
cert_data: Union[str, bytes, Path], output_format: str = "PEM"
|
350
|
+
) -> str:
|
351
|
+
"""
|
352
|
+
Convert certificate between formats.
|
353
|
+
|
354
|
+
Args:
|
355
|
+
cert_data: Certificate data as string, bytes, or file path
|
356
|
+
output_format: Output format (PEM, DER)
|
357
|
+
|
358
|
+
Returns:
|
359
|
+
Certificate in specified format
|
360
|
+
|
361
|
+
Raises:
|
362
|
+
CertificateError: If conversion fails
|
363
|
+
"""
|
364
|
+
try:
|
365
|
+
cert = parse_certificate(cert_data)
|
366
|
+
|
367
|
+
if output_format.upper() == "PEM":
|
368
|
+
return cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
|
369
|
+
elif output_format.upper() == "DER":
|
370
|
+
der_data = cert.public_bytes(serialization.Encoding.DER)
|
371
|
+
return base64.b64encode(der_data).decode("utf-8")
|
372
|
+
else:
|
373
|
+
raise CertificateError(f"Unsupported output format: {output_format}")
|
374
|
+
except Exception as e:
|
375
|
+
raise CertificateError(f"Certificate format conversion failed: {str(e)}")
|
376
|
+
|
377
|
+
|
378
|
+
def extract_public_key(cert_data: Union[str, bytes, Path]) -> str:
|
379
|
+
"""
|
380
|
+
Extract public key from certificate in PEM format.
|
381
|
+
|
382
|
+
Args:
|
383
|
+
cert_data: Certificate data as string, bytes, or file path
|
384
|
+
|
385
|
+
Returns:
|
386
|
+
Public key in PEM format
|
387
|
+
|
388
|
+
Raises:
|
389
|
+
CertificateError: If public key extraction fails
|
390
|
+
"""
|
391
|
+
try:
|
392
|
+
cert = parse_certificate(cert_data)
|
393
|
+
public_key = cert.public_key()
|
394
|
+
|
395
|
+
return public_key.public_bytes(
|
396
|
+
encoding=serialization.Encoding.PEM,
|
397
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
398
|
+
).decode("utf-8")
|
399
|
+
except Exception as e:
|
400
|
+
raise CertificateError(f"Public key extraction failed: {str(e)}")
|
401
|
+
|
402
|
+
|
403
|
+
def _get_key_size(public_key) -> int:
|
404
|
+
"""Get key size from public key."""
|
405
|
+
try:
|
406
|
+
if hasattr(public_key, "key_size"):
|
407
|
+
return public_key.key_size
|
408
|
+
elif isinstance(public_key, rsa.RSAPublicKey):
|
409
|
+
return public_key.key_size
|
410
|
+
else:
|
411
|
+
return 0
|
412
|
+
except Exception:
|
413
|
+
return 0
|
414
|
+
|
415
|
+
|
416
|
+
def _extract_extensions(cert: x509.Certificate) -> Dict:
|
417
|
+
"""Extract certificate extensions."""
|
418
|
+
extensions = {}
|
419
|
+
|
420
|
+
for extension in cert.extensions:
|
421
|
+
ext_name = extension.oid._name
|
422
|
+
ext_value = str(extension.value)
|
423
|
+
extensions[ext_name] = ext_value
|
424
|
+
|
425
|
+
return extensions
|
426
|
+
|
427
|
+
|
428
|
+
def validate_certificate_purpose(
|
429
|
+
cert_data: Union[str, bytes, Path], purpose: str
|
430
|
+
) -> bool:
|
431
|
+
"""
|
432
|
+
Validate certificate purpose (server, client, code signing, etc.).
|
433
|
+
|
434
|
+
Args:
|
435
|
+
cert_data: Certificate data as string, bytes, or file path
|
436
|
+
purpose: Purpose to validate (server, client, code_signing, email)
|
437
|
+
|
438
|
+
Returns:
|
439
|
+
True if certificate supports the purpose, False otherwise
|
440
|
+
|
441
|
+
Raises:
|
442
|
+
CertificateError: If validation fails
|
443
|
+
"""
|
444
|
+
try:
|
445
|
+
cert = parse_certificate(cert_data)
|
446
|
+
|
447
|
+
# Check extended key usage extension
|
448
|
+
try:
|
449
|
+
eku_extension = cert.extensions.get_extension_for_oid(
|
450
|
+
ExtensionOID.EXTENDED_KEY_USAGE
|
451
|
+
)
|
452
|
+
if eku_extension:
|
453
|
+
eku = eku_extension.value
|
454
|
+
|
455
|
+
if purpose == "server":
|
456
|
+
return x509.oid.ExtendedKeyUsageOID.SERVER_AUTH in eku
|
457
|
+
elif purpose == "client":
|
458
|
+
return x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH in eku
|
459
|
+
elif purpose == "code_signing":
|
460
|
+
return x509.oid.ExtendedKeyUsageOID.CODE_SIGNING in eku
|
461
|
+
elif purpose == "email":
|
462
|
+
return x509.oid.ExtendedKeyUsageOID.EMAIL_PROTECTION in eku
|
463
|
+
except x509.extensions.ExtensionNotFound:
|
464
|
+
pass
|
465
|
+
|
466
|
+
# Check key usage extension
|
467
|
+
try:
|
468
|
+
ku_extension = cert.extensions.get_extension_for_oid(ExtensionOID.KEY_USAGE)
|
469
|
+
if ku_extension:
|
470
|
+
ku = ku_extension.value
|
471
|
+
|
472
|
+
if purpose == "server":
|
473
|
+
return ku.digital_signature and ku.key_encipherment
|
474
|
+
elif purpose == "client":
|
475
|
+
return ku.digital_signature and ku.key_agreement
|
476
|
+
elif purpose == "code_signing":
|
477
|
+
return ku.digital_signature
|
478
|
+
elif purpose == "email":
|
479
|
+
return ku.digital_signature and ku.key_encipherment
|
480
|
+
except x509.extensions.ExtensionNotFound:
|
481
|
+
pass
|
482
|
+
|
483
|
+
return False
|
484
|
+
except Exception as e:
|
485
|
+
raise CertificateError(f"Certificate purpose validation failed: {str(e)}")
|
486
|
+
|
487
|
+
|
488
|
+
def get_certificate_serial_number(cert_data: Union[str, bytes, Path]) -> str:
|
489
|
+
"""
|
490
|
+
Get certificate serial number.
|
491
|
+
|
492
|
+
Args:
|
493
|
+
cert_data: Certificate data as string, bytes, or file path
|
494
|
+
|
495
|
+
Returns:
|
496
|
+
Certificate serial number as string
|
497
|
+
|
498
|
+
Raises:
|
499
|
+
CertificateError: If serial number extraction fails
|
500
|
+
"""
|
501
|
+
try:
|
502
|
+
cert = parse_certificate(cert_data)
|
503
|
+
return str(cert.serial_number)
|
504
|
+
except Exception as e:
|
505
|
+
raise CertificateError(f"Serial number extraction failed: {str(e)}")
|
506
|
+
|
507
|
+
|
508
|
+
def is_certificate_self_signed(cert_data: Union[str, bytes, Path]) -> bool:
|
509
|
+
"""
|
510
|
+
Check if certificate is self-signed.
|
511
|
+
|
512
|
+
Args:
|
513
|
+
cert_data: Certificate data as string, bytes, or file path
|
514
|
+
|
515
|
+
Returns:
|
516
|
+
True if certificate is self-signed, False otherwise
|
517
|
+
|
518
|
+
Raises:
|
519
|
+
CertificateError: If check fails
|
520
|
+
"""
|
521
|
+
try:
|
522
|
+
cert = parse_certificate(cert_data)
|
523
|
+
return cert.subject == cert.issuer
|
524
|
+
except Exception as e:
|
525
|
+
raise CertificateError(f"Self-signed check failed: {str(e)}")
|