commit 2f3bbf64b4ce3b63160a3c56b15748ba626de4a0
Author: Miguel Jacq <mig(a)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')