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,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)
|