1# Copyright 2014-present MongoDB, Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you
4# may not use this file except in compliance with the License.  You
5# may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12# implied.  See the License for the specific language governing
13# permissions and limitations under the License.
14
15"""Support for SSL in PyMongo."""
16
17import atexit
18import sys
19import threading
20
21from bson.py3compat import string_type
22from pymongo.errors import ConfigurationError
23
24HAVE_SSL = True
25
26try:
27    import pymongo.pyopenssl_context as _ssl
28except ImportError:
29    try:
30        import pymongo.ssl_context as _ssl
31    except ImportError:
32        HAVE_SSL = False
33
34HAVE_CERTIFI = False
35try:
36    import certifi
37    HAVE_CERTIFI = True
38except ImportError:
39    pass
40
41HAVE_WINCERTSTORE = False
42try:
43    from wincertstore import CertFile
44    HAVE_WINCERTSTORE = True
45except ImportError:
46    pass
47
48_WINCERTSLOCK = threading.Lock()
49_WINCERTS = None
50
51if HAVE_SSL:
52    # Note: The validate* functions below deal with users passing
53    # CPython ssl module constants to configure certificate verification
54    # at a high level. This is legacy behavior, but requires us to
55    # import the ssl module even if we're only using it for this purpose.
56    import ssl as _stdlibssl
57    from ssl import CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED
58    HAS_SNI = _ssl.HAS_SNI
59    IPADDR_SAFE = _ssl.IS_PYOPENSSL or sys.version_info[:2] >= (3, 7)
60    SSLError = _ssl.SSLError
61    def validate_cert_reqs(option, value):
62        """Validate the cert reqs are valid. It must be None or one of the
63        three values ``ssl.CERT_NONE``, ``ssl.CERT_OPTIONAL`` or
64        ``ssl.CERT_REQUIRED``.
65        """
66        if value is None:
67            return value
68        if isinstance(value, string_type) and hasattr(_stdlibssl, value):
69            value = getattr(_stdlibssl, value)
70
71        if value in (CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED):
72            return value
73        raise ValueError("The value of %s must be one of: "
74                         "`ssl.CERT_NONE`, `ssl.CERT_OPTIONAL` or "
75                         "`ssl.CERT_REQUIRED`" % (option,))
76
77    def validate_allow_invalid_certs(option, value):
78        """Validate the option to allow invalid certificates is valid."""
79        # Avoid circular import.
80        from pymongo.common import validate_boolean_or_string
81        boolean_cert_reqs = validate_boolean_or_string(option, value)
82        if boolean_cert_reqs:
83            return CERT_NONE
84        return CERT_REQUIRED
85
86    def _load_wincerts():
87        """Set _WINCERTS to an instance of wincertstore.Certfile."""
88        global _WINCERTS
89
90        certfile = CertFile()
91        certfile.addstore("CA")
92        certfile.addstore("ROOT")
93        atexit.register(certfile.close)
94
95        _WINCERTS = certfile
96
97    def get_ssl_context(*args):
98        """Create and return an SSLContext object."""
99        (certfile,
100         keyfile,
101         passphrase,
102         ca_certs,
103         cert_reqs,
104         crlfile,
105         match_hostname,
106         check_ocsp_endpoint) = args
107        verify_mode = CERT_REQUIRED if cert_reqs is None else cert_reqs
108        ctx = _ssl.SSLContext(_ssl.PROTOCOL_SSLv23)
109        # SSLContext.check_hostname was added in CPython 2.7.9 and 3.4.
110        if hasattr(ctx, "check_hostname"):
111            if _ssl.CHECK_HOSTNAME_SAFE and verify_mode != CERT_NONE:
112                ctx.check_hostname = match_hostname
113            else:
114                ctx.check_hostname = False
115        if hasattr(ctx, "check_ocsp_endpoint"):
116            ctx.check_ocsp_endpoint = check_ocsp_endpoint
117        if hasattr(ctx, "options"):
118            # Explicitly disable SSLv2, SSLv3 and TLS compression. Note that
119            # up to date versions of MongoDB 2.4 and above already disable
120            # SSLv2 and SSLv3, python disables SSLv2 by default in >= 2.7.7
121            # and >= 3.3.4 and SSLv3 in >= 3.4.3.
122            ctx.options |= _ssl.OP_NO_SSLv2
123            ctx.options |= _ssl.OP_NO_SSLv3
124            ctx.options |= _ssl.OP_NO_COMPRESSION
125            ctx.options |= _ssl.OP_NO_RENEGOTIATION
126        if certfile is not None:
127            try:
128                ctx.load_cert_chain(certfile, keyfile, passphrase)
129            except _ssl.SSLError as exc:
130                raise ConfigurationError(
131                    "Private key doesn't match certificate: %s" % (exc,))
132        if crlfile is not None:
133            if _ssl.IS_PYOPENSSL:
134                raise ConfigurationError(
135                    "ssl_crlfile cannot be used with PyOpenSSL")
136            if not hasattr(ctx, "verify_flags"):
137                raise ConfigurationError(
138                    "Support for ssl_crlfile requires "
139                    "python 2.7.9+ (pypy 2.5.1+) or  3.4+")
140            # Match the server's behavior.
141            ctx.verify_flags = getattr(_ssl, "VERIFY_CRL_CHECK_LEAF", 0)
142            ctx.load_verify_locations(crlfile)
143        if ca_certs is not None:
144            ctx.load_verify_locations(ca_certs)
145        elif cert_reqs != CERT_NONE:
146            # CPython >= 2.7.9 or >= 3.4.0, pypy >= 2.5.1
147            if hasattr(ctx, "load_default_certs"):
148                ctx.load_default_certs()
149            # Python >= 3.2.0, useless on Windows.
150            elif (sys.platform != "win32" and
151                  hasattr(ctx, "set_default_verify_paths")):
152                ctx.set_default_verify_paths()
153            elif sys.platform == "win32" and HAVE_WINCERTSTORE:
154                with _WINCERTSLOCK:
155                    if _WINCERTS is None:
156                        _load_wincerts()
157                ctx.load_verify_locations(_WINCERTS.name)
158            elif HAVE_CERTIFI:
159                ctx.load_verify_locations(certifi.where())
160            else:
161                raise ConfigurationError(
162                    "`ssl_cert_reqs` is not ssl.CERT_NONE and no system "
163                    "CA certificates could be loaded. `ssl_ca_certs` is "
164                    "required.")
165        ctx.verify_mode = verify_mode
166        return ctx
167else:
168    class SSLError(Exception):
169        pass
170    HAS_SNI = False
171    IPADDR_SAFE = False
172    def validate_cert_reqs(option, dummy):
173        """No ssl module, raise ConfigurationError."""
174        raise ConfigurationError("The value of %s is set but can't be "
175                                 "validated. The ssl module is not available"
176                                 % (option,))
177
178    def validate_allow_invalid_certs(option, dummy):
179        """No ssl module, raise ConfigurationError."""
180        return validate_cert_reqs(option, dummy)
181
182    def get_ssl_context(*dummy):
183        """No ssl module, raise ConfigurationError."""
184        raise ConfigurationError("The ssl module is not available.")
185