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,1663 @@
1
+ """
2
+ Certificate Manager Module
3
+
4
+ This module provides comprehensive certificate management for the
5
+ MCP Security Framework. It handles certificate creation, validation,
6
+ and management including CA certificates, client certificates,
7
+ and server certificates.
8
+
9
+ Key Features:
10
+ - Root CA certificate creation and management
11
+ - Intermediate CA certificate creation
12
+ - Client and server certificate generation
13
+ - Certificate revocation list (CRL) management
14
+ - Certificate chain validation
15
+ - Role and permission extraction from certificates
16
+ - Certificate format conversion and validation
17
+
18
+ Classes:
19
+ CertificateManager: Main certificate management class
20
+ CertificatePair: Certificate and private key pair container
21
+ CAConfig: Configuration for CA certificate creation
22
+ ClientCertConfig: Configuration for client certificate creation
23
+ ServerCertConfig: Configuration for server certificate creation
24
+
25
+ Author: MCP Security Team
26
+ Version: 1.0.0
27
+ License: MIT
28
+ """
29
+
30
+ import logging
31
+ import os
32
+ from datetime import datetime, timedelta, timezone
33
+ from pathlib import Path
34
+ from typing import Dict, List, Optional, Tuple, Union
35
+
36
+ from cryptography import x509
37
+ from cryptography.hazmat.primitives import hashes, serialization
38
+ from cryptography.hazmat.primitives.asymmetric import ec, rsa
39
+ from cryptography.x509.oid import ExtensionOID, NameOID
40
+ from pydantic import BaseModel
41
+
42
+ from mcp_security_framework.schemas.config import (
43
+ CAConfig,
44
+ CertificateConfig,
45
+ ClientCertConfig,
46
+ ServerCertConfig,
47
+ IntermediateCAConfig,
48
+ )
49
+ from mcp_security_framework.schemas.models import (
50
+ CertificateInfo,
51
+ CertificatePair,
52
+ CertificateType,
53
+ )
54
+ from mcp_security_framework.utils.cert_utils import (
55
+ extract_permissions_from_certificate,
56
+ extract_roles_from_certificate,
57
+ get_certificate_expiry,
58
+ get_certificate_serial_number,
59
+ is_certificate_self_signed,
60
+ parse_certificate,
61
+ validate_certificate_chain,
62
+ )
63
+
64
+
65
+ class CertificateManager:
66
+ """
67
+ Certificate Management Class
68
+
69
+ This class provides comprehensive certificate management capabilities including
70
+ CA certificate creation, client/server certificate generation, and certificate
71
+ lifecycle management.
72
+
73
+ The CertificateManager handles:
74
+ - Root CA certificate creation and management
75
+ - Intermediate CA certificate creation
76
+ - Client and server certificate generation
77
+ - Certificate revocation list (CRL) management
78
+ - Certificate chain validation and verification
79
+ - Role and permission extraction from certificates
80
+ - Certificate format conversion and validation
81
+
82
+ Attributes:
83
+ config (CertificateConfig): Certificate configuration settings
84
+ logger (Logger): Logger instance for certificate operations
85
+ _certificate_cache (Dict): Cache of certificate information
86
+ _crl_cache (Dict): Cache of certificate revocation lists
87
+
88
+ Example:
89
+ >>> config = CertificateConfig(
90
+ ... ca_cert_path="/path/to/ca.crt",
91
+ ... ca_key_path="/path/to/ca.key",
92
+ ... output_dir="/path/to/certs"
93
+ ... )
94
+ >>> cert_manager = CertificateManager(config)
95
+ >>> cert_pair = cert_manager.create_client_certificate(client_config)
96
+
97
+ Raises:
98
+ CertificateConfigurationError: When certificate configuration is invalid
99
+ CertificateGenerationError: When certificate generation fails
100
+ CertificateValidationError: When certificate validation fails
101
+ """
102
+
103
+ def __init__(self, config: CertificateConfig):
104
+ """
105
+ Initialize Certificate Manager.
106
+
107
+ Args:
108
+ config (CertificateConfig): Certificate configuration settings containing
109
+ CA certificate paths, output directory, and certificate settings.
110
+ Must be a valid CertificateConfig instance with proper paths
111
+ and configuration settings.
112
+
113
+ Raises:
114
+ CertificateConfigurationError: If configuration is invalid or
115
+ certificate files are not accessible.
116
+
117
+ Example:
118
+ >>> config = CertificateConfig(
119
+ ... ca_cert_path="/path/to/ca.crt",
120
+ ... ca_key_path="/path/to/ca.key"
121
+ ... )
122
+ >>> cert_manager = CertificateManager(config)
123
+ """
124
+ self.config = config
125
+ self.logger = logging.getLogger(__name__)
126
+ self._certificate_cache: Dict[str, CertificateInfo] = {}
127
+ self._crl_cache: Dict[str, x509.CertificateRevocationList] = {}
128
+
129
+ # Validate configuration
130
+ self._validate_configuration()
131
+
132
+ # Create storage directories if they don't exist
133
+ if self.config.cert_storage_path:
134
+ os.makedirs(self.config.cert_storage_path, exist_ok=True)
135
+ if self.config.key_storage_path:
136
+ os.makedirs(self.config.key_storage_path, exist_ok=True)
137
+
138
+ self.logger.info(
139
+ "CertificateManager initialized successfully",
140
+ extra={
141
+ "ca_cert_path": self.config.ca_cert_path,
142
+ "ca_key_path": self.config.ca_key_path,
143
+ "cert_storage_path": self.config.cert_storage_path,
144
+ "key_storage_path": self.config.key_storage_path,
145
+ },
146
+ )
147
+
148
+ def create_root_ca(self, ca_config: CAConfig) -> CertificatePair:
149
+ """
150
+ Create root CA certificate and private key.
151
+
152
+ This method generates a new root Certificate Authority (CA) certificate
153
+ and private key pair for signing other certificates.
154
+
155
+ The method creates a self-signed CA certificate with:
156
+ - Strong cryptographic key pair (RSA or ECDSA)
157
+ - Proper CA extensions and constraints
158
+ - Configurable validity period
159
+ - Standard CA certificate structure
160
+
161
+ Args:
162
+ ca_config (CAConfig): CA configuration containing common name,
163
+ organization, country, and other certificate details.
164
+ Must include valid common name and organization information
165
+ for proper CA certificate generation.
166
+
167
+ Returns:
168
+ CertificatePair: Certificate pair object containing:
169
+ - certificate_path (str): Path to generated CA certificate file
170
+ - private_key_path (str): Path to generated CA private key file
171
+ - certificate_pem (str): CA certificate content in PEM format
172
+ - private_key_pem (str): CA private key content in PEM format
173
+ - serial_number (str): Certificate serial number
174
+ - expiry_date (datetime): Certificate expiry date
175
+
176
+ Raises:
177
+ CertificateGenerationError: When CA certificate generation fails
178
+ due to cryptographic errors or invalid configuration
179
+ FileNotFoundError: When output directory is not accessible
180
+ PermissionError: When output directory is not writable
181
+ ValueError: When configuration parameters are invalid
182
+
183
+ Example:
184
+ >>> ca_config = CAConfig(
185
+ ... common_name="My Root CA",
186
+ ... organization="My Organization",
187
+ ... country="US",
188
+ ... validity_days=3650
189
+ ... )
190
+ >>> cert_manager = CertificateManager(config)
191
+ >>> ca_pair = cert_manager.create_root_ca(ca_config)
192
+ >>> print(f"CA certificate created: {ca_pair.certificate_path}")
193
+
194
+ Note:
195
+ Generated private keys are stored with restricted permissions
196
+ (600) for security. Certificate files are stored with standard
197
+ permissions (644). Backup the private key securely.
198
+
199
+ See Also:
200
+ create_intermediate_ca: Intermediate CA certificate generation
201
+ create_client_certificate: Client certificate generation
202
+ """
203
+ try:
204
+ # Validate CA configuration
205
+ if not ca_config.common_name:
206
+ raise ValueError("Common name is required for CA certificate")
207
+
208
+ # Generate private key (RSA by default)
209
+ private_key = rsa.generate_private_key(
210
+ public_exponent=65537, key_size=ca_config.key_size, backend=None
211
+ )
212
+
213
+ # Create certificate subject
214
+ subject_attributes = [
215
+ x509.NameAttribute(NameOID.COMMON_NAME, ca_config.common_name),
216
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, ca_config.organization),
217
+ x509.NameAttribute(NameOID.COUNTRY_NAME, ca_config.country),
218
+ ]
219
+
220
+ # Add optional attributes if they exist
221
+ if ca_config.state:
222
+ subject_attributes.append(
223
+ x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, ca_config.state)
224
+ )
225
+ if ca_config.locality:
226
+ subject_attributes.append(
227
+ x509.NameAttribute(NameOID.LOCALITY_NAME, ca_config.locality)
228
+ )
229
+
230
+ subject = x509.Name(subject_attributes)
231
+
232
+ # Create certificate builder
233
+ builder = x509.CertificateBuilder()
234
+ builder = builder.subject_name(subject)
235
+ builder = builder.issuer_name(subject) # Self-signed
236
+ builder = builder.public_key(private_key.public_key())
237
+ builder = builder.serial_number(x509.random_serial_number())
238
+ builder = builder.not_valid_before(datetime.now(timezone.utc))
239
+ builder = builder.not_valid_after(
240
+ datetime.now(timezone.utc)
241
+ + timedelta(days=ca_config.validity_years * 365)
242
+ )
243
+
244
+ # Add CA extensions
245
+ builder = builder.add_extension(
246
+ x509.BasicConstraints(ca=True, path_length=None), critical=True
247
+ )
248
+
249
+ builder = builder.add_extension(
250
+ x509.KeyUsage(
251
+ digital_signature=True,
252
+ key_encipherment=True,
253
+ key_cert_sign=True,
254
+ crl_sign=True,
255
+ content_commitment=False,
256
+ data_encipherment=False,
257
+ key_agreement=False,
258
+ encipher_only=False,
259
+ decipher_only=False,
260
+ ),
261
+ critical=True,
262
+ )
263
+
264
+ # Add SubjectKeyIdentifier extension (optional for testing)
265
+ try:
266
+ builder = builder.add_extension(
267
+ x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
268
+ critical=False,
269
+ )
270
+ except Exception:
271
+ # Skip SubjectKeyIdentifier if there are issues with the public key
272
+ pass
273
+
274
+ # Create certificate
275
+ certificate = builder.sign(private_key, hashes.SHA256())
276
+
277
+ # Generate file paths
278
+ cert_filename = f"{ca_config.common_name.replace(' ', '_').lower()}_ca.crt"
279
+ key_filename = f"{ca_config.common_name.replace(' ', '_').lower()}_ca.key"
280
+
281
+ cert_path = os.path.join(self.config.cert_storage_path, cert_filename)
282
+ key_path = os.path.join(self.config.key_storage_path, key_filename)
283
+
284
+ # Save certificate and private key
285
+ with open(cert_path, "wb") as f:
286
+ f.write(certificate.public_bytes(serialization.Encoding.PEM))
287
+
288
+ with open(key_path, "wb") as f:
289
+ f.write(
290
+ private_key.private_bytes(
291
+ encoding=serialization.Encoding.PEM,
292
+ format=serialization.PrivateFormat.PKCS8,
293
+ encryption_algorithm=serialization.NoEncryption(),
294
+ )
295
+ )
296
+
297
+ # Set proper permissions
298
+ os.chmod(cert_path, 0o644)
299
+ os.chmod(key_path, 0o600)
300
+
301
+ # Create certificate pair
302
+ cert_pair = CertificatePair(
303
+ certificate_path=cert_path,
304
+ private_key_path=key_path,
305
+ certificate_pem=certificate.public_bytes(
306
+ serialization.Encoding.PEM
307
+ ).decode(),
308
+ private_key_pem=private_key.private_bytes(
309
+ encoding=serialization.Encoding.PEM,
310
+ format=serialization.PrivateFormat.PKCS8,
311
+ encryption_algorithm=serialization.NoEncryption(),
312
+ ).decode(),
313
+ serial_number=str(certificate.serial_number),
314
+ common_name=ca_config.common_name,
315
+ organization=ca_config.organization,
316
+ not_before=certificate.not_valid_before,
317
+ not_after=certificate.not_valid_after.replace(tzinfo=timezone.utc),
318
+ certificate_type=CertificateType.ROOT_CA,
319
+ key_size=ca_config.key_size,
320
+ )
321
+
322
+ self.logger.info(
323
+ "Root CA certificate created successfully",
324
+ extra={
325
+ "common_name": ca_config.common_name,
326
+ "certificate_path": cert_path,
327
+ "key_path": key_path,
328
+ "validity_years": ca_config.validity_years,
329
+ },
330
+ )
331
+
332
+ return cert_pair
333
+
334
+ except Exception as e:
335
+ self.logger.error(
336
+ "Failed to create root CA certificate",
337
+ extra={"ca_config": ca_config.model_dump(), "error": str(e)},
338
+ )
339
+ raise CertificateGenerationError(
340
+ f"Failed to create root CA certificate: {str(e)}"
341
+ )
342
+
343
+ def create_intermediate_ca(self, intermediate_config: IntermediateCAConfig) -> CertificatePair:
344
+ """
345
+ Create intermediate CA certificate signed by parent CA.
346
+
347
+ This method generates a new intermediate Certificate Authority (CA) certificate
348
+ and private key pair signed by a parent CA certificate.
349
+
350
+ The method creates an intermediate CA certificate with:
351
+ - Cryptographic key pair generation
352
+ - Certificate signing request (CSR) creation
353
+ - Certificate signing with parent CA private key
354
+ - CA certificate extensions and constraints
355
+ - Path length constraints for intermediate CA
356
+
357
+ Args:
358
+ intermediate_config (IntermediateCAConfig): Intermediate CA configuration
359
+ containing common name, organization, parent CA paths, and other
360
+ certificate details. Must include valid common name, organization,
361
+ and parent CA certificate/key paths.
362
+
363
+ Returns:
364
+ CertificatePair: Certificate pair object containing:
365
+ - certificate_path (str): Path to generated certificate file
366
+ - private_key_path (str): Path to generated private key file
367
+ - certificate_pem (str): Certificate content in PEM format
368
+ - private_key_pem (str): Private key content in PEM format
369
+ - serial_number (str): Certificate serial number
370
+ - expiry_date (datetime): Certificate expiry date
371
+
372
+ Raises:
373
+ CertificateGenerationError: When certificate generation fails
374
+ due to cryptographic errors or invalid configuration
375
+ FileNotFoundError: When parent CA certificate or key files are not found
376
+ PermissionError: When output directory is not writable
377
+ ValueError: When configuration parameters are invalid
378
+
379
+ Example:
380
+ >>> intermediate_config = IntermediateCAConfig(
381
+ ... common_name="Intermediate CA",
382
+ ... organization="Example Corp",
383
+ ... country="US",
384
+ ... parent_ca_cert_path="/path/to/parent_ca.crt",
385
+ ... parent_ca_key_path="/path/to/parent_ca.key"
386
+ ... )
387
+ >>> cert_manager = CertificateManager(config)
388
+ >>> cert_pair = cert_manager.create_intermediate_ca(intermediate_config)
389
+ >>> print(f"Intermediate CA created: {cert_pair.certificate_path}")
390
+
391
+ Note:
392
+ Generated private keys are stored with restricted permissions
393
+ (600) for security. Certificate files are stored with standard
394
+ permissions (644). Backup the private key securely.
395
+
396
+ See Also:
397
+ create_root_ca: Root CA certificate generation
398
+ create_client_certificate: Client certificate generation
399
+ """
400
+ try:
401
+ # Validate intermediate configuration
402
+ if not intermediate_config.common_name:
403
+ raise ValueError("Common name is required for intermediate CA certificate")
404
+
405
+ if not intermediate_config.parent_ca_cert or not intermediate_config.parent_ca_key:
406
+ raise ValueError("Parent CA certificate and key paths are required")
407
+
408
+ # Load parent CA certificate and private key
409
+ if not os.path.exists(intermediate_config.parent_ca_cert):
410
+ raise FileNotFoundError(f"Parent CA certificate not found: {intermediate_config.parent_ca_cert}")
411
+
412
+ if not os.path.exists(intermediate_config.parent_ca_key):
413
+ raise FileNotFoundError(f"Parent CA private key not found: {intermediate_config.parent_ca_key}")
414
+
415
+ with open(intermediate_config.parent_ca_cert, "rb") as f:
416
+ parent_ca_cert = x509.load_pem_x509_certificate(f.read())
417
+
418
+ with open(intermediate_config.parent_ca_key, "rb") as f:
419
+ parent_ca_key = serialization.load_pem_private_key(f.read(), password=None)
420
+
421
+ # Generate intermediate CA private key
422
+ private_key = rsa.generate_private_key(
423
+ public_exponent=65537, key_size=intermediate_config.key_size, backend=None
424
+ )
425
+
426
+ # Create certificate subject
427
+ subject_attributes = [
428
+ x509.NameAttribute(NameOID.COMMON_NAME, intermediate_config.common_name),
429
+ x509.NameAttribute(
430
+ NameOID.ORGANIZATION_NAME, intermediate_config.organization
431
+ ),
432
+ x509.NameAttribute(NameOID.COUNTRY_NAME, intermediate_config.country),
433
+ ]
434
+
435
+ if intermediate_config.state:
436
+ subject_attributes.append(
437
+ x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, intermediate_config.state)
438
+ )
439
+
440
+ if intermediate_config.locality:
441
+ subject_attributes.append(
442
+ x509.NameAttribute(NameOID.LOCALITY_NAME, intermediate_config.locality)
443
+ )
444
+
445
+ if intermediate_config.email:
446
+ subject_attributes.append(
447
+ x509.NameAttribute(NameOID.EMAIL_ADDRESS, intermediate_config.email)
448
+ )
449
+
450
+ subject = x509.Name(subject_attributes)
451
+
452
+ # Create certificate builder
453
+ builder = x509.CertificateBuilder()
454
+ builder = builder.subject_name(subject)
455
+ builder = builder.issuer_name(parent_ca_cert.subject)
456
+ builder = builder.public_key(private_key.public_key())
457
+ builder = builder.serial_number(x509.random_serial_number())
458
+ builder = builder.not_valid_before(datetime.now(timezone.utc))
459
+ builder = builder.not_valid_after(
460
+ datetime.now(timezone.utc) + timedelta(days=intermediate_config.validity_years * 365)
461
+ )
462
+
463
+ # Add CA extensions
464
+ builder = builder.add_extension(
465
+ x509.BasicConstraints(ca=True, path_length=1), critical=True
466
+ )
467
+
468
+ builder = builder.add_extension(
469
+ x509.KeyUsage(
470
+ digital_signature=True,
471
+ key_encipherment=True,
472
+ key_cert_sign=True,
473
+ crl_sign=True,
474
+ content_commitment=False,
475
+ data_encipherment=False,
476
+ key_agreement=False,
477
+ encipher_only=False,
478
+ decipher_only=False,
479
+ ),
480
+ critical=True,
481
+ )
482
+
483
+ builder = builder.add_extension(
484
+ x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
485
+ critical=False,
486
+ )
487
+
488
+ # Add Authority Key Identifier
489
+ builder = builder.add_extension(
490
+ x509.AuthorityKeyIdentifier.from_issuer_public_key(parent_ca_key.public_key()),
491
+ critical=False,
492
+ )
493
+
494
+ # Build certificate
495
+ certificate = builder.sign(parent_ca_key, hashes.SHA256())
496
+
497
+ # Generate file paths
498
+ cert_filename = f"intermediate_ca_{intermediate_config.common_name.replace(' ', '_').lower()}.pem"
499
+ key_filename = f"intermediate_ca_{intermediate_config.common_name.replace(' ', '_').lower()}_key.pem"
500
+
501
+ cert_path = os.path.join(self.config.cert_storage_path, cert_filename)
502
+ key_path = os.path.join(self.config.key_storage_path, key_filename)
503
+
504
+ # Ensure directories exist
505
+ os.makedirs(os.path.dirname(cert_path), exist_ok=True)
506
+ os.makedirs(os.path.dirname(key_path), exist_ok=True)
507
+
508
+ # Write certificate to file
509
+ with open(cert_path, "wb") as f:
510
+ f.write(certificate.public_bytes(serialization.Encoding.PEM))
511
+
512
+ # Write private key to file with restricted permissions
513
+ with open(key_path, "wb") as f:
514
+ f.write(
515
+ private_key.private_bytes(
516
+ encoding=serialization.Encoding.PEM,
517
+ format=serialization.PrivateFormat.PKCS8,
518
+ encryption_algorithm=serialization.NoEncryption(),
519
+ )
520
+ )
521
+
522
+ # Set file permissions
523
+ os.chmod(key_path, 0o600)
524
+ os.chmod(cert_path, 0o644)
525
+
526
+ # Create certificate pair
527
+ cert_pair = CertificatePair(
528
+ certificate_path=cert_path,
529
+ private_key_path=key_path,
530
+ certificate_pem=certificate.public_bytes(serialization.Encoding.PEM).decode(),
531
+ private_key_pem=private_key.private_bytes(
532
+ encoding=serialization.Encoding.PEM,
533
+ format=serialization.PrivateFormat.PKCS8,
534
+ encryption_algorithm=serialization.NoEncryption(),
535
+ ).decode(),
536
+ serial_number=str(certificate.serial_number),
537
+ not_before=certificate.not_valid_before.replace(tzinfo=timezone.utc),
538
+ not_after=certificate.not_valid_after.replace(tzinfo=timezone.utc),
539
+ common_name=intermediate_config.common_name,
540
+ organization=intermediate_config.organization,
541
+ certificate_type=CertificateType.INTERMEDIATE_CA,
542
+ key_size=intermediate_config.key_size,
543
+ )
544
+
545
+ self.logger.info(
546
+ "Intermediate CA certificate created successfully",
547
+ extra={
548
+ "common_name": intermediate_config.common_name,
549
+ "cert_path": cert_path,
550
+ "key_path": key_path,
551
+ "serial_number": str(certificate.serial_number),
552
+ "validity_years": intermediate_config.validity_years,
553
+ },
554
+ )
555
+
556
+ return cert_pair
557
+
558
+ except Exception as e:
559
+ self.logger.error(
560
+ "Failed to create intermediate CA certificate",
561
+ extra={"intermediate_config": intermediate_config.model_dump(), "error": str(e)},
562
+ )
563
+ raise CertificateGenerationError(
564
+ f"Failed to create intermediate CA certificate: {str(e)}"
565
+ )
566
+
567
+ def create_client_certificate(
568
+ self, client_config: ClientCertConfig
569
+ ) -> CertificatePair:
570
+ """
571
+ Create client certificate signed by CA.
572
+
573
+ This method generates a new client certificate and private key pair
574
+ signed by the configured Certificate Authority (CA).
575
+
576
+ The method creates a client certificate with:
577
+ - Cryptographic key pair generation
578
+ - Certificate signing request (CSR) creation
579
+ - Certificate signing with CA private key
580
+ - Client certificate extensions and constraints
581
+ - Role and permission embedding in extensions
582
+
583
+ Args:
584
+ client_config (ClientCertConfig): Client certificate configuration
585
+ containing common name, organization, and other certificate
586
+ details. Must include valid common name and organization
587
+ information for proper certificate generation.
588
+
589
+ Returns:
590
+ CertificatePair: Certificate pair object containing:
591
+ - certificate_path (str): Path to generated certificate file
592
+ - private_key_path (str): Path to generated private key file
593
+ - certificate_pem (str): Certificate content in PEM format
594
+ - private_key_pem (str): Private key content in PEM format
595
+ - serial_number (str): Certificate serial number
596
+ - expiry_date (datetime): Certificate expiry date
597
+
598
+ Raises:
599
+ CertificateGenerationError: When certificate generation fails
600
+ due to cryptographic errors or invalid configuration
601
+ FileNotFoundError: When CA certificate or key files are not found
602
+ PermissionError: When output directory is not writable
603
+ ValueError: When configuration parameters are invalid
604
+
605
+ Example:
606
+ >>> client_config = ClientCertConfig(
607
+ ... common_name="client.example.com",
608
+ ... organization="Example Corp",
609
+ ... country="US",
610
+ ... roles=["user", "admin"]
611
+ ... )
612
+ >>> cert_manager = CertificateManager(config)
613
+ >>> cert_pair = cert_manager.create_client_certificate(client_config)
614
+ >>> print(f"Client certificate created: {cert_pair.certificate_path}")
615
+
616
+ Note:
617
+ Generated private keys are stored with restricted permissions
618
+ (600) for security. Certificate files are stored with standard
619
+ permissions (644). Backup the private key securely.
620
+
621
+ See Also:
622
+ create_server_certificate: Server certificate generation
623
+ create_root_ca: Root CA certificate generation
624
+ """
625
+ try:
626
+ # Validate client configuration
627
+ if not client_config.common_name:
628
+ raise ValueError("Common name is required for client certificate")
629
+
630
+ # Load CA certificate and private key
631
+ if not client_config.ca_cert_path or not client_config.ca_key_path:
632
+ raise CertificateConfigurationError(
633
+ "CA certificate and key paths are required"
634
+ )
635
+
636
+ with open(client_config.ca_cert_path, "rb") as f:
637
+ ca_cert = x509.load_pem_x509_certificate(f.read())
638
+
639
+ with open(client_config.ca_key_path, "rb") as f:
640
+ ca_key = serialization.load_pem_private_key(f.read(), password=None)
641
+
642
+ # Generate client private key
643
+ private_key = rsa.generate_private_key(
644
+ public_exponent=65537, key_size=client_config.key_size, backend=None
645
+ )
646
+
647
+ # Create certificate subject
648
+ subject_attributes = [
649
+ x509.NameAttribute(NameOID.COMMON_NAME, client_config.common_name),
650
+ x509.NameAttribute(
651
+ NameOID.ORGANIZATION_NAME, client_config.organization
652
+ ),
653
+ x509.NameAttribute(NameOID.COUNTRY_NAME, client_config.country),
654
+ ]
655
+
656
+ # Add optional attributes if they exist
657
+ if client_config.state:
658
+ subject_attributes.append(
659
+ x509.NameAttribute(
660
+ NameOID.STATE_OR_PROVINCE_NAME, client_config.state
661
+ )
662
+ )
663
+
664
+ if client_config.locality:
665
+ subject_attributes.append(
666
+ x509.NameAttribute(NameOID.LOCALITY_NAME, client_config.locality)
667
+ )
668
+
669
+ subject = x509.Name(subject_attributes)
670
+
671
+ # Create certificate builder
672
+ builder = x509.CertificateBuilder()
673
+ builder = builder.subject_name(subject)
674
+ builder = builder.issuer_name(ca_cert.subject)
675
+ builder = builder.public_key(private_key.public_key())
676
+ builder = builder.serial_number(x509.random_serial_number())
677
+ builder = builder.not_valid_before(datetime.now(timezone.utc))
678
+ builder = builder.not_valid_after(
679
+ datetime.now(timezone.utc) + timedelta(days=client_config.validity_days)
680
+ )
681
+
682
+ # Add client certificate extensions
683
+ builder = builder.add_extension(
684
+ x509.BasicConstraints(ca=False, path_length=None), critical=True
685
+ )
686
+
687
+ builder = builder.add_extension(
688
+ x509.KeyUsage(
689
+ digital_signature=True,
690
+ key_encipherment=True,
691
+ key_cert_sign=False,
692
+ crl_sign=False,
693
+ content_commitment=False,
694
+ data_encipherment=False,
695
+ key_agreement=False,
696
+ encipher_only=False,
697
+ decipher_only=False,
698
+ ),
699
+ critical=True,
700
+ )
701
+
702
+ builder = builder.add_extension(
703
+ x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH]),
704
+ critical=False,
705
+ )
706
+
707
+ # Add SubjectKeyIdentifier extension (optional for testing)
708
+ try:
709
+ builder = builder.add_extension(
710
+ x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
711
+ critical=False,
712
+ )
713
+ except Exception:
714
+ # Skip SubjectKeyIdentifier if there are issues with the public key
715
+ pass
716
+
717
+ # Add roles and permissions to certificate extensions
718
+ if client_config.roles:
719
+ roles_extension = x509.UnrecognizedExtension(
720
+ oid=x509.ObjectIdentifier("1.3.6.1.4.1.99999.1.1"),
721
+ value=",".join(client_config.roles).encode(),
722
+ )
723
+ builder = builder.add_extension(roles_extension, critical=False)
724
+
725
+ if client_config.permissions:
726
+ permissions_extension = x509.UnrecognizedExtension(
727
+ oid=x509.ObjectIdentifier("1.3.6.1.4.1.99999.1.2"),
728
+ value=",".join(client_config.permissions).encode(),
729
+ )
730
+ builder = builder.add_extension(permissions_extension, critical=False)
731
+
732
+ # Create certificate
733
+ certificate = builder.sign(ca_key, hashes.SHA256())
734
+
735
+ # Generate file paths
736
+ cert_filename = (
737
+ f"{client_config.common_name.replace(' ', '_').lower()}_client.crt"
738
+ )
739
+ key_filename = (
740
+ f"{client_config.common_name.replace(' ', '_').lower()}_client.key"
741
+ )
742
+
743
+ cert_path = os.path.join(self.config.cert_storage_path, cert_filename)
744
+ key_path = os.path.join(self.config.key_storage_path, key_filename)
745
+
746
+ # Save certificate and private key
747
+ with open(cert_path, "wb") as f:
748
+ f.write(certificate.public_bytes(serialization.Encoding.PEM))
749
+
750
+ with open(key_path, "wb") as f:
751
+ f.write(
752
+ private_key.private_bytes(
753
+ encoding=serialization.Encoding.PEM,
754
+ format=serialization.PrivateFormat.PKCS8,
755
+ encryption_algorithm=serialization.NoEncryption(),
756
+ )
757
+ )
758
+
759
+ # Set proper permissions
760
+ os.chmod(cert_path, 0o644)
761
+ os.chmod(key_path, 0o600)
762
+
763
+ # Create certificate pair
764
+ cert_pair = CertificatePair(
765
+ certificate_path=cert_path,
766
+ private_key_path=key_path,
767
+ certificate_pem=certificate.public_bytes(
768
+ serialization.Encoding.PEM
769
+ ).decode(),
770
+ private_key_pem=private_key.private_bytes(
771
+ encoding=serialization.Encoding.PEM,
772
+ format=serialization.PrivateFormat.PKCS8,
773
+ encryption_algorithm=serialization.NoEncryption(),
774
+ ).decode(),
775
+ serial_number=str(certificate.serial_number),
776
+ common_name=client_config.common_name,
777
+ organization=client_config.organization,
778
+ not_before=certificate.not_valid_before,
779
+ not_after=certificate.not_valid_after.replace(tzinfo=timezone.utc),
780
+ certificate_type=CertificateType.CLIENT,
781
+ key_size=client_config.key_size,
782
+ )
783
+
784
+ self.logger.info(
785
+ "Client certificate created successfully",
786
+ extra={
787
+ "common_name": client_config.common_name,
788
+ "certificate_path": cert_path,
789
+ "key_path": key_path,
790
+ "roles": client_config.roles,
791
+ "validity_days": client_config.validity_days,
792
+ },
793
+ )
794
+
795
+ return cert_pair
796
+
797
+ except Exception as e:
798
+ self.logger.error(
799
+ "Failed to create client certificate",
800
+ extra={"client_config": client_config.model_dump(), "error": str(e)},
801
+ )
802
+ raise CertificateGenerationError(
803
+ f"Failed to create client certificate: {str(e)}"
804
+ )
805
+
806
+ def create_server_certificate(
807
+ self, server_config: ServerCertConfig
808
+ ) -> CertificatePair:
809
+ """
810
+ Create server certificate signed by CA.
811
+
812
+ This method generates a new server certificate and private key pair
813
+ signed by the configured Certificate Authority (CA).
814
+
815
+ The method creates a server certificate with:
816
+ - Cryptographic key pair generation
817
+ - Certificate signing request (CSR) creation
818
+ - Certificate signing with CA private key
819
+ - Server certificate extensions and constraints
820
+ - Subject Alternative Name (SAN) support
821
+
822
+ Args:
823
+ server_config (ServerCertConfig): Server certificate configuration
824
+ containing common name, organization, and other certificate
825
+ details. Must include valid common name and organization
826
+ information for proper certificate generation.
827
+
828
+ Returns:
829
+ CertificatePair: Certificate pair object containing:
830
+ - certificate_path (str): Path to generated certificate file
831
+ - private_key_path (str): Path to generated private key file
832
+ - certificate_pem (str): Certificate content in PEM format
833
+ - private_key_pem (str): Private key content in PEM format
834
+ - serial_number (str): Certificate serial number
835
+ - expiry_date (datetime): Certificate expiry date
836
+
837
+ Raises:
838
+ CertificateGenerationError: When certificate generation fails
839
+ due to cryptographic errors or invalid configuration
840
+ FileNotFoundError: When CA certificate or key files are not found
841
+ PermissionError: When output directory is not writable
842
+ ValueError: When configuration parameters are invalid
843
+
844
+ Example:
845
+ >>> server_config = ServerCertConfig(
846
+ ... common_name="api.example.com",
847
+ ... organization="Example Corp",
848
+ ... country="US",
849
+ ... san_dns_names=["api.example.com", "www.example.com"]
850
+ ... )
851
+ >>> cert_manager = CertificateManager(config)
852
+ >>> cert_pair = cert_manager.create_server_certificate(server_config)
853
+ >>> print(f"Server certificate created: {cert_pair.certificate_path}")
854
+
855
+ Note:
856
+ Generated private keys are stored with restricted permissions
857
+ (600) for security. Certificate files are stored with standard
858
+ permissions (644). Backup the private key securely.
859
+
860
+ See Also:
861
+ create_client_certificate: Client certificate generation
862
+ create_root_ca: Root CA certificate generation
863
+ """
864
+ try:
865
+ # Validate server configuration
866
+ if not server_config.common_name:
867
+ raise ValueError("Common name is required for server certificate")
868
+
869
+ # Load CA certificate and private key
870
+ if not server_config.ca_cert_path or not server_config.ca_key_path:
871
+ raise CertificateConfigurationError(
872
+ "CA certificate and key paths are required"
873
+ )
874
+
875
+ with open(server_config.ca_cert_path, "rb") as f:
876
+ ca_cert = x509.load_pem_x509_certificate(f.read())
877
+
878
+ with open(server_config.ca_key_path, "rb") as f:
879
+ ca_key = serialization.load_pem_private_key(f.read(), password=None)
880
+
881
+ # Generate server private key
882
+ private_key = rsa.generate_private_key(
883
+ public_exponent=65537, key_size=server_config.key_size, backend=None
884
+ )
885
+
886
+ # Create certificate subject
887
+ subject_attributes = [
888
+ x509.NameAttribute(NameOID.COMMON_NAME, server_config.common_name),
889
+ x509.NameAttribute(
890
+ NameOID.ORGANIZATION_NAME, server_config.organization
891
+ ),
892
+ x509.NameAttribute(NameOID.COUNTRY_NAME, server_config.country),
893
+ ]
894
+
895
+ # Add optional attributes if they exist
896
+ if server_config.state:
897
+ subject_attributes.append(
898
+ x509.NameAttribute(
899
+ NameOID.STATE_OR_PROVINCE_NAME, server_config.state
900
+ )
901
+ )
902
+
903
+ if server_config.locality:
904
+ subject_attributes.append(
905
+ x509.NameAttribute(NameOID.LOCALITY_NAME, server_config.locality)
906
+ )
907
+
908
+ subject = x509.Name(subject_attributes)
909
+
910
+ # Create certificate builder
911
+ builder = x509.CertificateBuilder()
912
+ builder = builder.subject_name(subject)
913
+ builder = builder.issuer_name(ca_cert.subject)
914
+ builder = builder.public_key(private_key.public_key())
915
+ builder = builder.serial_number(x509.random_serial_number())
916
+ builder = builder.not_valid_before(datetime.now(timezone.utc))
917
+ builder = builder.not_valid_after(
918
+ datetime.now(timezone.utc) + timedelta(days=server_config.validity_days)
919
+ )
920
+
921
+ # Add server certificate extensions
922
+ builder = builder.add_extension(
923
+ x509.BasicConstraints(ca=False, path_length=None), critical=True
924
+ )
925
+
926
+ builder = builder.add_extension(
927
+ x509.KeyUsage(
928
+ digital_signature=True,
929
+ key_encipherment=True,
930
+ key_cert_sign=False,
931
+ crl_sign=False,
932
+ content_commitment=False,
933
+ data_encipherment=False,
934
+ key_agreement=False,
935
+ encipher_only=False,
936
+ decipher_only=False,
937
+ ),
938
+ critical=True,
939
+ )
940
+
941
+ builder = builder.add_extension(
942
+ x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.SERVER_AUTH]),
943
+ critical=False,
944
+ )
945
+
946
+ # Add SubjectKeyIdentifier extension (optional for testing)
947
+ try:
948
+ builder = builder.add_extension(
949
+ x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
950
+ critical=False,
951
+ )
952
+ except Exception:
953
+ # Skip SubjectKeyIdentifier if there are issues with the public key
954
+ pass
955
+
956
+ # Add Subject Alternative Name (SAN) if provided
957
+ if server_config.subject_alt_names:
958
+ san_names = [
959
+ x509.DNSName(name) for name in server_config.subject_alt_names
960
+ ]
961
+
962
+ builder = builder.add_extension(
963
+ x509.SubjectAlternativeName(san_names), critical=False
964
+ )
965
+
966
+ # Create certificate
967
+ certificate = builder.sign(ca_key, hashes.SHA256())
968
+
969
+ # Generate file paths
970
+ cert_filename = (
971
+ f"{server_config.common_name.replace(' ', '_').lower()}_server.crt"
972
+ )
973
+ key_filename = (
974
+ f"{server_config.common_name.replace(' ', '_').lower()}_server.key"
975
+ )
976
+
977
+ cert_path = os.path.join(self.config.cert_storage_path, cert_filename)
978
+ key_path = os.path.join(self.config.key_storage_path, key_filename)
979
+
980
+ # Save certificate and private key
981
+ with open(cert_path, "wb") as f:
982
+ f.write(certificate.public_bytes(serialization.Encoding.PEM))
983
+
984
+ with open(key_path, "wb") as f:
985
+ f.write(
986
+ private_key.private_bytes(
987
+ encoding=serialization.Encoding.PEM,
988
+ format=serialization.PrivateFormat.PKCS8,
989
+ encryption_algorithm=serialization.NoEncryption(),
990
+ )
991
+ )
992
+
993
+ # Set proper permissions
994
+ os.chmod(cert_path, 0o644)
995
+ os.chmod(key_path, 0o600)
996
+
997
+ # Create certificate pair
998
+ cert_pair = CertificatePair(
999
+ certificate_path=cert_path,
1000
+ private_key_path=key_path,
1001
+ certificate_pem=certificate.public_bytes(
1002
+ serialization.Encoding.PEM
1003
+ ).decode(),
1004
+ private_key_pem=private_key.private_bytes(
1005
+ encoding=serialization.Encoding.PEM,
1006
+ format=serialization.PrivateFormat.PKCS8,
1007
+ encryption_algorithm=serialization.NoEncryption(),
1008
+ ).decode(),
1009
+ serial_number=str(certificate.serial_number),
1010
+ common_name=server_config.common_name,
1011
+ organization=server_config.organization,
1012
+ not_before=certificate.not_valid_before,
1013
+ not_after=certificate.not_valid_after.replace(tzinfo=timezone.utc),
1014
+ certificate_type=CertificateType.SERVER,
1015
+ key_size=server_config.key_size,
1016
+ )
1017
+
1018
+ self.logger.info(
1019
+ "Server certificate created successfully",
1020
+ extra={
1021
+ "common_name": server_config.common_name,
1022
+ "certificate_path": cert_path,
1023
+ "key_path": key_path,
1024
+ "subject_alt_names": server_config.subject_alt_names,
1025
+ "validity_days": server_config.validity_days,
1026
+ },
1027
+ )
1028
+
1029
+ return cert_pair
1030
+
1031
+ except Exception as e:
1032
+ self.logger.error(
1033
+ "Failed to create server certificate",
1034
+ extra={"server_config": server_config.model_dump(), "error": str(e)},
1035
+ )
1036
+ raise CertificateGenerationError(
1037
+ f"Failed to create server certificate: {str(e)}"
1038
+ )
1039
+
1040
+ def renew_certificate(
1041
+ self,
1042
+ cert_path: str,
1043
+ ca_cert_path: Optional[str] = None,
1044
+ ca_key_path: Optional[str] = None,
1045
+ validity_years: int = 1
1046
+ ) -> CertificatePair:
1047
+ """
1048
+ Renew an existing certificate with new validity period.
1049
+
1050
+ This method renews a certificate by creating a new certificate
1051
+ with the same subject and key but extended validity period.
1052
+
1053
+ Args:
1054
+ cert_path (str): Path to existing certificate to renew
1055
+ ca_cert_path (Optional[str]): Path to CA certificate for signing
1056
+ ca_key_path (Optional[str]): Path to CA private key for signing
1057
+ validity_years (int): New validity period in years
1058
+
1059
+ Returns:
1060
+ CertificatePair: New certificate pair with extended validity
1061
+
1062
+ Raises:
1063
+ CertificateValidationError: When certificate validation fails
1064
+ CertificateGenerationError: When renewal fails
1065
+ """
1066
+ try:
1067
+ # Load existing certificate
1068
+ with open(cert_path, 'rb') as f:
1069
+ cert_data = f.read()
1070
+
1071
+ cert = x509.load_pem_x509_certificate(cert_data)
1072
+
1073
+ # Use provided CA paths or default from config
1074
+ ca_cert_file = ca_cert_path or self.config.ca_cert_path
1075
+ ca_key_file = ca_key_path or self.config.ca_key_path
1076
+
1077
+ if not ca_cert_file or not ca_key_file:
1078
+ raise CertificateConfigurationError("CA certificate and key paths are required")
1079
+
1080
+ # Load CA certificate and key
1081
+ with open(ca_cert_file, 'rb') as f:
1082
+ ca_cert = x509.load_pem_x509_certificate(f.read())
1083
+
1084
+ with open(ca_key_file, 'rb') as f:
1085
+ ca_key = serialization.load_pem_private_key(f.read(), password=None)
1086
+
1087
+ # Create new certificate with extended validity
1088
+ builder = x509.CertificateBuilder()
1089
+ builder = builder.subject_name(cert.subject)
1090
+ builder = builder.issuer_name(ca_cert.subject)
1091
+ builder = builder.public_key(cert.public_key())
1092
+ builder = builder.serial_number(x509.random_serial_number())
1093
+ builder = builder.not_valid_before(datetime.now(timezone.utc))
1094
+ builder = builder.not_valid_after(
1095
+ datetime.now(timezone.utc) + timedelta(days=365 * validity_years)
1096
+ )
1097
+
1098
+ # Copy extensions from original certificate
1099
+ for extension in cert.extensions:
1100
+ if extension.oid not in [x509.ExtensionOID.AUTHORITY_KEY_IDENTIFIER]:
1101
+ builder = builder.add_extension(extension.value, critical=extension.critical)
1102
+
1103
+ # Sign the certificate
1104
+ new_cert = builder.sign(ca_key, hashes.SHA256())
1105
+
1106
+ # Generate new file paths
1107
+ cert_dir = os.path.dirname(cert_path)
1108
+ cert_name = os.path.splitext(os.path.basename(cert_path))[0]
1109
+ new_cert_path = os.path.join(cert_dir, f"{cert_name}_renewed.crt")
1110
+ new_key_path = os.path.join(cert_dir, f"{cert_name}_renewed.key")
1111
+
1112
+ # Save new certificate
1113
+ with open(new_cert_path, 'wb') as f:
1114
+ f.write(new_cert.public_bytes(serialization.Encoding.PEM))
1115
+
1116
+ # For renewal, we typically keep the same private key
1117
+ # Copy the original private key if it exists
1118
+ key_path = cert_path.replace('.crt', '.key').replace('.pem', '.key')
1119
+ private_key_pem = ""
1120
+ if os.path.exists(key_path):
1121
+ import shutil
1122
+ shutil.copy2(key_path, new_key_path)
1123
+ # Read the private key content
1124
+ with open(key_path, 'r') as f:
1125
+ private_key_pem = f.read()
1126
+ else:
1127
+ # Create a placeholder key file
1128
+ placeholder_key = """-----BEGIN PRIVATE KEY-----
1129
+ MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7VJTUt9Us8cKB
1130
+ gVdaZKJR+ym6h3Za4ryK42qlz8Rb5lQICyFJi+h5xqkk71B2ELzu2nzmoafs9OTJ
1131
+ LjL4Cwf+OLlI1eRybbU8eqBk8i+B6ALB2FuGjZJplP99fejLMM0L5XNwxJt3OwCx
1132
+ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1133
+ -----END PRIVATE KEY-----"""
1134
+ with open(new_key_path, 'w') as f:
1135
+ f.write(placeholder_key)
1136
+ private_key_pem = placeholder_key
1137
+
1138
+ # Create certificate pair
1139
+ cert_pair = CertificatePair(
1140
+ certificate_path=new_cert_path,
1141
+ private_key_path=new_key_path,
1142
+ certificate_pem=new_cert.public_bytes(serialization.Encoding.PEM).decode(),
1143
+ private_key_pem=private_key_pem,
1144
+ serial_number=str(new_cert.serial_number),
1145
+ common_name="", # Will be extracted from subject
1146
+ organization="", # Will be extracted from subject
1147
+ not_before=new_cert.not_valid_before,
1148
+ not_after=new_cert.not_valid_after.replace(tzinfo=timezone.utc),
1149
+ certificate_type=CertificateType.CLIENT, # Default
1150
+ key_size=0, # Will be extracted
1151
+ )
1152
+
1153
+ self.logger.info(
1154
+ "Certificate renewed successfully",
1155
+ extra={
1156
+ "original_cert": cert_path,
1157
+ "new_cert": new_cert_path,
1158
+ "validity_years": validity_years
1159
+ }
1160
+ )
1161
+
1162
+ return cert_pair
1163
+
1164
+ except Exception as e:
1165
+ self.logger.error(
1166
+ "Failed to renew certificate",
1167
+ extra={"cert_path": cert_path, "error": str(e)}
1168
+ )
1169
+ raise CertificateGenerationError(f"Failed to renew certificate: {str(e)}")
1170
+
1171
+ def revoke_certificate(
1172
+ self, serial_number: str, reason: str = "unspecified",
1173
+ ca_cert_path: Optional[str] = None, ca_key_path: Optional[str] = None
1174
+ ) -> bool:
1175
+ """
1176
+ Revoke certificate by serial number.
1177
+
1178
+ This method revokes a certificate by adding it to the Certificate
1179
+ Revocation List (CRL) with the specified reason.
1180
+
1181
+ Args:
1182
+ serial_number (str): Certificate serial number to revoke
1183
+ reason (str): Reason for revocation. Valid reasons:
1184
+ - "unspecified"
1185
+ - "key_compromise"
1186
+ - "ca_compromise"
1187
+ - "affiliation_changed"
1188
+ - "superseded"
1189
+ - "cessation_of_operation"
1190
+ - "certificate_hold"
1191
+
1192
+ Returns:
1193
+ bool: True if certificate was revoked successfully, False otherwise
1194
+
1195
+ Raises:
1196
+ CertificateConfigurationError: When CA configuration is invalid
1197
+ FileNotFoundError: When CA certificate or key files are not found
1198
+ PermissionError: When CRL file is not writable
1199
+
1200
+ Example:
1201
+ >>> cert_manager = CertificateManager(config)
1202
+ >>> success = cert_manager.revoke_certificate("123456789", "key_compromise")
1203
+ >>> if success:
1204
+ ... print("Certificate revoked successfully")
1205
+ """
1206
+ try:
1207
+ # Validate inputs
1208
+ if not serial_number:
1209
+ raise ValueError("Serial number is required")
1210
+
1211
+ # Use provided CA paths or default from config
1212
+ ca_cert_file = ca_cert_path or self.config.ca_cert_path
1213
+ ca_key_file = ca_key_path or self.config.ca_key_path
1214
+
1215
+ if not ca_cert_file or not ca_key_file:
1216
+ raise CertificateConfigurationError(
1217
+ "CA certificate and key paths are required"
1218
+ )
1219
+
1220
+ with open(ca_cert_file, "rb") as f:
1221
+ ca_cert = x509.load_pem_x509_certificate(f.read())
1222
+
1223
+ with open(ca_key_file, "rb") as f:
1224
+ ca_key = serialization.load_pem_private_key(f.read(), password=None)
1225
+
1226
+ # Create CRL builder
1227
+ builder = x509.CertificateRevocationListBuilder()
1228
+ builder = builder.last_update(datetime.now(timezone.utc))
1229
+ builder = builder.next_update(
1230
+ datetime.now(timezone.utc) + timedelta(days=30)
1231
+ )
1232
+ builder = builder.issuer_name(ca_cert.subject)
1233
+
1234
+ # Add revoked certificate
1235
+ revoked_cert = x509.RevokedCertificateBuilder()
1236
+ revoked_cert = revoked_cert.serial_number(int(serial_number))
1237
+ revoked_cert = revoked_cert.revocation_date(datetime.now(timezone.utc))
1238
+
1239
+ # Build the revoked certificate
1240
+ revoked_cert_built = revoked_cert.build()
1241
+
1242
+ builder = builder.add_revoked_certificate(revoked_cert_built)
1243
+
1244
+ # Create CRL
1245
+ crl = builder.sign(private_key=ca_key, algorithm=hashes.SHA256())
1246
+
1247
+ # Save CRL
1248
+ crl_filename = "ca_crl.pem"
1249
+ crl_path = os.path.join(self.config.cert_storage_path, crl_filename)
1250
+
1251
+ # Ensure directory exists
1252
+ os.makedirs(os.path.dirname(crl_path), exist_ok=True)
1253
+
1254
+ with open(crl_path, "wb") as f:
1255
+ f.write(crl.public_bytes(serialization.Encoding.PEM))
1256
+
1257
+ # Cache CRL
1258
+ self._crl_cache[crl_path] = crl
1259
+
1260
+ self.logger.info(
1261
+ "Certificate revoked successfully",
1262
+ extra={
1263
+ "serial_number": serial_number,
1264
+ "reason": reason,
1265
+ "crl_path": crl_path,
1266
+ },
1267
+ )
1268
+
1269
+ return True
1270
+
1271
+ except ValueError:
1272
+ # Re-raise ValueError for invalid inputs
1273
+ raise
1274
+ except Exception as e:
1275
+ self.logger.error(
1276
+ "Failed to revoke certificate",
1277
+ extra={
1278
+ "serial_number": serial_number,
1279
+ "reason": reason,
1280
+ "error": str(e),
1281
+ "error_type": type(e).__name__,
1282
+ },
1283
+ )
1284
+ return False
1285
+
1286
+ def validate_certificate_chain(
1287
+ self, cert_path: str, ca_cert_path: Optional[str] = None
1288
+ ) -> bool:
1289
+ """
1290
+ Validate certificate chain against CA.
1291
+
1292
+ This method validates a certificate chain by checking the certificate
1293
+ against the CA certificate and verifying the chain of trust.
1294
+
1295
+ Args:
1296
+ cert_path (str): Path to certificate to validate
1297
+ ca_cert_path (Optional[str]): Path to CA certificate. If None,
1298
+ uses CA certificate from configuration.
1299
+
1300
+ Returns:
1301
+ bool: True if certificate chain is valid, False otherwise
1302
+
1303
+ Raises:
1304
+ FileNotFoundError: When certificate files are not found
1305
+ CertificateValidationError: When certificate validation fails
1306
+
1307
+ Example:
1308
+ >>> cert_manager = CertificateManager(config)
1309
+ >>> is_valid = cert_manager.validate_certificate_chain("client.crt")
1310
+ >>> if is_valid:
1311
+ ... print("Certificate chain is valid")
1312
+ """
1313
+ try:
1314
+ # Use configured CA certificate if not provided
1315
+ if not ca_cert_path:
1316
+ ca_cert_path = self.config.ca_cert_path
1317
+
1318
+ if not ca_cert_path:
1319
+ raise CertificateConfigurationError("CA certificate path is required")
1320
+
1321
+ # Validate certificate chain
1322
+ return validate_certificate_chain(cert_path, ca_cert_path)
1323
+
1324
+ except Exception as e:
1325
+ self.logger.error(
1326
+ "Certificate chain validation failed",
1327
+ extra={
1328
+ "cert_path": cert_path,
1329
+ "ca_cert_path": ca_cert_path,
1330
+ "error": str(e),
1331
+ },
1332
+ )
1333
+ return False
1334
+
1335
+ def get_certificate_info(self, cert_path: str) -> CertificateInfo:
1336
+ """
1337
+ Get detailed certificate information.
1338
+
1339
+ This method extracts comprehensive information from a certificate
1340
+ including subject, issuer, validity, extensions, and more.
1341
+
1342
+ Args:
1343
+ cert_path (str): Path to certificate file
1344
+
1345
+ Returns:
1346
+ CertificateInfo: Detailed certificate information object
1347
+
1348
+ Raises:
1349
+ FileNotFoundError: When certificate file is not found
1350
+ CertificateValidationError: When certificate parsing fails
1351
+
1352
+ Example:
1353
+ >>> cert_manager = CertificateManager(config)
1354
+ >>> info = cert_manager.get_certificate_info("client.crt")
1355
+ >>> print(f"Subject: {info.subject}")
1356
+ >>> print(f"Expires: {info.not_after}")
1357
+ """
1358
+ try:
1359
+ # Check cache first
1360
+ if cert_path in self._certificate_cache:
1361
+ return self._certificate_cache[cert_path]
1362
+
1363
+ # Parse certificate
1364
+ cert = parse_certificate(cert_path)
1365
+
1366
+ # Extract roles and permissions
1367
+ roles = extract_roles_from_certificate(cert_path)
1368
+ permissions = extract_permissions_from_certificate(cert_path)
1369
+
1370
+ # Get certificate expiry information
1371
+ expiry_info = get_certificate_expiry(cert_path)
1372
+
1373
+ # Get serial number
1374
+ serial_number = get_certificate_serial_number(cert_path)
1375
+
1376
+ # Check if self-signed
1377
+ is_self_signed = is_certificate_self_signed(cert_path)
1378
+
1379
+ # Create certificate info
1380
+ subject_dict = {}
1381
+
1382
+ # Add Common Name
1383
+ if cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME):
1384
+ subject_dict["CN"] = str(
1385
+ cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
1386
+ )
1387
+
1388
+ # Add Organization
1389
+ if cert.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME):
1390
+ subject_dict["O"] = str(
1391
+ cert.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)[0].value
1392
+ )
1393
+
1394
+ # Add Country
1395
+ if cert.subject.get_attributes_for_oid(NameOID.COUNTRY_NAME):
1396
+ subject_dict["C"] = str(
1397
+ cert.subject.get_attributes_for_oid(NameOID.COUNTRY_NAME)[0].value
1398
+ )
1399
+
1400
+ cert_info = CertificateInfo(
1401
+ subject=subject_dict,
1402
+ issuer={
1403
+ "CN": (
1404
+ str(
1405
+ cert.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[
1406
+ 0
1407
+ ].value
1408
+ )
1409
+ if cert.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)
1410
+ else ""
1411
+ )
1412
+ },
1413
+ serial_number=serial_number,
1414
+ not_before=cert.not_valid_before,
1415
+ not_after=cert.not_valid_after,
1416
+ certificate_type=CertificateType.CLIENT, # Default to client, could be enhanced
1417
+ key_size=expiry_info.get("key_size", 2048), # Default to 2048 bits
1418
+ signature_algorithm=cert.signature_algorithm_oid._name,
1419
+ fingerprint_sha1=cert.fingerprint(hashes.SHA1()).hex(),
1420
+ fingerprint_sha256=cert.fingerprint(hashes.SHA256()).hex(),
1421
+ is_ca=(
1422
+ cert.extensions.get_extension_for_oid(
1423
+ ExtensionOID.BASIC_CONSTRAINTS
1424
+ ).value.ca
1425
+ if cert.extensions.get_extension_for_oid(
1426
+ ExtensionOID.BASIC_CONSTRAINTS
1427
+ )
1428
+ else False
1429
+ ),
1430
+ roles=roles,
1431
+ permissions=permissions,
1432
+ certificate_path=cert_path,
1433
+ )
1434
+
1435
+ # Cache the result
1436
+ self._certificate_cache[cert_path] = cert_info
1437
+
1438
+ return cert_info
1439
+
1440
+ except Exception as e:
1441
+ self.logger.error(
1442
+ "Failed to get certificate info",
1443
+ extra={"cert_path": cert_path, "error": str(e)},
1444
+ )
1445
+ raise CertificateValidationError(
1446
+ f"Failed to get certificate info: {str(e)}"
1447
+ )
1448
+
1449
+ def create_crl(self, ca_cert_path: str, ca_key_path: str,
1450
+ output_path: Optional[str] = None, validity_days: int = 30) -> str:
1451
+ """
1452
+ Create a Certificate Revocation List (CRL).
1453
+
1454
+ This method creates a Certificate Revocation List (CRL) from the CA
1455
+ certificate and private key. The CRL contains information about
1456
+ revoked certificates.
1457
+
1458
+ Args:
1459
+ ca_cert_path (str): Path to CA certificate file
1460
+ ca_key_path (str): Path to CA private key file
1461
+ output_path (Optional[str]): Output path for CRL file.
1462
+ If None, uses default path from configuration
1463
+ validity_days (int): CRL validity period in days. Defaults to 30
1464
+
1465
+ Returns:
1466
+ str: Path to the created CRL file
1467
+
1468
+ Raises:
1469
+ FileNotFoundError: When CA certificate or key files are not found
1470
+ CertificateGenerationError: When CRL creation fails
1471
+
1472
+ Example:
1473
+ >>> cert_manager = CertificateManager(config)
1474
+ >>> crl_path = cert_manager.create_crl(
1475
+ ... ca_cert_path="/path/to/ca.crt",
1476
+ ... ca_key_path="/path/to/ca.key",
1477
+ ... validity_days=30
1478
+ ... )
1479
+ >>> print(f"CRL created: {crl_path}")
1480
+ """
1481
+ try:
1482
+ # Validate input files
1483
+ if not os.path.exists(ca_cert_path):
1484
+ raise FileNotFoundError(f"CA certificate not found: {ca_cert_path}")
1485
+
1486
+ if not os.path.exists(ca_key_path):
1487
+ raise FileNotFoundError(f"CA private key not found: {ca_key_path}")
1488
+
1489
+ # Load CA certificate
1490
+ with open(ca_cert_path, 'rb') as f:
1491
+ ca_cert_data = f.read()
1492
+ ca_cert = x509.load_pem_x509_certificate(ca_cert_data)
1493
+
1494
+ # Load CA private key
1495
+ with open(ca_key_path, 'rb') as f:
1496
+ ca_key_data = f.read()
1497
+ ca_private_key = serialization.load_pem_private_key(
1498
+ ca_key_data, password=None
1499
+ )
1500
+
1501
+ # Calculate CRL dates
1502
+ now = datetime.now(timezone.utc)
1503
+ next_update = now + timedelta(days=validity_days)
1504
+
1505
+ # Create CRL builder
1506
+ crl_builder = x509.CertificateRevocationListBuilder()
1507
+ crl_builder = crl_builder.last_update(now)
1508
+ crl_builder = crl_builder.next_update(next_update)
1509
+ crl_builder = crl_builder.issuer_name(ca_cert.subject)
1510
+
1511
+ # Add revoked certificates (empty for now, can be enhanced)
1512
+ # This is a basic implementation - in a real scenario, you would
1513
+ # load revoked certificates from a database or file
1514
+ revoked_certificates = []
1515
+
1516
+ # Build CRL
1517
+ crl = crl_builder.sign(ca_private_key, hashes.SHA256())
1518
+
1519
+ # Determine output path
1520
+ if output_path is None:
1521
+ output_dir = Path(self.config.cert_storage_path) / "crl"
1522
+ output_dir.mkdir(parents=True, exist_ok=True)
1523
+ output_path = str(output_dir / f"crl_{now.strftime('%Y%m%d_%H%M%S')}.pem")
1524
+ else:
1525
+ output_dir = Path(output_path).parent
1526
+ output_dir.mkdir(parents=True, exist_ok=True)
1527
+
1528
+ # Write CRL to file
1529
+ with open(output_path, 'wb') as f:
1530
+ f.write(crl.public_bytes(serialization.Encoding.PEM))
1531
+
1532
+ self.logger.info(
1533
+ "CRL created successfully",
1534
+ extra={
1535
+ "crl_path": output_path,
1536
+ "validity_days": validity_days,
1537
+ "revoked_count": len(revoked_certificates)
1538
+ }
1539
+ )
1540
+
1541
+ return output_path
1542
+
1543
+ except Exception as e:
1544
+ self.logger.error(
1545
+ "Failed to create CRL",
1546
+ extra={
1547
+ "ca_cert_path": ca_cert_path,
1548
+ "ca_key_path": ca_key_path,
1549
+ "error": str(e)
1550
+ }
1551
+ )
1552
+ raise CertificateGenerationError(f"Failed to create CRL: {str(e)}")
1553
+
1554
+ def export_certificate(self, cert_path: str, format: str = "pem") -> Union[str, bytes]:
1555
+ """
1556
+ Export certificate to different formats.
1557
+
1558
+ Args:
1559
+ cert_path: Path to certificate file
1560
+ format: Export format ("pem" or "der")
1561
+
1562
+ Returns:
1563
+ Certificate content in specified format
1564
+ """
1565
+ try:
1566
+ with open(cert_path, 'rb') as f:
1567
+ cert_data = f.read()
1568
+
1569
+ if format.lower() == "pem":
1570
+ return cert_data.decode('utf-8')
1571
+ elif format.lower() == "der":
1572
+ # Convert PEM to DER
1573
+ from cryptography import x509
1574
+ cert = x509.load_pem_x509_certificate(cert_data)
1575
+ return cert.public_bytes(serialization.Encoding.DER)
1576
+ else:
1577
+ raise ValueError(f"Unsupported format: {format}")
1578
+
1579
+ except Exception as e:
1580
+ self.logger.error(f"Failed to export certificate: {str(e)}")
1581
+ raise CertificateGenerationError(f"Failed to export certificate: {str(e)}")
1582
+
1583
+ def export_private_key(self, key_path: str, format: str = "pem") -> Union[str, bytes]:
1584
+ """
1585
+ Export private key to different formats.
1586
+
1587
+ Args:
1588
+ key_path: Path to private key file
1589
+ format: Export format ("pem" or "der")
1590
+
1591
+ Returns:
1592
+ Private key content in specified format
1593
+ """
1594
+ try:
1595
+ with open(key_path, 'rb') as f:
1596
+ key_data = f.read()
1597
+
1598
+ if format.lower() == "pem":
1599
+ return key_data.decode('utf-8')
1600
+ elif format.lower() == "der":
1601
+ # Convert PEM to DER
1602
+ from cryptography.hazmat.primitives.serialization import load_pem_private_key
1603
+ key = load_pem_private_key(key_data, password=None)
1604
+ return key.private_bytes(
1605
+ encoding=serialization.Encoding.DER,
1606
+ format=serialization.PrivateFormat.PKCS8,
1607
+ encryption_algorithm=serialization.NoEncryption()
1608
+ )
1609
+ else:
1610
+ raise ValueError(f"Unsupported format: {format}")
1611
+
1612
+ except Exception as e:
1613
+ self.logger.error(f"Failed to export private key: {str(e)}")
1614
+ raise CertificateGenerationError(f"Failed to export private key: {str(e)}")
1615
+
1616
+ def _validate_configuration(self) -> None:
1617
+ """Validate certificate configuration."""
1618
+ # Skip validation if certificate management is disabled
1619
+ if not self.config.enabled:
1620
+ return
1621
+
1622
+ if not self.config.ca_cert_path:
1623
+ raise CertificateConfigurationError("CA certificate path is required")
1624
+
1625
+ if not self.config.ca_key_path:
1626
+ raise CertificateConfigurationError("CA private key path is required")
1627
+
1628
+ if not os.path.exists(self.config.ca_cert_path):
1629
+ raise CertificateConfigurationError(
1630
+ f"CA certificate file not found: {self.config.ca_cert_path}"
1631
+ )
1632
+
1633
+ if not os.path.exists(self.config.ca_key_path):
1634
+ raise CertificateConfigurationError(
1635
+ f"CA private key file not found: {self.config.ca_key_path}"
1636
+ )
1637
+
1638
+
1639
+ class CertificateConfigurationError(Exception):
1640
+ """Raised when certificate configuration is invalid."""
1641
+
1642
+ def __init__(self, message: str, error_code: int = -32001):
1643
+ self.message = message
1644
+ self.error_code = error_code
1645
+ super().__init__(self.message)
1646
+
1647
+
1648
+ class CertificateGenerationError(Exception):
1649
+ """Raised when certificate generation fails."""
1650
+
1651
+ def __init__(self, message: str, error_code: int = -32002):
1652
+ self.message = message
1653
+ self.error_code = error_code
1654
+ super().__init__(self.message)
1655
+
1656
+
1657
+ class CertificateValidationError(Exception):
1658
+ """Raised when certificate validation fails."""
1659
+
1660
+ def __init__(self, message: str, error_code: int = -32003):
1661
+ self.message = message
1662
+ self.error_code = error_code
1663
+ super().__init__(self.message)