[tor-commits] [stem/master] Adds support for ONION_CLIENT_AUTH_ADD, ONION_CLIENT_AUTH_REMOVE and ONION_CLIENT_AUTH_VIEW

atagar at torproject.org atagar at torproject.org
Fri Aug 7 00:08:37 UTC 2020


commit 0dba06fd54d6e1ce844e3a1518f15e9feb375db1
Author: Miguel Jacq <mig at mig5.net>
Date:   Thu Jun 18 13:24:40 2020 +1000

    Adds support for ONION_CLIENT_AUTH_ADD, ONION_CLIENT_AUTH_REMOVE and ONION_CLIENT_AUTH_VIEW
---
 stem/control.py                    | 82 ++++++++++++++++++++++++++++++++++++++
 stem/interpreter/settings.cfg      | 12 ++++++
 stem/response/__init__.py          | 41 ++++++++++++++-----
 stem/response/onion_client_auth.py | 58 +++++++++++++++++++++++++++
 test/integ/control/controller.py   | 48 ++++++++++++++++++++++
 5 files changed, 230 insertions(+), 11 deletions(-)

diff --git a/stem/control.py b/stem/control.py
index 0b5721e8..5bdb4a5a 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -112,6 +112,10 @@ If you're fine with allowing your script to raise exceptions then this can be mo
     |- create_ephemeral_hidden_service - create a new ephemeral hidden service
     |- remove_ephemeral_hidden_service - removes an ephemeral hidden service
     |
+    |- add_onion_client_auth - add Client Authentication for a v3 onion service
+    |- remove_onion_client_auth - remove Client Authentication for a v3 onion service
+    |- view_onion_client_auth - view Client Authentication for a v3 onion service
+    |
     |- add_event_listener - attaches an event listener to be notified of tor events
     |- remove_event_listener - removes a listener so it isn't notified of further events
     |
@@ -257,6 +261,7 @@ import stem.exit_policy
 import stem.response
 import stem.response.add_onion
 import stem.response.events
+import stem.response.onion_client_auth
 import stem.response.protocolinfo
 import stem.socket
 import stem.util
@@ -2900,6 +2905,12 @@ class Controller(BaseController):
     response. For instance, only bob can access using the given newly generated
     credentials...
 
+    Note that **basic_auth** only works for legacy (v2) onion services.
+    There is not yet any Control Port support for adding Client Auth to the
+    server side of a v3 onion service.
+
+    To add Client Authentication on the client side of a v3 onion, you can use
+    :func`~stem.control.Controller.add_onion_client_auth`.
     ::
 
       >>> response = controller.create_ephemeral_hidden_service(80, basic_auth = {'bob': None})
@@ -3074,6 +3085,77 @@ class Controller(BaseController):
     else:
       raise stem.ProtocolError('DEL_ONION returned unexpected response code: %s' % response.code)
 
+  async def add_onion_client_auth(self, service_id: str, private_key_blob: str, key_type: str = 'x25519', client_name: Optional[str] = None, permanent: Optional[bool] = False) -> stem.response.onion_client_auth.OnionClientAuthAddResponse:
+    """
+    Adds Client Authentication for a v3 onion service.
+
+    :param service_id: hidden service address without the '.onion' suffix
+    :param key_type: the type of private key in use. x25519 is the only one supported right now
+    :param private_key_blob: base64 encoding of x25519 private key
+    :param client_name: optional nickname for this client
+    :param permanent: optionally flag that this client's credentials should be stored in the filesystem.
+      If this is not set, the client's credentials are epheremal and stored in memory.
+
+    :returns: **True* if the client authentication was added or replaced, **False** if it
+      was rejected by the Tor controller
+
+    :raises: :class:`stem.ControllerError` if the call fails
+    """
+
+    request = 'ONION_CLIENT_AUTH_ADD %s %s:%s' % (service_id, key_type, private_key_blob)
+
+    if client_name:
+      request += ' ClientName=%s' % client_name
+
+    flags = []
+
+    if permanent:
+      flags.append('Permanent')
+
+    if flags:
+      request += ' Flags=%s' % ','.join(flags)
+
+    response = stem.response._convert_to_onion_client_auth_add(stem.response._convert_to_onion_client_auth_add(await self.msg(request)))
+
+    return response
+
+  async def remove_onion_client_auth(self, service_id: str) -> stem.response.onion_client_auth.OnionClientAuthRemoveResponse:
+    """
+    Removes Client Authentication for a v3 onion service.
+
+    :param service_id: hidden service address without the '.onion' suffix
+
+    :returns: **True* if the client authentication was removed, (or if no such
+      service ID existed), **False** if it was rejected by the Tor controller
+
+    :raises: :class:`stem.ControllerError` if the call fails
+    """
+
+    request = 'ONION_CLIENT_AUTH_REMOVE %s' % service_id
+
+    response = stem.response._convert_to_onion_client_auth_remove(stem.response._convert_to_onion_client_auth_remove(await self.msg(request)))
+
+    return response
+
+  async def view_onion_client_auth(self, service_id: str) -> stem.response.onion_client_auth.OnionClientAuthViewResponse:
+    """
+    View Client Authentication for a v3 onion service.
+
+    :param service_id: hidden service address without the '.onion' suffix
+
+    :returns: :class:`~stem.response.onion_client_auth.OnionClientAuthViewResponse` with the
+      client_auth_credential if there were credentials to view, **True** if the service ID
+      was valid but no credentials existed, **False** if the service ID was invalid
+
+    :raises: :class:`stem.ControllerError` if the call fails
+    """
+
+    request = 'ONION_CLIENT_AUTH_VIEW %s' % service_id
+
+    response = stem.response._convert_to_onion_client_auth_view(stem.response._convert_to_onion_client_auth_view(await self.msg(request)))
+
+    return response
+
   async def add_event_listener(self, listener: Callable[[stem.response.events.Event], Union[None, Awaitable[None]]], *events: 'stem.control.EventType') -> None:
     """
     Directs further tor controller events to a given function. The function is
diff --git a/stem/interpreter/settings.cfg b/stem/interpreter/settings.cfg
index af96d599..a373e617 100644
--- a/stem/interpreter/settings.cfg
+++ b/stem/interpreter/settings.cfg
@@ -87,6 +87,9 @@ help.general
 |  CLOSESTREAM - closes the given stream
 |  ADD_ONION - create a new hidden service
 |  DEL_ONION - delete a hidden service that was created with ADD_ONION
+|  ONION_CLIENT_AUTH_ADD - add Client Authentication for a v3 onion service
+|  ONION_CLIENT_AUTH_REMOVE - remove Client Authentication for a v3 onion service
+|  ONION_CLIENT_AUTH_VIEW - view Client Authentication for a v3 onion service
 |  HSFETCH - retrieve a hidden service descriptor, providing it in a HS_DESC_CONTENT event
 |  HSPOST - uploads a hidden service descriptor
 |  RESOLVE - issues an asynchronous dns or rdns request over tor
@@ -122,6 +125,9 @@ help.usage REDIRECTSTREAM => REDIRECTSTREAM StreamID Address [Port]
 help.usage CLOSESTREAM => CLOSESTREAM StreamID Reason [Flag]
 help.usage ADD_ONION => KeyType:KeyBlob [Flags=Flag] (Port=Port [,Target])...
 help.usage DEL_ONION => ServiceID
+help.usage ONION_CLIENT_AUTH_ADD => ServiceID KeyType PrivateKeyBlob [ClientName] [Permanent]
+help.usage ONION_CLIENT_AUTH_REMOVE => ServiceID
+help.usage ONION_CLIENT_AUTH_VIEW => ServiceID
 help.usage HSFETCH => HSFETCH (HSAddress/v2-DescId) [SERVER=Server]...
 help.usage HSPOST => [SERVER=Server] DESCRIPTOR
 help.usage RESOLVE => RESOLVE [mode=reverse] address
@@ -264,6 +270,9 @@ help.description.add_onion
 help.description.del_onion
 |Delete a hidden service that was created with ADD_ONION.
 
+help.description.onion_client_auth
+|Add, remove or view Client Authentication for a v3 onion service.
+
 help.description.hsfetch
 |Retrieves the descriptor for a hidden service. This is an asynchronous
 |request, with the descriptor provided by a HS_DESC_CONTENT event.
@@ -326,6 +335,9 @@ autocomplete ADD_ONION NEW:RSA1024
 autocomplete ADD_ONION NEW:ED25519-V3
 autocomplete ADD_ONION RSA1024:
 autocomplete ADD_ONION ED25519-V3:
+autocomplete ONION_CLIENT_AUTH_ADD
+autocomplete ONION_CLIENT_AUTH_REMOVE
+autocomplete ONION_CLIENT_AUTH_VIEW
 autocomplete DEL_ONION
 autocomplete HSFETCH
 autocomplete HSPOST
diff --git a/stem/response/__init__.py b/stem/response/__init__.py
index 2e251144..e77312c8 100644
--- a/stem/response/__init__.py
+++ b/stem/response/__init__.py
@@ -45,6 +45,7 @@ __all__ = [
   'events',
   'getinfo',
   'getconf',
+  'onion_client_auth',
   'protocolinfo',
   'authchallenge',
   'convert',
@@ -66,14 +67,17 @@ def convert(response_type: str, message: 'stem.response.ControlMessage', **kwarg
   =================== =====
   response_type       Class
   =================== =====
-  **ADD_ONION**       :class:`stem.response.add_onion.AddOnionResponse`
-  **AUTHCHALLENGE**   :class:`stem.response.authchallenge.AuthChallengeResponse`
-  **EVENT**           :class:`stem.response.events.Event` subclass
-  **GETCONF**         :class:`stem.response.getconf.GetConfResponse`
-  **GETINFO**         :class:`stem.response.getinfo.GetInfoResponse`
-  **MAPADDRESS**      :class:`stem.response.mapaddress.MapAddressResponse`
-  **PROTOCOLINFO**    :class:`stem.response.protocolinfo.ProtocolInfoResponse`
-  **SINGLELINE**      :class:`stem.response.SingleLineResponse`
+  **ADD_ONION**                :class:`stem.response.add_onion.AddOnionResponse`
+  **AUTHCHALLENGE**            :class:`stem.response.authchallenge.AuthChallengeResponse`
+  **EVENT**                    :class:`stem.response.events.Event` subclass
+  **GETCONF**                  :class:`stem.response.getconf.GetConfResponse`
+  **GETINFO**                  :class:`stem.response.getinfo.GetInfoResponse`
+  **MAPADDRESS**               :class:`stem.response.mapaddress.MapAddressResponse`
+  **ONION_CLIENT_AUTH_ADD**    :class:`stem.response.onion_client_auth.OnionClientAuthAddResponse`
+  **ONION_CLIENT_AUTH_REMOVE** :class:`stem.response.onion_client_auth.OnionClientAuthRemoveResponse`
+  **ONION_CLIENT_AUTH_VIEW**   :class:`stem.response.onion_client_auth.OnionClientAuthViewResponse`
+  **PROTOCOLINFO**             :class:`stem.response.protocolinfo.ProtocolInfoResponse`
+  **SINGLELINE**               :class:`stem.response.SingleLineResponse`
   =================== =====
 
   :param response_type: type of tor response to convert to
@@ -101,6 +105,7 @@ def convert(response_type: str, message: 'stem.response.ControlMessage', **kwarg
   import stem.response.getinfo
   import stem.response.getconf
   import stem.response.mapaddress
+  import stem.response.onion_client_auth
   import stem.response.protocolinfo
 
   if not isinstance(message, ControlMessage):
@@ -113,6 +118,9 @@ def convert(response_type: str, message: 'stem.response.ControlMessage', **kwarg
     'GETCONF': stem.response.getconf.GetConfResponse,
     'GETINFO': stem.response.getinfo.GetInfoResponse,
     'MAPADDRESS': stem.response.mapaddress.MapAddressResponse,
+    'ONION_CLIENT_AUTH_ADD': stem.response.onion_client_auth.OnionClientAuthAddResponse,
+    'ONION_CLIENT_AUTH_REMOVE': stem.response.onion_client_auth.OnionClientAuthRemoveResponse,
+    'ONION_CLIENT_AUTH_VIEW': stem.response.onion_client_auth.OnionClientAuthViewResponse,
     'PROTOCOLINFO': stem.response.protocolinfo.ProtocolInfoResponse,
     'SINGLELINE': SingleLineResponse,
   }
@@ -153,6 +161,17 @@ def _convert_to_add_onion(message: 'stem.response.ControlMessage', **kwargs: Any
   stem.response.convert('ADD_ONION', message)
   return message  # type: ignore
 
+def _convert_to_onion_client_auth_add(message: 'stem.response.ControlMessage', **kwargs: Any) -> 'stem.response.onion_client_auth.OnionClientAuthAddResponse':
+  stem.response.convert('ONION_CLIENT_AUTH_ADD', message)
+  return message  # type: ignore
+
+def _convert_to_onion_client_auth_remove(message: 'stem.response.ControlMessage', **kwargs: Any) -> 'stem.response.onion_client_auth.OnionClientAuthRemoveResponse':
+  stem.response.convert('ONION_CLIENT_AUTH_REMOVE', message)
+  return message  # type: ignore
+
+def _convert_to_onion_client_auth_view(message: 'stem.response.ControlMessage', **kwargs: Any) -> 'stem.response.onion_client_auth.OnionClientAuthViewResponse':
+  stem.response.convert('ONION_CLIENT_AUTH_VIEW', message)
+  return message  # type: ignore
 
 def _convert_to_mapaddress(message: 'stem.response.ControlMessage', **kwargs: Any) -> 'stem.response.mapaddress.MapAddressResponse':
   stem.response.convert('MAPADDRESS', message)
@@ -226,13 +245,13 @@ class ControlMessage(object):
 
   def is_ok(self) -> bool:
     """
-    Checks if any of our lines have a 250 response.
+    Checks if any of our lines have a 250, 251 or 252 response.
 
-    :returns: **True** if any lines have a 250 response code, **False** otherwise
+    :returns: **True** if any lines have a 250, 251 or 252 response code, **False** otherwise
     """
 
     for code, _, _ in self._parsed_content:
-      if code == '250':
+      if code in ['250', '251', '252']:
         return True
 
     return False
diff --git a/stem/response/onion_client_auth.py b/stem/response/onion_client_auth.py
new file mode 100644
index 00000000..80800bf3
--- /dev/null
+++ b/stem/response/onion_client_auth.py
@@ -0,0 +1,58 @@
+# Copyright 2015-2020, Damian Johnson and The Tor Project
+# See LICENSE for licensing information
+
+import stem.response
+from stem.util import log
+
+class OnionClientAuthAddResponse(stem.response.ControlMessage):
+  """
+  ONION_CLIENT_AUTH_ADD response.
+  """
+
+  def _parse_message(self) -> None:
+    # ONION_CLIENT_AUTH_ADD responds with:
+    # '250 OK',
+    # '251 Client for onion existed and replaced',
+    # '252 Registered client and decrypted desc',
+    # '512 Invalid v3 address [service id]',
+    # '553 Unable to store creds for [service id]'
+
+    if not self.is_ok():
+      raise stem.ProtocolError("ONION_CLIENT_AUTH_ADD response didn't have an OK status: %s" % self)
+
+class OnionClientAuthRemoveResponse(stem.response.ControlMessage):
+  """
+  ONION_CLIENT_AUTH_REMOVE response.
+  """
+
+  def _parse_message(self) -> None:
+    # ONION_CLIENT_AUTH_REMOVE responds with:
+    # '250 OK', 
+    # '251 No credentials for [service id]',
+    # '512 Invalid v3 address [service id]'
+
+    if not self.is_ok():
+      raise stem.ProtocolError("ONION_CLIENT_AUTH_REMOVE response didn't have an OK status: %s" % self)
+
+class OnionClientAuthViewResponse(stem.response.ControlMessage):
+  """
+  ONION_CLIENT_AUTH_VIEW response.
+  """
+
+  def _parse_message(self) -> None:
+    # ONION_CLIENT_AUTH_VIEW responds with:
+    # '250 OK' if there was Client Auth for this service or if the service is a valid address,
+    # ''512 Invalid v3 address [service id]'
+
+    self.client_auth_credential = None
+
+    if not self.is_ok():
+      raise stem.ProtocolError("ONION_CLIENT_AUTH_VIEW response didn't have an OK status: %s" % self)
+    else:
+      for line in list(self):
+        if line.startswith('CLIENT'):
+          key, value = line.split(' ', 1)
+          log.debug(key)
+          log.debug(value)
+
+          self.client_auth_credential = value
diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py
index 19d4ba85..47c51caf 100644
--- a/test/integ/control/controller.py
+++ b/test/integ/control/controller.py
@@ -1602,6 +1602,54 @@ class TestController(unittest.TestCase):
       finally:
         await controller.set_conf('OrPort', str(test.runner.ORPORT))
 
+  @test.require.controller
+  @async_test
+  async def test_client_auth_for_v3_onion(self):
+    """
+    Exercises adding, viewing and removing Client Auth for a v3 ephemeral hidden service.
+    """
+
+    runner = test.runner.get_runner()
+
+    async with await runner.get_tor_controller() as controller:
+      service_id = 'yvhz3ofkv7gwf5hpzqvhonpr3gbax2cc7dee3xcnt7dmtlx2gu7vyvid'
+      # This is an invalid key, it should throw an error
+      private_key = 'XXXXXXXXXFCV0c0ELDKKDpSFgVIB8Yow8Evj5iD+GoiTtK878NkQ='
+      exc_msg = "ONION_CLIENT_AUTH_ADD response didn't have an OK status: Failed to decode x25519 private key"
+
+      with self.assertRaisesWith(stem.ProtocolError, exc_msg):
+        await controller.add_onion_client_auth(service_id, private_key)
+
+      # This is a valid key
+      private_key = 'FCV0c0ELDKKDpSFgVIB8Yow8Evj5iD+GoiTtK878NkQ='
+      response = await controller.add_onion_client_auth(service_id, private_key)
+
+      # View the credential
+      response = await controller.view_onion_client_auth(service_id)
+      self.assertEqual(response.client_auth_credential, '%s x25519:%s' % (service_id, private_key))
+
+      # Remove the credential
+      await controller.remove_onion_client_auth(service_id)
+      response = await controller.view_onion_client_auth(service_id)
+      self.assertTrue(response.client_auth_credential is None)
+
+      # Test that an invalid service ID throws the appropriate error for adding, removing or viewing client auth
+      service_id = 'xxxxxxxxyvhz3ofkv7gwf5hpzqvhonpr3gbax2cc7dee3xcnt7dmtlx2gu7vyvid'
+      exc_msg = "ONION_CLIENT_AUTH_ADD response didn't have an OK status: Invalid v3 address \"%s\"" % service_id
+
+      with self.assertRaisesWith(stem.ProtocolError, exc_msg):
+        await controller.add_onion_client_auth(service_id, private_key)
+
+      exc_msg = "ONION_CLIENT_AUTH_REMOVE response didn't have an OK status: Invalid v3 address \"%s\"" % service_id
+
+      with self.assertRaisesWith(stem.ProtocolError, exc_msg):
+        await controller.remove_onion_client_auth(service_id)
+
+      exc_msg = "ONION_CLIENT_AUTH_VIEW response didn't have an OK status: Invalid v3 address \"%s\"" % service_id
+
+      with self.assertRaisesWith(stem.ProtocolError, exc_msg):
+        await controller.view_onion_client_auth(service_id)
+
   async def _get_router_status_entry(self, controller):
     """
     Provides a router status entry for a relay with a nickname other than





More information about the tor-commits mailing list