[tor-commits] [stem/master] Adds ClientAuthV3 support to the controller, for setting Client Auth with ADD_ONION on v3 onions

atagar at torproject.org atagar at torproject.org
Wed May 5 22:26:19 UTC 2021


commit 2f3bbf64b4ce3b63160a3c56b15748ba626de4a0
Author: Miguel Jacq <mig at mig5.net>
Date:   Wed May 5 11:50:47 2021 +1000

    Adds ClientAuthV3 support to the controller, for setting Client Auth with ADD_ONION on v3 onions
---
 stem/control.py                  | 25 +++++++++++++++++++----
 stem/version.py                  | 18 +++++++++--------
 test/integ/control/controller.py | 43 +++++++++++++++++++++++++++++++++++++++-
 test/require.py                  | 10 ++++++++++
 4 files changed, 83 insertions(+), 13 deletions(-)

diff --git a/stem/control.py b/stem/control.py
index 91d1b3ed..f1b4f3f7 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -2889,7 +2889,7 @@ class Controller(BaseController):
 
     return [r for r in result if r]  # drop any empty responses (GETINFO is blank if unset)
 
-  async def create_ephemeral_hidden_service(self, ports: Union[int, Sequence[int], Mapping[int, str]], key_type: str = 'NEW', key_content: str = 'BEST', discard_key: bool = False, detached: bool = False, await_publication: bool = False, timeout: Optional[float] = None, basic_auth: Optional[Mapping[str, str]] = None, max_streams: Optional[int] = None) -> stem.response.add_onion.AddOnionResponse:
+  async def create_ephemeral_hidden_service(self, ports: Union[int, Sequence[int], Mapping[int, str]], key_type: str = 'NEW', key_content: str = 'BEST', discard_key: bool = False, detached: bool = False, await_publication: bool = False, timeout: Optional[float] = None, basic_auth: Optional[Mapping[str, str]] = None, max_streams: Optional[int] = None, client_auth_v3: Optional[str] = None) -> stem.response.add_onion.AddOnionResponse:
     """
     Creates a new hidden service. Unlike
     :func:`~stem.control.Controller.create_hidden_service` this style of
@@ -2940,8 +2940,10 @@ class Controller(BaseController):
       })
 
     Please note that **basic_auth** only works for legacy (v2) hidden services.
-    Version 3 can't enable service authentication through the control protocol
-    (:ticket:`tor-40084`).
+
+    To use client auth with a **version 3** service, pass the **client_auth_v3**
+    argument. The value must be a base32-encoded public key from a key pair you
+    have generated elsewhere.
 
     To create a **version 3** service simply specify **ED25519-V3** as the
     our key type, and to create a **version 2** service use **RSA1024**. The
@@ -2958,6 +2960,7 @@ class Controller(BaseController):
 
       print('service established at %s.onion' % response.service_id)
 
+
     .. versionadded:: 1.4.0
 
     .. versionchanged:: 1.5.0
@@ -2971,6 +2974,9 @@ class Controller(BaseController):
     .. versionchanged:: 1.7.0
        Added the timeout and max_streams arguments.
 
+    .. versionchanged:: 2.0.0
+       Added the client_auth_v3 argument.
+
     :param ports: hidden service port(s) or mapping of hidden
       service ports to their targets
     :param key_type: type of key being provided, generates a new key if
@@ -2984,9 +2990,11 @@ class Controller(BaseController):
     :param await_publication: blocks until our descriptor is successfully
       published if **True**
     :param timeout: seconds to wait when **await_result** is **True**
-    :param basic_auth: required user credentials to access this service
+    :param basic_auth: required user credentials to access a v2 service
     :param max_streams: maximum number of streams the hidden service will
       accept, unlimited if zero or not set
+    :param str client_auth_v3: base32-encoded public key for **version 3**
+      onion services that require client authentication
 
     :returns: :class:`~stem.response.add_onion.AddOnionResponse` with the response
 
@@ -3024,6 +3032,12 @@ class Controller(BaseController):
     if (await self.get_conf('HiddenServiceSingleHopMode', None)) == '1' and (await self.get_conf('HiddenServiceNonAnonymousMode', None)) == '1':
       flags.append('NonAnonymous')
 
+    if client_auth_v3 is not None:
+      if await self.get_version() < stem.version.Requirement.ONION_SERVICE_AUTH_ADD:
+        raise stem.UnsatisfiableRequest(message = 'Client authentication support for v3 onions was added to ADD_ONION in tor version %s' % stem.version.Requirement.ONION_SERVICE_AUTH_ADD)
+
+      flags.append('V3Auth')
+
     if flags:
       request += ' Flags=%s' % ','.join(flags)
 
@@ -3048,6 +3062,9 @@ class Controller(BaseController):
         else:
           request += ' ClientAuth=%s' % client_name
 
+    if client_auth_v3 is not None:
+      request += ' ClientAuthV3=%s' % client_auth_v3
+
     response = stem.response._convert_to_add_onion(stem.response._convert_to_add_onion(await self.msg(request)))
 
     if await_publication:
diff --git a/stem/version.py b/stem/version.py
index 6edf93ae..2ca78245 100644
--- a/stem/version.py
+++ b/stem/version.py
@@ -26,14 +26,15 @@ easily parsed and compared, for instance...
 
   Enumerations for the version requirements of features.
 
-  =========================== ===========
-  Requirement                 Description
-  =========================== ===========
-  **DORMANT_MODE**            **DORMANT** and **ACTIVE** :data:`~stem.Signal`
-  **DROPTIMEOUTS**            **DROPTIMEOUTS** controller command
-  **HSFETCH_V3**              HSFETCH for version 3 hidden services
-  **ONION_CLIENT_AUTH_ADD**   **ONION_CLIENT_AUTH_ADD** controller command
-  =========================== ===========
+  ========================== ===========
+  Requirement                Description
+  ========================== ===========
+  **DORMANT_MODE**           **DORMANT** and **ACTIVE** :data:`~stem.Signal`
+  **DROPTIMEOUTS**           **DROPTIMEOUTS** controller command
+  **HSFETCH_V3**             HSFETCH for version 3 hidden services
+  **ONION_CLIENT_AUTH_ADD**  **ONION_CLIENT_AUTH_ADD** controller command
+  **ONION_SERVICE_AUTH_ADD** For adding ClientAuthV3 to a v3 onion service via ADD_ONION
+  ========================== ===========
 """
 
 import functools
@@ -223,4 +224,5 @@ Requirement = stem.util.enum.Enum(
   ('DROPTIMEOUTS', Version('0.4.5.0-alpha')),
   ('HSFETCH_V3', Version('0.4.1.1-alpha')),
   ('ONION_CLIENT_AUTH_ADD', Version('0.4.3.1-alpha')),
+  ('ONION_SERVICE_AUTH_ADD', Version('0.4.6.1-alpha')),
 )
diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py
index a2cc175d..1c9fd366 100644
--- a/test/integ/control/controller.py
+++ b/test/integ/control/controller.py
@@ -475,6 +475,7 @@ class TestController(unittest.TestCase):
       self.assertFalse(await controller.is_set('ConnLimit'))
 
   @test.require.controller
+  @test.require.version_older_than(stem.version.Version('0.4.6.1-alpha'))
   @async_test
   async def test_hidden_services_conf(self):
     """
@@ -593,7 +594,7 @@ class TestController(unittest.TestCase):
     async with await test.runner.get_runner().get_tor_controller() as controller:
       self.assertEqual([], await controller.list_ephemeral_hidden_services())
       self.assertEqual([], await controller.list_ephemeral_hidden_services(detached = True))
-      self.assertEqual(False, await controller.remove_ephemeral_hidden_service('gfzprpioee3hoppz'))
+      self.assertEqual(False, await controller.remove_ephemeral_hidden_service(SERVICE_ID))
 
   @test.require.controller
   @async_test
@@ -604,6 +605,7 @@ class TestController(unittest.TestCase):
           await controller.create_ephemeral_hidden_service(ports)
 
   @test.require.controller
+  @test.require.version_older_than(stem.version.Version('0.4.6.1-alpha'))
   @async_test
   async def test_ephemeral_hidden_services_v2(self):
     """
@@ -694,6 +696,7 @@ class TestController(unittest.TestCase):
         self.assertEqual(0, len(await second_controller.list_ephemeral_hidden_services()))
 
   @test.require.controller
+  @test.require.version_older_than(stem.version.Version('0.4.6.1-alpha'))
   @async_test
   async def test_with_ephemeral_hidden_services_basic_auth(self):
     """
@@ -714,6 +717,44 @@ class TestController(unittest.TestCase):
       self.assertEqual([], await controller.list_ephemeral_hidden_services())
 
   @test.require.controller
+  @test.require.version(stem.version.Requirement.ONION_SERVICE_AUTH_ADD)
+  @async_test
+  async def test_with_ephemeral_hidden_services_v3_client_auth(self):
+    """
+    Exercises creating v3 ephemeral hidden services with ClientAuthV3.
+    """
+
+    runner = test.runner.get_runner()
+
+    async with await runner.get_tor_controller() as controller:
+      response = await controller.create_ephemeral_hidden_service(4567, key_content = 'ED25519-V3', client_auth_v3='FGTORMIDKR7T2PR632HSHLWA4G6HF5TCWSGMHDUU4LWBEFTAVYQQ')
+      self.assertEqual([response.service_id], await controller.list_ephemeral_hidden_services())
+      self.assertTrue(response.private_key is not None)
+      self.assertEqual('ED25519-V3', response.private_key_type)
+      self.assertEqual({}, response.client_auth)
+
+      # drop the service
+
+      self.assertEqual(True, await controller.remove_ephemeral_hidden_service(response.service_id))
+      self.assertEqual([], await controller.list_ephemeral_hidden_services())
+
+  @test.require.controller
+  @test.require.version(stem.version.Requirement.ONION_SERVICE_AUTH_ADD)
+  @async_test
+  async def test_with_ephemeral_hidden_services_v3_client_auth_invalid(self):
+    """
+    Exercises creating v3 ephemeral hidden services with ClientAuthV3 but
+    with an invalid public key.
+    """
+
+    runner = test.runner.get_runner()
+
+    async with await runner.get_tor_controller() as controller:
+      with self.assertRaisesWith(stem.ProtocolError, "ADD_ONION response didn't have an OK status: Cannot decode v3 client auth key"):
+        await controller.create_ephemeral_hidden_service(4567, key_content = 'ED25519-V3', client_auth_v3='badkey')
+
+  @test.require.controller
+  @test.require.version_older_than(stem.version.Version('0.4.6.1-alpha'))
   @async_test
   async def test_with_ephemeral_hidden_services_basic_auth_no_credentials(self):
     """
diff --git a/test/require.py b/test/require.py
index ff185073..9672a140 100644
--- a/test/require.py
+++ b/test/require.py
@@ -125,6 +125,16 @@ def version(req_version):
   return needs(lambda: test.tor_version() >= req_version, 'requires %s' % req_version)
 
 
+def version_older_than(req_version):
+  """
+  Skips the test unless we meet a version older than the requested version.
+
+  :param stem.version.Version req_version: the version that tor should be older than
+  """
+
+  return needs(lambda: test.tor_version() < req_version, 'requires %s' % req_version)
+
+
 cryptography = needs(lambda: CRYPTOGRAPHY_AVAILABLE, 'requires cryptography')
 proc = needs(stem.util.proc.is_available, 'proc unavailable')
 controller = needs(_can_access_controller, 'no connection')





More information about the tor-commits mailing list