Description
Is your feature request related to a problem? Please describe.
The Splunk Python SDK lacks the possibility to provide a custom ca certificate that is not installed in the local system certificates storage for server certificate validation.
The possible arguments (key_file, cert_file) are for client authentifaction - not for server certificate verficitation.
The problematic Code snippet is (current upstream):
binding.py::1354
def handler(key_file=None, cert_file=None, timeout=None, verify=False):
"""This class returns an instance of the default HTTP request handler using
the values you provide.
:param `key_file`: A path to a PEM (Privacy Enhanced Mail) formatted file containing your private key (optional).
:type key_file: ``string``
:param `cert_file`: A path to a PEM (Privacy Enhanced Mail) formatted file containing a certificate chain file (optional).
:type cert_file: ``string``
:param `timeout`: The request time-out period, in seconds (optional).
:type timeout: ``integer`` or "None"
:param `verify`: Set to False to disable SSL verification on https connections.
:type verify: ``Boolean``
"""
def connect(scheme, host, port):
**kwargs = {} <-----**
if timeout is not None: kwargs['timeout'] = timeout
if scheme == "http":
return six.moves.http_client.HTTPConnection(host, port, **kwargs)
if scheme == "https":
if key_file is not None: kwargs['key_file'] = key_file
if cert_file is not None: kwargs['cert_file'] = cert_file
if not verify:
kwargs['context'] = ssl._create_unverified_context()
return six.moves.http_client.HTTPSConnection(host, port, **kwargs) <-----------------
raise ValueError("unsupported scheme: %s" % scheme)
So, if verification is enabled - a default SSL Context is created and CA's .pem
file have to be present in the system certificate storage. Otherwise it's an error like that:
import http.client
c = http.client.HTTPSConnection("my-splunk-host", "8000")
c.request("GET", "/")
[Out] SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED]
What's actually desired is to provide additional ssl lookup definitions:
import ssl
import http.client
context = ssl.create_default_context()
context.load_verify_locations("my-root-ca.pem")
c = http.client.HTTPSConnection("my-splunk-host", "8000", context=context)
test = c.request("GET", "/")
Or in the above snippet from the original code in binding.py
def connect(scheme, host, port):
kwargs = {}
if timeout is not None: kwargs['timeout'] = timeout
if scheme == "http":
return six.moves.http_client.HTTPConnection(host, port, **kwargs)
if scheme == "https":
if key_file is not None: kwargs['key_file'] = key_file
if cert_file is not None: kwargs['cert_file'] = cert_file
if not verify:
kwargs['context'] = ssl._create_unverified_context()
else:
context = ssl.create_default_context() <--------------------
context.load_verify_locations("my-root-ca.pem") <--------------------
kwargs['context'] = context <--------------------
return six.moves.http_client.HTTPSConnection(host, port, **kwargs)
raise ValueError("unsupported scheme: %s" % scheme)
Describe the solution you'd like
With the pull request, a User can optionally provide the entire SSLContext object with verify locations and all other settings, such as TLS-Version. A user can fully control the transport channel, which a user should be able to do.
import ssl
import splunklib.client as client
context = ssl.create_default_context()
context.load_verify_locations("my-root-ca-cert.pem")
self.service = client.connect(host=self.host, app='my-app', port=8089,
username=self.username, scheme='https', password=self.password, verify=True, context=context)
The following parts of the code are slightly changes (details, see Pull Request).
Simply add a new argument that either allows the passing of a entire SSL-Context object or the CA-Verify-Locations at:
- client.py :: def connect(**kwargs): (kwargs covers that already, no changes needed - just documentation)
- client.py :: 401 :: Service(**kwargs): (kwargs covers that already, no changes needed - just documentation)
- binding.py :: 472 init
self.http = HttpLib(handler, kwargs.get("verify", False), key_file=kwargs.get("key_file"),
cert_file=kwargs.get("cert_file")) # Default to False for backward compat
pass the new argument to HttpLib (either context or verify location)
- binding.py :: 1142 ->
self.handler = handler(verify=verify, key_file=key_file, cert_file=cert_file)
-> pass the argument - binding.py :: 1354 -> Final change (see code above)
if not verify:
kwargs['context'] = ssl._create_unverified_context()
else:
context = ssl.create_default_context()
context.load_verify_locations(verify_path)
kwargs['context'] = context
It's not a big change - I am going to create a pull request tomorrow with the option to pass an entire SSLContext object to HTTPSConnection(...), thus enabling the user to specify custom ca certficates, tls versions and so on. I think the user should have the possibility to fully control the transport channel, if desired.
Best Regards
Describe alternatives you've considered
The only alternative is to install custom ca certificates on the local system beforehand. This is suboptimal for certificates shipped with a packaged application and varies depending on the Operating System.
Additional context
Add any other context or screenshots about the feature request here.