commit 5c0a4650a88d4b616fb36afc2a57ebb78ab7f539 Author: Alex Catarineu acat@torproject.org Date: Fri Jul 3 00:53:46 2020 +0200
Bug 40002: Add test for 'Add v3 onion services client authentication prompt' TB patch --- TBBTestSuite/TestSuite/BrowserBundleTests.pm | 7 + TBBTestSuite/Tests/TorBootstrap.pm | 4 + .../tor_browser_tests/test_onion_client_auth.py | 215 +++++++++++++++++++++ 3 files changed, 226 insertions(+)
diff --git a/TBBTestSuite/TestSuite/BrowserBundleTests.pm b/TBBTestSuite/TestSuite/BrowserBundleTests.pm index 37efae6..7d6c110 100644 --- a/TBBTestSuite/TestSuite/BrowserBundleTests.pm +++ b/TBBTestSuite/TestSuite/BrowserBundleTests.pm @@ -494,6 +494,13 @@ our @tests = ( use_net => 1, descr => 'Check that onion alias urlbar rewrites work properly', }, + { + name => 'onion_client_auth', + type => 'marionette', + use_net => 1, + descr => 'Check that onion client auth works properly', + run_once => 1, + }, { name => 'security_level_ui', type => 'marionette', diff --git a/TBBTestSuite/Tests/TorBootstrap.pm b/TBBTestSuite/Tests/TorBootstrap.pm index f35b4a2..006be59 100644 --- a/TBBTestSuite/Tests/TorBootstrap.pm +++ b/TBBTestSuite/Tests/TorBootstrap.pm @@ -153,11 +153,15 @@ sub start_tor { } write_file("$tbbinfos->{datadir}/Tor/torrc", ()) unless -f "$tbbinfos->{datadir}/Tor/torrc"; + # These arguments should be kept in sync with tor-launcher + mkdir "$tbbinfos->{datadir}/Tor/onion-auth" unless -d "$tbbinfos->{datadir}/Tor/onion-auth"; my @cmd = (winpath($tbbinfos->{torbin}), '--defaults-torrc', winpath($torrc_file), '-f', winpath("$tbbinfos->{datadir}/Tor/torrc"), 'DataDirectory', winpath("$tbbinfos->{datadir}/Tor"), 'GeoIPFile', winpath($tbbinfos->{torgeoip}), + 'ClientOnionAuthDir', winpath("$tbbinfos->{datadir}/Tor/onion-auth"), + '__SocksPort', "$options->{'tor-socks-port'} ExtendedErrors IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth", '__OwningControllerProcess', winpid($$)); $tbbinfos->{torpid} = fork; if ($tbbinfos->{torpid} == 0) { diff --git a/marionette/tor_browser_tests/test_onion_client_auth.py b/marionette/tor_browser_tests/test_onion_client_auth.py new file mode 100644 index 0000000..38c8a34 --- /dev/null +++ b/marionette/tor_browser_tests/test_onion_client_auth.py @@ -0,0 +1,215 @@ +from marionette_driver import By, Wait +from marionette_driver.errors import MarionetteException, NoSuchElementException, TimeoutException +from marionette_driver.legacy_actions import Actions +from marionette_harness import MarionetteTestCase, WindowManagerMixin + +import testsuite + +from stem.control import Controller +from stem.process import launch_tor_with_config + +from urlparse import urlparse +from tempfile import mkdtemp +import shutil +import os +import base64 + +import time + + +class Test(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(Test, self).setUp() + + self.public_key = 'E4ST65PDZDVZRAW2FLT5RBFKYEM3GW73SRQDMEBLBDHQP3Y4NADQ' + self.private_key = 'B7H4TVVQNEOIENRS3GW3GI4VLVTSZPKS7NVSJAIDTNLBRWKWPHLQ' + + self.tmp_dir = mkdtemp() + fixtures_port = urlparse(self.marionette.absolute_url('')).port + os.mkdir(os.path.join(self.tmp_dir, 'hidden_service'), 0700) + os.mkdir(os.path.join(self.tmp_dir, 'hidden_service', + 'authorized_clients'), 0700) + with open(os.path.join(self.tmp_dir, 'hidden_service', 'authorized_clients', 'alice.auth'), "w") as myfile: + myfile.write("descriptor:x25519:" + self.public_key + '\n') + + # Add tor executable directory to the LD_LIBRARY_PATH + ld_lib_list = filter(len, os.environ.get( + "LD_LIBRARY_PATH", "").split(":")) + tor_dirname = os.path.dirname( + testsuite.TestSuite().t['tbbinfos']['torbin']) + if tor_dirname not in ld_lib_list: + ld_lib_list = [tor_dirname] + ld_lib_list + os.environ["LD_LIBRARY_PATH"] = ":".join(ld_lib_list) + + self.tor_process = launch_tor_with_config( + config={ + 'ControlPort': '9999', + 'SOCKSPort': '0', + 'DataDirectory': self.tmp_dir, + 'HiddenServiceDir': os.path.join(self.tmp_dir, 'hidden_service'), + 'HiddenServicePort': '80 127.0.0.1:' + str(fixtures_port), + }, + take_ownership=True, + tor_cmd=testsuite.TestSuite().t['tbbinfos']['torbin'] + ) + + with open(os.path.join(self.tmp_dir, 'hidden_service', 'hostname'), 'r') as myfile: + self.onion = myfile.read().strip() + + self.controller = Controller.from_port(port=9999) + self.controller.authenticate() + + def is_published(_): + try: + self.controller.get_hidden_service_descriptor(self.onion) + return True + except: + return False + Wait(self.marionette, timeout=10).until(is_published) + # Wait a reasonable amount of time to increase the chances of the service to work + time.sleep(10) + + def tearDown(self): + self.controller.close() + self.tor_process.terminate() + shutil.rmtree(self.tmp_dir, ignore_errors=True) + super(Test, self).tearDown() + + def load_onion(self, onion=None, wait_auth=True): + if not onion: + onion = self.onion + m = self.marionette + with m.using_context('content'): + self.marionette.execute_script( + 'window.location = "http://' + onion + '/dom-objects-enumeration.html";') + if wait_auth: + with m.using_context('chrome'): + Wait(m, timeout=m.timeout.page_load).until(lambda _: m.find_element( + 'id', 'tor-clientauth-notification-key').is_displayed()) + + def check_errors(self, title, short, long): + m = self.marionette + with m.using_context('content'): + Wait(m, timeout=m.timeout.page_load).until(lambda _: m.find_element( + 'css selector', '#text-container .title-text').text != '') + self.assertEqual(m.find_element( + 'css selector', '#text-container .title-text').text, title) + self.assertEqual(m.find_element( + 'id', 'errorShortDescText').text, short) + self.assertEqual(m.find_element('id', 'errorLongDesc').text, long) + + def get_keys(self): + with self.marionette.using_context('content'): + self.marionette.navigate("about:preferences#privacy") + self.marionette.find_element( + 'id', 'torOnionServiceKeys-savedKeys').click() + return self.marionette.execute_script(''' + const dialog = document.querySelector('.dialogFrame'); + const tree = dialog.contentDocument.querySelector('#onionservices-savedkeys-tree'); + const view = tree.view; + const rowCount = view.rowCount; + const result = []; + for (let i = 0; i < rowCount; ++i) { + result.push([ + view.getCellText(i, tree.columns[0]), + view.getCellText(i, tree.columns[1]), + ]); + } + return result; + ''') + + def new_identity(self): + m = self.marionette + with m.using_context('chrome'): + m.set_pref('extensions.torbutton.confirm_newnym', False) + m.find_element('id', 'new-identity-button').click() + # Wait some time for new identity to finish. + time.sleep(2) + # Reload marionette session after new identity. + self.marionette.quit() + self.marionette.start_session() + self.marionette.timeout.implicit = 10 + + def delete_all_keys(self): + with self.marionette.using_context('content'): + self.marionette.navigate("about:preferences#privacy") + self.marionette.find_element( + 'id', 'torOnionServiceKeys-savedKeys').click() + return self.marionette.execute_script(''' + const dialog = document.querySelector('.dialogFrame'); + dialog.contentDocument.querySelector('#onionservices-savedkeys-removeall').click(); + ''') + + def test_client_auth(self): + m = self.marionette + m.timeout.implicit = 10 + + # Cancel auth + self.load_onion() + with m.using_context('chrome'): + cancel = m.find_element( + 'css selector', '#tor-clientauth-notification .popup-notification-secondary-button') + cancel.click() + + self.check_errors('Onionsite Requires Authentication', 'Access to the onionsite requires a key but none was provided.', + u'Details: 0xF4 \u2014 The client downloaded the requested onion service descriptor but was unable to decrypt its content because client authorization information is missing.') + + # Wrong auth + self.load_onion() + with m.using_context('chrome'): + m.find_element('id', 'tor-clientauth-notification-key').send_keys( + 'E4ST65PDZDVZRAW2FLT5RBFKYEM3GW73SRQDMEBLBDHQP3Y4NADQ') + m.find_element( + 'css selector', '#tor-clientauth-notification .popup-notification-primary-button').click() + Wait(m, timeout=m.timeout.page_load).until(lambda _: not m.find_element( + 'id', 'tor-clientauth-notification-key').is_displayed()) + Wait(m, timeout=m.timeout.page_load).until(lambda _: m.find_element( + 'id', 'tor-clientauth-notification-key').is_displayed()) + + # Good auth, don't remember key + with m.using_context('chrome'): + m.find_element( + 'id', 'tor-clientauth-notification-key').send_keys(self.private_key) + m.find_element( + 'css selector', '#tor-clientauth-notification .popup-notification-primary-button').click() + with m.using_context('content'): + m.find_element('id', 'enumeration') + keys = self.get_keys() + self.assertEqual(len(keys), 0) + self.new_identity() + self.load_onion() # This will block if the auth prompt is not displayed + + # Good auth, remember key + with m.using_context('chrome'): + m.find_element( + 'id', 'tor-clientauth-notification-key').send_keys(self.private_key) + m.find_element('id', 'tor-clientauth-persistkey-checkbox').click() + m.find_element( + 'css selector', '#tor-clientauth-notification .popup-notification-primary-button').click() + with m.using_context('content'): + m.find_element('id', 'enumeration') + keys = self.get_keys() + self.assertEqual(len(keys), 1) + self.assertEqual(keys[0][0], self.onion[:-6]) + self.assertEqual(keys[0][1], base64.b64encode( + base64.b32decode(self.private_key + '===='))) + self.new_identity() + with m.using_context('content'): + m.navigate('http://' + self.onion + + '/dom-objects-enumeration.html') # Should not block + + self.delete_all_keys() + # Wait a bit, otherwise it sometimes loads the onion without the auth prompt + time.sleep(5) + with m.using_context('content'): + self.load_onion() # Load and wait for the auth doorgahnger + + # Check invalid onion address error + with m.using_context('content'): + bad_char = 'a' if self.onion[-7:-6] != 'a' else 'b' + self.load_onion( + onion=self.onion[:-7] + bad_char + ".onion", wait_auth=False) + self.check_errors('Invalid Onionsite Address', 'The provided onionsite address is invalid. Please check that you entered it correctly.', + u'Details: 0xF6 \u2014 The provided .onion address is invalid. This error is returned due to one of the following reasons: the address checksum doesn't match, the ed25519 public key is invalid, or the encoding is invalid.') + + # TODO: check other onion errors
tbb-commits@lists.torproject.org