From 06e1a651823934710f23a138f4bbfb344ca002b0 Mon Sep 17 00:00:00 2001 From: Ben Darnell Date: Mon, 13 Nov 2023 22:02:32 -0500 Subject: [PATCH 1/2] iostream_test: Test check_hostname functionality. In #3337, the removal of ssl.match_hostname revealed that we did not have any test coverage of hostname checking in tornado.iostream. Since we were forced to remove the manual check that we had in place for old versions of Python, we need a test to make sure that we didn't inadvertently break hostname checking. --- tornado/test/iostream_test.py | 75 +++++++++++++++++++++++++++++++++++ tornado/test/test.crt | 34 ++++++++-------- tornado/test/test.key | 52 ++++++++++++------------ tornado/testing.py | 4 +- 4 files changed, 120 insertions(+), 45 deletions(-) diff --git a/tornado/test/iostream_test.py b/tornado/test/iostream_test.py index e22e83e60f..8a28518001 100644 --- a/tornado/test/iostream_test.py +++ b/tornado/test/iostream_test.py @@ -1149,6 +1149,81 @@ def handle_stream(self, stream, address): yield handshake_future +class TestIOStreamCheckHostname(AsyncTestCase): + # This test ensures that hostname checks are working correctly after + # #3337 revealed that we have no test coverage in this area, and we + # removed a manual hostname check that was needed only for very old + # versions of python. + def setUp(self): + super().setUp() + self.listener, self.port = bind_unused_port() + + def accept_callback(connection, address): + ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ssl_ctx.load_cert_chain( + os.path.join(os.path.dirname(__file__), "test.crt"), + os.path.join(os.path.dirname(__file__), "test.key"), + ) + connection = ssl_ctx.wrap_socket( + connection, + server_side=True, + do_handshake_on_connect=False, + ) + SSLIOStream(connection) + + netutil.add_accept_handler(self.listener, accept_callback) + + # Our self-signed cert is its own CA. We have to pass the CA check before + # the hostname check will be performed. + self.client_ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + self.client_ssl_ctx.load_verify_locations( + os.path.join(os.path.dirname(__file__), "test.crt") + ) + + def tearDown(self): + self.io_loop.remove_handler(self.listener.fileno()) + self.listener.close() + super().tearDown() + + @gen_test + async def test_match(self): + stream = SSLIOStream(socket.socket(), ssl_options=self.client_ssl_ctx) + await stream.connect( + ("127.0.0.1", self.port), + server_hostname="foo.example.com", + ) + stream.close() + + @gen_test + async def test_no_match(self): + stream = SSLIOStream(socket.socket(), ssl_options=self.client_ssl_ctx) + with ExpectLog(gen_log, ".*alert bad certificate", level=logging.WARNING): + with self.assertRaises(ssl.SSLCertVerificationError): + with ExpectLog( + gen_log, + ".*(certificate verify failed: Hostname mismatch)", + level=logging.WARNING, + ): + await stream.connect( + ("127.0.0.1", self.port), + server_hostname="bar.example.com", + ) + # The server logs a warning while cleaning up the failed connection. + # Unfortunately there's no good hook to wait for this logging. + await asyncio.sleep(0.1) + + @gen_test + async def test_check_disabled(self): + # check_hostname can be set to false and the connection will succeed even though it doesn't + # have the right hostname. + self.client_ssl_ctx.check_hostname = False + stream = SSLIOStream(socket.socket(), ssl_options=self.client_ssl_ctx) + await stream.connect( + ("127.0.0.1", self.port), + server_hostname="bar.example.com", + ) + + @skipIfNonUnix class TestPipeIOStream(TestReadWriteMixin, AsyncTestCase): @gen.coroutine diff --git a/tornado/test/test.crt b/tornado/test/test.crt index ffc49b06c1..c7f19e3e62 100644 --- a/tornado/test/test.crt +++ b/tornado/test/test.crt @@ -1,20 +1,18 @@ -----BEGIN CERTIFICATE----- -MIIDWzCCAkOgAwIBAgIUV4spou0CenmvKqa7Hml/MC+JKiAwDQYJKoZIhvcNAQEL -BQAwPTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExGTAXBgNVBAoM -EFRvcm5hZG8gV2ViIFRlc3QwHhcNMTgwOTI5MTM1NjQ1WhcNMjgwOTI2MTM1NjQ1 -WjA9MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEZMBcGA1UECgwQ -VG9ybmFkbyBXZWIgVGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -AKT0LdyI8tW5uwP3ahE8BFSz+j3SsKBDv/0cKvqxVVE6sLEST2s3HjArZvIIG5sb -iBkWDrqnZ6UKDvB4jlobLGAkepxDbrxHWxK53n0C28XXGLqJQ01TlTZ5rpjttMeg -5SKNjHbxpOvpUwwQS4br4WjZKKyTGiXpFkFUty+tYVU35/U2yyvreWHmzpHx/25t -H7O2RBARVwJYKOGPtlH62lQjpIWfVfklY4Ip8Hjl3B6rBxPyBULmVQw0qgoZn648 -oa4oLjs0wnYBz01gVjNMDHej52SsB/ieH7W1TxFMzqOlcvHh41uFbQJPgcXsruSS -9Z4twzSWkUp2vk/C//4Sz38CAwEAAaNTMFEwHQYDVR0OBBYEFLf8fQ5+u8sDWAd3 -r5ZjZ5MmDWJeMB8GA1UdIwQYMBaAFLf8fQ5+u8sDWAd3r5ZjZ5MmDWJeMA8GA1Ud -EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADkkm3pIb9IeqVNmQ2uhQOgw -UwyToTYUHNTb/Nm5lzBTBqC8gbXAS24RQ30AB/7G115Uxeo+YMKfITxm/CgR+vhF -F59/YrzwXj+G8bdbuVl/UbB6f9RSp+Zo93rUZAtPWr77gxLUrcwSRzzDwxFjC2nC -6eigbkvt1OQY775RwnFAt7HKPclE0Out+cGJIboJuO1f3r57ZdyFH0GzbZEff/7K -atGXohijWJjYvU4mk0KFHORZrcBpsv9cfkFbmgVmiRwxRJ1tLauHM3Ne+VfqYE5M -4rTStSyz3ASqVKJ2iFMQueNR/tUOuDlfRt+0nhJMuYSSkW+KTgnwyOGU9cv+mxA= +MIIC1TCCAb2gAwIBAgIJAOV36k+idrqDMA0GCSqGSIb3DQEBCwUAMBoxGDAWBgNV +BAMMD2Zvby5leGFtcGxlLmNvbTAeFw0yMzExMTIwMTQ3MzhaFw0zMzExMDkwMTQ3 +MzhaMBoxGDAWBgNVBAMMD2Zvby5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAKjfAL8hQ1G5yoR29D0NwqhL3EE9RShYLvzKvSNhOceR +e390XJLAi8PN8Xv8LkmoMITaLdRDtBwXcdw+kfHjcfXZ0cORJkxJFdk/38SsiBKV +ZzMO+1PVULfnQa92tHtahNsTGI5367WEALn9UNJLmP+jpX+3zohatUTbhlnRSruH +O/Mo5mLs1XJhQpdvp8BQNksJhiTQ7FsbcjGq6gZ75SnbfUR0PyohY0LTsrql00Tz +hCAEvm2TNiQ5s+PT5fFOg6Jh2ZGj1lYLQY3dDeqt9sdabvj7LANqfygbt2cf9yYn +a25UTRcAN7CNdWwTEfvnOVMITzCE8F2FmKDvJR+TX30CAwEAAaMeMBwwGgYDVR0R +BBMwEYIPZm9vLmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBjKz4gM4Bz +JO7Ny1fwbBtraHCGYnDG8gBID3+/sQlMMFeuquJK+oc+1DOpr9wFlmgih67OszdM +X2Xl/HjtHPKwNqaDHXu5bQPFT5fXzAZ8HHEOXSV9IpHaNyS7TC7bYmD/ClCZeqXU +h7MBe5yPXfCCIqWyjZMZDQfT1v6J+WX3+lO9josMJCfNR5DzvJiPmSTUxrLD5SkT ++7iKxhM6eI83D+I188sGc2IMinkFp8jSRTlaH8WYiOd5QQ2r8GSYNM9M3z1sK7zv +0Bw3hWEQgpFbEaSH0OB72KYkMUZBqK9UoeSZWBrMXHFBNaY23tEKInEwlBGBELGc +acSinK6OBC0z -----END CERTIFICATE----- diff --git a/tornado/test/test.key b/tornado/test/test.key index 7cb7d8d23c..8eea05db5f 100644 --- a/tornado/test/test.key +++ b/tornado/test/test.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCk9C3ciPLVubsD -92oRPARUs/o90rCgQ7/9HCr6sVVROrCxEk9rNx4wK2byCBubG4gZFg66p2elCg7w -eI5aGyxgJHqcQ268R1sSud59AtvF1xi6iUNNU5U2ea6Y7bTHoOUijYx28aTr6VMM -EEuG6+Fo2Siskxol6RZBVLcvrWFVN+f1Nssr63lh5s6R8f9ubR+ztkQQEVcCWCjh -j7ZR+tpUI6SFn1X5JWOCKfB45dweqwcT8gVC5lUMNKoKGZ+uPKGuKC47NMJ2Ac9N -YFYzTAx3o+dkrAf4nh+1tU8RTM6jpXLx4eNbhW0CT4HF7K7kkvWeLcM0lpFKdr5P -wv/+Es9/AgMBAAECggEABi6AaXtYXloPgB6NgwfUwbfc8OQsalUfpMShd7OdluW0 -KW6eO05de0ClIvzay/1EJGyHMMeFQtIVrT1XWFkcWJ4FWkXMqJGkABenFtg8lDVz -X8o1E3jGZrw4ptKBq9mDvL/BO9PiclTUH+ecbPn6AIvi0lTQ7grGIryiAM9mjmLy -jpCwoutF2LD4RPNg8vqWe/Z1rQw5lp8FOHhRwPooHHeoq1bSrp8dqvVAwAam7Mmf -uFgI8jrNycPgr2cwEEtbq2TQ625MhVnCpwT+kErmAStfbXXuqv1X1ZZgiNxf+61C -OL0bhPRVIHmmjiK/5qHRuN4Q5u9/Yp2SJ4W5xadSQQKBgQDR7dnOlYYQiaoPJeD/ -7jcLVJbWwbr7bE19O/QpYAtkA/FtGlKr+hQxPhK6OYp+in8eHf+ga/NSAjCWRBoh -MNAVCJtiirHo2tFsLFOmlJpGL9n3sX8UnkJN90oHfWrzJ8BZnXaSw2eOuyw8LLj+ -Q+ISl6Go8/xfsuy3EDv4AP1wCwKBgQDJJ4vEV3Kr+bc6N/xeu+G0oHvRAWwuQpcx -9D+XpnqbJbFDnWKNE7oGsDCs8Qjr0CdFUN1pm1ppITDZ5N1cWuDg/47ZAXqEK6D1 -z13S7O0oQPlnsPL7mHs2Vl73muAaBPAojFvceHHfccr7Z94BXqKsiyfaWz6kclT/ -Nl4JTdsC3QKBgQCeYgozL2J/da2lUhnIXcyPstk+29kbueFYu/QBh2HwqnzqqLJ4 -5+t2H3P3plQUFp/DdDSZrvhcBiTsKiNgqThEtkKtfSCvIvBf4a2W/4TJsW6MzxCm -2KQDuK/UqM4Y+APKWN/N6Lln2VWNbNyBkWuuRVKFatccyJyJnSjxeqW7cwKBgGyN -idCYPIrwROAHLItXKvOWE5t0ABRq3TsZC2RkdA/b5HCPs4pclexcEriRjvXrK/Yt -MH94Ve8b+UftSUQ4ytjBMS6MrLg87y0YDhLwxv8NKUq65DXAUOW+8JsAmmWQOqY3 -MK+m1BT4TMklgVoN3w3sPsKIsSJ/jLz5cv/kYweFAoGAG4iWU1378tI2Ts/Fngsv -7eoWhoda77Y9D0Yoy20aN9VdMHzIYCBOubtRPEuwgaReNwbUBWap01J63yY/fF3K -8PTz6covjoOJqxQJOvM7nM0CsJawG9ccw3YXyd9KgRIdSt6ooEhb7N8W2EXYoKl3 -g1i2t41Q/SC3HUGC5mJjpO8= +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCo3wC/IUNRucqE +dvQ9DcKoS9xBPUUoWC78yr0jYTnHkXt/dFySwIvDzfF7/C5JqDCE2i3UQ7QcF3Hc +PpHx43H12dHDkSZMSRXZP9/ErIgSlWczDvtT1VC350GvdrR7WoTbExiOd+u1hAC5 +/VDSS5j/o6V/t86IWrVE24ZZ0Uq7hzvzKOZi7NVyYUKXb6fAUDZLCYYk0OxbG3Ix +quoGe+Up231EdD8qIWNC07K6pdNE84QgBL5tkzYkObPj0+XxToOiYdmRo9ZWC0GN +3Q3qrfbHWm74+ywDan8oG7dnH/cmJ2tuVE0XADewjXVsExH75zlTCE8whPBdhZig +7yUfk199AgMBAAECggEBAIGFmXL/Nj0GvVfgTPBPD5A5rxOyxMpu6IsnjO4H8mMp +KInXW/GLESf7W053W6FPCPe8yA3YZ9pr+P6uVw4qHwwsJwFS4Qb9v25D2YNluXBX +ezHkOcxQ/novO2gzKba69M961Ajh3b35Iv2EV2sUZKMehx9wgU6AFCxeG6vkJOez +UCX0WG467cdo4alfe/oQZLioU3t+GGCb23m13B9xaN2tqONNh2E2yp73MVJ1Q74R +HVBkQxciHd3iJee5/4AGUJl9TLv8wAT1cf3OhcGlvOlcfSYtuNUY32TPWit1Or1y +i9fPkjo8SBw52TN5RRmjIlpNMxeK+G4+XtO1Y47TlZkCgYEA3Y+NK8mz9kTnorkf +R7CSOAaiste8c3CJbbbaTk7oTxK4MISL1RX+sFK65cnV54Wo75hCZxsjV2oEQ+4r +UOGw1JxcV16V6iP/AaQS41xsxZca/xnC//YojBN6kP+OV+/ByF4HNs5eDN6uHg0y +OOfBWi6oc449CFFMxVnrQ0SymaMCgYEAwx7M9xQ1eth902Wq2VNszFIgdgptGYhj +XbWsAFYKII+aUXC92sVL5sO33vNyhBbyMN1aSRXYe8B5EnwsP31o5csrHQw6i/Dp +jUx1AUBYkNPgL9ctqlTQf1nb0LenGlCUBD6jrSrJVHeOF4y+HIZHXNZ++otH7+eu +b3dbHgV/9F8CgYBTopO0utAvH3WdDGqNYk7fzUlvX1ao8Qs/mi2wL8MrzjIvRmmO +h137607X3Sfc3KyXvQ8b4relkMSJbAd34aohp+CHrpHCr9HcKbZjkwkQUWkEcRIW +EzLdJaE3yPBPq5an7y6j9qS0EP8DIxIZPwrS4xf9fuz1DdOAD+BqJS2SJwKBgQCt +zZzTpduxbnA+Qrx503cBVVJ28viVmsiwK2hn8DwbHu9eBegHnGDs0H/Th9UE1g+r ++TA4E85/BUaTcapUb5hlwKDJwh/QkaroYyeCEtgRQbnbw3d41w3Vsqw78atWpFoE +oetYD9nAdLJMReD+NZoRlzsKX9CXYS8fORkf19RPTwKBgQCQdvDMicrtnJ4U2MOA +y+59K7V77VRfHLecjAMGblBGmrtoQSBvQiFznXm0DUOSy3eZRITwe01/t+RUDhx9 +MVLlyNzwRCVHzPe7kUI10GM4W5ZKAf8f/t0KjBrQBeYtRUOEI3QVzsVzc1hY8Fv8 +YHOhmI8Tdd2biF+lqXKC6vGlvQ== -----END PRIVATE KEY----- diff --git a/tornado/testing.py b/tornado/testing.py index da33335733..bdbff87bc3 100644 --- a/tornado/testing.py +++ b/tornado/testing.py @@ -517,7 +517,9 @@ def get_ssl_options(self) -> Dict[str, Any]: def default_ssl_options() -> Dict[str, Any]: # Testing keys were generated with: # openssl req -new -keyout tornado/test/test.key \ - # -out tornado/test/test.crt -nodes -days 3650 -x509 + # -out tornado/test/test.crt \ + # -nodes -days 3650 -x509 \ + # -subj "/CN=foo.example.com" -addext "subjectAltName = DNS:foo.example.com" module_dir = os.path.dirname(__file__) return dict( certfile=os.path.join(module_dir, "test", "test.crt"), From 2da0a9912bc5207e2ac8207b40035377de3e1cd5 Mon Sep 17 00:00:00 2001 From: Ben Darnell Date: Mon, 13 Nov 2023 22:11:59 -0500 Subject: [PATCH 2/2] iostream_test: Don't require server-side log on windows --- tornado/test/iostream_test.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tornado/test/iostream_test.py b/tornado/test/iostream_test.py index 8a28518001..02fcd3e13f 100644 --- a/tornado/test/iostream_test.py +++ b/tornado/test/iostream_test.py @@ -1197,7 +1197,12 @@ async def test_match(self): @gen_test async def test_no_match(self): stream = SSLIOStream(socket.socket(), ssl_options=self.client_ssl_ctx) - with ExpectLog(gen_log, ".*alert bad certificate", level=logging.WARNING): + with ExpectLog( + gen_log, + ".*alert bad certificate", + level=logging.WARNING, + required=platform.system() != "Windows", + ): with self.assertRaises(ssl.SSLCertVerificationError): with ExpectLog( gen_log, @@ -1210,7 +1215,9 @@ async def test_no_match(self): ) # The server logs a warning while cleaning up the failed connection. # Unfortunately there's no good hook to wait for this logging. - await asyncio.sleep(0.1) + # It doesn't seem to happen on windows; I'm not sure why. + if platform.system() != "Windows": + await asyncio.sleep(0.1) @gen_test async def test_check_disabled(self):