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.
Files changed (76) hide show
  1. mcp_security_framework/__init__.py +96 -0
  2. mcp_security_framework/cli/__init__.py +18 -0
  3. mcp_security_framework/cli/cert_cli.py +511 -0
  4. mcp_security_framework/cli/security_cli.py +791 -0
  5. mcp_security_framework/constants.py +209 -0
  6. mcp_security_framework/core/__init__.py +61 -0
  7. mcp_security_framework/core/auth_manager.py +1011 -0
  8. mcp_security_framework/core/cert_manager.py +1663 -0
  9. mcp_security_framework/core/permission_manager.py +735 -0
  10. mcp_security_framework/core/rate_limiter.py +602 -0
  11. mcp_security_framework/core/security_manager.py +943 -0
  12. mcp_security_framework/core/ssl_manager.py +735 -0
  13. mcp_security_framework/examples/__init__.py +75 -0
  14. mcp_security_framework/examples/django_example.py +615 -0
  15. mcp_security_framework/examples/fastapi_example.py +472 -0
  16. mcp_security_framework/examples/flask_example.py +506 -0
  17. mcp_security_framework/examples/gateway_example.py +803 -0
  18. mcp_security_framework/examples/microservice_example.py +690 -0
  19. mcp_security_framework/examples/standalone_example.py +576 -0
  20. mcp_security_framework/middleware/__init__.py +250 -0
  21. mcp_security_framework/middleware/auth_middleware.py +292 -0
  22. mcp_security_framework/middleware/fastapi_auth_middleware.py +447 -0
  23. mcp_security_framework/middleware/fastapi_middleware.py +757 -0
  24. mcp_security_framework/middleware/flask_auth_middleware.py +465 -0
  25. mcp_security_framework/middleware/flask_middleware.py +591 -0
  26. mcp_security_framework/middleware/mtls_middleware.py +439 -0
  27. mcp_security_framework/middleware/rate_limit_middleware.py +403 -0
  28. mcp_security_framework/middleware/security_middleware.py +507 -0
  29. mcp_security_framework/schemas/__init__.py +109 -0
  30. mcp_security_framework/schemas/config.py +694 -0
  31. mcp_security_framework/schemas/models.py +709 -0
  32. mcp_security_framework/schemas/responses.py +686 -0
  33. mcp_security_framework/tests/__init__.py +0 -0
  34. mcp_security_framework/utils/__init__.py +121 -0
  35. mcp_security_framework/utils/cert_utils.py +525 -0
  36. mcp_security_framework/utils/crypto_utils.py +475 -0
  37. mcp_security_framework/utils/validation_utils.py +571 -0
  38. mcp_security_framework-0.1.0.dist-info/METADATA +411 -0
  39. mcp_security_framework-0.1.0.dist-info/RECORD +76 -0
  40. mcp_security_framework-0.1.0.dist-info/WHEEL +5 -0
  41. mcp_security_framework-0.1.0.dist-info/entry_points.txt +3 -0
  42. mcp_security_framework-0.1.0.dist-info/top_level.txt +2 -0
  43. tests/__init__.py +0 -0
  44. tests/test_cli/__init__.py +0 -0
  45. tests/test_cli/test_cert_cli.py +379 -0
  46. tests/test_cli/test_security_cli.py +657 -0
  47. tests/test_core/__init__.py +0 -0
  48. tests/test_core/test_auth_manager.py +582 -0
  49. tests/test_core/test_cert_manager.py +795 -0
  50. tests/test_core/test_permission_manager.py +395 -0
  51. tests/test_core/test_rate_limiter.py +626 -0
  52. tests/test_core/test_security_manager.py +841 -0
  53. tests/test_core/test_ssl_manager.py +532 -0
  54. tests/test_examples/__init__.py +8 -0
  55. tests/test_examples/test_fastapi_example.py +264 -0
  56. tests/test_examples/test_flask_example.py +238 -0
  57. tests/test_examples/test_standalone_example.py +292 -0
  58. tests/test_integration/__init__.py +0 -0
  59. tests/test_integration/test_auth_flow.py +502 -0
  60. tests/test_integration/test_certificate_flow.py +527 -0
  61. tests/test_integration/test_fastapi_integration.py +341 -0
  62. tests/test_integration/test_flask_integration.py +398 -0
  63. tests/test_integration/test_standalone_integration.py +493 -0
  64. tests/test_middleware/__init__.py +0 -0
  65. tests/test_middleware/test_fastapi_middleware.py +523 -0
  66. tests/test_middleware/test_flask_middleware.py +582 -0
  67. tests/test_middleware/test_security_middleware.py +493 -0
  68. tests/test_schemas/__init__.py +0 -0
  69. tests/test_schemas/test_config.py +811 -0
  70. tests/test_schemas/test_models.py +879 -0
  71. tests/test_schemas/test_responses.py +1054 -0
  72. tests/test_schemas/test_serialization.py +493 -0
  73. tests/test_utils/__init__.py +0 -0
  74. tests/test_utils/test_cert_utils.py +510 -0
  75. tests/test_utils/test_crypto_utils.py +603 -0
  76. 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)}")