Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to Ignore "The certificate's CN name does not match the passed value." with TrustStore? #145

Open
achapkowski opened this issue Jul 31, 2024 · 5 comments

Comments

@achapkowski
Copy link

I am trying to create a truststore object that ignores common name errors and ignores all certificate errors.

I have this:

        context = truststore.SSLContext()
        context.verify_mode = 0
        context.hostname_checks_common_name = False
        context.check_hostname = False

When I pass this into requests I still get:

The certificate's CN name does not match the passed value.

Is there something else that needs to be set to get around this exception being raised?

@achapkowski
Copy link
Author

If I do ssl._create_unverified_context() I can get it to work via requests, but if I mimic the same logic using truststore it fails:

def _create_unverified_context_trust_store(protocol=None, *, cert_reqs=ssl.CERT_NONE,
                           check_hostname=False, purpose=ssl.Purpose.SERVER_AUTH,
                           certfile=None, keyfile=None,
                           cafile=None, capath=None, cadata=None):
    """Create a SSLContext object for Python stdlib modules

    All Python stdlib modules shall use this function to create SSLContext
    objects in order to keep common settings in one place. The configuration
    is less restrict than create_default_context()'s to increase backward
    compatibility.
    """
    if not isinstance(purpose, ssl._ASN1Object):
        raise TypeError(purpose)

    # SSLContext sets OP_NO_SSLv2, OP_NO_SSLv3, OP_NO_COMPRESSION,
    # OP_CIPHER_SERVER_PREFERENCE, OP_SINGLE_DH_USE and OP_SINGLE_ECDH_USE
    # by default.
    if purpose == ssl.Purpose.SERVER_AUTH:
        # verify certs and host name in client mode
        if protocol is None:
            protocol = ssl.PROTOCOL_TLS_CLIENT
    elif purpose == ssl.Purpose.CLIENT_AUTH:
        if protocol is None:
            protocol = ssl.PROTOCOL_TLS_SERVER
    else:
        raise ValueError(purpose)

    context = truststore.SSLContext(protocol) # changed to TRUSTSTORE HERE
    context.check_hostname = check_hostname
    if cert_reqs is not None:
        context.verify_mode = cert_reqs
    if check_hostname:
        context.check_hostname = True

    if keyfile and not certfile:
        raise ValueError("certfile must be specified")
    if certfile or keyfile:
        context.load_cert_chain(certfile, keyfile)

    # load CA root certs
    if cafile or capath or cadata:
        context.load_verify_locations(cafile, capath, cadata)
    elif context.verify_mode != ssl.CERT_NONE:
        # no explicit cafile, capath or cadata but the verify mode is
        # CERT_OPTIONAL or CERT_REQUIRED. Let's try to load default system
        # root CA certificates for the given purpose. This may fail silently.
        context.load_default_certs(purpose)
    # OpenSSL 1.1.1 keylog file
    if hasattr(context, 'keylog_filename'):
        keylogfile = os.environ.get('SSLKEYLOGFILE')
        if keylogfile and not sys.flags.ignore_environment:
            context.keylog_filename = keylogfile
    return context

So this leads be to believe something is incorrect on truststore's side possibly.

@achapkowski
Copy link
Author

Fully reproducible sample code:

import ssl
import truststore
import os, sys
from requests.adapters import HTTPAdapter

DEFAULT_POOLBLOCK = False
DEFAULT_POOLSIZE = 10
DEFAULT_RETRIES = 0
DEFAULT_POOL_TIMEOUT = None

def _create_unverified_context_trust_store(protocol=None, *, cert_reqs=ssl.CERT_NONE,
                           check_hostname=False, purpose=ssl.Purpose.SERVER_AUTH,
                           certfile=None, keyfile=None,
                           cafile=None, capath=None, cadata=None):
    """Create a SSLContext object for Python stdlib modules

    All Python stdlib modules shall use this function to create SSLContext
    objects in order to keep common settings in one place. The configuration
    is less restrict than create_default_context()'s to increase backward
    compatibility.
    """
    if not isinstance(purpose, ssl._ASN1Object):
        raise TypeError(purpose)

    # SSLContext sets OP_NO_SSLv2, OP_NO_SSLv3, OP_NO_COMPRESSION,
    # OP_CIPHER_SERVER_PREFERENCE, OP_SINGLE_DH_USE and OP_SINGLE_ECDH_USE
    # by default.
    if purpose == ssl.Purpose.SERVER_AUTH:
        # verify certs and host name in client mode
        if protocol is None:
            protocol = ssl.PROTOCOL_TLS_CLIENT
    elif purpose == ssl.Purpose.CLIENT_AUTH:
        if protocol is None:
            protocol = ssl.PROTOCOL_TLS_SERVER
    else:
        raise ValueError(purpose)

    context = truststore.SSLContext(protocol)
    context.check_hostname = check_hostname
    if cert_reqs is not None:
        context.verify_mode = cert_reqs
    if check_hostname:
        context.check_hostname = True

    if keyfile and not certfile:
        raise ValueError("certfile must be specified")
    if certfile or keyfile:
        context.load_cert_chain(certfile, keyfile)

    # load CA root certs
    if cafile or capath or cadata:
        context.load_verify_locations(cafile, capath, cadata)
    elif context.verify_mode != ssl.CERT_NONE:
        # no explicit cafile, capath or cadata but the verify mode is
        # CERT_OPTIONAL or CERT_REQUIRED. Let's try to load default system
        # root CA certificates for the given purpose. This may fail silently.
        context.load_default_certs(purpose)
    # OpenSSL 1.1.1 keylog file
    if hasattr(context, 'keylog_filename'):
        keylogfile = os.environ.get('SSLKEYLOGFILE')
        if keylogfile and not sys.flags.ignore_environment:
            context.keylog_filename = keylogfile
    return context

class TruststoreAdapter(HTTPAdapter):
    """An adapter for requests <=2.31.0. This supplies a custom ssl_context to a set of requests."""

    custom_context: ssl.SSLContext = None

    def __init__(
        self,
        pool_connections=DEFAULT_POOLSIZE,
        pool_maxsize=DEFAULT_POOLSIZE,
        max_retries=DEFAULT_RETRIES,
        pool_block=DEFAULT_POOLBLOCK,
        ssl_context: ssl.SSLContext | None = None,
        check_hostname: bool = True,
    ):
        if ssl_context is None and check_hostname:
            ssl_context = ssl.create_default_context()
        elif ssl_context is None and check_hostname == False:
            ssl_context = _create_unverified_context_trust_store()# works with ssl._create_unverified_context()
        self.ssl_context = ssl_context
        self.ssl_context.check_hostname = check_hostname
        self.ssl_context.verify_mode = ssl.CERT_NONE if not check_hostname else ssl.CERT_REQUIRED

        super().__init__(
            pool_connections=pool_connections,
            pool_maxsize=pool_maxsize,
            max_retries=max_retries,
            pool_block=pool_block,
        )

    def __str__(self) -> str:
        return f"< {self.__class__.__name__} >"

    def __repr__(self) -> str:
        return f"< {self.__class__.__name__} >"

    def init_poolmanager(self, *args, **kwargs):
        if self.ssl_context:
            kwargs['ssl_context'] = self.ssl_context
        return super(TruststoreAdapter, self).init_poolmanager(*args, **kwargs)

    def proxy_manager_for(self, *args, **kwargs):
        if self.ssl_context:
            kwargs['ssl_context'] = self.ssl_context
        return super(TruststoreAdapter, self).proxy_manager_for(*args, **kwargs)

    def cert_verify(self, conn, url, verify, cert):
        check_hostname = self.ssl_context.check_hostname
        try:
            if verify is False:
                self.ssl_context.check_hostname = False
                self.ssl_context.verify_mode = ssl.CERT_NONE
            return super(TruststoreAdapter, self).cert_verify(conn, url, verify, cert)
        finally:
            self.ssl_context.check_hostname = check_hostname
            if check_hostname:
                self.ssl_context.verify_mode = ssl.CERT_REQUIRED

    def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None):
        check_hostname = self.ssl_context.check_hostname
        try:
            if verify is False:
                self.ssl_context.check_hostname = False
                self.ssl_context.verify_mode = ssl.CERT_NONE
            return super(TruststoreAdapter, self).send(request, stream, timeout, verify, cert, proxies)
        finally:
            self.ssl_context.check_hostname = check_hostname
            if check_hostname:
                self.ssl_context.verify_mode = ssl.CERT_REQUIRED

# Example usage
import requests

session = requests.Session()
adapter = TruststoreAdapter(check_hostname=False)
session.mount('https://', adapter)

response = session.get("https://somesitewithbadcert.com/?f=json", verify=False)# update the URL
print(response.content)

@sethmlarson
Copy link
Owner

Thanks for reporting this issue. Which operating system are you using?

@achapkowski
Copy link
Author

Windows

@achapkowski
Copy link
Author

@sethmlarson let me know if you need anymore information.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants