[tbb-commits] [tor-browser-bundle-testsuite/master] Bug 40002: Add test for 'Add v3 onion services client authentication prompt' TB patch

gk at torproject.org gk at torproject.org
Thu Oct 15 18:41:27 UTC 2020


commit 5c0a4650a88d4b616fb36afc2a57ebb78ab7f539
Author: Alex Catarineu <acat at 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





More information about the tbb-commits mailing list