Coverage for mcpgateway / utils / ssl_key_manager.py: 100%
46 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-02-11 07:10 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-02-11 07:10 +0000
1# -*- coding: utf-8 -*-
2"""Location: ./mcpgateway/utils/ssl_key_manager.py
3Copyright 2025
4SPDX-License-Identifier: Apache-2.0
5Authors: Keval Mahajan
7SSL key management utilities for handling passphrase-protected keys.
9This module provides utilities for managing SSL private keys, including support
10for passphrase-protected keys. It handles decryption and secure temporary file
11management for use with Gunicorn and other servers that don't natively support
12passphrase-protected keys.
13"""
15# Standard
16import atexit
17from contextlib import suppress
18import logging
19import os
20from pathlib import Path
21import tempfile
22from typing import Optional
24# Third-Party
25from cryptography.hazmat.primitives import serialization
26from cryptography.hazmat.primitives.serialization import load_pem_private_key
28logger = logging.getLogger(__name__)
31class SSLKeyManager:
32 """Manages SSL private keys, including passphrase-protected keys.
34 This class handles the decryption of passphrase-protected private keys
35 and creates temporary unencrypted key files for use with servers that
36 don't support passphrase-protected keys directly (like Gunicorn).
38 The temporary files are created with secure permissions (0o600) and are
39 automatically cleaned up on process exit.
41 Examples:
42 >>> manager = SSLKeyManager()
43 >>> key_path = manager.prepare_key_file("certs/key.pem") # doctest: +SKIP
44 >>> # Use key_path with Gunicorn
45 >>> manager.cleanup() # doctest: +SKIP
46 """
48 def __init__(self):
49 """Initialize the SSL key manager."""
50 self._temp_key_file: Optional[Path] = None
52 def prepare_key_file(
53 self,
54 key_file: str | Path,
55 password: Optional[str] = None,
56 ) -> str:
57 """Prepare a key file for use with Gunicorn.
59 If the key is passphrase-protected, decrypt it and write to a
60 temporary file with secure permissions. Otherwise, return the
61 original path.
63 Args:
64 key_file: Path to the private key file
65 password: Optional passphrase for encrypted key
67 Returns:
68 Path to the usable key file (original or temporary)
70 Raises:
71 FileNotFoundError: If the key file doesn't exist
72 ValueError: If decryption fails (wrong passphrase, invalid key, etc.)
74 Examples:
75 >>> manager = SSLKeyManager()
76 >>> # Unencrypted key - returns original path
77 >>> path = manager.prepare_key_file("certs/key.pem") # doctest: +SKIP
78 >>> # Encrypted key - returns temporary decrypted path
79 >>> path = manager.prepare_key_file("certs/key-enc.pem", "secret") # doctest: +SKIP
80 """
81 key_path = Path(key_file)
83 if not key_path.exists():
84 raise FileNotFoundError(f"Key file not found: {key_file}")
86 # If no password, use the key as-is
87 if not password:
88 logger.info(f"Using unencrypted key file: {key_file}")
89 return str(key_path)
91 # Decrypt the key and write to temporary file
92 logger.info("Decrypting passphrase-protected key...")
94 try:
95 # Read and decrypt the key
96 with open(key_path, "rb") as f:
97 key_data = f.read()
99 private_key = load_pem_private_key(
100 key_data,
101 password=password.encode() if password else None,
102 )
104 # Serialize to unencrypted PEM
105 unencrypted_pem = private_key.private_bytes(
106 encoding=serialization.Encoding.PEM,
107 format=serialization.PrivateFormat.TraditionalOpenSSL,
108 encryption_algorithm=serialization.NoEncryption(),
109 )
111 # Write to temporary file with secure permissions
112 fd, temp_path = tempfile.mkstemp(suffix=".pem", prefix="ssl_key_")
113 self._temp_key_file = Path(temp_path)
115 # Set restrictive permissions (owner read/write only)
116 os.chmod(temp_path, 0o600)
118 # Write the decrypted key
119 with os.fdopen(fd, "wb") as f:
120 f.write(unencrypted_pem)
122 logger.info(f"Decrypted key written to temporary file: {temp_path}")
124 # Register cleanup on exit
125 atexit.register(self.cleanup)
127 return temp_path
129 except Exception as e:
130 logger.error(f"Failed to decrypt key: {e}")
131 self.cleanup()
132 raise ValueError("Failed to decrypt private key. Check that the passphrase is correct.") from e
134 def cleanup(self):
135 """Remove temporary key file if it exists.
137 This method is automatically called on process exit via atexit,
138 but can also be called manually for explicit cleanup.
139 """
140 if self._temp_key_file and self._temp_key_file.exists():
141 with suppress(FileNotFoundError, PermissionError, OSError):
142 self._temp_key_file.unlink()
143 self._temp_key_file = None
146# Global instance for convenience
147_key_manager = SSLKeyManager()
150def prepare_ssl_key(key_file: str, password: Optional[str] = None) -> str:
151 """Prepare an SSL key file for use with Gunicorn.
153 This is a convenience function that uses the global key manager instance.
155 Args:
156 key_file: Path to the private key file
157 password: Optional passphrase for encrypted key
159 Returns:
160 Path to the usable key file (original or temporary)
162 Raises:
163 FileNotFoundError: If the key file doesn't exist
164 ValueError: If decryption fails
166 Examples:
167 >>> from mcpgateway.utils.ssl_key_manager import prepare_ssl_key
168 >>> key_path = prepare_ssl_key("certs/key.pem") # doctest: +SKIP
169 >>> key_path = prepare_ssl_key("certs/key-enc.pem", "secret") # doctest: +SKIP
170 """
171 return _key_manager.prepare_key_file(key_file, password)