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

1# -*- coding: utf-8 -*- 

2"""Location: ./mcpgateway/utils/ssl_key_manager.py 

3Copyright 2025 

4SPDX-License-Identifier: Apache-2.0 

5Authors: Keval Mahajan 

6 

7SSL key management utilities for handling passphrase-protected keys. 

8 

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""" 

14 

15# Standard 

16import atexit 

17from contextlib import suppress 

18import logging 

19import os 

20from pathlib import Path 

21import tempfile 

22from typing import Optional 

23 

24# Third-Party 

25from cryptography.hazmat.primitives import serialization 

26from cryptography.hazmat.primitives.serialization import load_pem_private_key 

27 

28logger = logging.getLogger(__name__) 

29 

30 

31class SSLKeyManager: 

32 """Manages SSL private keys, including passphrase-protected keys. 

33 

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

37 

38 The temporary files are created with secure permissions (0o600) and are 

39 automatically cleaned up on process exit. 

40 

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 """ 

47 

48 def __init__(self): 

49 """Initialize the SSL key manager.""" 

50 self._temp_key_file: Optional[Path] = None 

51 

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. 

58 

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. 

62 

63 Args: 

64 key_file: Path to the private key file 

65 password: Optional passphrase for encrypted key 

66 

67 Returns: 

68 Path to the usable key file (original or temporary) 

69 

70 Raises: 

71 FileNotFoundError: If the key file doesn't exist 

72 ValueError: If decryption fails (wrong passphrase, invalid key, etc.) 

73 

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) 

82 

83 if not key_path.exists(): 

84 raise FileNotFoundError(f"Key file not found: {key_file}") 

85 

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) 

90 

91 # Decrypt the key and write to temporary file 

92 logger.info("Decrypting passphrase-protected key...") 

93 

94 try: 

95 # Read and decrypt the key 

96 with open(key_path, "rb") as f: 

97 key_data = f.read() 

98 

99 private_key = load_pem_private_key( 

100 key_data, 

101 password=password.encode() if password else None, 

102 ) 

103 

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 ) 

110 

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) 

114 

115 # Set restrictive permissions (owner read/write only) 

116 os.chmod(temp_path, 0o600) 

117 

118 # Write the decrypted key 

119 with os.fdopen(fd, "wb") as f: 

120 f.write(unencrypted_pem) 

121 

122 logger.info(f"Decrypted key written to temporary file: {temp_path}") 

123 

124 # Register cleanup on exit 

125 atexit.register(self.cleanup) 

126 

127 return temp_path 

128 

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 

133 

134 def cleanup(self): 

135 """Remove temporary key file if it exists. 

136 

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 

144 

145 

146# Global instance for convenience 

147_key_manager = SSLKeyManager() 

148 

149 

150def prepare_ssl_key(key_file: str, password: Optional[str] = None) -> str: 

151 """Prepare an SSL key file for use with Gunicorn. 

152 

153 This is a convenience function that uses the global key manager instance. 

154 

155 Args: 

156 key_file: Path to the private key file 

157 password: Optional passphrase for encrypted key 

158 

159 Returns: 

160 Path to the usable key file (original or temporary) 

161 

162 Raises: 

163 FileNotFoundError: If the key file doesn't exist 

164 ValueError: If decryption fails 

165 

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)