[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