commit 5b1fc94f6cb6719ff9bc2ab2c3c5620ac158d08b Author: Damian Johnson atagar@torproject.org Date: Sat Aug 24 16:21:24 2019 -0700
Validate mandatory fields are present --- docs/change_log.rst | 3 ++- docs/contents.rst | 1 + docs/tutorials/mirror_mirror_on_the_wall.rst | 2 +- stem/descriptor/hidden_service.py | 32 +++++++++++++++++++++++----- test/unit/descriptor/hidden_service_v2.py | 4 ++-- test/unit/descriptor/hidden_service_v3.py | 25 +++++++++++++++++++++- 6 files changed, 57 insertions(+), 10 deletions(-)
diff --git a/docs/change_log.rst b/docs/change_log.rst index d9c6bfa3..c5b5051e 100644 --- a/docs/change_log.rst +++ b/docs/change_log.rst @@ -60,6 +60,7 @@ The following are only available within Stem's `git repository * **Descriptors**
* Added the `stem.descriptor.collector <api/descriptor/collector.html>`_ module (:trac:`17979`) + * Added `v3 hidden service descriptor support <api/descriptor/hidden_service.html>`_ (:trac:`31369`) * `Bandwidth file support <api/descriptor/bandwidth_file.html>`_ (:trac:`29056`) * `stem.descriptor.remote <api/descriptor/remote.html>`_ methods now raise :class:`stem.DownloadFailed` * Check Ed25519 validity though the cryptography module rather than PyNaCl (:trac:`22022`) @@ -364,7 +365,7 @@ And last, Stem also now runs directly under both python2 and python3 without a * **Descriptors**
* Lazy-loading descriptors, improving performance by 25-70% depending on what type it is (:trac:`14011`) - * Added `support for hidden service descriptors <api/descriptor/hidden_service_descriptor.html>`_ (:trac:`15004`) + * Added `support for hidden service descriptors <api/descriptor/hidden_service.html>`_ (:trac:`15004`) * When reading sanitised bridge descriptors (server or extrainfo), :func:`~stem.descriptor.__init__.parse_file` treated the whole file as a single descriptor * The :class:`~stem.descriptor.networkstatus.DirectoryAuthority` 'fingerprint' attribute was actually its 'v3ident' * Added consensus' new package attribute (:spec:`ab64534`) diff --git a/docs/contents.rst b/docs/contents.rst index 98e80a5f..87e75220 100644 --- a/docs/contents.rst +++ b/docs/contents.rst @@ -14,6 +14,7 @@ Contents tutorials/down_the_rabbit_hole tutorials/double_double_toil_and_trouble
+ tutorials/examples/bandwidth_stats tutorials/examples/check_digests tutorials/examples/compare_flags tutorials/examples/download_descriptor diff --git a/docs/tutorials/mirror_mirror_on_the_wall.rst b/docs/tutorials/mirror_mirror_on_the_wall.rst index 699625e4..f16df19b 100644 --- a/docs/tutorials/mirror_mirror_on_the_wall.rst +++ b/docs/tutorials/mirror_mirror_on_the_wall.rst @@ -34,7 +34,7 @@ Descriptor Type `Microdescriptor <../api/descriptor/microdescriptor.html>`_ Minimalistic document that just includes the information necessary for Tor clients to work. `Network Status Document <../api/descriptor/networkstatus.html>`_ Though Tor relays are decentralized, the directories that track the overall network are not. These central points are called **directory authorities**, and every hour they publish a document called a **consensus** (aka, network status document). The consensus in turn is made up of **router status entries**. `Router Status Entry <../api/descriptor/router_status_entry.html>`_ Relay information provided by the directory authorities including flags, heuristics used for relay selection, etc. -`Hidden Service Descriptor <../api/descriptor/hidden_service.html>`_ Information pertaining to a `Hidden Service https://www.torproject.org/docs/hidden-services.html.en`_. These can only be `queried through the tor process <over_the_river.html#hidden-service-descriptors>`_. +`Hidden Service Descriptor <../api/descriptor/hidden_service.html>`_ Information pertaining to a `Hidden Service https://www.torproject.org/docs/hidden-services.html.en`_. These can only be `queried through the tor process <over_the_river.html#hidden-service-descriptors>`_. ================================================================================ ===========
.. _where-do-descriptors-come-from: diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py index 1ab450fd..52e1b0b1 100644 --- a/stem/descriptor/hidden_service.py +++ b/stem/descriptor/hidden_service.py @@ -2,15 +2,17 @@ # See LICENSE for licensing information
""" -Parsing for Tor hidden service descriptors as described in Tor's `rend-spec -https://gitweb.torproject.org/torspec.git/tree/rend-spec.txt`_. +Parsing for Tor hidden service descriptors as described in Tor's `version 2 +https://gitweb.torproject.org/torspec.git/tree/rend-spec-v2.txt`_ and +`version 3 https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt`_ +rend-spec.
Unlike other descriptor types these describe a hidden service rather than a relay. They're created by the service, and can only be fetched via relays with the HSDir flag.
These are only available through the Controller's -:func:`~stem.control.get_hidden_service_descriptor` method. +:func:`~stem.control.Controller.get_hidden_service_descriptor` method.
**Module Overview:**
@@ -54,7 +56,7 @@ if stem.prereq._is_lru_cache_available(): else: from stem.util.lru_cache import lru_cache
-REQUIRED_FIELDS = ( +REQUIRED_V2_FIELDS = ( 'rendezvous-service-descriptor', 'version', 'permanent-key', @@ -64,6 +66,15 @@ REQUIRED_FIELDS = ( 'signature', )
+REQUIRED_V3_FIELDS = ( + 'hs-descriptor', + 'descriptor-lifetime', + 'descriptor-signing-key-cert', + 'revision-counter', + 'superencrypted', + 'signature', +) + INTRODUCTION_POINTS_ATTR = { 'identifier': None, 'address': None, @@ -284,7 +295,7 @@ class HiddenServiceDescriptorV2(BaseHiddenServiceDescriptor): entries = _descriptor_components(raw_contents, validate, non_ascii_fields = ('introduction-points'))
if validate: - for keyword in REQUIRED_FIELDS: + for keyword in REQUIRED_V2_FIELDS: if keyword not in entries: raise ValueError("Hidden service descriptor must have a '%s' entry" % keyword) elif keyword in entries and len(entries[keyword]) > 1: @@ -520,6 +531,17 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor): entries = _descriptor_components(raw_contents, validate)
if validate: + for keyword in REQUIRED_V3_FIELDS: + if keyword not in entries: + raise ValueError("Hidden service descriptor must have a '%s' entry" % keyword) + elif keyword in entries and len(entries[keyword]) > 1: + raise ValueError("The '%s' entry can only appear once in a hidden service descriptor" % keyword) + + if 'hs-descriptor' != list(entries.keys())[0]: + raise ValueError("Hidden service descriptor must start with a 'hs-descriptor' entry") + elif 'signature' != list(entries.keys())[-1]: + raise ValueError("Hidden service descriptor must end with a 'signature' entry") + self._parse(entries, validate) else: self._entries = entries diff --git a/test/unit/descriptor/hidden_service_v2.py b/test/unit/descriptor/hidden_service_v2.py index ac64fa19..5191775e 100644 --- a/test/unit/descriptor/hidden_service_v2.py +++ b/test/unit/descriptor/hidden_service_v2.py @@ -11,7 +11,7 @@ import stem.prereq import test.require
from stem.descriptor.hidden_service import ( - REQUIRED_FIELDS, + REQUIRED_V2_FIELDS, DecryptionFailure, HiddenServiceDescriptorV2, ) @@ -468,7 +468,7 @@ class TestHiddenServiceDescriptorV2(unittest.TestCase): 'signature': 'signature', }
- for line in REQUIRED_FIELDS: + for line in REQUIRED_V2_FIELDS: desc_text = HiddenServiceDescriptorV2.content(exclude = (line,))
expected = [] if line == 'protocol-versions' else None diff --git a/test/unit/descriptor/hidden_service_v3.py b/test/unit/descriptor/hidden_service_v3.py index d441c8cc..f6407623 100644 --- a/test/unit/descriptor/hidden_service_v3.py +++ b/test/unit/descriptor/hidden_service_v3.py @@ -7,14 +7,19 @@ import unittest
import stem.descriptor
-from stem.descriptor.hidden_service import HiddenServiceDescriptorV3 +from stem.descriptor.hidden_service import ( + REQUIRED_V3_FIELDS, + HiddenServiceDescriptorV3, +)
from test.unit.descriptor import ( get_resource, base_expect_invalid_attr, + base_expect_invalid_attr_for_text, )
expect_invalid_attr = functools.partial(base_expect_invalid_attr, HiddenServiceDescriptorV3, 'version', 3) +expect_invalid_attr_for_text = functools.partial(base_expect_invalid_attr_for_text, HiddenServiceDescriptorV3, 'version', 3)
EXPECTED_SIGNING_CERT = """\ -----BEGIN ED25519 CERT----- @@ -43,6 +48,24 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase): self.assertTrue('k9uKnDpxhkH0h1h' in desc.superencrypted) self.assertEqual('wdc7ffr+dPZJ/mIQ1l4WYqNABcmsm6SHW/NL3M3wG7bjjqOJWoPR5TimUXxH52n5Zk0Gc7hl/hz3YYmAx5MvAg', desc.signature)
+ def test_required_fields(self): + """ + Check that we require the mandatory fields. + """ + + line_to_attr = { + 'hs-descriptor': 'version', + 'descriptor-lifetime': 'lifetime', + 'descriptor-signing-key-cert': 'signing_cert', + 'revision-counter': 'revision_counter', + 'superencrypted': 'superencrypted', + 'signature': 'signature', + } + + for line in REQUIRED_V3_FIELDS: + desc_text = HiddenServiceDescriptorV3.content(exclude = (line,)) + expect_invalid_attr_for_text(self, desc_text, line_to_attr[line], None) + def test_invalid_version(self): """ Checks that our version field expects a numeric value.