From 61d478c71c5341cdc54e6bfb4ace4252852fd972 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Sat, 27 Jan 2018 15:51:38 +0100 Subject: [PATCH] bpo-31399: Let OpenSSL verify hostname and IP address (#3462) bpo-31399: Let OpenSSL verify hostname and IP The ssl module now uses OpenSSL's X509_VERIFY_PARAM_set1_host() and X509_VERIFY_PARAM_set1_ip() API to verify hostname and IP addresses. * Remove match_hostname calls * Check for libssl with set1_host, libssl must provide X509_VERIFY_PARAM_set1_host() * Add documentation for OpenSSL 1.0.2 requirement * Don't support OpenSSL special mode with a leading dot, e.g. ".example.org" matches "www.example.org". It's not standard conform. * Add hostname_checks_common_name Signed-off-by: Christian Heimes --- Doc/library/ssl.rst | 44 ++++- Doc/whatsnew/3.7.rst | 32 ++++ Lib/asyncio/sslproto.py | 6 - Lib/http/client.py | 10 +- Lib/ssl.py | 29 ++- Lib/test/test_asyncio/test_events.py | 6 +- Lib/test/test_ftplib.py | 3 + Lib/test/test_imaplib.py | 6 +- Lib/test/test_poplib.py | 3 + Lib/test/test_ssl.py | 28 ++- Lib/test/test_urllib2_localnet.py | 2 +- .../2017-09-08-14-05-33.bpo-31399.FtBrrt.rst | 4 + Modules/_ssl.c | 172 ++++++++++++++---- PC/pyconfig.h | 3 + setup.py | 27 ++- 15 files changed, 302 insertions(+), 73 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2017-09-08-14-05-33.bpo-31399.FtBrrt.rst diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 4c44ffa23ac..aa1075d4b02 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -146,9 +146,10 @@ Functions, Constants, and Exceptions .. exception:: CertificateError - Raised to signal an error with a certificate (such as mismatching - hostname). Certificate errors detected by OpenSSL, though, raise - an :exc:`SSLCertVerificationError`. + An alias for :exc:`SSLCertVerificationError`. + + .. versionchanged:: 3.7 + The exception is now an alias for :exc:`SSLCertVerificationError`. Socket creation @@ -430,8 +431,14 @@ Certificate handling of the certificate, is now supported. .. versionchanged:: 3.7 + The function is no longer used to TLS connections. Hostname matching + is now performed by OpenSSL. + Allow wildcard when it is the leftmost and the only character - in that segment. + in that segment. Partial wildcards like ``www*.example.com`` are no + longer supported. + + .. deprecated:: 3.7 .. function:: cert_time_to_seconds(cert_time) @@ -850,6 +857,14 @@ Constants .. versionadded:: 3.5 +.. data:: HAS_NEVER_CHECK_COMMON_NAME + + Whether the OpenSSL library has built-in support not checking subject + common name and :attr:`SSLContext.hostname_checks_common_name` is + writeable. + + .. versionadded:: 3.7 + .. data:: HAS_ECDH Whether the OpenSSL library has built-in support for Elliptic Curve-based @@ -1075,6 +1090,12 @@ SSL sockets also have the following additional methods and attributes: The socket timeout is no more reset each time bytes are received or sent. The socket timeout is now to maximum total duration of the handshake. + .. versionchanged:: 3.7 + Hostname or IP address is matched by OpenSSL during handshake. The + function :func:`match_hostname` is no longer used. In case OpenSSL + refuses a hostname or IP address, the handshake is aborted early and + a TLS alert message is send to the peer. + .. method:: SSLSocket.getpeercert(binary_form=False) If there is no certificate for the peer on the other end of the connection, @@ -1730,6 +1751,17 @@ to speed up repeated connections from the same clients. The protocol version chosen when constructing the context. This attribute is read-only. +.. attribute:: SSLContext.hostname_checks_common_name + + Whether :attr:`~SSLContext.check_hostname` falls back to verify the cert's + subject common name in the absence of a subject alternative name + extension (default: true). + + .. versionadded:: 3.7 + + .. note:: + Only writeable with OpenSSL 1.1.0 or higher. + .. attribute:: SSLContext.verify_flags The flags for certificate verification operations. You can set flags like @@ -2324,6 +2356,10 @@ in this case, the :func:`match_hostname` function can be used. This common check is automatically performed when :attr:`SSLContext.check_hostname` is enabled. +.. versionchanged:: 3.7 + Hostname matchings is now performed by OpenSSL. Python no longer uses + :func:`match_hostname`. + In server mode, if you want to authenticate your clients using the SSL layer (rather than using a higher-level authentication mechanism), you'll also have to specify :const:`CERT_REQUIRED` and similarly check the client certificate. diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst index 133975a04dd..1ece6a3befd 100644 --- a/Doc/whatsnew/3.7.rst +++ b/Doc/whatsnew/3.7.rst @@ -568,6 +568,32 @@ can be set within the scope of a group. ``'^$'`` or ``(?=-)`` that matches an empty string. (Contributed by Serhiy Storchaka in :issue:`25054`.) +ssl +--- + +The ssl module now uses OpenSSL's builtin API instead of +:func:`~ssl.match_hostname` to check host name or IP address. Values +are validated during TLS handshake. Any cert validation error including +a failing host name match now raises :exc:`~ssl.SSLCertVerificationError` and +aborts the handshake with a proper TLS Alert message. The new exception +contains additional information. Host name validation can be customized +with :attr:`~ssl.SSLContext.host_flags`. +(Contributed by Christian Heimes in :issue:`31399`.) + +.. note:: + The improved host name check requires an OpenSSL 1.0.2 or 1.1 compatible + libssl. OpenSSL 0.9.8 and 1.0.1 are no longer supported. LibreSSL is + temporarily not supported until it gains the necessary OpenSSL 1.0.2 APIs. + +The ssl module no longer sends IP addresses in SNI TLS extension. +(Contributed by Christian Heimes in :issue:`32185`.) + +:func:`~ssl.match_hostname` no longer supports partial wildcards like +``www*.example.org``. :attr:`~ssl.SSLContext.host_flags` has partial +wildcard matching disabled by default. +(Contributed by Mandeep Singh in :issue:`23033` and Christian Heimes in +:issue:`31399`.) + string ------ @@ -1120,6 +1146,12 @@ Other CPython implementation changes emitted in the first place), and an explicit ``error::BytesWarning`` warnings filter added to convert them to exceptions. +* CPython' :mod:`ssl` module requires OpenSSL 1.0.2 or 1.1 compatible libssl. + OpenSSL 1.0.1 has reached end of lifetime on 2016-12-31 and is no longer + supported. LibreSSL is temporarily not supported as well. LibreSSL releases + up to version 2.6.4 are missing required OpenSSL 1.0.2 APIs. + + Documentation ============= diff --git a/Lib/asyncio/sslproto.py b/Lib/asyncio/sslproto.py index 2d377c4ae39..1130bced8ae 100644 --- a/Lib/asyncio/sslproto.py +++ b/Lib/asyncio/sslproto.py @@ -590,12 +590,6 @@ class SSLProtocol(protocols.Protocol): raise handshake_exc peercert = sslobj.getpeercert() - if not hasattr(self._sslcontext, 'check_hostname'): - # Verify hostname if requested, Python 3.4+ uses check_hostname - # and checks the hostname in do_handshake() - if (self._server_hostname and - self._sslcontext.verify_mode != ssl.CERT_NONE): - ssl.match_hostname(peercert, self._server_hostname) except BaseException as exc: if self._loop.get_debug(): if isinstance(exc, ssl.CertificateError): diff --git a/Lib/http/client.py b/Lib/http/client.py index 1a852cd76e4..1292db74784 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -1375,7 +1375,8 @@ else: if key_file or cert_file: context.load_cert_chain(cert_file, key_file) self._context = context - self._check_hostname = check_hostname + if check_hostname is not None: + self._context.check_hostname = check_hostname def connect(self): "Connect to a host on a given (SSL) port." @@ -1389,13 +1390,6 @@ else: self.sock = self._context.wrap_socket(self.sock, server_hostname=server_hostname) - if not self._context.check_hostname and self._check_hostname: - try: - ssl.match_hostname(self.sock.getpeercert(), server_hostname) - except Exception: - self.sock.shutdown(socket.SHUT_RDWR) - self.sock.close() - raise __all__.append("HTTPSConnection") diff --git a/Lib/ssl.py b/Lib/ssl.py index 7c4cccf4c0c..5f972e1d3d8 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -148,7 +148,6 @@ _IntEnum._convert( lambda name: name.startswith('CERT_'), source=_ssl) - PROTOCOL_SSLv23 = _SSLMethod.PROTOCOL_SSLv23 = _SSLMethod.PROTOCOL_TLS _PROTOCOL_NAMES = {value: name for name, value in _SSLMethod.__members__.items()} @@ -172,6 +171,8 @@ if _ssl.HAS_TLS_UNIQUE: else: CHANNEL_BINDING_TYPES = [] +HAS_NEVER_CHECK_COMMON_NAME = hasattr(_ssl, 'HOSTFLAG_NEVER_CHECK_SUBJECT') + # Disable weak or insecure ciphers by default # (OpenSSL's default setting is 'DEFAULT:!aNULL:!eNULL') @@ -216,9 +217,7 @@ _RESTRICTED_SERVER_CIPHERS = ( '!aNULL:!eNULL:!MD5:!DSS:!RC4:!3DES' ) - -class CertificateError(ValueError): - pass +CertificateError = SSLCertVerificationError def _dnsname_match(dn, hostname): @@ -473,6 +472,23 @@ class SSLContext(_SSLContext): def options(self, value): super(SSLContext, SSLContext).options.__set__(self, value) + if hasattr(_ssl, 'HOSTFLAG_NEVER_CHECK_SUBJECT'): + @property + def hostname_checks_common_name(self): + ncs = self._host_flags & _ssl.HOSTFLAG_NEVER_CHECK_SUBJECT + return ncs != _ssl.HOSTFLAG_NEVER_CHECK_SUBJECT + + @hostname_checks_common_name.setter + def hostname_checks_common_name(self, value): + if value: + self._host_flags &= ~_ssl.HOSTFLAG_NEVER_CHECK_SUBJECT + else: + self._host_flags |= _ssl.HOSTFLAG_NEVER_CHECK_SUBJECT + else: + @property + def hostname_checks_common_name(self): + return True + @property def verify_flags(self): return VerifyFlags(super().verify_flags) @@ -699,11 +715,6 @@ class SSLObject: def do_handshake(self): """Start the SSL/TLS handshake.""" self._sslobj.do_handshake() - if self.context.check_hostname: - if not self.server_hostname: - raise ValueError("check_hostname needs server_hostname " - "argument") - match_hostname(self.getpeercert(), self.server_hostname) def unwrap(self): """Start the SSL shutdown handshake.""" diff --git a/Lib/test/test_asyncio/test_events.py b/Lib/test/test_asyncio/test_events.py index e4b053681b4..cf217538a06 100644 --- a/Lib/test/test_asyncio/test_events.py +++ b/Lib/test/test_asyncio/test_events.py @@ -1148,11 +1148,13 @@ class EventLoopTestsMixin: with test_utils.disable_logger(): with self.assertRaisesRegex( ssl.CertificateError, - "hostname '127.0.0.1' doesn't match 'localhost'"): + "IP address mismatch, certificate is not valid for " + "'127.0.0.1'"): self.loop.run_until_complete(f_c) # close connection - proto.transport.close() + # transport is None because TLS ALERT aborted the handshake + self.assertIsNone(proto.transport) server.close() @support.skip_unless_bind_unix_socket diff --git a/Lib/test/test_ftplib.py b/Lib/test/test_ftplib.py index f1b0185b2bf..bb5a67f27e3 100644 --- a/Lib/test/test_ftplib.py +++ b/Lib/test/test_ftplib.py @@ -330,6 +330,9 @@ if ssl is not None: return elif err.args[0] == ssl.SSL_ERROR_EOF: return self.handle_close() + # TODO: SSLError does not expose alert information + elif "SSLV3_ALERT_BAD_CERTIFICATE" in err.args[1]: + return self.handle_close() raise except OSError as err: if err.args[0] == errno.ECONNABORTED: diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py index 4a45be65724..f16bacd0006 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -485,7 +485,8 @@ class NewIMAPSSLTests(NewIMAPTestsMixin, unittest.TestCase): ssl_context.load_verify_locations(CAFILE) with self.assertRaisesRegex(ssl.CertificateError, - "hostname '127.0.0.1' doesn't match 'localhost'"): + "IP address mismatch, certificate is not valid for " + "'127.0.0.1'"): _, server = self._setup(SimpleIMAPHandler) client = self.imap_class(*server.server_address, ssl_context=ssl_context) @@ -874,7 +875,8 @@ class ThreadedNetworkedTestsSSL(ThreadedNetworkedTests): with self.assertRaisesRegex( ssl.CertificateError, - "hostname '127.0.0.1' doesn't match 'localhost'"): + "IP address mismatch, certificate is not valid for " + "'127.0.0.1'"): with self.reaped_server(SimpleIMAPHandler) as server: client = self.imap_class(*server.server_address, ssl_context=ssl_context) diff --git a/Lib/test/test_poplib.py b/Lib/test/test_poplib.py index 9ba678f2039..4d7a4394086 100644 --- a/Lib/test/test_poplib.py +++ b/Lib/test/test_poplib.py @@ -176,6 +176,9 @@ class DummyPOP3Handler(asynchat.async_chat): return elif err.args[0] == ssl.SSL_ERROR_EOF: return self.handle_close() + # TODO: SSLError does not expose alert information + elif "SSLV3_ALERT_BAD_CERTIFICATE" in err.args[1]: + return self.handle_close() raise except OSError as err: if err.args[0] == errno.ECONNABORTED: diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index e3fa4233ed6..fdf727f788c 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -988,6 +988,19 @@ class ContextTests(unittest.TestCase): self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED) self.assertTrue(ctx.check_hostname) + def test_hostname_checks_common_name(self): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + self.assertTrue(ctx.hostname_checks_common_name) + if ssl.HAS_NEVER_CHECK_COMMON_NAME: + ctx.hostname_checks_common_name = True + self.assertTrue(ctx.hostname_checks_common_name) + ctx.hostname_checks_common_name = False + self.assertFalse(ctx.hostname_checks_common_name) + ctx.hostname_checks_common_name = True + self.assertTrue(ctx.hostname_checks_common_name) + else: + with self.assertRaises(AttributeError): + ctx.hostname_checks_common_name = True @unittest.skipUnless(have_verify_flags(), "verify_flags need OpenSSL > 0.9.8") @@ -1511,6 +1524,16 @@ class SSLErrorTests(unittest.TestCase): ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), server_hostname="xn--.com") + def test_bad_server_hostname(self): + ctx = ssl.create_default_context() + with self.assertRaises(ValueError): + ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), + server_hostname="") + with self.assertRaises(ValueError): + ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), + server_hostname=".example.org") + + class MemoryBIOTests(unittest.TestCase): def test_read_write(self): @@ -2536,8 +2559,9 @@ class ThreadedTests(unittest.TestCase): with server: with client_context.wrap_socket(socket.socket(), server_hostname="invalid") as s: - with self.assertRaisesRegex(ssl.CertificateError, - "hostname 'invalid' doesn't match 'localhost'"): + with self.assertRaisesRegex( + ssl.CertificateError, + "Hostname mismatch, certificate is not valid for 'invalid'."): s.connect((HOST, server.port)) # missing server_hostname arg should cause an exception, too diff --git a/Lib/test/test_urllib2_localnet.py b/Lib/test/test_urllib2_localnet.py index b2d1e5f9804..52c897af433 100644 --- a/Lib/test/test_urllib2_localnet.py +++ b/Lib/test/test_urllib2_localnet.py @@ -573,7 +573,7 @@ class TestUrlopen(unittest.TestCase): cafile=CERT_fakehostname) # Good cert, but mismatching hostname handler = self.start_https_server(certfile=CERT_fakehostname) - with self.assertRaises(ssl.CertificateError) as cm: + with self.assertRaises(urllib.error.URLError) as cm: self.urlopen("https://localhost:%s/bizarre" % handler.port, cafile=CERT_fakehostname) diff --git a/Misc/NEWS.d/next/Library/2017-09-08-14-05-33.bpo-31399.FtBrrt.rst b/Misc/NEWS.d/next/Library/2017-09-08-14-05-33.bpo-31399.FtBrrt.rst new file mode 100644 index 00000000000..e50ce2a1a93 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2017-09-08-14-05-33.bpo-31399.FtBrrt.rst @@ -0,0 +1,4 @@ +The ssl module now uses OpenSSL's X509_VERIFY_PARAM_set1_host() and +X509_VERIFY_PARAM_set1_ip() API to verify hostname and IP addresses. Subject +common name fallback can be disabled with +SSLContext.hostname_checks_common_name. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index c5eec7eded6..ec8c8af54bd 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -64,10 +64,13 @@ static PySocketModule_APIObject PySocketModule; #include "openssl/rand.h" #include "openssl/bio.h" -/* Set HAVE_X509_VERIFY_PARAM_SET1_HOST for non-autoconf builds */ #ifndef HAVE_X509_VERIFY_PARAM_SET1_HOST -# if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER > 0x1000200fL +# ifdef LIBRESSL_VERSION_NUMBER +# error "LibreSSL is missing X509_VERIFY_PARAM_set1_host(), see https://github.com/libressl-portable/portable/issues/381" +# elif OPENSSL_VERSION_NUMBER > 0x1000200fL # define HAVE_X509_VERIFY_PARAM_SET1_HOST +# else +# error "libssl is too old and does not support X509_VERIFY_PARAM_set1_host()" # endif #endif @@ -217,11 +220,6 @@ static STACK_OF(X509_OBJECT) *X509_STORE_get0_objects(X509_STORE *store) { return store->objs; } -static X509_VERIFY_PARAM *X509_STORE_get0_param(X509_STORE *store) -{ - return store->param; -} - static int SSL_SESSION_has_ticket(const SSL_SESSION *s) { @@ -317,6 +315,10 @@ typedef struct { PyObject *set_hostname; #endif int check_hostname; + /* OpenSSL has no API to get hostflags from X509_VERIFY_PARAM* struct. + * We have to maintain our own copy. OpenSSL's hostflags default to 0. + */ + unsigned int hostflags; } PySSLContext; typedef struct { @@ -701,6 +703,74 @@ _setSSLError (const char *errstr, int errcode, const char *filename, int lineno) return NULL; } +/* + * SSL objects + */ + +static int +_ssl_configure_hostname(PySSLSocket *self, const char* server_hostname) +{ + int retval = -1; + ASN1_OCTET_STRING *ip; + PyObject *hostname; + size_t len; + + assert(server_hostname); + + /* Disable OpenSSL's special mode with leading dot in hostname: + * When name starts with a dot (e.g ".example.com"), it will be + * matched by a certificate valid for any sub-domain of name. + */ + len = strlen(server_hostname); + if (len == 0 || *server_hostname == '.') { + PyErr_SetString( + PyExc_ValueError, + "server_hostname cannot be an empty string or start with a " + "leading dot."); + return retval; + } + + /* inet_pton is not available on all platforms. */ + ip = a2i_IPADDRESS(server_hostname); + if (ip == NULL) { + ERR_clear_error(); + } + + hostname = PyUnicode_Decode(server_hostname, len, "idna", "strict"); + if (hostname == NULL) { + goto error; + } + self->server_hostname = hostname; + + /* Only send SNI extension for non-IP hostnames */ + if (ip == NULL) { + if (!SSL_set_tlsext_host_name(self->ssl, server_hostname)) { + _setSSLError(NULL, 0, __FILE__, __LINE__); + } + } + if (self->ctx->check_hostname) { + X509_VERIFY_PARAM *param = SSL_get0_param(self->ssl); + if (ip == NULL) { + if (!X509_VERIFY_PARAM_set1_host(param, server_hostname, 0)) { + _setSSLError(NULL, 0, __FILE__, __LINE__); + goto error; + } + } else { + if (!X509_VERIFY_PARAM_set1_ip(param, ASN1_STRING_data(ip), + ASN1_STRING_length(ip))) { + _setSSLError(NULL, 0, __FILE__, __LINE__); + goto error; + } + } + } + retval = 0; + error: + if (ip != NULL) { + ASN1_OCTET_STRING_free(ip); + } + return retval; +} + static PySSLSocket * newPySSLSocket(PySSLContext *sslctx, PySocketSockObject *sock, enum py_ssl_server_or_client socket_type, @@ -722,15 +792,6 @@ newPySSLSocket(PySSLContext *sslctx, PySocketSockObject *sock, self->shutdown_seen_zero = 0; self->owner = NULL; self->server_hostname = NULL; - if (server_hostname != NULL) { - PyObject *hostname = PyUnicode_Decode(server_hostname, strlen(server_hostname), - "idna", "strict"); - if (hostname == NULL) { - Py_DECREF(self); - return NULL; - } - self->server_hostname = hostname; - } self->ssl_errno = 0; self->c_errno = 0; #ifdef MS_WINDOWS @@ -761,10 +822,12 @@ newPySSLSocket(PySSLContext *sslctx, PySocketSockObject *sock, #endif SSL_set_mode(self->ssl, mode); -#if HAVE_SNI - if (server_hostname != NULL) - SSL_set_tlsext_host_name(self->ssl, server_hostname); -#endif + if (server_hostname != NULL) { + if (_ssl_configure_hostname(self, server_hostname) < 0) { + Py_DECREF(self); + return NULL; + } + } /* If the socket is in non-blocking mode or timeout mode, set the BIO * to non-blocking mode (blocking is the default) */ @@ -2711,6 +2774,7 @@ _ssl__SSLContext_impl(PyTypeObject *type, int proto_version) PySSLContext *self; long options; SSL_CTX *ctx = NULL; + X509_VERIFY_PARAM *params; int result; #if defined(SSL_MODE_RELEASE_BUFFERS) unsigned long libver; @@ -2760,6 +2824,7 @@ _ssl__SSLContext_impl(PyTypeObject *type, int proto_version) return NULL; } self->ctx = ctx; + self->hostflags = X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS; #if defined(OPENSSL_NPN_NEGOTIATED) && !defined(OPENSSL_NO_NEXTPROTONEG) self->npn_protocols = NULL; #endif @@ -2858,14 +2923,13 @@ _ssl__SSLContext_impl(PyTypeObject *type, int proto_version) sizeof(SID_CTX)); #undef SID_CTX + params = SSL_CTX_get0_param(self->ctx); #ifdef X509_V_FLAG_TRUSTED_FIRST - { - /* Improve trust chain building when cross-signed intermediate - certificates are present. See https://bugs.python.org/issue23476. */ - X509_STORE *store = SSL_CTX_get_cert_store(self->ctx); - X509_STORE_set_flags(store, X509_V_FLAG_TRUSTED_FIRST); - } + /* Improve trust chain building when cross-signed intermediate + certificates are present. See https://bugs.python.org/issue23476. */ + X509_VERIFY_PARAM_set_flags(params, X509_V_FLAG_TRUSTED_FIRST); #endif + X509_VERIFY_PARAM_set_hostflags(params, self->hostflags); return (PyObject *)self; } @@ -3152,12 +3216,10 @@ set_verify_mode(PySSLContext *self, PyObject *arg, void *c) static PyObject * get_verify_flags(PySSLContext *self, void *c) { - X509_STORE *store; X509_VERIFY_PARAM *param; unsigned long flags; - store = SSL_CTX_get_cert_store(self->ctx); - param = X509_STORE_get0_param(store); + param = SSL_CTX_get0_param(self->ctx); flags = X509_VERIFY_PARAM_get_flags(param); return PyLong_FromUnsignedLong(flags); } @@ -3165,14 +3227,12 @@ get_verify_flags(PySSLContext *self, void *c) static int set_verify_flags(PySSLContext *self, PyObject *arg, void *c) { - X509_STORE *store; X509_VERIFY_PARAM *param; unsigned long new_flags, flags, set, clear; if (!PyArg_Parse(arg, "k", &new_flags)) return -1; - store = SSL_CTX_get_cert_store(self->ctx); - param = X509_STORE_get0_param(store); + param = SSL_CTX_get0_param(self->ctx); flags = X509_VERIFY_PARAM_get_flags(param); clear = flags & ~new_flags; set = ~flags & new_flags; @@ -3220,6 +3280,27 @@ set_options(PySSLContext *self, PyObject *arg, void *c) return 0; } +static PyObject * +get_host_flags(PySSLContext *self, void *c) +{ + return PyLong_FromUnsignedLong(self->hostflags); +} + +static int +set_host_flags(PySSLContext *self, PyObject *arg, void *c) +{ + X509_VERIFY_PARAM *param; + unsigned int new_flags = 0; + + if (!PyArg_Parse(arg, "I", &new_flags)) + return -1; + + param = SSL_CTX_get0_param(self->ctx); + self->hostflags = new_flags; + X509_VERIFY_PARAM_set_hostflags(param, new_flags); + return 0; +} + static PyObject * get_check_hostname(PySSLContext *self, void *c) { @@ -4104,6 +4185,8 @@ _ssl__SSLContext_get_ca_certs_impl(PySSLContext *self, int binary_form) static PyGetSetDef context_getsetlist[] = { {"check_hostname", (getter) get_check_hostname, (setter) set_check_hostname, NULL}, + {"_host_flags", (getter) get_host_flags, + (setter) set_host_flags, NULL}, {"options", (getter) get_options, (setter) set_options, NULL}, {"verify_flags", (getter) get_verify_flags, @@ -5491,6 +5574,31 @@ PyInit__ssl(void) SSL_OP_NO_COMPRESSION); #endif +#ifdef X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT + PyModule_AddIntConstant(m, "HOSTFLAG_ALWAYS_CHECK_SUBJECT", + X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT); +#endif +#ifdef X509_CHECK_FLAG_NEVER_CHECK_SUBJECT + PyModule_AddIntConstant(m, "HOSTFLAG_NEVER_CHECK_SUBJECT", + X509_CHECK_FLAG_NEVER_CHECK_SUBJECT); +#endif +#ifdef X509_CHECK_FLAG_NO_WILDCARDS + PyModule_AddIntConstant(m, "HOSTFLAG_NO_WILDCARDS", + X509_CHECK_FLAG_NO_WILDCARDS); +#endif +#ifdef X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS + PyModule_AddIntConstant(m, "HOSTFLAG_NO_PARTIAL_WILDCARDS", + X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); +#endif +#ifdef X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS + PyModule_AddIntConstant(m, "HOSTFLAG_MULTI_LABEL_WILDCARDS", + X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS); +#endif +#ifdef X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS + PyModule_AddIntConstant(m, "HOSTFLAG_SINGLE_LABEL_SUBDOMAINS", + X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS); +#endif + #if HAVE_SNI r = Py_True; #else diff --git a/PC/pyconfig.h b/PC/pyconfig.h index db745dee761..d2a3f5dd39b 100644 --- a/PC/pyconfig.h +++ b/PC/pyconfig.h @@ -687,4 +687,7 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */ /* framework name */ #define _PYTHONFRAMEWORK "" +/* Define if libssl has X509_VERIFY_PARAM_set1_host and related function */ +#define HAVE_X509_VERIFY_PARAM_SET1_HOST 1 + #endif /* !Py_CONFIG_H */ diff --git a/setup.py b/setup.py index a6f4488cc99..ba0a7624cfc 100644 --- a/setup.py +++ b/setup.py @@ -363,6 +363,16 @@ class PyBuildExt(build_ext): print_three_column(failed) print() + if any('_ssl' in l + for l in (missing, self.failed, self.failed_on_import)): + print() + print("Could not build the ssl module!") + print("Python requires an OpenSSL 1.0.2 or 1.1 compatible " + "libssl with X509_VERIFY_PARAM_set1_host().") + print("LibreSSL 2.6.4 and earlier do not provide the necessary " + "APIs, https://github.com/libressl-portable/portable/issues/381") + print() + def build_extension(self, ext): if ext.name == '_ctypes': @@ -2144,13 +2154,16 @@ class PyBuildExt(build_ext): if krb5_h: ssl_incs.extend(krb5_h) - ssl_ext = Extension( - '_ssl', ['_ssl.c'], - include_dirs=openssl_includes, - library_dirs=openssl_libdirs, - libraries=openssl_libs, - depends=['socketmodule.h'] - ) + if config_vars.get("HAVE_X509_VERIFY_PARAM_SET1_HOST"): + ssl_ext = Extension( + '_ssl', ['_ssl.c'], + include_dirs=openssl_includes, + library_dirs=openssl_libdirs, + libraries=openssl_libs, + depends=['socketmodule.h'] + ) + else: + ssl_ext = None hashlib_ext = Extension( '_hashlib', ['_hashopenssl.c'],