diff -r a6ea3714774f Lib/ftplib.py --- a/Lib/ftplib.py Mon Nov 25 08:29:54 2013 +0100 +++ b/Lib/ftplib.py Mon Nov 25 10:43:39 2013 +0100 @@ -718,7 +718,8 @@ def __init__(self, host='', user='', passwd='', acct='', keyfile=None, certfile=None, context=None, - timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None): + timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None, + check_hostname=False): if context is not None and keyfile is not None: raise ValueError("context and keyfile arguments are mutually " "exclusive") @@ -731,7 +732,14 @@ context = ssl._create_stdlib_context(self.ssl_version, certfile=certfile, keyfile=keyfile) + will_verify = context.verify_mode != ssl.CERT_NONE + if check_hostname is None: + check_hostname = will_verify + elif check_hostname and not will_verify: + raise ValueError("check_hostname needs a SSL context with " + "either CERT_OPTIONAL or CERT_REQUIRED") self.context = context + self._check_hostname = check_hostname self._prot_p = False FTP.__init__(self, host, user, passwd, acct, timeout, source_address) @@ -748,7 +756,13 @@ resp = self.voidcmd('AUTH TLS') else: resp = self.voidcmd('AUTH SSL') - self.sock = self.context.wrap_socket(self.sock) + try: + self.sock = self.context.wrap_socket(self.sock) + if self._check_hostname: + ssl.match_hostname(self.sock.getpeercert(), self.host) + except Exception: + self.sock.close() + raise self.file = self.sock.makefile(mode='r', encoding=self.encoding) return resp @@ -787,7 +801,13 @@ def ntransfercmd(self, cmd, rest=None): conn, size = FTP.ntransfercmd(self, cmd, rest) if self._prot_p: - conn = self.context.wrap_socket(conn) + try: + conn = self.context.wrap_socket(conn) + if self._check_hostname: + ssl.match_hostname(conn.getpeercert(), self.host) + except Exception: + conn.close() + raise return conn, size def abort(self): diff -r a6ea3714774f Lib/imaplib.py --- a/Lib/imaplib.py Mon Nov 25 08:29:54 2013 +0100 +++ b/Lib/imaplib.py Mon Nov 25 10:43:39 2013 +0100 @@ -732,7 +732,7 @@ return self._untagged_response(typ, dat, name) - def starttls(self, ssl_context=None): + def starttls(self, ssl_context=None, check_hostname=False): name = 'STARTTLS' if not HAVE_SSL: raise self.error('SSL support missing') @@ -743,9 +743,21 @@ # Generate a default SSL context if none was passed. if ssl_context is None: ssl_context = ssl._create_stdlib_context() + will_verify = ssl_context.verify_mode != ssl.CERT_NONE + if check_hostname is None: + check_hostname = will_verify + elif check_hostname and not will_verify: + raise ValueError("check_hostname needs a SSL context with " + "either CERT_OPTIONAL or CERT_REQUIRED") typ, dat = self._simple_command(name) if typ == 'OK': - self.sock = ssl_context.wrap_socket(self.sock) + try: + self.sock = ssl_context.wrap_socket(self.sock) + if check_hostname: + ssl.match_hostname(self.sock.getpeercert(), self.host) + except Exception: + self.sock.close() + raise self.file = self.sock.makefile('rb') self._tls_established = True self._get_capabilities() @@ -1198,7 +1210,8 @@ """ - def __init__(self, host='', port=IMAP4_SSL_PORT, keyfile=None, certfile=None, ssl_context=None): + def __init__(self, host='', port=IMAP4_SSL_PORT, keyfile=None, + certfile=None, ssl_context=None, check_hostname): if ssl_context is not None and keyfile is not None: raise ValueError("ssl_context and keyfile arguments are mutually " "exclusive") @@ -1211,12 +1224,26 @@ if ssl_context is None: ssl_context = ssl._create_stdlib_context(certfile=certfile, keyfile=keyfile) + will_verify = ssl_context.verify_mode != ssl.CERT_NONE + if check_hostname is None: + check_hostname = will_verify + elif check_hostname and not will_verify: + raise ValueError("check_hostname needs a SSL context with " + "either CERT_OPTIONAL or CERT_REQUIRED") self.ssl_context = ssl_context + self._check_hostname = check_hostname IMAP4.__init__(self, host, port) def _create_socket(self): sock = IMAP4._create_socket(self) - return self.ssl_context.wrap_socket(sock) + try: + sock = self.ssl_context.wrap_socket(sock) + if self._check_hostname: + ssl.match_hostname(sock.getpeercert(), self.host) + except Exception: + sock.close() + raise + return sock def open(self, host='', port=IMAP4_SSL_PORT): """Setup connection to remote server on "host:port". diff -r a6ea3714774f Lib/nntplib.py --- a/Lib/nntplib.py Mon Nov 25 08:29:54 2013 +0100 +++ b/Lib/nntplib.py Mon Nov 25 10:43:39 2013 +0100 @@ -279,17 +279,32 @@ if _have_ssl: - def _encrypt_on(sock, context): + def _encrypt_on(sock, context, check_hostname, hostname): """Wrap a socket in SSL/TLS. Arguments: - sock: Socket to wrap - context: SSL context to use for the encrypted connection + - check_hostname: boolean + - hostname: Hostname to match Returns: - sock: New, encrypted socket. """ # Generate a default SSL context if none was passed. if context is None: context = ssl._create_stdlib_context() - return context.wrap_socket(sock) + will_verify = context.verify_mode != ssl.CERT_NONE + if check_hostname is None: + check_hostname = will_verify + elif check_hostname and not will_verify: + raise ValueError("check_hostname needs a SSL context with " + "either CERT_OPTIONAL or CERT_REQUIRED") + try: + sock = context.wrap_socket(sock) + if check_hostname: + ssl.match_hostname(sock.getpeercert(), hostname) + except Exception: + sock.close() + raise + return sock # The classes themselves @@ -992,7 +1007,7 @@ raise if _have_ssl: - def starttls(self, context=None): + def starttls(self, context=None, check_hostname=False): """Process a STARTTLS command. Arguments: - context: SSL context to use for the encrypted connection """ @@ -1005,7 +1020,8 @@ resp = self._shortcmd('STARTTLS') if resp.startswith('382'): self.file.close() - self.sock = _encrypt_on(self.sock, context) + self.sock = _encrypt_on(self.sock, context, check_hostname, + self.hostname) self.file = self.sock.makefile("rwb") self.tls_on = True # Capabilities may change after TLS starts up, so ask for them @@ -1060,12 +1076,13 @@ def __init__(self, host, port=NNTP_SSL_PORT, user=None, password=None, ssl_context=None, readermode=None, usenetrc=False, - timeout=_GLOBAL_DEFAULT_TIMEOUT): + timeout=_GLOBAL_DEFAULT_TIMEOUT, + check_hostname=False): """This works identically to NNTP.__init__, except for the change in default port and the `ssl_context` argument for SSL connections. """ - self.sock = socket.create_connection((host, port), timeout) - self.sock = _encrypt_on(self.sock, ssl_context) + sock = socket.create_connection((host, port), timeout) + self.sock = _encrypt_on(sock, ssl_context, check_hostname, host) file = self.sock.makefile("rwb") _NNTPBase.__init__(self, file, host, readermode=readermode, timeout=timeout) diff -r a6ea3714774f Lib/poplib.py --- a/Lib/poplib.py Mon Nov 25 08:29:54 2013 +0100 +++ b/Lib/poplib.py Mon Nov 25 10:43:39 2013 +0100 @@ -372,7 +372,7 @@ return caps - def stls(self, context=None): + def stls(self, context=None, check_hostname=False): """Start a TLS session on the active connection as specified in RFC 2595. context - a ssl.SSLContext @@ -386,8 +386,20 @@ raise error_proto('-ERR STLS not supported by server') if context is None: context = ssl._create_stdlib_context() + will_verify = context.verify_mode != ssl.CERT_NONE + if check_hostname is None: + check_hostname = will_verify + elif check_hostname and not will_verify: + raise ValueError("check_hostname needs a SSL context with " + "either CERT_OPTIONAL or CERT_REQUIRED") resp = self._shortcmd('STLS') - self.sock = context.wrap_socket(self.sock) + try: + self.sock = context.wrap_socket(self.sock) + if check_hostname: + ssl.match_hostname(self.sock.getpeercert, self.host) + except Exception: + self.sock.close() + raise self.file = self.sock.makefile('rb') self._tls_established = True return resp @@ -411,7 +423,8 @@ """ def __init__(self, host, port=POP3_SSL_PORT, keyfile=None, certfile=None, - timeout=socket._GLOBAL_DEFAULT_TIMEOUT, context=None): + timeout=socket._GLOBAL_DEFAULT_TIMEOUT, context=None, + check_hostname=False): if context is not None and keyfile is not None: raise ValueError("context and keyfile arguments are mutually " "exclusive") @@ -423,12 +436,25 @@ if context is None: context = ssl._create_stdlib_context(certfile=certfile, keyfile=keyfile) + will_verify = context.verify_mode != ssl.CERT_NONE + if check_hostname is None: + check_hostname = will_verify + elif check_hostname and not will_verify: + raise ValueError("check_hostname needs a SSL context with " + "either CERT_OPTIONAL or CERT_REQUIRED") self.context = context + self._check_hostname = check_hostname POP3.__init__(self, host, port, timeout) def _create_socket(self, timeout): sock = POP3._create_socket(self, timeout) - sock = self.context.wrap_socket(sock) + try: + sock = self.context.wrap_socket(sock) + if self._check_hostname: + ssl.match_hostname(sock.getpeercert(), self.host) + except Exception: + sock.close() + raise return sock def stls(self, keyfile=None, certfile=None, context=None): diff -r a6ea3714774f Lib/smtplib.py --- a/Lib/smtplib.py Mon Nov 25 08:29:54 2013 +0100 +++ b/Lib/smtplib.py Mon Nov 25 10:43:39 2013 +0100 @@ -634,7 +634,8 @@ # We could not login sucessfully. Return result of last attempt. raise SMTPAuthenticationError(code, resp) - def starttls(self, keyfile=None, certfile=None, context=None): + def starttls(self, keyfile=None, certfile=None, context=None, + check_hostname=False): """Puts the connection to the SMTP server into TLS mode. If there has been no previous EHLO or HELO command this session, this @@ -667,7 +668,19 @@ if context is None: context = ssl._create_stdlib_context(certfile=certfile, keyfile=keyfile) - self.sock = context.wrap_socket(self.sock) + will_verify = context.verify_mode != ssl.CERT_NONE + if check_hostname is None: + check_hostname = will_verify + elif check_hostname and not will_verify: + raise ValueError("check_hostname needs a SSL context with " + "either CERT_OPTIONAL or CERT_REQUIRED") + try: + self.sock = context.wrap_socket(self.sock) + if check_hostname: + ssl.match_hostname(self.sock.getpeercert(), self.host) + except Exception: + self.sock.close() + raise self.file = None # RFC 3207: # The client MUST discard any knowledge obtained from @@ -871,7 +884,8 @@ def __init__(self, host='', port=0, local_hostname=None, keyfile=None, certfile=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, - source_address=None, context=None): + source_address=None, context=None, + check_hostname=False): if context is not None and keyfile is not None: raise ValueError("context and keyfile arguments are mutually " "exclusive") @@ -883,7 +897,14 @@ if context is None: context = ssl._create_stdlib_context(certfile=certfile, keyfile=keyfile) + will_verify = context.verify_mode != ssl.CERT_NONE + if check_hostname is None: + check_hostname = will_verify + elif check_hostname and not will_verify: + raise ValueError("check_hostname needs a SSL context with " + "either CERT_OPTIONAL or CERT_REQUIRED") self.context = context + self._check_hostname = check_hostname SMTP.__init__(self, host, port, local_hostname, timeout, source_address) @@ -892,7 +913,13 @@ print('connect:', (host, port), file=stderr) new_socket = socket.create_connection((host, port), timeout, self.source_address) - new_socket = self.context.wrap_socket(new_socket) + try: + new_socket = self.context.wrap_socket(new_socket) + if self._check_hostname: + ssl.match_hostname(new_sock.getpeercert(), host) + except Exception: + new_socket.close() + raise return new_socket __all__.append("SMTP_SSL") diff -r a6ea3714774f Lib/test/test_ftplib.py --- a/Lib/test/test_ftplib.py Mon Nov 25 08:29:54 2013 +0100 +++ b/Lib/test/test_ftplib.py Mon Nov 25 10:43:39 2013 +0100 @@ -301,7 +301,8 @@ if ssl is not None: - CERTFILE = os.path.join(os.path.dirname(__file__), "keycert.pem") + CERTFILE = os.path.join(os.path.dirname(__file__), "keycert3.pem") + CAFILE = os.path.join(os.path.dirname(__file__), "pycacert.pem") class SSLConnection(asyncore.dispatcher): """An asyncore.dispatcher subclass supporting TLS/SSL.""" @@ -923,6 +924,36 @@ self.client.ccc() self.assertRaises(ValueError, self.client.sock.unwrap) + def test_check_hostname(self): + self.client.quit() + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + ctx.verify_mode = ssl.CERT_REQUIRED + ctx.load_verify_locations(CAFILE) + self.client = ftplib.FTP_TLS(context=ctx, timeout=TIMEOUT, + check_hostname=True) + + # 127.0.0.1 doesn't match SAN + self.client.connect(self.server.host, self.server.port) + with self.assertRaises(ssl.CertificateError): + self.client.auth() + # exception quits connection + + self.client.connect(self.server.host, self.server.port) + self.client.prot_p() + with self.assertRaises(ssl.CertificateError): + with self.client.transfercmd("list") as sock: + pass + self.client.quit() + + self.client.connect("localhost", self.server.port) + self.client.auth() + self.client.quit() + + self.client.connect("localhost", self.server.port) + self.client.prot_p() + with self.client.transfercmd("list") as sock: + pass + class TestTimeouts(TestCase): diff -r a6ea3714774f Lib/test/test_poplib.py --- a/Lib/test/test_poplib.py Mon Nov 25 08:29:54 2013 +0100 +++ b/Lib/test/test_poplib.py Mon Nov 25 10:43:39 2013 +0100 @@ -23,7 +23,10 @@ import ssl SUPPORTS_SSL = True - CERTFILE = os.path.join(os.path.dirname(__file__) or os.curdir, "keycert.pem") + BASEDIR = os.path.dirname(__file__) or os.curdir + CERTFILE = os.path.join(BASEDIR, "keycert3.pem") + CAFILE = os.path.join(BASEDIR, "pycacert.pem") + requires_ssl = skipUnless(SUPPORTS_SSL, 'SSL not supported') # the dummy data returned by server when LIST and RETR commands are issued diff -r a6ea3714774f Lib/test/test_smtpnet.py --- a/Lib/test/test_smtpnet.py Mon Nov 25 08:29:54 2013 +0100 +++ b/Lib/test/test_smtpnet.py Mon Nov 25 10:43:39 2013 +0100 @@ -32,7 +32,6 @@ class SmtpSSLTest(unittest.TestCase): testServer = 'smtp.gmail.com' remotePort = 465 - context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) def test_connect(self): support.get_attribute(smtplib, 'SMTP_SSL')