Coverage for mcpgateway / plugins / framework / external / mcp / tls_utils.py: 100%
27 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/plugins/framework/external/mcp/tls_utils.py
3Copyright 2025
4SPDX-License-Identifier: Apache-2.0
5Authors: Teryl Taylor
7TLS/SSL utility functions for external MCP plugin connections.
9This module provides utilities for creating and configuring SSL contexts for
10secure communication with external MCP plugin servers. It implements the
11certificate validation logic that is tested in test_client_certificate_validation.py.
13Examples:
14 Create a basic SSL context with default settings:
16 >>> from mcpgateway.plugins.framework.models import MCPClientTLSConfig
17 >>> import ssl
18 >>> config = MCPClientTLSConfig()
19 >>> ctx = create_ssl_context(config, "ExamplePlugin")
20 >>> ctx.verify_mode == ssl.CERT_REQUIRED
21 True
23 Create an SSL context with hostname verification disabled:
25 >>> config = MCPClientTLSConfig(verify=True, check_hostname=False)
26 >>> ctx = create_ssl_context(config, "NoHostnamePlugin")
27 >>> ctx.verify_mode == ssl.CERT_REQUIRED
28 True
29 >>> ctx.check_hostname
30 False
32 Verify that TLS version is enforced:
34 >>> config = MCPClientTLSConfig(verify=True)
35 >>> ctx = create_ssl_context(config, "VersionTestPlugin")
36 >>> ctx.minimum_version >= ssl.TLSVersion.TLSv1_2
37 True
39 All SSL contexts have TLS 1.2 minimum:
41 >>> config1 = MCPClientTLSConfig(verify=True)
42 >>> config2 = MCPClientTLSConfig(verify=False)
43 >>> ctx1 = create_ssl_context(config1, "Plugin1")
44 >>> ctx2 = create_ssl_context(config2, "Plugin2")
45 >>> ctx1.minimum_version == ctx2.minimum_version
46 True
47 >>> ctx1.minimum_version.name
48 'TLSv1_2'
50 Verify mode differs based on configuration:
52 >>> config_secure = MCPClientTLSConfig(verify=True)
53 >>> config_insecure = MCPClientTLSConfig(verify=False)
54 >>> ctx_secure = create_ssl_context(config_secure, "SecureP")
55 >>> ctx_insecure = create_ssl_context(config_insecure, "InsecureP")
56 >>> ctx_secure.verify_mode != ctx_insecure.verify_mode
57 True
58 >>> import ssl
59 >>> ctx_secure.verify_mode == ssl.CERT_REQUIRED
60 True
61 >>> ctx_insecure.verify_mode == ssl.CERT_NONE
62 True
63"""
65# Standard
66import logging
67import ssl
69# First-Party
70from mcpgateway.plugins.framework.errors import PluginError
71from mcpgateway.plugins.framework.models import MCPClientTLSConfig, PluginErrorModel
73logger = logging.getLogger(__name__)
76def create_ssl_context(tls_config: MCPClientTLSConfig, plugin_name: str) -> ssl.SSLContext:
77 """Create and configure an SSL context for external plugin connections.
79 This function implements the SSL/TLS security configuration for connecting to
80 external MCP plugin servers. It supports both standard TLS and mutual TLS (mTLS)
81 authentication.
83 Security Features Implemented (per Python ssl docs and OpenSSL):
85 1. **Invalid Certificate Rejection**: ssl.create_default_context() with CERT_REQUIRED
86 automatically validates certificate signatures and chains via OpenSSL.
88 2. **Expired Certificate Handling**: OpenSSL automatically checks notBefore and
89 notAfter fields per RFC 5280 Section 6. Expired or not-yet-valid certificates
90 are rejected during the handshake.
92 3. **Certificate Chain Validation**: Full chain validation up to a trusted CA.
93 Each certificate in the chain is verified for validity period, signature, etc.
95 4. **Hostname Verification**: When check_hostname is enabled, the certificate's
96 Subject Alternative Name (SAN) or Common Name (CN) must match the hostname.
98 5. **MITM Prevention**: Via mutual authentication when client certificates are
99 provided (mTLS mode).
101 Args:
102 tls_config: TLS configuration containing CA bundle, client certs, and verification settings
103 plugin_name: Name of the plugin (for error messages)
105 Returns:
106 Configured SSLContext ready for use with httpx or other SSL connections
108 Raises:
109 PluginError: If SSL context configuration fails
111 Examples:
112 Create SSL context with verification enabled (default secure mode):
114 >>> from mcpgateway.plugins.framework.models import MCPClientTLSConfig
115 >>> tls_config = MCPClientTLSConfig(verify=True)
116 >>> ssl_context = create_ssl_context(tls_config, "TestPlugin")
117 >>> ssl_context.verify_mode == 2 # ssl.CERT_REQUIRED
118 True
119 >>> ssl_context.check_hostname
120 True
122 Create SSL context with verification disabled (development/testing):
124 >>> tls_config = MCPClientTLSConfig(verify=False, check_hostname=False)
125 >>> ssl_context = create_ssl_context(tls_config, "DevPlugin")
126 >>> ssl_context.verify_mode == 0 # ssl.CERT_NONE
127 True
128 >>> ssl_context.check_hostname
129 False
131 Verify TLS 1.2 minimum version enforcement:
133 >>> tls_config = MCPClientTLSConfig(verify=True)
134 >>> ssl_context = create_ssl_context(tls_config, "SecurePlugin")
135 >>> ssl_context.minimum_version.name
136 'TLSv1_2'
138 Mixed security settings (verify enabled, hostname check disabled):
140 >>> tls_config = MCPClientTLSConfig(verify=True, check_hostname=False)
141 >>> ssl_context = create_ssl_context(tls_config, "MixedPlugin")
142 >>> ssl_context.verify_mode == 2 # ssl.CERT_REQUIRED
143 True
144 >>> ssl_context.check_hostname
145 False
147 Default configuration is secure:
149 >>> tls_config = MCPClientTLSConfig()
150 >>> ssl_context = create_ssl_context(tls_config, "DefaultPlugin")
151 >>> ssl_context.verify_mode == 2 # ssl.CERT_REQUIRED
152 True
153 >>> ssl_context.check_hostname
154 True
155 >>> ssl_context.minimum_version.name
156 'TLSv1_2'
158 Test error handling with invalid certificate file:
160 >>> import tempfile
161 >>> import os
162 >>> tmp_dir = tempfile.mkdtemp()
163 >>> bad_cert = os.path.join(tmp_dir, "bad.pem")
164 >>> with open(bad_cert, 'w') as f:
165 ... _ = f.write("INVALID CERT")
166 >>> tls_config = MCPClientTLSConfig(certfile=bad_cert, keyfile=bad_cert, verify=False)
167 >>> try:
168 ... ssl_context = create_ssl_context(tls_config, "BadCertPlugin")
169 ... except PluginError as e:
170 ... "Failed to configure SSL context" in e.error.message
171 True
173 Verify logging occurs for different configurations:
175 >>> import logging
176 >>> tls_config = MCPClientTLSConfig(verify=False)
177 >>> ssl_context = create_ssl_context(tls_config, "LogTestPlugin")
178 >>> ssl_context is not None
179 True
180 """
181 try:
182 # Create SSL context with secure defaults
183 # Per Python docs: "The settings are chosen by the ssl module, and usually
184 # represent a higher security level than when calling the SSLContext
185 # constructor directly."
186 # This sets verify_mode to CERT_REQUIRED by default, which enables:
187 # - Certificate signature validation
188 # - Certificate chain validation up to trusted CA
189 # - Automatic expiration checking (notBefore/notAfter per RFC 5280)
190 ssl_context = ssl.create_default_context() # NOSONAR as this will fail check_hostname from NOT tls_config.verify
192 # Enforce TLS 1.2 or higher for security
193 ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
195 if not tls_config.verify:
196 # Disable certificate verification (not recommended for production)
197 logger.warning(f"Certificate verification disabled for plugin '{plugin_name}'. This is not recommended for production use.")
198 ssl_context.check_hostname = False # NOSONAR as this is specifically NOT tls_config.verify
199 ssl_context.verify_mode = ssl.CERT_NONE # nosec B502 # noqa: DUO122 #NOSONAR as this is specifically NOT tls_config.verify
200 else:
201 # Enable strict certificate verification (production mode)
202 # Load CA certificate bundle for server certificate validation
203 if tls_config.ca_bundle:
204 # This CA bundle will be used to validate the server's certificate
205 # OpenSSL will check:
206 # - Certificate is signed by a trusted CA in this bundle
207 # - Certificate hasn't expired (notAfter > now)
208 # - Certificate is already valid (notBefore < now)
209 # - Certificate chain is complete and valid
210 ssl_context.load_verify_locations(cafile=tls_config.ca_bundle)
212 # Hostname verification
213 # When enabled, certificate's SAN or CN must match the server hostname
214 if not tls_config.check_hostname:
215 logger.warning(f"Hostname verification disabled for plugin '{plugin_name}'. This increases risk of MITM attacks.")
216 ssl_context.check_hostname = False
218 # Load client certificate for mTLS (mutual authentication)
219 # If provided, the client will authenticate itself to the server
220 if tls_config.certfile:
221 ssl_context.load_cert_chain(
222 certfile=tls_config.certfile,
223 keyfile=tls_config.keyfile,
224 password=tls_config.keyfile_password,
225 )
226 logger.debug(f"mTLS enabled for plugin '{plugin_name}' with client certificate: {tls_config.certfile}")
228 # Log security configuration
229 logger.debug(
230 f"SSL context created for plugin '{plugin_name}': verify_mode={ssl_context.verify_mode}, check_hostname={ssl_context.check_hostname}, minimum_version={ssl_context.minimum_version}"
231 )
233 return ssl_context
235 except Exception as exc:
236 error_msg = f"Failed to configure SSL context for plugin '{plugin_name}': {exc}"
237 logger.error(error_msg)
238 raise PluginError(error=PluginErrorModel(message=error_msg, plugin_name=plugin_name)) from exc