mcp-security-framework 0.1.0__py3-none-any.whl → 1.1.1__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 (58) hide show
  1. mcp_security_framework/__init__.py +26 -15
  2. mcp_security_framework/cli/__init__.py +1 -1
  3. mcp_security_framework/cli/cert_cli.py +233 -197
  4. mcp_security_framework/cli/security_cli.py +324 -234
  5. mcp_security_framework/constants.py +21 -27
  6. mcp_security_framework/core/auth_manager.py +49 -20
  7. mcp_security_framework/core/cert_manager.py +398 -104
  8. mcp_security_framework/core/permission_manager.py +13 -9
  9. mcp_security_framework/core/rate_limiter.py +10 -0
  10. mcp_security_framework/core/security_manager.py +286 -229
  11. mcp_security_framework/examples/__init__.py +6 -0
  12. mcp_security_framework/examples/comprehensive_example.py +954 -0
  13. mcp_security_framework/examples/django_example.py +276 -202
  14. mcp_security_framework/examples/fastapi_example.py +897 -393
  15. mcp_security_framework/examples/flask_example.py +311 -200
  16. mcp_security_framework/examples/gateway_example.py +373 -214
  17. mcp_security_framework/examples/microservice_example.py +337 -172
  18. mcp_security_framework/examples/standalone_example.py +719 -478
  19. mcp_security_framework/examples/test_all_examples.py +572 -0
  20. mcp_security_framework/middleware/__init__.py +46 -55
  21. mcp_security_framework/middleware/auth_middleware.py +62 -63
  22. mcp_security_framework/middleware/fastapi_auth_middleware.py +179 -110
  23. mcp_security_framework/middleware/fastapi_middleware.py +156 -148
  24. mcp_security_framework/middleware/flask_auth_middleware.py +267 -107
  25. mcp_security_framework/middleware/flask_middleware.py +183 -157
  26. mcp_security_framework/middleware/mtls_middleware.py +106 -117
  27. mcp_security_framework/middleware/rate_limit_middleware.py +105 -101
  28. mcp_security_framework/middleware/security_middleware.py +109 -124
  29. mcp_security_framework/schemas/config.py +2 -1
  30. mcp_security_framework/schemas/models.py +19 -6
  31. mcp_security_framework/utils/cert_utils.py +14 -8
  32. mcp_security_framework/utils/datetime_compat.py +116 -0
  33. {mcp_security_framework-0.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/METADATA +2 -1
  34. mcp_security_framework-1.1.1.dist-info/RECORD +84 -0
  35. tests/conftest.py +303 -0
  36. tests/test_cli/test_cert_cli.py +194 -174
  37. tests/test_cli/test_security_cli.py +274 -247
  38. tests/test_core/test_cert_manager.py +33 -19
  39. tests/test_core/test_security_manager.py +2 -2
  40. tests/test_examples/test_comprehensive_example.py +613 -0
  41. tests/test_examples/test_fastapi_example.py +290 -169
  42. tests/test_examples/test_flask_example.py +304 -162
  43. tests/test_examples/test_standalone_example.py +106 -168
  44. tests/test_integration/test_auth_flow.py +214 -198
  45. tests/test_integration/test_certificate_flow.py +181 -150
  46. tests/test_integration/test_fastapi_integration.py +140 -149
  47. tests/test_integration/test_flask_integration.py +144 -141
  48. tests/test_integration/test_standalone_integration.py +331 -300
  49. tests/test_middleware/test_fastapi_auth_middleware.py +745 -0
  50. tests/test_middleware/test_fastapi_middleware.py +147 -132
  51. tests/test_middleware/test_flask_auth_middleware.py +696 -0
  52. tests/test_middleware/test_flask_middleware.py +201 -179
  53. tests/test_middleware/test_security_middleware.py +151 -130
  54. tests/test_utils/test_datetime_compat.py +147 -0
  55. mcp_security_framework-0.1.0.dist-info/RECORD +0 -76
  56. {mcp_security_framework-0.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/WHEEL +0 -0
  57. {mcp_security_framework-0.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/entry_points.txt +0 -0
  58. {mcp_security_framework-0.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/top_level.txt +0 -0
@@ -27,13 +27,13 @@ from typing import Any, Dict, List, Optional, Union
27
27
 
28
28
  from ..core.security_manager import SecurityManager
29
29
  from ..schemas.config import SecurityConfig
30
- from ..schemas.models import AuthResult, ValidationResult, ValidationStatus, AuthStatus
31
- from ..schemas.responses import SecurityResponse, ResponseStatus
30
+ from ..schemas.models import AuthResult, AuthStatus, ValidationResult, ValidationStatus
31
+ from ..schemas.responses import ResponseStatus, SecurityResponse
32
32
 
33
33
 
34
34
  class SecurityMiddlewareError(Exception):
35
35
  """Raised when security middleware encounters an error."""
36
-
36
+
37
37
  def __init__(self, message: str, error_code: int = -32003):
38
38
  self.message = message
39
39
  self.error_code = error_code
@@ -43,12 +43,12 @@ class SecurityMiddlewareError(Exception):
43
43
  class SecurityMiddleware(ABC):
44
44
  """
45
45
  Abstract Security Middleware Class
46
-
46
+
47
47
  This is the base class for all framework-specific security middleware
48
48
  implementations. It provides common security logic and a unified
49
49
  interface for request processing, authentication, authorization,
50
50
  and rate limiting.
51
-
51
+
52
52
  The SecurityMiddleware implements the security processing pipeline:
53
53
  1. Rate limiting check
54
54
  2. Public path validation
@@ -56,7 +56,7 @@ class SecurityMiddleware(ABC):
56
56
  4. Authorization
57
57
  5. Security headers addition
58
58
  6. Response processing
59
-
59
+
60
60
  Key Responsibilities:
61
61
  - Process incoming requests through security pipeline
62
62
  - Handle authentication using multiple methods
@@ -65,7 +65,7 @@ class SecurityMiddleware(ABC):
65
65
  - Add security headers to responses
66
66
  - Log security events and violations
67
67
  - Provide framework-specific request/response handling
68
-
68
+
69
69
  Attributes:
70
70
  security_manager (SecurityManager): Main security manager instance
71
71
  config (SecurityConfig): Security configuration
@@ -73,32 +73,32 @@ class SecurityMiddleware(ABC):
73
73
  _public_paths (List[str]): List of public paths that bypass security
74
74
  _rate_limit_cache (Dict): Cache for rate limiting data
75
75
  _auth_cache (Dict): Cache for authentication results
76
-
76
+
77
77
  Example:
78
78
  >>> config = SecurityConfig(auth=AuthConfig(enabled=True))
79
79
  >>> security_manager = SecurityManager(config)
80
80
  >>> middleware = FastAPISecurityMiddleware(security_manager)
81
81
  >>> app.add_middleware(middleware)
82
-
82
+
83
83
  Note:
84
84
  This is an abstract base class. Implementations must provide
85
85
  framework-specific request/response handling methods.
86
86
  """
87
-
87
+
88
88
  def __init__(self, security_manager: SecurityManager):
89
89
  """
90
90
  Initialize Security Middleware.
91
-
91
+
92
92
  Args:
93
93
  security_manager (SecurityManager): Security manager instance
94
94
  containing all security components and configuration.
95
95
  Must be a properly initialized SecurityManager with
96
96
  valid configuration.
97
-
97
+
98
98
  Raises:
99
99
  SecurityMiddlewareError: If security manager is invalid or
100
100
  configuration is missing.
101
-
101
+
102
102
  Example:
103
103
  >>> security_manager = SecurityManager(config)
104
104
  >>> middleware = FastAPISecurityMiddleware(security_manager)
@@ -106,43 +106,43 @@ class SecurityMiddleware(ABC):
106
106
  if not isinstance(security_manager, SecurityManager):
107
107
  raise SecurityMiddlewareError(
108
108
  "Invalid security manager: must be SecurityManager instance",
109
- error_code=-32003
109
+ error_code=-32003,
110
110
  )
111
-
111
+
112
112
  self.security_manager = security_manager
113
113
  self.config = security_manager.config
114
114
  self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
115
-
115
+
116
116
  # Initialize caches and state
117
117
  self._public_paths = self.config.auth.public_paths if self.config.auth else []
118
118
  self._rate_limit_cache: Dict[str, Dict[str, Any]] = {}
119
119
  self._auth_cache: Dict[str, AuthResult] = {}
120
-
120
+
121
121
  self.logger.info(
122
122
  "Security middleware initialized",
123
123
  extra={
124
124
  "middleware_type": self.__class__.__name__,
125
125
  "auth_enabled": self.config.auth.enabled if self.config.auth else False,
126
- "public_paths_count": len(self._public_paths)
127
- }
126
+ "public_paths_count": len(self._public_paths),
127
+ },
128
128
  )
129
-
129
+
130
130
  @abstractmethod
131
131
  def __call__(self, request: Any, call_next: Any) -> Any:
132
132
  """
133
133
  Process request through security middleware.
134
-
134
+
135
135
  This is the main entry point for the middleware. It implements
136
136
  the security processing pipeline and delegates framework-specific
137
137
  operations to abstract methods.
138
-
138
+
139
139
  Args:
140
140
  request: Framework-specific request object
141
141
  call_next: Framework-specific call_next function
142
-
142
+
143
143
  Returns:
144
144
  Framework-specific response object
145
-
145
+
146
146
  Raises:
147
147
  SecurityMiddlewareError: If security processing fails
148
148
  AuthenticationError: If authentication fails
@@ -150,71 +150,68 @@ class SecurityMiddleware(ABC):
150
150
  RateLimitExceededError: If rate limit is exceeded
151
151
  """
152
152
  pass
153
-
153
+
154
154
  def _check_rate_limit(self, request: Any) -> bool:
155
155
  """
156
156
  Check if request is within rate limits.
157
-
157
+
158
158
  This method checks if the current request exceeds rate limits
159
159
  based on the request identifier (IP, user, etc.).
160
-
160
+
161
161
  Args:
162
162
  request: Framework-specific request object
163
-
163
+
164
164
  Returns:
165
165
  bool: True if request is within rate limits, False otherwise
166
-
166
+
167
167
  Raises:
168
168
  SecurityMiddlewareError: If rate limiting check fails
169
169
  """
170
170
  try:
171
171
  if not self.config.rate_limit.enabled:
172
172
  return True
173
-
173
+
174
174
  identifier = self._get_rate_limit_identifier(request)
175
175
  if not identifier:
176
176
  self.logger.warning("Could not determine rate limit identifier")
177
177
  return True
178
-
178
+
179
179
  # Check rate limit using security manager
180
180
  is_allowed = self.security_manager.rate_limiter.check_rate_limit(identifier)
181
-
181
+
182
182
  if not is_allowed:
183
183
  self.logger.warning(
184
184
  "Rate limit exceeded",
185
185
  extra={
186
186
  "identifier": identifier,
187
187
  "rate_limit": self.config.rate_limit.default_requests_per_minute,
188
- "window_seconds": self.config.rate_limit.window_size_seconds
189
- }
188
+ "window_seconds": self.config.rate_limit.window_size_seconds,
189
+ },
190
190
  )
191
-
191
+
192
192
  return is_allowed
193
-
193
+
194
194
  except Exception as e:
195
195
  self.logger.error(
196
- "Rate limit check failed",
197
- extra={"error": str(e)},
198
- exc_info=True
196
+ "Rate limit check failed", extra={"error": str(e)}, exc_info=True
199
197
  )
200
198
  raise SecurityMiddlewareError(
201
- f"Rate limit check failed: {str(e)}",
202
- error_code=-32004
199
+ f"Rate limit check failed: {str(e)}", error_code=-32004
203
200
  )
204
-
201
+
205
202
  def _authenticate_request(self, request: Any) -> AuthResult:
206
203
  """
207
204
  Authenticate the request using configured methods.
208
-
205
+
209
206
  This method attempts to authenticate the request using all
210
207
  configured authentication methods in order of preference.
211
-
208
+
212
209
  Args:
213
210
  request: Framework-specific request object
214
-
211
+
215
212
  Returns:
216
213
  AuthResult: Authentication result with user information
217
-
214
+
218
215
  Raises:
219
216
  SecurityMiddlewareError: If authentication process fails
220
217
  """
@@ -225,39 +222,40 @@ class SecurityMiddleware(ABC):
225
222
  status=AuthStatus.SUCCESS,
226
223
  username="anonymous",
227
224
  roles=[],
228
- auth_method=None
225
+ auth_method=None,
229
226
  )
230
-
227
+
231
228
  # Try each authentication method in order
232
229
  for method in self.config.auth.methods:
233
230
  auth_result = self._try_auth_method(request, method)
234
231
  # Handle async methods
235
- if hasattr(auth_result, '__await__'):
232
+ if hasattr(auth_result, "__await__"):
236
233
  import asyncio
234
+
237
235
  try:
238
236
  auth_result = asyncio.run(auth_result)
239
237
  except RuntimeError:
240
238
  # If we're already in an event loop, use create_task
241
239
  loop = asyncio.get_event_loop()
242
240
  auth_result = loop.run_until_complete(auth_result)
243
-
241
+
244
242
  if auth_result.is_valid:
245
243
  self.logger.info(
246
244
  "Authentication successful",
247
245
  extra={
248
246
  "username": auth_result.username,
249
247
  "auth_method": auth_result.auth_method,
250
- "user_roles": auth_result.roles
251
- }
248
+ "user_roles": auth_result.roles,
249
+ },
252
250
  )
253
251
  return auth_result
254
-
252
+
255
253
  # All authentication methods failed
256
254
  self.logger.warning(
257
255
  "All authentication methods failed",
258
- extra={"auth_methods": self.config.auth.methods}
256
+ extra={"auth_methods": self.config.auth.methods},
259
257
  )
260
-
258
+
261
259
  return AuthResult(
262
260
  is_valid=False,
263
261
  status=AuthStatus.FAILED,
@@ -265,53 +263,49 @@ class SecurityMiddleware(ABC):
265
263
  roles=[],
266
264
  auth_method=None,
267
265
  error_code=-32005,
268
- error_message="All authentication methods failed"
266
+ error_message="All authentication methods failed",
269
267
  )
270
-
268
+
271
269
  except Exception as e:
272
270
  self.logger.error(
273
- "Authentication process failed",
274
- extra={"error": str(e)},
275
- exc_info=True
271
+ "Authentication process failed", extra={"error": str(e)}, exc_info=True
276
272
  )
277
273
  raise SecurityMiddlewareError(
278
- f"Authentication process failed: {str(e)}",
279
- error_code=-32006
274
+ f"Authentication process failed: {str(e)}", error_code=-32006
280
275
  )
281
-
276
+
282
277
  def _validate_permissions(self, request: Any, auth_result: AuthResult) -> bool:
283
278
  """
284
279
  Validate user permissions for the requested resource.
285
-
280
+
286
281
  This method checks if the authenticated user has the required
287
282
  permissions to access the requested resource.
288
-
283
+
289
284
  Args:
290
285
  request: Framework-specific request object
291
286
  auth_result (AuthResult): Authentication result with user info
292
-
287
+
293
288
  Returns:
294
289
  bool: True if user has required permissions, False otherwise
295
-
290
+
296
291
  Raises:
297
292
  SecurityMiddlewareError: If permission validation fails
298
293
  """
299
294
  try:
300
295
  if not auth_result.is_valid:
301
296
  return False
302
-
297
+
303
298
  # Get required permissions for the request
304
299
  required_permissions = self._get_required_permissions(request)
305
300
  if not required_permissions:
306
301
  # No specific permissions required
307
302
  return True
308
-
303
+
309
304
  # Check permissions using security manager
310
305
  validation_result = self.security_manager.check_permissions(
311
- auth_result.roles,
312
- required_permissions
306
+ auth_result.roles, required_permissions
313
307
  )
314
-
308
+
315
309
  if not validation_result.is_valid:
316
310
  self.logger.warning(
317
311
  "Permission validation failed",
@@ -319,30 +313,27 @@ class SecurityMiddleware(ABC):
319
313
  "username": auth_result.username,
320
314
  "user_roles": auth_result.roles,
321
315
  "required_permissions": required_permissions,
322
- "error_message": validation_result.error_message
323
- }
316
+ "error_message": validation_result.error_message,
317
+ },
324
318
  )
325
-
319
+
326
320
  return validation_result.is_valid
327
-
321
+
328
322
  except Exception as e:
329
323
  self.logger.error(
330
- "Permission validation failed",
331
- extra={"error": str(e)},
332
- exc_info=True
324
+ "Permission validation failed", extra={"error": str(e)}, exc_info=True
333
325
  )
334
326
  raise SecurityMiddlewareError(
335
- f"Permission validation failed: {str(e)}",
336
- error_code=-32007
327
+ f"Permission validation failed: {str(e)}", error_code=-32007
337
328
  )
338
-
329
+
339
330
  def _is_public_path(self, request: Any) -> bool:
340
331
  """
341
332
  Check if the request path is public (bypasses security).
342
-
333
+
343
334
  Args:
344
335
  request: Framework-specific request object
345
-
336
+
346
337
  Returns:
347
338
  bool: True if path is public, False otherwise
348
339
  """
@@ -350,26 +341,24 @@ class SecurityMiddleware(ABC):
350
341
  path = self._get_request_path(request)
351
342
  if not path:
352
343
  return False
353
-
344
+
354
345
  # Check if path matches any public path pattern
355
346
  for public_path in self._public_paths:
356
347
  if path == public_path or path.startswith(public_path):
357
348
  return True
358
-
349
+
359
350
  return False
360
-
351
+
361
352
  except Exception as e:
362
353
  self.logger.error(
363
- "Public path check failed",
364
- extra={"error": str(e)},
365
- exc_info=True
354
+ "Public path check failed", extra={"error": str(e)}, exc_info=True
366
355
  )
367
356
  return False
368
-
357
+
369
358
  def _add_security_headers(self, response: Any) -> None:
370
359
  """
371
360
  Add security headers to the response.
372
-
361
+
373
362
  Args:
374
363
  response: Framework-specific response object
375
364
  """
@@ -381,27 +370,25 @@ class SecurityMiddleware(ABC):
381
370
  "X-XSS-Protection": "1; mode=block",
382
371
  "Strict-Transport-Security": "max-age=31536000; includeSubDomains",
383
372
  "Content-Security-Policy": "default-src 'self'",
384
- "Referrer-Policy": "strict-origin-when-cross-origin"
373
+ "Referrer-Policy": "strict-origin-when-cross-origin",
385
374
  }
386
-
375
+
387
376
  # Add custom security headers from config
388
377
  if self.config.auth and self.config.auth.security_headers:
389
378
  headers.update(self.config.auth.security_headers)
390
-
379
+
391
380
  # Apply headers using framework-specific method
392
381
  self._apply_security_headers(response, headers)
393
-
382
+
394
383
  except Exception as e:
395
384
  self.logger.error(
396
- "Failed to add security headers",
397
- extra={"error": str(e)},
398
- exc_info=True
385
+ "Failed to add security headers", extra={"error": str(e)}, exc_info=True
399
386
  )
400
-
387
+
401
388
  def _log_security_event(self, event_type: str, details: Dict[str, Any]) -> None:
402
389
  """
403
390
  Log security event for monitoring and auditing.
404
-
391
+
405
392
  Args:
406
393
  event_type (str): Type of security event
407
394
  details (Dict[str, Any]): Event details
@@ -416,91 +403,89 @@ class SecurityMiddleware(ABC):
416
403
  "username": details.get("username"),
417
404
  "path": details.get("path"),
418
405
  "method": details.get("method"),
419
- **details
420
- }
406
+ **details,
407
+ },
421
408
  )
422
409
  except Exception as e:
423
410
  self.logger.error(
424
- "Failed to log security event",
425
- extra={"error": str(e)},
426
- exc_info=True
411
+ "Failed to log security event", extra={"error": str(e)}, exc_info=True
427
412
  )
428
-
413
+
429
414
  # Abstract methods for framework-specific implementations
430
-
415
+
431
416
  @abstractmethod
432
417
  def _get_rate_limit_identifier(self, request: Any) -> str:
433
418
  """
434
419
  Get rate limit identifier from request.
435
-
420
+
436
421
  Args:
437
422
  request: Framework-specific request object
438
-
423
+
439
424
  Returns:
440
425
  str: Rate limit identifier (IP, user ID, etc.)
441
426
  """
442
427
  pass
443
-
428
+
444
429
  @abstractmethod
445
430
  def _get_request_path(self, request: Any) -> str:
446
431
  """
447
432
  Get request path from request object.
448
-
433
+
449
434
  Args:
450
435
  request: Framework-specific request object
451
-
436
+
452
437
  Returns:
453
438
  str: Request path
454
439
  """
455
440
  pass
456
-
441
+
457
442
  @abstractmethod
458
443
  def _get_required_permissions(self, request: Any) -> List[str]:
459
444
  """
460
445
  Get required permissions for the request.
461
-
446
+
462
447
  Args:
463
448
  request: Framework-specific request object
464
-
449
+
465
450
  Returns:
466
451
  List[str]: List of required permissions
467
452
  """
468
453
  pass
469
-
454
+
470
455
  @abstractmethod
471
456
  def _try_auth_method(self, request: Any, method: str) -> AuthResult:
472
457
  """
473
458
  Try authentication using specific method.
474
-
459
+
475
460
  Args:
476
461
  request: Framework-specific request object
477
462
  method (str): Authentication method to try
478
-
463
+
479
464
  Returns:
480
465
  AuthResult: Authentication result
481
466
  """
482
467
  pass
483
-
468
+
484
469
  @abstractmethod
485
470
  def _apply_security_headers(self, response: Any, headers: Dict[str, str]) -> None:
486
471
  """
487
472
  Apply security headers to response.
488
-
473
+
489
474
  Args:
490
475
  response: Framework-specific response object
491
476
  headers (Dict[str, str]): Headers to apply
492
477
  """
493
478
  pass
494
-
479
+
495
480
  @abstractmethod
496
481
  def _create_error_response(self, status_code: int, message: str) -> Any:
497
482
  """
498
483
  Create error response for security violations.
499
-
484
+
500
485
  Args:
501
486
  status_code (int): HTTP status code
502
487
  message (str): Error message
503
-
488
+
504
489
  Returns:
505
490
  Framework-specific error response object
506
491
  """
@@ -209,7 +209,8 @@ class AuthConfig(BaseModel):
209
209
  default=None, description="OAuth2 configuration"
210
210
  )
211
211
  public_paths: List[str] = Field(
212
- default_factory=list, description="List of public paths that bypass authentication"
212
+ default_factory=list,
213
+ description="List of public paths that bypass authentication",
213
214
  )
214
215
  security_headers: Optional[Dict[str, str]] = Field(
215
216
  default=None, description="Custom security headers to add to responses"
@@ -88,6 +88,7 @@ class AuthMethod(str, Enum):
88
88
  CERTIFICATE = "certificate"
89
89
  BASIC = "basic"
90
90
  OAUTH2 = "oauth2"
91
+ UNKNOWN = "unknown"
91
92
 
92
93
 
93
94
  class AuthResult(BaseModel):
@@ -322,7 +323,11 @@ class CertificateInfo(BaseModel):
322
323
  """Check if certificate is expired."""
323
324
  now = datetime.now(timezone.utc)
324
325
  # Ensure not_after has timezone info
325
- not_after = self.not_after.replace(tzinfo=timezone.utc) if self.not_after.tzinfo is None else self.not_after
326
+ not_after = (
327
+ self.not_after.replace(tzinfo=timezone.utc)
328
+ if self.not_after.tzinfo is None
329
+ else self.not_after
330
+ )
326
331
  return now > not_after
327
332
 
328
333
  @property
@@ -330,7 +335,11 @@ class CertificateInfo(BaseModel):
330
335
  """Check if certificate expires soon (within 30 days)."""
331
336
  now = datetime.now(timezone.utc)
332
337
  # Ensure not_after has timezone info
333
- not_after = self.not_after.replace(tzinfo=timezone.utc) if self.not_after.tzinfo is None else self.not_after
338
+ not_after = (
339
+ self.not_after.replace(tzinfo=timezone.utc)
340
+ if self.not_after.tzinfo is None
341
+ else self.not_after
342
+ )
334
343
  return now + timedelta(days=30) > not_after
335
344
 
336
345
  @property
@@ -340,7 +349,11 @@ class CertificateInfo(BaseModel):
340
349
  return 0
341
350
  now = datetime.now(timezone.utc)
342
351
  # Ensure not_after has timezone info
343
- not_after = self.not_after.replace(tzinfo=timezone.utc) if self.not_after.tzinfo is None else self.not_after
352
+ not_after = (
353
+ self.not_after.replace(tzinfo=timezone.utc)
354
+ if self.not_after.tzinfo is None
355
+ else self.not_after
356
+ )
344
357
  delta = not_after - now
345
358
  return delta.days
346
359
 
@@ -430,9 +443,9 @@ class CertificatePair(BaseModel):
430
443
  "-----BEGIN RSA PRIVATE KEY-----"
431
444
  ):
432
445
  raise ValueError("Invalid private key PEM format")
433
- if not v.strip().endswith("-----END PRIVATE KEY-----") and not v.strip().endswith(
434
- "-----END RSA PRIVATE KEY-----"
435
- ):
446
+ if not v.strip().endswith(
447
+ "-----END PRIVATE KEY-----"
448
+ ) and not v.strip().endswith("-----END RSA PRIVATE KEY-----"):
436
449
  raise ValueError("Invalid private key PEM format")
437
450
  return v
438
451