diff options
| author | Damian Johnson <atagar@torproject.org> | 2019-08-24 17:17:15 -0700 |
|---|---|---|
| committer | Damian Johnson <atagar@torproject.org> | 2019-08-24 17:17:15 -0700 |
| commit | 6790035c5756a8022e389f2cb95d2afb78d8c953 (patch) | |
| tree | ed78872275d98b6dbbb05606b85a41ea17cbf5e5 | |
| parent | acfcc58b011adaa43d19b80869f54404dfd480b6 (diff) | |
| parent | 5b1fc94f6cb6719ff9bc2ab2c3c5620ac158d08b (diff) | |
Hidden service v3 descriptor support
Support for the outer later of v3 hidden service descriptors...
https://trac.torproject.org/projects/tor/ticket/31369
https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt#n1046
This does *not* include support for reading HS-DESC-ENC (superencrypted blobs)
nor validating the signature. Patches welcome for both!
| -rw-r--r-- | docs/api.rst | 2 | ||||
| -rw-r--r-- | docs/api/descriptor/hidden_service.rst | 5 | ||||
| -rw-r--r-- | docs/api/descriptor/hidden_service_descriptor.rst | 5 | ||||
| -rw-r--r-- | docs/change_log.rst | 10 | ||||
| -rw-r--r-- | docs/contents.rst | 3 | ||||
| -rw-r--r-- | docs/tutorials/mirror_mirror_on_the_wall.rst | 2 | ||||
| -rw-r--r-- | docs/tutorials/over_the_river.rst | 2 | ||||
| -rw-r--r-- | stem/control.py | 2 | ||||
| -rw-r--r-- | stem/descriptor/__init__.py | 38 | ||||
| -rw-r--r-- | stem/descriptor/extrainfo_descriptor.py | 15 | ||||
| -rw-r--r-- | stem/descriptor/hidden_service.py | 552 | ||||
| -rw-r--r-- | stem/descriptor/hidden_service_descriptor.py | 446 | ||||
| -rw-r--r-- | stem/descriptor/server_descriptor.py | 34 | ||||
| -rw-r--r-- | stem/response/events.py | 4 | ||||
| -rw-r--r-- | test/settings.cfg | 11 | ||||
| -rw-r--r-- | test/unit/descriptor/data/hidden_service_v3 | 223 | ||||
| -rw-r--r-- | test/unit/descriptor/hidden_service_v2.py (renamed from test/unit/descriptor/hidden_service_descriptor.py) | 60 | ||||
| -rw-r--r-- | test/unit/descriptor/hidden_service_v3.py | 109 |
18 files changed, 987 insertions, 536 deletions
diff --git a/docs/api.rst b/docs/api.rst index a8ba7e24..cbbf0dd0 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -35,7 +35,7 @@ remotely like Tor does. * `stem.descriptor.microdescriptor <api/descriptor/microdescriptor.html>`_ - Minimalistic counterpart for server descriptors. * `stem.descriptor.networkstatus <api/descriptor/networkstatus.html>`_ - Network status documents which make up the Tor consensus. * `stem.descriptor.router_status_entry <api/descriptor/router_status_entry.html>`_ - Relay entries within a network status document. - * `stem.descriptor.hidden_service_descriptor <api/descriptor/hidden_service_descriptor.html>`_ - Descriptors generated for hidden services. + * `stem.descriptor.hidden_service <api/descriptor/hidden_service.html>`_ - Descriptors generated for hidden services. * `stem.descriptor.bandwidth_file <api/descriptor/bandwidth_file.html>`_ - Bandwidth authority metrics. * `stem.descriptor.tordnsel <api/descriptor/tordnsel.html>`_ - `TorDNSEL <https://www.torproject.org/projects/tordnsel.html.en>`_ exit lists. * `stem.descriptor.certificate <api/descriptor/certificate.html>`_ - `Ed25519 certificates <https://gitweb.torproject.org/torspec.git/tree/cert-spec.txt>`_. diff --git a/docs/api/descriptor/hidden_service.rst b/docs/api/descriptor/hidden_service.rst new file mode 100644 index 00000000..21b9bd7b --- /dev/null +++ b/docs/api/descriptor/hidden_service.rst @@ -0,0 +1,5 @@ +Hidden Service Descriptor +========================= + +.. automodule:: stem.descriptor.hidden_service + diff --git a/docs/api/descriptor/hidden_service_descriptor.rst b/docs/api/descriptor/hidden_service_descriptor.rst deleted file mode 100644 index 145203e6..00000000 --- a/docs/api/descriptor/hidden_service_descriptor.rst +++ /dev/null @@ -1,5 +0,0 @@ -Hidden Service Descriptor -========================= - -.. automodule:: stem.descriptor.hidden_service_descriptor - diff --git a/docs/change_log.rst b/docs/change_log.rst index c244d7d2..c5b5051e 100644 --- a/docs/change_log.rst +++ b/docs/change_log.rst @@ -52,16 +52,17 @@ The following are only available within Stem's `git repository * Controller events could fail to be delivered in a timely fashion (:trac:`27173`) * Adjusted :func:`~stem.control.Controller.get_microdescriptors` fallback to also use '.new' cache files (:trac:`28508`) * ExitPolicies could raise TypeError when read concurrently (:trac:`29899`) - * **STALE_DESC** :data:`~stem.Flag` (:spec:`d14164d8`) + * **STALE_DESC** :data:`~stem.Flag` (:spec:`d14164d`) * **DORMANT** and **ACTIVE** :data:`~stem.Signal` (:spec:`4421149`) * **QUERY_RATE_LIMITED** :data:`~stem.HSDescReason` (:spec:`bd80679`) - * **EXTOR** and **HTTPTUNNEL** :data:`~stem.Listener` + * **EXTOR** and **HTTPTUNNEL** :data:`~stem.control.Listener` * **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>`_ now raise :class:`stem.DownloadFailed` + * `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`) * Download compressed descriptors by default (:trac:`29186`) * Added :class:`~stem.descriptor.Compression` class @@ -78,6 +79,7 @@ The following are only available within Stem's `git repository * Replaced the **digest** attribute of :class:`~stem.descriptor.microdescriptor.Microdescriptor` with a method by the same name (:trac:`28398`) * Default the **version_flavor** attribute of :class:`~stem.descriptor.networkstatus.NetworkStatusDocumentV3` to 'ns' (:spec:`d97f8d9`) * DescriptorDownloader crashed if **use_mirrors** is set (:trac:`28393`) + * Renamed stem.descriptor.hidden_service_descriptor to stem.descriptor.hidden_service * Don't download from Serge, a bridge authority that frequently timeout * Updated dizum authority's address (:trac:`31406`) @@ -363,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 267979e0..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 @@ -50,7 +51,7 @@ Contents api/descriptor/microdescriptor api/descriptor/networkstatus api/descriptor/router_status_entry - api/descriptor/hidden_service_descriptor + api/descriptor/hidden_service api/descriptor/tordnsel api/descriptor/export diff --git a/docs/tutorials/mirror_mirror_on_the_wall.rst b/docs/tutorials/mirror_mirror_on_the_wall.rst index 04cc86de..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_descriptor.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/docs/tutorials/over_the_river.rst b/docs/tutorials/over_the_river.rst index dac78827..ff8c7feb 100644 --- a/docs/tutorials/over_the_river.rst +++ b/docs/tutorials/over_the_river.rst @@ -171,7 +171,7 @@ its :func:`~stem.control.Controller.get_hidden_service_descriptor` method... A hidden service's introduction points are a base64 encoded field that's possibly encrypted. These can be decoded (and decrypted if necessary) with the descriptor's -:func:`~stem.descriptor.hidden_service_descriptor.HiddenServiceDescriptor.introduction_points` +:func:`~stem.descriptor.hidden_service.HiddenServiceDescriptor.introduction_points` method. .. literalinclude:: /_static/example/introduction_points.py diff --git a/stem/control.py b/stem/control.py index d8423ffa..86f4e787 100644 --- a/stem/control.py +++ b/stem/control.py @@ -2136,7 +2136,7 @@ class Controller(BaseController): :param list servers: requrest the descriptor from these specific servers :param float timeout: seconds to wait when **await_result** is **True** - :returns: :class:`~stem.descriptor.hidden_service_descriptor.HiddenServiceDescriptor` + :returns: :class:`~stem.descriptor.hidden_service.HiddenServiceDescriptorV2` for the given service if **await_result** is **True**, or **None** otherwise :raises: diff --git a/stem/descriptor/__init__.py b/stem/descriptor/__init__.py index c099ca86..3a3d1838 100644 --- a/stem/descriptor/__init__.py +++ b/stem/descriptor/__init__.py @@ -118,7 +118,7 @@ __all__ = [ 'collector', 'export', 'extrainfo_descriptor', - 'hidden_service_descriptor', + 'hidden_service', 'microdescriptor', 'networkstatus', 'reader', @@ -329,7 +329,7 @@ def parse_file(descriptor_file, descriptor_type = None, validate = False, docume torperf 1.0 **unsupported** bridge-pool-assignment 1.0 **unsupported** tordnsel 1.0 :class:`~stem.descriptor.tordnsel.TorDNSEL` - hidden-service-descriptor 1.0 :class:`~stem.descriptor.hidden_service_descriptor.HiddenServiceDescriptor` + hidden-service-descriptor 1.0 :class:`~stem.descriptor.hidden_service.HiddenServiceDescriptorV2` ========================================= ===== If you're using **python 3** then beware that the open() function defaults to @@ -532,18 +532,19 @@ def _parse_metrics_file(descriptor_type, major_version, minor_version, descripto for desc in stem.descriptor.networkstatus._parse_file(descriptor_file, document_type, validate = validate, document_handler = document_handler, **kwargs): yield desc elif descriptor_type == stem.descriptor.tordnsel.TorDNSEL.TYPE_ANNOTATION_NAME and major_version == 1: - document_type = stem.descriptor.tordnsel.TorDNSEL - for desc in stem.descriptor.tordnsel._parse_file(descriptor_file, validate = validate, **kwargs): yield desc - elif descriptor_type == stem.descriptor.hidden_service_descriptor.HiddenServiceDescriptor.TYPE_ANNOTATION_NAME and major_version == 1: - document_type = stem.descriptor.hidden_service_descriptor.HiddenServiceDescriptor + elif descriptor_type == stem.descriptor.hidden_service.HiddenServiceDescriptorV2.TYPE_ANNOTATION_NAME and major_version == 1: + desc_type = stem.descriptor.hidden_service.HiddenServiceDescriptorV2 - for desc in stem.descriptor.hidden_service_descriptor._parse_file(descriptor_file, validate = validate, **kwargs): + for desc in stem.descriptor.hidden_service._parse_file(descriptor_file, desc_type, validate = validate, **kwargs): yield desc - elif descriptor_type == stem.descriptor.bandwidth_file.BandwidthFile.TYPE_ANNOTATION_NAME and major_version == 1: - document_type = stem.descriptor.bandwidth_file.BandwidthFile + elif descriptor_type == stem.descriptor.hidden_service.HiddenServiceDescriptorV3.TYPE_ANNOTATION_NAME and major_version == 1: + desc_type = stem.descriptor.hidden_service.HiddenServiceDescriptorV3 + for desc in stem.descriptor.hidden_service._parse_file(descriptor_file, desc_type, validate = validate, **kwargs): + yield desc + elif descriptor_type == stem.descriptor.bandwidth_file.BandwidthFile.TYPE_ANNOTATION_NAME and major_version == 1: for desc in stem.descriptor.bandwidth_file._parse_file(descriptor_file, validate = validate, **kwargs): yield desc else: @@ -656,6 +657,23 @@ def _parse_bytes_line(keyword, attribute): return _parse +def _parse_int_line(keyword, attribute, allow_negative = True): + def _parse(descriptor, entries): + value = _value(keyword, entries) + + try: + int_val = int(value) + except ValueError: + raise ValueError('%s must have a numeric value: %s' % (keyword, value)) + + if not allow_negative and int_val < 0: + raise ValueError('%s must have a positive value: %s' % (keyword, value)) + + setattr(descriptor, attribute, int_val) + + return _parse + + def _parse_timestamp_line(keyword, attribute): # "<keyword>" YYYY-MM-DD HH:MM:SS @@ -1521,7 +1539,7 @@ def _descriptor_components(raw_contents, validate, extra_keywords = (), non_asci import stem.descriptor.bandwidth_file import stem.descriptor.extrainfo_descriptor -import stem.descriptor.hidden_service_descriptor +import stem.descriptor.hidden_service import stem.descriptor.microdescriptor import stem.descriptor.networkstatus import stem.descriptor.server_descriptor diff --git a/stem/descriptor/extrainfo_descriptor.py b/stem/descriptor/extrainfo_descriptor.py index 82e3154f..d55b8eef 100644 --- a/stem/descriptor/extrainfo_descriptor.py +++ b/stem/descriptor/extrainfo_descriptor.py @@ -88,6 +88,7 @@ from stem.descriptor import ( _value, _values, _parse_simple_line, + _parse_int_line, _parse_timestamp_line, _parse_forty_character_hex, _parse_key_block, @@ -306,19 +307,6 @@ def _parse_transport_line(descriptor, entries): descriptor.transport = transports -def _parse_cell_circuits_per_decline_line(descriptor, entries): - # "cell-circuits-per-decile" num - - value = _value('cell-circuits-per-decile', entries) - - if not value.isdigit(): - raise ValueError('Non-numeric cell-circuits-per-decile value: %s' % value) - elif int(value) < 0: - raise ValueError('Negative cell-circuits-per-decile value: %s' % value) - - descriptor.cell_circuits_per_decile = int(value) - - def _parse_padding_counts_line(descriptor, entries): # "padding-counts" YYYY-MM-DD HH:MM:SS (NSEC s) key=val key=val... @@ -538,6 +526,7 @@ _parse_dirreq_v3_share_line = functools.partial(_parse_dirreq_share_line, 'dirre _parse_cell_processed_cells_line = functools.partial(_parse_cell_line, 'cell-processed-cells', 'cell_processed_cells') _parse_cell_queued_cells_line = functools.partial(_parse_cell_line, 'cell-queued-cells', 'cell_queued_cells') _parse_cell_time_in_queue_line = functools.partial(_parse_cell_line, 'cell-time-in-queue', 'cell_time_in_queue') +_parse_cell_circuits_per_decline_line = _parse_int_line('cell-circuits-per-decile', 'cell_circuits_per_decile', allow_negative = False) _parse_published_line = _parse_timestamp_line('published', 'published') _parse_geoip_start_time_line = _parse_timestamp_line('geoip-start-time', 'geoip_start_time') _parse_cell_stats_end_line = functools.partial(_parse_timestamp_and_interval_line, 'cell-stats-end', 'cell_stats_end', 'cell_stats_interval') diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py new file mode 100644 index 00000000..52e1b0b1 --- /dev/null +++ b/stem/descriptor/hidden_service.py @@ -0,0 +1,552 @@ +# Copyright 2015-2019, Damian Johnson and The Tor Project +# See LICENSE for licensing information + +""" +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.Controller.get_hidden_service_descriptor` method. + +**Module Overview:** + +:: + + BaseHiddenServiceDescriptor - Common parent for hidden service descriptors + |- HiddenServiceDescriptorV2 - Version 2 hidden service descriptor + +- HiddenServiceDescriptorV3 - Version 3 hidden service descriptor + +.. versionadded:: 1.4.0 +""" + +import base64 +import binascii +import collections +import hashlib +import io + +import stem.prereq +import stem.util.connection +import stem.util.str_tools + +from stem.descriptor import ( + PGP_BLOCK_END, + Descriptor, + _descriptor_content, + _descriptor_components, + _read_until_keywords, + _bytes_for_block, + _value, + _parse_simple_line, + _parse_int_line, + _parse_timestamp_line, + _parse_key_block, + _random_date, + _random_crypto_blob, +) + +if stem.prereq._is_lru_cache_available(): + from functools import lru_cache +else: + from stem.util.lru_cache import lru_cache + +REQUIRED_V2_FIELDS = ( + 'rendezvous-service-descriptor', + 'version', + 'permanent-key', + 'secret-id-part', + 'publication-time', + 'protocol-versions', + 'signature', +) + +REQUIRED_V3_FIELDS = ( + 'hs-descriptor', + 'descriptor-lifetime', + 'descriptor-signing-key-cert', + 'revision-counter', + 'superencrypted', + 'signature', +) + +INTRODUCTION_POINTS_ATTR = { + 'identifier': None, + 'address': None, + 'port': None, + 'onion_key': None, + 'service_key': None, + 'intro_authentication': [], +} + +# introduction-point fields that can only appear once + +SINGLE_INTRODUCTION_POINT_FIELDS = [ + 'introduction-point', + 'ip-address', + 'onion-port', + 'onion-key', + 'service-key', +] + +BASIC_AUTH = 1 +STEALTH_AUTH = 2 + + +class IntroductionPoints(collections.namedtuple('IntroductionPoints', INTRODUCTION_POINTS_ATTR.keys())): + """ + :var str identifier: hash of this introduction point's identity key + :var str address: address of this introduction point + :var int port: port where this introduction point is listening + :var str onion_key: public key for communicating with this introduction point + :var str service_key: public key for communicating with this hidden service + :var list intro_authentication: tuples of the form (auth_type, auth_data) for + establishing a connection + """ + + +class DecryptionFailure(Exception): + """ + Failure to decrypt the hidden service descriptor's introduction-points. + """ + + +def _parse_file(descriptor_file, desc_type = None, validate = False, **kwargs): + """ + Iterates over the hidden service descriptors in a file. + + :param file descriptor_file: file with descriptor content + :param class desc_type: BaseHiddenServiceDescriptor subclass + :param bool validate: checks the validity of the descriptor's content if + **True**, skips these checks otherwise + :param dict kwargs: additional arguments for the descriptor constructor + + :returns: iterator for :class:`~stem.descriptor.hidden_service.HiddenServiceDescriptorV2` + instances in the file + + :raises: + * **ValueError** if the contents is malformed and validate is **True** + * **IOError** if the file can't be read + """ + + if desc_type is None: + desc_type = HiddenServiceDescriptorV2 + + # Hidden service v3 ends with a signature line, whereas v2 has a pgp style + # block following it. + + while True: + descriptor_content = _read_until_keywords('signature', descriptor_file, True) + + if desc_type == HiddenServiceDescriptorV2: + block_end_prefix = PGP_BLOCK_END.split(' ', 1)[0] + descriptor_content += _read_until_keywords(block_end_prefix, descriptor_file, True) + + if descriptor_content: + if descriptor_content[0].startswith(b'@type'): + descriptor_content = descriptor_content[1:] + + yield desc_type(bytes.join(b'', descriptor_content), validate, **kwargs) + else: + break # done parsing file + + +def _parse_protocol_versions_line(descriptor, entries): + value = _value('protocol-versions', entries) + + try: + versions = [int(entry) for entry in value.split(',')] + except ValueError: + raise ValueError('protocol-versions line has non-numeric versoins: protocol-versions %s' % value) + + for v in versions: + if v <= 0: + raise ValueError('protocol-versions must be positive integers: %s' % value) + + descriptor.protocol_versions = versions + + +def _parse_introduction_points_line(descriptor, entries): + _, block_type, block_contents = entries['introduction-points'][0] + + if not block_contents or block_type != 'MESSAGE': + raise ValueError("'introduction-points' should be followed by a MESSAGE block, but was a %s" % block_type) + + descriptor.introduction_points_encoded = block_contents + descriptor.introduction_points_auth = [] # field was never implemented in tor (#15190) + + try: + descriptor.introduction_points_content = _bytes_for_block(block_contents) + except TypeError: + raise ValueError("'introduction-points' isn't base64 encoded content:\n%s" % block_contents) + + +_parse_v2_version_line = _parse_int_line('version', 'version', allow_negative = False) +_parse_rendezvous_service_descriptor_line = _parse_simple_line('rendezvous-service-descriptor', 'descriptor_id') +_parse_permanent_key_line = _parse_key_block('permanent-key', 'permanent_key', 'RSA PUBLIC KEY') +_parse_secret_id_part_line = _parse_simple_line('secret-id-part', 'secret_id_part') +_parse_publication_time_line = _parse_timestamp_line('publication-time', 'published') +_parse_v2_signature_line = _parse_key_block('signature', 'signature', 'SIGNATURE') + +_parse_v3_version_line = _parse_int_line('hs-descriptor', 'version', allow_negative = False) +_parse_lifetime_line = _parse_int_line('descriptor-lifetime', 'lifetime', allow_negative = False) +_parse_signing_key_line = _parse_key_block('descriptor-signing-key-cert', 'signing_cert', 'ED25519 CERT') +_parse_revision_counter_line = _parse_int_line('revision-counter', 'revision_counter', allow_negative = False) +_parse_superencrypted_line = _parse_key_block('superencrypted', 'superencrypted', 'MESSAGE') +_parse_v3_signature_line = _parse_simple_line('signature', 'signature') + + +class BaseHiddenServiceDescriptor(Descriptor): + """ + Hidden service descriptor. + + .. versionadded:: 1.8.0 + """ + + # TODO: rename this class to HiddenServiceDescriptor in stem 2.x + + +class HiddenServiceDescriptorV2(BaseHiddenServiceDescriptor): + """ + Version 2 hidden service descriptor. + + :var str descriptor_id: **\\*** identifier for this descriptor, this is a base32 hash of several fields + :var int version: **\\*** hidden service descriptor version + :var str permanent_key: **\\*** long term key of the hidden service + :var str secret_id_part: **\\*** hash of the time period, cookie, and replica + values so our descriptor_id can be validated + :var datetime published: **\\*** time in UTC when this descriptor was made + :var list protocol_versions: **\\*** list of **int** versions that are supported when establishing a connection + :var str introduction_points_encoded: raw introduction points blob + :var list introduction_points_auth: **\\*** tuples of the form + (auth_method, auth_data) for our introduction_points_content + (**deprecated**, always **[]**) + :var bytes introduction_points_content: decoded introduction-points content + without authentication data, if using cookie authentication this is + encrypted + :var str signature: signature of the descriptor content + + **\\*** attribute is either required when we're parsed with validation or has + a default value, others are left as **None** if undefined + + .. versionchanged:: 1.6.0 + Moved from the deprecated `pycrypto + <https://www.dlitz.net/software/pycrypto/>`_ module to `cryptography + <https://pypi.org/project/cryptography/>`_ for validating signatures. + + .. versionchanged:: 1.6.0 + Added the **skip_crypto_validation** constructor argument. + """ + + TYPE_ANNOTATION_NAME = 'hidden-service-descriptor' + + ATTRIBUTES = { + 'descriptor_id': (None, _parse_rendezvous_service_descriptor_line), + 'version': (None, _parse_v2_version_line), + 'permanent_key': (None, _parse_permanent_key_line), + 'secret_id_part': (None, _parse_secret_id_part_line), + 'published': (None, _parse_publication_time_line), + 'protocol_versions': ([], _parse_protocol_versions_line), + 'introduction_points_encoded': (None, _parse_introduction_points_line), + 'introduction_points_auth': ([], _parse_introduction_points_line), + 'introduction_points_content': (None, _parse_introduction_points_line), + 'signature': (None, _parse_v2_signature_line), + } + + PARSER_FOR_LINE = { + 'rendezvous-service-descriptor': _parse_rendezvous_service_descriptor_line, + 'version': _parse_v2_version_line, + 'permanent-key': _parse_permanent_key_line, + 'secret-id-part': _parse_secret_id_part_line, + 'publication-time': _parse_publication_time_line, + 'protocol-versions': _parse_protocol_versions_line, + 'introduction-points': _parse_introduction_points_line, + 'signature': _parse_v2_signature_line, + } + + @classmethod + def content(cls, attr = None, exclude = (), sign = False): + if sign: + raise NotImplementedError('Signing of %s not implemented' % cls.__name__) + + return _descriptor_content(attr, exclude, ( + ('rendezvous-service-descriptor', 'y3olqqblqw2gbh6phimfuiroechjjafa'), + ('version', '2'), + ('permanent-key', _random_crypto_blob('RSA PUBLIC KEY')), + ('secret-id-part', 'e24kgecavwsznj7gpbktqsiwgvngsf4e'), + ('publication-time', _random_date()), + ('protocol-versions', '2,3'), + ('introduction-points', '\n-----BEGIN MESSAGE-----\n-----END MESSAGE-----'), + ), ( + ('signature', _random_crypto_blob('SIGNATURE')), + )) + + @classmethod + def create(cls, attr = None, exclude = (), validate = True, sign = False): + return cls(cls.content(attr, exclude, sign), validate = validate, skip_crypto_validation = not sign) + + def __init__(self, raw_contents, validate = False, skip_crypto_validation = False): + super(HiddenServiceDescriptorV2, self).__init__(raw_contents, lazy_load = not validate) + entries = _descriptor_components(raw_contents, validate, non_ascii_fields = ('introduction-points')) + + if validate: + 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: + raise ValueError("The '%s' entry can only appear once in a hidden service descriptor" % keyword) + + if 'rendezvous-service-descriptor' != list(entries.keys())[0]: + raise ValueError("Hidden service descriptor must start with a 'rendezvous-service-descriptor' entry") + elif 'signature' != list(entries.keys())[-1]: + raise ValueError("Hidden service descriptor must end with a 'signature' entry") + + self._parse(entries, validate) + + if not skip_crypto_validation and stem.prereq.is_crypto_available(): + signed_digest = self._digest_for_signature(self.permanent_key, self.signature) + digest_content = self._content_range('rendezvous-service-descriptor ', '\nsignature\n') + content_digest = hashlib.sha1(digest_content).hexdigest().upper() + + if signed_digest != content_digest: + raise ValueError('Decrypted digest does not match local digest (calculated: %s, local: %s)' % (signed_digest, content_digest)) + else: + self._entries = entries + + @lru_cache() + def introduction_points(self, authentication_cookie = None): + """ + Provided this service's introduction points. + + :returns: **list** of :class:`~stem.descriptor.hidden_service.IntroductionPoints` + + :raises: + * **ValueError** if the our introduction-points is malformed + * **DecryptionFailure** if unable to decrypt this field + """ + + content = self.introduction_points_content + + if not content: + return [] + elif authentication_cookie: + if not stem.prereq.is_crypto_available(): + raise DecryptionFailure('Decrypting introduction-points requires the cryptography module') + + try: + missing_padding = len(authentication_cookie) % 4 + authentication_cookie = base64.b64decode(stem.util.str_tools._to_bytes(authentication_cookie) + b'=' * missing_padding) + except TypeError as exc: + raise DecryptionFailure('authentication_cookie must be a base64 encoded string (%s)' % exc) + + authentication_type = int(binascii.hexlify(content[0:1]), 16) + + if authentication_type == BASIC_AUTH: + content = HiddenServiceDescriptorV2._decrypt_basic_auth(content, authentication_cookie) + elif authentication_type == STEALTH_AUTH: + content = HiddenServiceDescriptorV2._decrypt_stealth_auth(content, authentication_cookie) + else: + raise DecryptionFailure("Unrecognized authentication type '%s', currently we only support basic auth (%s) and stealth auth (%s)" % (authentication_type, BASIC_AUTH, STEALTH_AUTH)) + + if not content.startswith(b'introduction-point '): + raise DecryptionFailure('Unable to decrypt the introduction-points, maybe this is the wrong key?') + elif not content.startswith(b'introduction-point '): + raise DecryptionFailure('introduction-points content is encrypted, you need to provide its authentication_cookie') + + return HiddenServiceDescriptorV2._parse_introduction_points(content) + + @staticmethod + def _decrypt_basic_auth(content, authentication_cookie): + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + from cryptography.hazmat.backends import default_backend + + try: + client_blocks = int(binascii.hexlify(content[1:2]), 16) + except ValueError: + raise DecryptionFailure("When using basic auth the content should start with a number of blocks but wasn't a hex digit: %s" % binascii.hexlify(content[1:2])) + + # parse the client id and encrypted session keys + + client_entries_length = client_blocks * 16 * 20 + client_entries = content[2:2 + client_entries_length] + client_keys = [(client_entries[i:i + 4], client_entries[i + 4:i + 20]) for i in range(0, client_entries_length, 4 + 16)] + + iv = content[2 + client_entries_length:2 + client_entries_length + 16] + encrypted = content[2 + client_entries_length + 16:] + + client_id = hashlib.sha1(authentication_cookie + iv).digest()[:4] + + for entry_id, encrypted_session_key in client_keys: + if entry_id != client_id: + continue # not the session key for this client + + # try decrypting the session key + + cipher = Cipher(algorithms.AES(authentication_cookie), modes.CTR(b'\x00' * len(iv)), default_backend()) + decryptor = cipher.decryptor() + session_key = decryptor.update(encrypted_session_key) + decryptor.finalize() + + # attempt to decrypt the intro points with the session key + + cipher = Cipher(algorithms.AES(session_key), modes.CTR(iv), default_backend()) + decryptor = cipher.decryptor() + decrypted = decryptor.update(encrypted) + decryptor.finalize() + + # check if the decryption looks correct + + if decrypted.startswith(b'introduction-point '): + return decrypted + + return content # nope, unable to decrypt the content + + @staticmethod + def _decrypt_stealth_auth(content, authentication_cookie): + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + from cryptography.hazmat.backends import default_backend + + # byte 1 = authentication type, 2-17 = input vector, 18 on = encrypted content + iv, encrypted = content[1:17], content[17:] + cipher = Cipher(algorithms.AES(authentication_cookie), modes.CTR(iv), default_backend()) + decryptor = cipher.decryptor() + + return decryptor.update(encrypted) + decryptor.finalize() + + @staticmethod + def _parse_introduction_points(content): + """ + Provides the parsed list of IntroductionPoints for the unencrypted content. + """ + + introduction_points = [] + content_io = io.BytesIO(content) + + while True: + content = b''.join(_read_until_keywords('introduction-point', content_io, ignore_first = True)) + + if not content: + break # reached the end + + attr = dict(INTRODUCTION_POINTS_ATTR) + entries = _descriptor_components(content, False) + + for keyword, values in list(entries.items()): + value, block_type, block_contents = values[0] + + if keyword in SINGLE_INTRODUCTION_POINT_FIELDS and len(values) > 1: + raise ValueError("'%s' can only appear once in an introduction-point block, but appeared %i times" % (keyword, len(values))) + + if keyword == 'introduction-point': + attr['identifier'] = value + elif keyword == 'ip-address': + if not stem.util.connection.is_valid_ipv4_address(value): + raise ValueError("'%s' is an invalid IPv4 address" % value) + + attr['address'] = value + elif keyword == 'onion-port': + if not stem.util.connection.is_valid_port(value): + raise ValueError("'%s' is an invalid port" % value) + + attr['port'] = int(value) + elif keyword == 'onion-key': + attr['onion_key'] = block_contents + elif keyword == 'service-key': + attr['service_key'] = block_contents + elif keyword == 'intro-authentication': + auth_entries = [] + + for auth_value, _, _ in values: + if ' ' not in auth_value: + raise ValueError("We expected 'intro-authentication [auth_type] [auth_data]', but had '%s'" % auth_value) + + auth_type, auth_data = auth_value.split(' ')[:2] + auth_entries.append((auth_type, auth_data)) + + introduction_points.append(IntroductionPoints(**attr)) + + return introduction_points + + +class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor): + """ + Version 3 hidden service descriptor. + + :var int version: **\\*** hidden service descriptor version + :var int lifetime: **\\*** minutes after publication this descriptor is valid + :var str signing_cert: **\\*** cross-certifier for the short-term descriptor signing key + :var int revision_counter: **\\*** descriptor revision number + :var str superencrypted: **\\*** encrypted HS-DESC-ENC payload + :var str signature: **\\*** signature of this descriptor + + **\\*** attribute is either required when we're parsed with validation or has + a default value, others are left as **None** if undefined + """ + + # TODO: requested this @type on https://trac.torproject.org/projects/tor/ticket/31481 + + TYPE_ANNOTATION_NAME = 'hidden-service-descriptor-3' + + ATTRIBUTES = { + 'version': (None, _parse_v3_version_line), + 'lifetime': (None, _parse_lifetime_line), + 'signing_cert': (None, _parse_signing_key_line), + 'revision_counter': (None, _parse_revision_counter_line), + 'superencrypted': (None, _parse_superencrypted_line), + 'signature': (None, _parse_v3_signature_line), + } + + PARSER_FOR_LINE = { + 'hs-descriptor': _parse_v3_version_line, + 'descriptor-lifetime': _parse_lifetime_line, + 'descriptor-signing-key-cert': _parse_signing_key_line, + 'revision-counter': _parse_revision_counter_line, + 'superencrypted': _parse_superencrypted_line, + 'signature': _parse_v3_signature_line, + } + + @classmethod + def content(cls, attr = None, exclude = (), sign = False): + if sign: + raise NotImplementedError('Signing of %s not implemented' % cls.__name__) + + return _descriptor_content(attr, exclude, ( + ('hs-descriptor', '3'), + ('descriptor-lifetime', '180'), + ('descriptor-signing-key-cert', _random_crypto_blob('ED25519 CERT')), + ('revision-counter', '15'), + ('superencrypted', _random_crypto_blob('MESSAGE')), + ('signature', 'wdc7ffr+dPZJ/mIQ1l4WYqNABcmsm6SHW/NL3M3wG7bjjqOJWoPR5TimUXxH52n5Zk0Gc7hl/hz3YYmAx5MvAg'), + ), ()) + + @classmethod + def create(cls, attr = None, exclude = (), validate = True, sign = False): + return cls(cls.content(attr, exclude, sign), validate = validate, skip_crypto_validation = not sign) + + def __init__(self, raw_contents, validate = False, skip_crypto_validation = False): + super(HiddenServiceDescriptorV3, self).__init__(raw_contents, lazy_load = not validate) + 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 + + +# TODO: drop this alias in stem 2.x + +HiddenServiceDescriptor = HiddenServiceDescriptorV2 diff --git a/stem/descriptor/hidden_service_descriptor.py b/stem/descriptor/hidden_service_descriptor.py index 99d6414e..d77d88aa 100644 --- a/stem/descriptor/hidden_service_descriptor.py +++ b/stem/descriptor/hidden_service_descriptor.py @@ -1,444 +1,4 @@ -# Copyright 2015-2019, Damian Johnson and The Tor Project -# See LICENSE for licensing information +# TODO: This module (hidden_service_descriptor) is a temporary alias for +# hidden_service. This alias will be removed in Stem 2.x. -""" -Parsing for Tor hidden service descriptors as described in Tor's `rend-spec -<https://gitweb.torproject.org/torspec.git/tree/rend-spec.txt>`_. - -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. - -**Module Overview:** - -:: - - HiddenServiceDescriptor - Tor hidden service descriptor. - -.. versionadded:: 1.4.0 -""" - -# TODO: In stem 2.x rename this module to 'hidden_service' (ie, drop the -# redundant '_descriptor' suffix). - -import base64 -import binascii -import collections -import hashlib -import io - -import stem.prereq -import stem.util.connection -import stem.util.str_tools - -from stem.descriptor import ( - PGP_BLOCK_END, - Descriptor, - _descriptor_content, - _descriptor_components, - _read_until_keywords, - _bytes_for_block, - _value, - _parse_simple_line, - _parse_timestamp_line, - _parse_key_block, - _random_date, - _random_crypto_blob, -) - -if stem.prereq._is_lru_cache_available(): - from functools import lru_cache -else: - from stem.util.lru_cache import lru_cache - -REQUIRED_FIELDS = ( - 'rendezvous-service-descriptor', - 'version', - 'permanent-key', - 'secret-id-part', - 'publication-time', - 'protocol-versions', - 'signature', -) - -INTRODUCTION_POINTS_ATTR = { - 'identifier': None, - 'address': None, - 'port': None, - 'onion_key': None, - 'service_key': None, - 'intro_authentication': [], -} - -# introduction-point fields that can only appear once - -SINGLE_INTRODUCTION_POINT_FIELDS = [ - 'introduction-point', - 'ip-address', - 'onion-port', - 'onion-key', - 'service-key', -] - -BASIC_AUTH = 1 -STEALTH_AUTH = 2 - - -class IntroductionPoints(collections.namedtuple('IntroductionPoints', INTRODUCTION_POINTS_ATTR.keys())): - """ - :var str identifier: hash of this introduction point's identity key - :var str address: address of this introduction point - :var int port: port where this introduction point is listening - :var str onion_key: public key for communicating with this introduction point - :var str service_key: public key for communicating with this hidden service - :var list intro_authentication: tuples of the form (auth_type, auth_data) for - establishing a connection - """ - - -class DecryptionFailure(Exception): - """ - Failure to decrypt the hidden service descriptor's introduction-points. - """ - - -def _parse_file(descriptor_file, validate = False, **kwargs): - """ - Iterates over the hidden service descriptors in a file. - - :param file descriptor_file: file with descriptor content - :param bool validate: checks the validity of the descriptor's content if - **True**, skips these checks otherwise - :param dict kwargs: additional arguments for the descriptor constructor - - :returns: iterator for :class:`~stem.descriptor.hidden_service_descriptor.HiddenServiceDescriptor` - instances in the file - - :raises: - * **ValueError** if the contents is malformed and validate is **True** - * **IOError** if the file can't be read - """ - - while True: - descriptor_content = _read_until_keywords('signature', descriptor_file) - - # we've reached the 'signature', now include the pgp style block - block_end_prefix = PGP_BLOCK_END.split(' ', 1)[0] - descriptor_content += _read_until_keywords(block_end_prefix, descriptor_file, True) - - if descriptor_content: - if descriptor_content[0].startswith(b'@type'): - descriptor_content = descriptor_content[1:] - - yield HiddenServiceDescriptor(bytes.join(b'', descriptor_content), validate, **kwargs) - else: - break # done parsing file - - -def _parse_version_line(descriptor, entries): - value = _value('version', entries) - - if value.isdigit(): - descriptor.version = int(value) - else: - raise ValueError('version line must have a positive integer value: %s' % value) - - -def _parse_protocol_versions_line(descriptor, entries): - value = _value('protocol-versions', entries) - - try: - versions = [int(entry) for entry in value.split(',')] - except ValueError: - raise ValueError('protocol-versions line has non-numeric versoins: protocol-versions %s' % value) - - for v in versions: - if v <= 0: - raise ValueError('protocol-versions must be positive integers: %s' % value) - - descriptor.protocol_versions = versions - - -def _parse_introduction_points_line(descriptor, entries): - _, block_type, block_contents = entries['introduction-points'][0] - - if not block_contents or block_type != 'MESSAGE': - raise ValueError("'introduction-points' should be followed by a MESSAGE block, but was a %s" % block_type) - - descriptor.introduction_points_encoded = block_contents - descriptor.introduction_points_auth = [] # field was never implemented in tor (#15190) - - try: - descriptor.introduction_points_content = _bytes_for_block(block_contents) - except TypeError: - raise ValueError("'introduction-points' isn't base64 encoded content:\n%s" % block_contents) - - -_parse_rendezvous_service_descriptor_line = _parse_simple_line('rendezvous-service-descriptor', 'descriptor_id') -_parse_permanent_key_line = _parse_key_block('permanent-key', 'permanent_key', 'RSA PUBLIC KEY') -_parse_secret_id_part_line = _parse_simple_line('secret-id-part', 'secret_id_part') -_parse_publication_time_line = _parse_timestamp_line('publication-time', 'published') -_parse_signature_line = _parse_key_block('signature', 'signature', 'SIGNATURE') - - -class HiddenServiceDescriptor(Descriptor): - """ - Hidden service descriptor. - - :var str descriptor_id: **\\*** identifier for this descriptor, this is a base32 hash of several fields - :var int version: **\\*** hidden service descriptor version - :var str permanent_key: **\\*** long term key of the hidden service - :var str secret_id_part: **\\*** hash of the time period, cookie, and replica - values so our descriptor_id can be validated - :var datetime published: **\\*** time in UTC when this descriptor was made - :var list protocol_versions: **\\*** list of **int** versions that are supported when establishing a connection - :var str introduction_points_encoded: raw introduction points blob - :var list introduction_points_auth: **\\*** tuples of the form - (auth_method, auth_data) for our introduction_points_content - (**deprecated**, always **[]**) - :var bytes introduction_points_content: decoded introduction-points content - without authentication data, if using cookie authentication this is - encrypted - :var str signature: signature of the descriptor content - - **\\*** attribute is either required when we're parsed with validation or has - a default value, others are left as **None** if undefined - - .. versionchanged:: 1.6.0 - Moved from the deprecated `pycrypto - <https://www.dlitz.net/software/pycrypto/>`_ module to `cryptography - <https://pypi.org/project/cryptography/>`_ for validating signatures. - - .. versionchanged:: 1.6.0 - Added the **skip_crypto_validation** constructor argument. - """ - - TYPE_ANNOTATION_NAME = 'hidden-service-descriptor' - - ATTRIBUTES = { - 'descriptor_id': (None, _parse_rendezvous_service_descriptor_line), - 'version': (None, _parse_version_line), - 'permanent_key': (None, _parse_permanent_key_line), - 'secret_id_part': (None, _parse_secret_id_part_line), - 'published': (None, _parse_publication_time_line), - 'protocol_versions': ([], _parse_protocol_versions_line), - 'introduction_points_encoded': (None, _parse_introduction_points_line), - 'introduction_points_auth': ([], _parse_introduction_points_line), - 'introduction_points_content': (None, _parse_introduction_points_line), - 'signature': (None, _parse_signature_line), - } - - PARSER_FOR_LINE = { - 'rendezvous-service-descriptor': _parse_rendezvous_service_descriptor_line, - 'version': _parse_version_line, - 'permanent-key': _parse_permanent_key_line, - 'secret-id-part': _parse_secret_id_part_line, - 'publication-time': _parse_publication_time_line, - 'protocol-versions': _parse_protocol_versions_line, - 'introduction-points': _parse_introduction_points_line, - 'signature': _parse_signature_line, - } - - @classmethod - def content(cls, attr = None, exclude = (), sign = False): - if sign: - raise NotImplementedError('Signing of %s not implemented' % cls.__name__) - - return _descriptor_content(attr, exclude, ( - ('rendezvous-service-descriptor', 'y3olqqblqw2gbh6phimfuiroechjjafa'), - ('version', '2'), - ('permanent-key', _random_crypto_blob('RSA PUBLIC KEY')), - ('secret-id-part', 'e24kgecavwsznj7gpbktqsiwgvngsf4e'), - ('publication-time', _random_date()), - ('protocol-versions', '2,3'), - ('introduction-points', '\n-----BEGIN MESSAGE-----\n-----END MESSAGE-----'), - ), ( - ('signature', _random_crypto_blob('SIGNATURE')), - )) - - @classmethod - def create(cls, attr = None, exclude = (), validate = True, sign = False): - return cls(cls.content(attr, exclude, sign), validate = validate, skip_crypto_validation = not sign) - - def __init__(self, raw_contents, validate = False, skip_crypto_validation = False): - super(HiddenServiceDescriptor, self).__init__(raw_contents, lazy_load = not validate) - entries = _descriptor_components(raw_contents, validate, non_ascii_fields = ('introduction-points')) - - if validate: - for keyword in REQUIRED_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 'rendezvous-service-descriptor' != list(entries.keys())[0]: - raise ValueError("Hidden service descriptor must start with a 'rendezvous-service-descriptor' entry") - elif 'signature' != list(entries.keys())[-1]: - raise ValueError("Hidden service descriptor must end with a 'signature' entry") - - self._parse(entries, validate) - - if not skip_crypto_validation and stem.prereq.is_crypto_available(): - signed_digest = self._digest_for_signature(self.permanent_key, self.signature) - digest_content = self._content_range('rendezvous-service-descriptor ', '\nsignature\n') - content_digest = hashlib.sha1(digest_content).hexdigest().upper() - - if signed_digest != content_digest: - raise ValueError('Decrypted digest does not match local digest (calculated: %s, local: %s)' % (signed_digest, content_digest)) - else: - self._entries = entries - - @lru_cache() - def introduction_points(self, authentication_cookie = None): - """ - Provided this service's introduction points. - - :returns: **list** of :class:`~stem.descriptor.hidden_service_descriptor.IntroductionPoints` - - :raises: - * **ValueError** if the our introduction-points is malformed - * **DecryptionFailure** if unable to decrypt this field - """ - - content = self.introduction_points_content - - if not content: - return [] - elif authentication_cookie: - if not stem.prereq.is_crypto_available(): - raise DecryptionFailure('Decrypting introduction-points requires the cryptography module') - - try: - missing_padding = len(authentication_cookie) % 4 - authentication_cookie = base64.b64decode(stem.util.str_tools._to_bytes(authentication_cookie) + b'=' * missing_padding) - except TypeError as exc: - raise DecryptionFailure('authentication_cookie must be a base64 encoded string (%s)' % exc) - - authentication_type = int(binascii.hexlify(content[0:1]), 16) - - if authentication_type == BASIC_AUTH: - content = HiddenServiceDescriptor._decrypt_basic_auth(content, authentication_cookie) - elif authentication_type == STEALTH_AUTH: - content = HiddenServiceDescriptor._decrypt_stealth_auth(content, authentication_cookie) - else: - raise DecryptionFailure("Unrecognized authentication type '%s', currently we only support basic auth (%s) and stealth auth (%s)" % (authentication_type, BASIC_AUTH, STEALTH_AUTH)) - - if not content.startswith(b'introduction-point '): - raise DecryptionFailure('Unable to decrypt the introduction-points, maybe this is the wrong key?') - elif not content.startswith(b'introduction-point '): - raise DecryptionFailure('introduction-points content is encrypted, you need to provide its authentication_cookie') - - return HiddenServiceDescriptor._parse_introduction_points(content) - - @staticmethod - def _decrypt_basic_auth(content, authentication_cookie): - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - from cryptography.hazmat.backends import default_backend - - try: - client_blocks = int(binascii.hexlify(content[1:2]), 16) - except ValueError: - raise DecryptionFailure("When using basic auth the content should start with a number of blocks but wasn't a hex digit: %s" % binascii.hexlify(content[1:2])) - - # parse the client id and encrypted session keys - - client_entries_length = client_blocks * 16 * 20 - client_entries = content[2:2 + client_entries_length] - client_keys = [(client_entries[i:i + 4], client_entries[i + 4:i + 20]) for i in range(0, client_entries_length, 4 + 16)] - - iv = content[2 + client_entries_length:2 + client_entries_length + 16] - encrypted = content[2 + client_entries_length + 16:] - - client_id = hashlib.sha1(authentication_cookie + iv).digest()[:4] - - for entry_id, encrypted_session_key in client_keys: - if entry_id != client_id: - continue # not the session key for this client - - # try decrypting the session key - - cipher = Cipher(algorithms.AES(authentication_cookie), modes.CTR(b'\x00' * len(iv)), default_backend()) - decryptor = cipher.decryptor() - session_key = decryptor.update(encrypted_session_key) + decryptor.finalize() - - # attempt to decrypt the intro points with the session key - - cipher = Cipher(algorithms.AES(session_key), modes.CTR(iv), default_backend()) - decryptor = cipher.decryptor() - decrypted = decryptor.update(encrypted) + decryptor.finalize() - - # check if the decryption looks correct - - if decrypted.startswith(b'introduction-point '): - return decrypted - - return content # nope, unable to decrypt the content - - @staticmethod - def _decrypt_stealth_auth(content, authentication_cookie): - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - from cryptography.hazmat.backends import default_backend - - # byte 1 = authentication type, 2-17 = input vector, 18 on = encrypted content - iv, encrypted = content[1:17], content[17:] - cipher = Cipher(algorithms.AES(authentication_cookie), modes.CTR(iv), default_backend()) - decryptor = cipher.decryptor() - - return decryptor.update(encrypted) + decryptor.finalize() - - @staticmethod - def _parse_introduction_points(content): - """ - Provides the parsed list of IntroductionPoints for the unencrypted content. - """ - - introduction_points = [] - content_io = io.BytesIO(content) - - while True: - content = b''.join(_read_until_keywords('introduction-point', content_io, ignore_first = True)) - - if not content: - break # reached the end - - attr = dict(INTRODUCTION_POINTS_ATTR) - entries = _descriptor_components(content, False) - - for keyword, values in list(entries.items()): - value, block_type, block_contents = values[0] - - if keyword in SINGLE_INTRODUCTION_POINT_FIELDS and len(values) > 1: - raise ValueError("'%s' can only appear once in an introduction-point block, but appeared %i times" % (keyword, len(values))) - - if keyword == 'introduction-point': - attr['identifier'] = value - elif keyword == 'ip-address': - if not stem.util.connection.is_valid_ipv4_address(value): - raise ValueError("'%s' is an invalid IPv4 address" % value) - - attr['address'] = value - elif keyword == 'onion-port': - if not stem.util.connection.is_valid_port(value): - raise ValueError("'%s' is an invalid port" % value) - - attr['port'] = int(value) - elif keyword == 'onion-key': - attr['onion_key'] = block_contents - elif keyword == 'service-key': - attr['service_key'] = block_contents - elif keyword == 'intro-authentication': - auth_entries = [] - - for auth_value, _, _ in values: - if ' ' not in auth_value: - raise ValueError("We expected 'intro-authentication [auth_type] [auth_data]', but had '%s'" % auth_value) - - auth_type, auth_data = auth_value.split(' ')[:2] - auth_entries.append((auth_type, auth_data)) - - introduction_points.append(IntroductionPoints(**attr)) - - return introduction_points +from stem.descriptor.hidden_service import * diff --git a/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py index 85e35f57..7eaf6e93 100644 --- a/stem/descriptor/server_descriptor.py +++ b/stem/descriptor/server_descriptor.py @@ -78,6 +78,7 @@ from stem.descriptor import ( _value, _values, _parse_simple_line, + _parse_int_line, _parse_if_present, _parse_bytes_line, _parse_timestamp_line, @@ -337,26 +338,6 @@ def _parse_hibernating_line(descriptor, entries): descriptor.hibernating = value == '1' -def _parse_uptime_line(descriptor, entries): - # We need to be tolerant of negative uptimes to accommodate a past tor - # bug... - # - # Changes in version 0.1.2.7-alpha - 2007-02-06 - # - If our system clock jumps back in time, don't publish a negative - # uptime in the descriptor. Also, don't let the global rate limiting - # buckets go absurdly negative. - # - # After parsing all of the attributes we'll double check that negative - # uptimes only occurred prior to this fix. - - value = _value('uptime', entries) - - try: - descriptor.uptime = int(value) - except ValueError: - raise ValueError('Uptime line must have an integer value: %s' % value) - - def _parse_protocols_line(descriptor, entries): value = _value('protocols', entries) protocols_match = re.match('^Link (.*) Circuit (.*)$', value) @@ -454,6 +435,19 @@ _parse_router_sig_ed25519_line = _parse_simple_line('router-sig-ed25519', 'ed255 _parse_router_digest_sha256_line = _parse_simple_line('router-digest-sha256', 'router_digest_sha256') _parse_router_digest_line = _parse_forty_character_hex('router-digest', '_digest') +# TODO: We need to be tolerant of negative uptimes to accommodate a past tor +# bug... +# +# Changes in version 0.1.2.7-alpha - 2007-02-06 +# - If our system clock jumps back in time, don't publish a negative +# uptime in the descriptor. Also, don't let the global rate limiting +# buckets go absurdly negative. +# +# After parsing all of the attributes we'll double check that negative +# uptimes only occurred prior to this fix. + +_parse_uptime_line = _parse_int_line('uptime', 'uptime', allow_negative = True) + class ServerDescriptor(Descriptor): """ diff --git a/stem/response/events.py b/stem/response/events.py index a9f563c6..27a3e405 100644 --- a/stem/response/events.py +++ b/stem/response/events.py @@ -701,7 +701,7 @@ class HSDescContentEvent(Event): :var str directory: hidden service directory servicing the request :var str directory_fingerprint: hidden service directory's finterprint :var str directory_nickname: hidden service directory's nickname if it was provided - :var stem.descriptor.hidden_service_descriptor.HiddenServiceDescriptor descriptor: descriptor that was retrieved + :var stem.descriptor.hidden_service.HiddenServiceDescriptorV2 descriptor: descriptor that was retrieved """ _VERSION_ADDED = stem.version.Requirement.EVENT_HS_DESC_CONTENT @@ -726,7 +726,7 @@ class HSDescContentEvent(Event): self.descriptor = None if desc_content: - self.descriptor = list(stem.descriptor.hidden_service_descriptor._parse_file(io.BytesIO(desc_content)))[0] + self.descriptor = list(stem.descriptor.hidden_service._parse_file(io.BytesIO(desc_content)))[0] class LogEvent(Event): diff --git a/test/settings.cfg b/test/settings.cfg index 1bdb1a0a..1b971bb4 100644 --- a/test/settings.cfg +++ b/test/settings.cfg @@ -172,7 +172,7 @@ pycodestyle.ignore E722 pycodestyle.ignore stem/__init__.py => E402: import stem.util.connection pycodestyle.ignore stem/descriptor/__init__.py => E402: import stem.descriptor.bandwidth_file pycodestyle.ignore stem/descriptor/__init__.py => E402: import stem.descriptor.extrainfo_descriptor -pycodestyle.ignore stem/descriptor/__init__.py => E402: import stem.descriptor.hidden_service_descriptor +pycodestyle.ignore stem/descriptor/__init__.py => E402: import stem.descriptor.hidden_service pycodestyle.ignore stem/descriptor/__init__.py => E402: import stem.descriptor.microdescriptor pycodestyle.ignore stem/descriptor/__init__.py => E402: import stem.descriptor.networkstatus pycodestyle.ignore stem/descriptor/__init__.py => E402: import stem.descriptor.server_descriptor @@ -184,9 +184,7 @@ pycodestyle.ignore test/unit/util/connection.py => W291: _tor tor 158 # issue. pyflakes.ignore run_tests.py => 'unittest' imported but unused -pyflakes.ignore stem/client/datatype.py => redefinition of unused 'pop' from * pyflakes.ignore stem/control.py => undefined name 'controller' -pyflakes.ignore stem/interpreter/__init__.py => undefined name 'raw_input' pyflakes.ignore stem/manual.py => undefined name 'unichr' pyflakes.ignore stem/prereq.py => 'int_to_bytes' imported but unused pyflakes.ignore stem/prereq.py => 'int_from_bytes' imported but unused @@ -210,6 +208,10 @@ pyflakes.ignore stem/prereq.py => 'cryptography.hazmat.primitives.ciphers.modes' pyflakes.ignore stem/prereq.py => 'cryptography.hazmat.primitives.ciphers.Cipher' imported but unused pyflakes.ignore stem/prereq.py => 'cryptography.hazmat.primitives.ciphers.algorithms' imported but unused pyflakes.ignore stem/prereq.py => 'lzma' imported but unused +pyflakes.ignore stem/client/datatype.py => redefinition of unused 'pop' from * +pyflakes.ignore stem/descriptor/hidden_service_descriptor.py => 'stem.descriptor.hidden_service.*' imported but unused +pyflakes.ignore stem/descriptor/hidden_service_descriptor.py => 'from stem.descriptor.hidden_service import *' used; unable to detect undefined names +pyflakes.ignore stem/interpreter/__init__.py => undefined name 'raw_input' pyflakes.ignore stem/response/events.py => undefined name 'long' pyflakes.ignore stem/util/__init__.py => undefined name 'long' pyflakes.ignore stem/util/__init__.py => undefined name 'unicode' @@ -256,7 +258,8 @@ test.unit_tests |test.unit.descriptor.networkstatus.document_v2.TestNetworkStatusDocument |test.unit.descriptor.networkstatus.document_v3.TestNetworkStatusDocument |test.unit.descriptor.networkstatus.bridge_document.TestBridgeNetworkStatusDocument -|test.unit.descriptor.hidden_service_descriptor.TestHiddenServiceDescriptor +|test.unit.descriptor.hidden_service_v2.TestHiddenServiceDescriptorV2 +|test.unit.descriptor.hidden_service_v3.TestHiddenServiceDescriptorV3 |test.unit.descriptor.certificate.TestEd25519Certificate |test.unit.descriptor.bandwidth_file.TestBandwidthFile |test.unit.exit_policy.rule.TestExitPolicyRule diff --git a/test/unit/descriptor/data/hidden_service_v3 b/test/unit/descriptor/data/hidden_service_v3 new file mode 100644 index 00000000..c67ae292 --- /dev/null +++ b/test/unit/descriptor/data/hidden_service_v3 @@ -0,0 +1,223 @@ +hs-descriptor 3 +descriptor-lifetime 180 +descriptor-signing-key-cert +-----BEGIN ED25519 CERT----- +AQgABqKwAQVql1QZETyEwJjg+Cv6f2w/cp+c3juj01NPBaJqihboAQAgBACx+FKK +oDrFE1+ztSxzN8sApKOb5UuDtoe/E03DxZU5+r/K5AV6G0hYn21V7Xbu2pZHvIkT +2oVY4hypWNJE58eFBRFRzBA0J2h0GyFs1pIuRh5QDJuxB5j92V0aRCNZFgM= +-----END ED25519 CERT----- +revision-counter 15 +superencrypted +-----BEGIN MESSAGE----- +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 +usMPytqLsZg2WXj4ikmGug== +-----END MESSAGE----- +signature wdc7ffr+dPZJ/mIQ1l4WYqNABcmsm6SHW/NL3M3wG7bjjqOJWoPR5TimUXxH52n5Zk0Gc7hl/hz3YYmAx5MvAg diff --git a/test/unit/descriptor/hidden_service_descriptor.py b/test/unit/descriptor/hidden_service_v2.py index e9ba012b..5191775e 100644 --- a/test/unit/descriptor/hidden_service_descriptor.py +++ b/test/unit/descriptor/hidden_service_v2.py @@ -1,5 +1,5 @@ """ -Unit tests for stem.descriptor.hidden_service_descriptor. +Unit tests for stem.descriptor.hidden_service for version 2. """ import datetime @@ -10,10 +10,10 @@ import stem.descriptor import stem.prereq import test.require -from stem.descriptor.hidden_service_descriptor import ( - REQUIRED_FIELDS, +from stem.descriptor.hidden_service import ( + REQUIRED_V2_FIELDS, DecryptionFailure, - HiddenServiceDescriptor, + HiddenServiceDescriptorV2, ) from test.unit.descriptor import ( @@ -236,41 +236,41 @@ lj/7xMZWDrfyw5H86L0QiaZnkmD+nig1+S+Rn39mmuEgl2iwZO/ihlncUJQTEULb -----END MESSAGE-----\ """ -expect_invalid_attr = functools.partial(base_expect_invalid_attr, HiddenServiceDescriptor, 'descriptor_id', 'y3olqqblqw2gbh6phimfuiroechjjafa') -expect_invalid_attr_for_text = functools.partial(base_expect_invalid_attr_for_text, HiddenServiceDescriptor, 'descriptor_id', 'y3olqqblqw2gbh6phimfuiroechjjafa') +expect_invalid_attr = functools.partial(base_expect_invalid_attr, HiddenServiceDescriptorV2, 'descriptor_id', 'y3olqqblqw2gbh6phimfuiroechjjafa') +expect_invalid_attr_for_text = functools.partial(base_expect_invalid_attr_for_text, HiddenServiceDescriptorV2, 'descriptor_id', 'y3olqqblqw2gbh6phimfuiroechjjafa') -class TestHiddenServiceDescriptor(unittest.TestCase): +class TestHiddenServiceDescriptorV2(unittest.TestCase): def test_from_str(self): - sig = HiddenServiceDescriptor.create() - self.assertEqual(sig, HiddenServiceDescriptor.from_str(str(sig))) + sig = HiddenServiceDescriptorV2.create() + self.assertEqual(sig, HiddenServiceDescriptorV2.from_str(str(sig))) def test_for_duckduckgo_with_validation(self): """ Parse duckduckgo's descriptor. """ - descriptor_file = open(get_resource('hidden_service_duckduckgo'), 'rb') - desc = next(stem.descriptor.parse_file(descriptor_file, 'hidden-service-descriptor 1.0', validate = True)) - self._assert_matches_duckduckgo(desc) + with open(get_resource('hidden_service_duckduckgo'), 'rb') as descriptor_file: + desc = next(stem.descriptor.parse_file(descriptor_file, 'hidden-service-descriptor 1.0', validate = True)) + self._assert_matches_duckduckgo(desc) def test_for_duckduckgo_without_validation(self): """ Parse duckduckgo's descriptor """ - descriptor_file = open(get_resource('hidden_service_duckduckgo'), 'rb') - desc = next(stem.descriptor.parse_file(descriptor_file, 'hidden-service-descriptor 1.0', validate = False)) - self._assert_matches_duckduckgo(desc) + with open(get_resource('hidden_service_duckduckgo'), 'rb') as descriptor_file: + desc = next(stem.descriptor.parse_file(descriptor_file, 'hidden-service-descriptor 1.0', validate = False)) + self._assert_matches_duckduckgo(desc) def test_for_facebook(self): """ Parse facebook's descriptor. """ - descriptor_file = open(get_resource('hidden_service_facebook'), 'rb') + with open(get_resource('hidden_service_facebook'), 'rb') as descriptor_file: + desc = next(stem.descriptor.parse_file(descriptor_file, 'hidden-service-descriptor 1.0', validate = True)) - desc = next(stem.descriptor.parse_file(descriptor_file, 'hidden-service-descriptor 1.0', validate = True)) self.assertEqual('utjk4arxqg6s6zzo7n6cjnq6ot34udhr', desc.descriptor_id) self.assertEqual(2, desc.version) self.assertEqual('6355jaerje3bqozopwq2qmpf4iviizdn', desc.secret_id_part) @@ -279,7 +279,7 @@ class TestHiddenServiceDescriptor(unittest.TestCase): @test.require.cryptography def test_descriptor_signing(self): - self.assertRaisesWith(NotImplementedError, 'Signing of HiddenServiceDescriptor not implemented', HiddenServiceDescriptor.create, sign = True) + self.assertRaisesWith(NotImplementedError, 'Signing of HiddenServiceDescriptorV2 not implemented', HiddenServiceDescriptorV2.create, sign = True) @test.require.cryptography def test_with_basic_auth(self): @@ -287,9 +287,9 @@ class TestHiddenServiceDescriptor(unittest.TestCase): Parse a descriptor with introduction-points encrypted with basic auth. """ - descriptor_file = open(get_resource('hidden_service_basic_auth'), 'rb') + with open(get_resource('hidden_service_basic_auth'), 'rb') as descriptor_file: + desc = next(stem.descriptor.parse_file(descriptor_file, 'hidden-service-descriptor 1.0', validate = True)) - desc = next(stem.descriptor.parse_file(descriptor_file, 'hidden-service-descriptor 1.0', validate = True)) self.assertEqual('yfmvdrkdbyquyqk5vygyeylgj2qmrvrd', desc.descriptor_id) self.assertEqual(2, desc.version) self.assertEqual('fluw7z3s5cghuuirq3imh5jjj5ljips6', desc.secret_id_part) @@ -334,9 +334,9 @@ class TestHiddenServiceDescriptor(unittest.TestCase): Parse a descriptor with introduction-points encrypted with stealth auth. """ - descriptor_file = open(get_resource('hidden_service_stealth_auth'), 'rb') + with open(get_resource('hidden_service_stealth_auth'), 'rb') as descriptor_file: + desc = next(stem.descriptor.parse_file(descriptor_file, 'hidden-service-descriptor 1.0', validate = True)) - desc = next(stem.descriptor.parse_file(descriptor_file, 'hidden-service-descriptor 1.0', validate = True)) self.assertEqual('ubf3xeibzlfil6s4larq6y5peup2z3oj', desc.descriptor_id) self.assertEqual(2, desc.version) self.assertEqual('jczvydhzetbpdiylj3d5nsnjvaigs7xm', desc.secret_id_part) @@ -418,7 +418,7 @@ class TestHiddenServiceDescriptor(unittest.TestCase): Basic sanity check that we can parse a hidden service descriptor with minimal attributes. """ - desc = HiddenServiceDescriptor.create() + desc = HiddenServiceDescriptorV2.create() self.assertEqual('y3olqqblqw2gbh6phimfuiroechjjafa', desc.descriptor_id) self.assertEqual(2, desc.version) @@ -435,7 +435,7 @@ class TestHiddenServiceDescriptor(unittest.TestCase): Includes unrecognized content in the descriptor. """ - desc = HiddenServiceDescriptor.create({'pepperjack': 'is oh so tasty!'}) + desc = HiddenServiceDescriptorV2.create({'pepperjack': 'is oh so tasty!'}) self.assertEqual(['pepperjack is oh so tasty!'], desc.get_unrecognized_lines()) def test_proceeding_line(self): @@ -443,14 +443,14 @@ class TestHiddenServiceDescriptor(unittest.TestCase): Includes a line prior to the 'rendezvous-service-descriptor' entry. """ - expect_invalid_attr_for_text(self, b'hibernate 1\n' + HiddenServiceDescriptor.content()) + expect_invalid_attr_for_text(self, b'hibernate 1\n' + HiddenServiceDescriptorV2.content()) def test_trailing_line(self): """ Includes a line after the 'router-signature' entry. """ - expect_invalid_attr_for_text(self, HiddenServiceDescriptor.content() + b'\nhibernate 1') + expect_invalid_attr_for_text(self, HiddenServiceDescriptorV2.content() + b'\nhibernate 1') def test_required_fields(self): """ @@ -468,8 +468,8 @@ class TestHiddenServiceDescriptor(unittest.TestCase): 'signature': 'signature', } - for line in REQUIRED_FIELDS: - desc_text = HiddenServiceDescriptor.content(exclude = (line,)) + for line in REQUIRED_V2_FIELDS: + desc_text = HiddenServiceDescriptorV2.content(exclude = (line,)) expected = [] if line == 'protocol-versions' else None expect_invalid_attr_for_text(self, desc_text, line_to_attr[line], expected) @@ -514,14 +514,14 @@ class TestHiddenServiceDescriptor(unittest.TestCase): are valid according to the spec. """ - missing_field_desc = HiddenServiceDescriptor.create(exclude = ('introduction-points',)) + missing_field_desc = HiddenServiceDescriptorV2.create(exclude = ('introduction-points',)) self.assertEqual(None, missing_field_desc.introduction_points_encoded) self.assertEqual([], missing_field_desc.introduction_points_auth) self.assertEqual(None, missing_field_desc.introduction_points_content) self.assertEqual([], missing_field_desc.introduction_points()) - empty_field_desc = HiddenServiceDescriptor.create({'introduction-points': MESSAGE_BLOCK % ''}) + empty_field_desc = HiddenServiceDescriptorV2.create({'introduction-points': MESSAGE_BLOCK % ''}) self.assertEqual((MESSAGE_BLOCK % '').strip(), empty_field_desc.introduction_points_encoded) self.assertEqual([], empty_field_desc.introduction_points_auth) diff --git a/test/unit/descriptor/hidden_service_v3.py b/test/unit/descriptor/hidden_service_v3.py new file mode 100644 index 00000000..f6407623 --- /dev/null +++ b/test/unit/descriptor/hidden_service_v3.py @@ -0,0 +1,109 @@ +""" +Unit tests for stem.descriptor.hidden_service for version 3. +""" + +import functools +import unittest + +import stem.descriptor + +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----- +AQgABqKwAQVql1QZETyEwJjg+Cv6f2w/cp+c3juj01NPBaJqihboAQAgBACx+FKK +oDrFE1+ztSxzN8sApKOb5UuDtoe/E03DxZU5+r/K5AV6G0hYn21V7Xbu2pZHvIkT +2oVY4hypWNJE58eFBRFRzBA0J2h0GyFs1pIuRh5QDJuxB5j92V0aRCNZFgM= +-----END ED25519 CERT-----\ +""" + + +class TestHiddenServiceDescriptorV3(unittest.TestCase): + def test_for_riseup(self): + """ + Parse riseup's descriptor... + + vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd.onion + """ + + with open(get_resource('hidden_service_v3'), 'rb') as descriptor_file: + desc = next(stem.descriptor.parse_file(descriptor_file, 'hidden-service-descriptor-3 1.0', validate = True)) + + self.assertEqual(3, desc.version) + self.assertEqual(180, desc.lifetime) + self.assertEqual(EXPECTED_SIGNING_CERT, desc.signing_cert) + self.assertEqual(15, desc.revision_counter) + 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. + """ + + test_values = ( + '', + '-10', + 'hello', + ) + + for test_value in test_values: + expect_invalid_attr(self, {'hs-descriptor': test_value}, 'version') + + def test_invalid_lifetime(self): + """ + Checks that our lifetime field expects a numeric value. + """ + + test_values = ( + '', + '-10', + 'hello', + ) + + for test_value in test_values: + expect_invalid_attr(self, {'descriptor-lifetime': test_value}, 'lifetime') + + def test_invalid_revision_counter(self): + """ + Checks that our revision counter field expects a numeric value. + """ + + test_values = ( + '', + '-10', + 'hello', + ) + + for test_value in test_values: + expect_invalid_attr(self, {'revision-counter': test_value}, 'revision_counter') |
