commit 2f3bbf64b4ce3b63160a3c56b15748ba626de4a0 Author: Miguel Jacq mig@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')
tor-commits@lists.torproject.org