diff options
| author | Damian Johnson <atagar@torproject.org> | 2015-02-28 21:12:46 -0800 |
|---|---|---|
| committer | Damian Johnson <atagar@torproject.org> | 2015-02-28 21:12:53 -0800 |
| commit | 14beceaa9da6e816ad3da0c28049e682003efb85 (patch) | |
| tree | 1522b7cfb95ac3e523902d4a8735c1c42f5eb952 | |
| parent | 48abb6894de6b2d29593f28f326bbbf18919b8b9 (diff) | |
| parent | 44f3e5fe4d8586da2dd2cf0a662d4ab801676d6e (diff) | |
Hidden service descriptor support
Support for parsing hidden service descriptors, as defined in the rend-spec...
https://trac.torproject.org/projects/tor/ticket/15004
https://gitweb.torproject.org/torspec.git/tree/rend-spec.txt#n222
This doesn't yet support encrypted introduction-points, but otherwise is a
complete parser. Like server descriptors, this validates our signature's
integrity if pycrypto is available.
| -rw-r--r-- | docs/api.rst | 1 | ||||
| -rw-r--r-- | docs/change_log.rst | 1 | ||||
| -rw-r--r-- | docs/contents.rst | 1 | ||||
| -rw-r--r-- | stem/descriptor/__init__.py | 119 | ||||
| -rw-r--r-- | stem/descriptor/hidden_service_descriptor.py | 311 | ||||
| -rw-r--r-- | stem/descriptor/server_descriptor.py | 138 | ||||
| -rw-r--r-- | stem/util/connection.py | 44 | ||||
| -rw-r--r-- | test/mocking.py | 47 | ||||
| -rw-r--r-- | test/settings.cfg | 1 | ||||
| -rw-r--r-- | test/unit/descriptor/data/hidden_service_basic_auth | 67 | ||||
| -rw-r--r-- | test/unit/descriptor/data/hidden_service_duckduckgo | 60 | ||||
| -rw-r--r-- | test/unit/descriptor/data/hidden_service_facebook | 60 | ||||
| -rw-r--r-- | test/unit/descriptor/data/hidden_service_stealth_auth | 60 | ||||
| -rw-r--r-- | test/unit/descriptor/hidden_service_descriptor.py | 499 | ||||
| -rw-r--r-- | test/unit/descriptor/server_descriptor.py | 4 | ||||
| -rw-r--r-- | test/unit/tutorial.py | 2 |
16 files changed, 1265 insertions, 150 deletions
diff --git a/docs/api.rst b/docs/api.rst index 4f1a01c6..85cfdcb7 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -34,6 +34,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.tordnsel <api/descriptor/tordnsel.html>`_ - `TorDNSEL <https://www.torproject.org/projects/tordnsel.html.en>`_ exit lists. * `stem.descriptor.reader <api/descriptor/reader.html>`_ - Reads and parses descriptor files from disk. diff --git a/docs/change_log.rst b/docs/change_log.rst index 71436552..a9228940 100644 --- a/docs/change_log.rst +++ b/docs/change_log.rst @@ -58,6 +58,7 @@ conversion (:trac:`14075`). * **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`) * The :class:`~stem.descriptor.networkstatus.DirectoryAuthority` 'fingerprint' attribute was actually its 'v3ident' * Updating Faravahar's address (:trac:`14487`) diff --git a/docs/contents.rst b/docs/contents.rst index db703a24..a402dd61 100644 --- a/docs/contents.rst +++ b/docs/contents.rst @@ -42,6 +42,7 @@ Contents api/descriptor/microdescriptor api/descriptor/networkstatus api/descriptor/router_status_entry + api/descriptor/hidden_service_descriptor api/descriptor/tordnsel api/descriptor/export diff --git a/stem/descriptor/__init__.py b/stem/descriptor/__init__.py index abcdab21..85ff986d 100644 --- a/stem/descriptor/__init__.py +++ b/stem/descriptor/__init__.py @@ -50,7 +50,10 @@ __all__ = [ 'Descriptor', ] +import base64 +import codecs import copy +import hashlib import os import re import tarfile @@ -122,6 +125,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` ========================================= ===== If you're using **python 3** then beware that the open() function defaults to @@ -307,6 +311,11 @@ def _parse_metrics_file(descriptor_type, major_version, minor_version, descripto for desc in stem.descriptor.tordnsel._parse_file(descriptor_file, validate = validate, **kwargs): yield desc + elif descriptor_type == 'hidden-service-descriptor' and major_version == 1: + document_type = stem.descriptor.hidden_service_descriptor.HiddenServiceDescriptor + + for desc in stem.descriptor.hidden_service_descriptor._parse_file(descriptor_file, validate = validate, **kwargs): + yield desc else: raise TypeError("Unrecognized metrics descriptor format. type: '%s', version: '%i.%i'" % (descriptor_type, major_version, minor_version)) @@ -493,6 +502,97 @@ class Descriptor(object): def _name(self, is_plural = False): return str(type(self)) + def _digest_for_signature(self, signing_key, signature): + """ + Provides the signed digest we should have given this key and signature. + + :param str signing_key: key block used to make this signature + :param str signature: signed digest for this descriptor content + + :returns: the digest string encoded in uppercase hex + + :raises: ValueError if unable to provide a validly signed digest + """ + + if not stem.prereq.is_crypto_available(): + raise ValueError('Generating the signed digest requires pycrypto') + + from Crypto.Util import asn1 + from Crypto.Util.number import bytes_to_long, long_to_bytes + + # get the ASN.1 sequence + + seq = asn1.DerSequence() + seq.decode(_bytes_for_block(signing_key)) + modulus, public_exponent = seq[0], seq[1] + + sig_as_bytes = _bytes_for_block(signature) + sig_as_long = bytes_to_long(sig_as_bytes) # convert signature to an int + blocksize = 128 # block size will always be 128 for a 1024 bit key + + # use the public exponent[e] & the modulus[n] to decrypt the int + + decrypted_int = pow(sig_as_long, public_exponent, modulus) + + # convert the int to a byte array + + decrypted_bytes = long_to_bytes(decrypted_int, blocksize) + + ############################################################################ + # The decrypted bytes should have a structure exactly along these lines. + # 1 byte - [null '\x00'] + # 1 byte - [block type identifier '\x01'] - Should always be 1 + # N bytes - [padding '\xFF' ] + # 1 byte - [separator '\x00' ] + # M bytes - [message] + # Total - 128 bytes + # More info here http://www.ietf.org/rfc/rfc2313.txt + # esp the Notes in section 8.1 + ############################################################################ + + try: + if decrypted_bytes.index(b'\x00\x01') != 0: + raise ValueError('Verification failed, identifier missing') + except ValueError: + raise ValueError('Verification failed, malformed data') + + try: + identifier_offset = 2 + + # find the separator + seperator_index = decrypted_bytes.index(b'\x00', identifier_offset) + except ValueError: + raise ValueError('Verification failed, seperator not found') + + digest_hex = codecs.encode(decrypted_bytes[seperator_index + 1:], 'hex_codec') + return stem.util.str_tools._to_unicode(digest_hex.upper()) + + def _digest_for_content(self, start, end): + """ + Provides the digest of our descriptor's content in a given range. + + :param bytes start: start of the range to generate a digest for + :param bytes end: end of the range to generate a digest for + + :returns: the digest string encoded in uppercase hex + + :raises: ValueError if the digest canot be calculated + """ + + raw_descriptor = self.get_bytes() + + start_index = raw_descriptor.find(start) + end_index = raw_descriptor.find(end, start_index) + + if start_index == -1: + raise ValueError("Digest is for the range starting with '%s' but that isn't in our descriptor" % start) + elif end_index == -1: + raise ValueError("Digest is for the range ending with '%s' but that isn't in our descriptor" % end) + + digest_content = raw_descriptor[start_index:end_index + len(end)] + digest_hash = hashlib.sha1(stem.util.str_tools._to_bytes(digest_content)) + return stem.util.str_tools._to_unicode(digest_hash.hexdigest().upper()) + def __getattr__(self, name): # If attribute isn't already present we might be lazy loading it... @@ -587,6 +687,24 @@ def _read_until_keywords(keywords, descriptor_file, inclusive = False, ignore_fi return content +def _bytes_for_block(content): + """ + Provides the base64 decoded content of a pgp-style block. + + :param str content: block to be decoded + + :returns: decoded block content + + :raises: **TypeError** if this isn't base64 encoded content + """ + + # strip the '-----BEGIN RSA PUBLIC KEY-----' header and footer + + content = ''.join(content.split('\n')[1:-1]) + + return base64.b64decode(stem.util.str_tools._to_bytes(content)) + + def _get_pseudo_pgp_block(remaining_contents): """ Checks if given contents begins with a pseudo-Open-PGP-style block and, if @@ -720,3 +838,4 @@ import stem.descriptor.extrainfo_descriptor import stem.descriptor.networkstatus import stem.descriptor.microdescriptor import stem.descriptor.tordnsel +import stem.descriptor.hidden_service_descriptor diff --git a/stem/descriptor/hidden_service_descriptor.py b/stem/descriptor/hidden_service_descriptor.py new file mode 100644 index 00000000..73f5edf2 --- /dev/null +++ b/stem/descriptor/hidden_service_descriptor.py @@ -0,0 +1,311 @@ +# Copyright 2015, Damian Johnson and The Tor Project +# 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>`_. + +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. + +**Module Overview:** + +:: + + HiddenServiceDescriptor - Tor hidden service descriptor. +""" + +# TODO: Add a description for how to retrieve them when tor supports that +# (#14847) and then update #15009. + +import collections +import io + +import stem.util.connection + +from stem.descriptor import ( + PGP_BLOCK_END, + Descriptor, + _get_descriptor_components, + _read_until_keywords, + _bytes_for_block, + _value, + _parse_simple_line, + _parse_timestamp_line, + _parse_key_block, +) + +try: + # added in python 3.2 + from functools import lru_cache +except ImportError: + 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': [], +} + +IntroductionPoint = collections.namedtuple('IntroductionPoints', INTRODUCTION_POINTS_ATTR.keys()) + + +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 + + try: + decoded_field = _bytes_for_block(block_contents) + except TypeError: + raise ValueError("'introduction-points' isn't base64 encoded content:\n%s" % block_contents) + + auth_types = [] + + while decoded_field.startswith(b'service-authentication ') and b'\n' in decoded_field: + auth_line, decoded_field = decoded_field.split(b'\n', 1) + auth_line_comp = auth_line.split(b' ') + + if len(auth_line_comp) < 3: + raise ValueError("Within introduction-points we expected 'service-authentication [auth_type] [auth_data]', but had '%s'" % auth_line) + + auth_types.append((auth_line_comp[1], auth_line_comp[2])) + + descriptor.introduction_points_auth = auth_types + descriptor.introduction_points_content = decoded_field + +_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 + :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 + """ + + 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, + } + + def __init__(self, raw_contents, validate = False): + super(HiddenServiceDescriptor, self).__init__(raw_contents, lazy_load = not validate) + entries = _get_descriptor_components(raw_contents, validate) + + 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 stem.prereq.is_crypto_available(): + signed_digest = self._digest_for_signature(self.permanent_key, self.signature) + content_digest = self._digest_for_content(b'rendezvous-service-descriptor ', b'\nsignature\n') + + 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): + """ + Provided this service's introduction points. This provides a list of + IntroductionPoint instances, which have the following attributes... + + * **identifier** (str): hash of this introduction point's identity key + * **address** (str): address of this introduction point + * **port** (int): port where this introduction point is listening + * **onion_key** (str): public key for communicating with this introduction point + * **service_key** (str): public key for communicating with this hidden service + * **intro_authentication** (list): tuples of the form (auth_type, auth_data) + for establishing a connection + + :returns: **list** of IntroductionPoints instances + + :raises: + * **ValueError** if the our introduction-points is malformed + * **DecryptionFailure** if unable to decrypt this field + """ + + # TODO: Support fields encrypted with a desriptor-cookie. (#15004) + + if not self.introduction_points_content: + return [] + elif not self.introduction_points_content.startswith(b'introduction-point '): + raise DecryptionFailure('introduction-point content is encrypted') + + introduction_points = [] + content_io = io.BytesIO(self.introduction_points_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 = _get_descriptor_components(content, False) + + # TODO: most fields can only appear once, we should check for that + + for keyword, values in list(entries.items()): + value, block_type, block_contents = values[0] + + 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(IntroductionPoint(**attr)) + + return introduction_points diff --git a/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py index 55b4183e..54ee645e 100644 --- a/stem/descriptor/server_descriptor.py +++ b/stem/descriptor/server_descriptor.py @@ -31,8 +31,6 @@ etc). This information is provided from a few sources... +- get_annotation_lines - lines that provided the annotations """ -import base64 -import codecs import functools import hashlib import re @@ -46,13 +44,13 @@ import stem.util.tor_tools import stem.version from stem import str_type -from stem.util import log from stem.descriptor import ( PGP_BLOCK_END, Descriptor, _get_descriptor_components, _read_until_keywords, + _bytes_for_block, _value, _values, _parse_simple_line, @@ -670,9 +668,18 @@ class RelayDescriptor(ServerDescriptor): def __init__(self, raw_contents, validate = False, annotations = None): super(RelayDescriptor, self).__init__(raw_contents, validate, annotations) - # validate the descriptor if required if validate: - self._validate_content() + if self.fingerprint: + key_hash = hashlib.sha1(_bytes_for_block(self.signing_key)).hexdigest() + + if key_hash != self.fingerprint.lower(): + raise ValueError('Fingerprint does not match the hash of our signing key (fingerprint: %s, signing key hash: %s)' % (self.fingerprint.lower(), key_hash)) + + if stem.prereq.is_crypto_available(): + signed_digest = self._digest_for_signature(self.signing_key, self.signature) + + if signed_digest != self.digest(): + raise ValueError('Decrypted digest does not match local digest (calculated: %s, local: %s)' % (signed_digest, self.digest())) @lru_cache() def digest(self): @@ -684,112 +691,7 @@ class RelayDescriptor(ServerDescriptor): :raises: ValueError if the digest canot be calculated """ - # Digest is calculated from everything in the - # descriptor except the router-signature. - - raw_descriptor = self.get_bytes() - start_token = b'router ' - sig_token = b'\nrouter-signature\n' - start = raw_descriptor.find(start_token) - sig_start = raw_descriptor.find(sig_token) - end = sig_start + len(sig_token) - - if start >= 0 and sig_start > 0 and end > start: - for_digest = raw_descriptor[start:end] - digest_hash = hashlib.sha1(stem.util.str_tools._to_bytes(for_digest)) - return stem.util.str_tools._to_unicode(digest_hash.hexdigest().upper()) - else: - raise ValueError('unable to calculate digest for descriptor') - - def _validate_content(self): - """ - Validates that the descriptor content matches the signature. - - :raises: ValueError if the signature does not match the content - """ - - key_as_bytes = RelayDescriptor._get_key_bytes(self.signing_key) - - # ensure the fingerprint is a hash of the signing key - - if self.fingerprint: - # calculate the signing key hash - - key_der_as_hash = hashlib.sha1(stem.util.str_tools._to_bytes(key_as_bytes)).hexdigest() - - if key_der_as_hash != self.fingerprint.lower(): - log.warn('Signing key hash: %s != fingerprint: %s' % (key_der_as_hash, self.fingerprint.lower())) - raise ValueError('Fingerprint does not match hash') - - self._verify_digest(key_as_bytes) - - def _verify_digest(self, key_as_der): - # check that our digest matches what was signed - - if not stem.prereq.is_crypto_available(): - return - - from Crypto.Util import asn1 - from Crypto.Util.number import bytes_to_long, long_to_bytes - - # get the ASN.1 sequence - - seq = asn1.DerSequence() - seq.decode(key_as_der) - modulus = seq[0] - public_exponent = seq[1] # should always be 65537 - - sig_as_bytes = RelayDescriptor._get_key_bytes(self.signature) - - # convert the descriptor signature to an int - - sig_as_long = bytes_to_long(sig_as_bytes) - - # use the public exponent[e] & the modulus[n] to decrypt the int - - decrypted_int = pow(sig_as_long, public_exponent, modulus) - - # block size will always be 128 for a 1024 bit key - - blocksize = 128 - - # convert the int to a byte array. - - decrypted_bytes = long_to_bytes(decrypted_int, blocksize) - - ############################################################################ - # The decrypted bytes should have a structure exactly along these lines. - # 1 byte - [null '\x00'] - # 1 byte - [block type identifier '\x01'] - Should always be 1 - # N bytes - [padding '\xFF' ] - # 1 byte - [separator '\x00' ] - # M bytes - [message] - # Total - 128 bytes - # More info here http://www.ietf.org/rfc/rfc2313.txt - # esp the Notes in section 8.1 - ############################################################################ - - try: - if decrypted_bytes.index(b'\x00\x01') != 0: - raise ValueError('Verification failed, identifier missing') - except ValueError: - raise ValueError('Verification failed, malformed data') - - try: - identifier_offset = 2 - - # find the separator - seperator_index = decrypted_bytes.index(b'\x00', identifier_offset) - except ValueError: - raise ValueError('Verification failed, seperator not found') - - digest_hex = codecs.encode(decrypted_bytes[seperator_index + 1:], 'hex_codec') - digest = stem.util.str_tools._to_unicode(digest_hex.upper()) - - local_digest = self.digest() - - if digest != local_digest: - raise ValueError('Decrypted digest does not match local digest (calculated: %s, local: %s)' % (digest, local_digest)) + return self._digest_for_content(b'router ', b'\nrouter-signature\n') def _compare(self, other, method): if not isinstance(other, RelayDescriptor): @@ -809,20 +711,6 @@ class RelayDescriptor(ServerDescriptor): def __le__(self, other): return self._compare(other, lambda s, o: s <= o) - @staticmethod - def _get_key_bytes(key_string): - # Remove the newlines from the key string & strip off the - # '-----BEGIN RSA PUBLIC KEY-----' header and - # '-----END RSA PUBLIC KEY-----' footer - - key_as_string = ''.join(key_string.split('\n')[1:4]) - - # get the key representation in bytes - - key_bytes = base64.b64decode(stem.util.str_tools._to_bytes(key_as_string)) - - return key_bytes - class BridgeDescriptor(ServerDescriptor): """ diff --git a/stem/util/connection.py b/stem/util/connection.py index fa2a3d5f..475c7f56 100644 --- a/stem/util/connection.py +++ b/stem/util/connection.py @@ -26,19 +26,21 @@ Connection and networking based utility functions. Method for resolving a process' connections. .. versionadded:: 1.1.0 - - ================= =========== - Resolver Description - ================= =========== - **PROC** /proc contents - **NETSTAT** netstat - **NETSTAT_WINDOWS** netstat command under Windows - **SS** ss command - **LSOF** lsof command - **SOCKSTAT** sockstat command under *nix - **BSD_SOCKSTAT** sockstat command under FreeBSD - **BSD_PROCSTAT** procstat command under FreeBSD - ================= =========== + .. versionchanged:: 1.4.0 + Added **NETSTAT_WINDOWS**. + + ==================== =========== + Resolver Description + ==================== =========== + **PROC** /proc contents + **NETSTAT** netstat + **NETSTAT_WINDOWS** netstat command under Windows + **SS** ss command + **LSOF** lsof command + **SOCKSTAT** sockstat command under *nix + **BSD_SOCKSTAT** sockstat command under FreeBSD + **BSD_PROCSTAT** procstat command under FreeBSD + ==================== =========== """ import collections @@ -140,14 +142,14 @@ RESOLVER_FILTER = { def get_connections(resolver, process_pid = None, process_name = None): """ - Retrieves a list of the current connections for a given process. The provides - a list of Connection instances, which have five attributes... - - * local_address (str) - * local_port (int) - * remote_address (str) - * remote_port (int) - * protocol (str, generally either 'tcp' or 'udp') + Retrieves a list of the current connections for a given process. This + provides a list of Connection instances, which have five attributes... + + * **local_address** (str) + * **local_port** (int) + * **remote_address** (str) + * **remote_port** (int) + * **protocol** (str, generally either 'tcp' or 'udp') .. versionadded:: 1.1.0 diff --git a/test/mocking.py b/test/mocking.py index fdfc71d8..156ac4af 100644 --- a/test/mocking.py +++ b/test/mocking.py @@ -33,14 +33,19 @@ Helper functions for creating mock objects. get_router_status_entry_v2 - RouterStatusEntryV2 get_router_status_entry_v3 - RouterStatusEntryV3 get_router_status_entry_micro_v3 - RouterStatusEntryMicroV3 + + stem.descriptor.hidden-service_descriptor + get_hidden_service_descriptor - HiddenServiceDescriptor """ import base64 import hashlib import itertools import re +import textwrap import stem.descriptor.extrainfo_descriptor +import stem.descriptor.hidden_service_descriptor import stem.descriptor.microdescriptor import stem.descriptor.networkstatus import stem.descriptor.router_status_entry @@ -178,6 +183,20 @@ NETWORK_STATUS_DOCUMENT_FOOTER = ( ('directory-signature', '%s %s\n%s' % (DOC_SIG.identity, DOC_SIG.key_digest, DOC_SIG.signature)), ) +HIDDEN_SERVICE_HEADER = ( + ('rendezvous-service-descriptor', 'y3olqqblqw2gbh6phimfuiroechjjafa'), + ('version', '2'), + ('permanent-key', '\n-----BEGIN RSA PUBLIC KEY-----%s-----END RSA PUBLIC KEY-----' % CRYPTO_BLOB), + ('secret-id-part', 'e24kgecavwsznj7gpbktqsiwgvngsf4e'), + ('publication-time', '2015-02-23 20:00:00'), + ('protocol-versions', '2,3'), + ('introduction-points', '\n-----BEGIN MESSAGE-----\n-----END MESSAGE-----'), +) + +HIDDEN_SERVICE_FOOTER = ( + ('signature', '\n-----BEGIN SIGNATURE-----%s-----END SIGNATURE-----' % CRYPTO_BLOB), +) + def get_all_combinations(attr, include_empty = False): """ @@ -353,7 +372,7 @@ def get_relay_server_descriptor(attr = None, exclude = (), content = False, sign if sign_content: desc_content = sign_descriptor_content(desc_content) - with patch('stem.descriptor.server_descriptor.RelayDescriptor._verify_digest', Mock()): + with patch('stem.prereq.is_crypto_available', Mock(return_value = False)): desc = stem.descriptor.server_descriptor.RelayDescriptor(desc_content, validate = True) return desc @@ -499,6 +518,32 @@ def get_router_status_entry_micro_v3(attr = None, exclude = (), content = False) return stem.descriptor.router_status_entry.RouterStatusEntryMicroV3(desc_content, validate = True) +def get_hidden_service_descriptor(attr = None, exclude = (), content = False, introduction_points_lines = None): + """ + Provides the descriptor content for... + stem.descriptor.hidden_service_descriptor.HidenServiceDescriptor + + :param dict attr: keyword/value mappings to be included in the descriptor + :param list exclude: mandatory keywords to exclude from the descriptor + :param bool content: provides the str content of the descriptor rather than the class if True + :param list introduction_points_lines: lines to be included in the introduction-points field + + :returns: HidenServiceDescriptor for the requested descriptor content + """ + + if (not attr or 'introduction-points' not in attr) and introduction_points_lines is not None: + encoded = base64.b64encode(introduction_points_lines('\n')) + attr['introduction-points'] = '\n-----BEGIN MESSAGE-----\n%s\n-----END MESSAGE-----' % '\n'.join(textwrap.wrap(encoded, 64)) + + desc_content = _get_descriptor_content(attr, exclude, HIDDEN_SERVICE_HEADER, HIDDEN_SERVICE_FOOTER) + + if content: + return desc_content + else: + with patch('stem.prereq.is_crypto_available', Mock(return_value = False)): + return stem.descriptor.hidden_service_descriptor.HiddenServiceDescriptor(desc_content, validate = True) + + def get_directory_authority(attr = None, exclude = (), is_vote = False, content = False): """ Provides the descriptor content for... diff --git a/test/settings.cfg b/test/settings.cfg index c0c83424..21f5eb29 100644 --- a/test/settings.cfg +++ b/test/settings.cfg @@ -172,6 +172,7 @@ 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.exit_policy.rule.TestExitPolicyRule |test.unit.exit_policy.policy.TestExitPolicy |test.unit.version.TestVersion diff --git a/test/unit/descriptor/data/hidden_service_basic_auth b/test/unit/descriptor/data/hidden_service_basic_auth new file mode 100644 index 00000000..d93c42a6 --- /dev/null +++ b/test/unit/descriptor/data/hidden_service_basic_auth @@ -0,0 +1,67 @@ +@type hidden-service-descriptor 1.0 +rendezvous-service-descriptor yfmvdrkdbyquyqk5vygyeylgj2qmrvrd +version 2 +permanent-key +-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBANHC3bZRrlFFlSu22u18wBG57JpvIhglJE+u0ctMwTnw6iyJJqqxgU6U +R1gnXJlorQYDC5eHoM8nbBJu7LvUjYnqINz+En+FlpZhT+eqLk0v83IB4r0Fd6Bg +ypYLygEXUfh0bwHwYfF4mkvl6YOB6I6G5jHC46I3LH15qrdHW40FAgMBAAE= +-----END RSA PUBLIC KEY----- +secret-id-part fluw7z3s5cghuuirq3imh5jjj5ljips6 +publication-time 2015-02-24 20:00:00 +protocol-versions 2,3 +introduction-points +-----BEGIN MESSAGE----- +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 +-----END MESSAGE----- +signature +-----BEGIN SIGNATURE----- +mhGQNtyvf5QqeFrn1SNzdp8wN5bhEH5gxZhST5t4pFxxNCv3//ZgaQ83kIzcwex9 +xzgGREFHpTrqVPXXs8nZZgMpmnhWuloAyT1c3HSCvjnbWXX9Y82wPbVV5OYx1CYb +jLides7vbdQuS0UwEkcGMl62Ripwi0pkesgw9ZvJy+k= +-----END SIGNATURE----- diff --git a/test/unit/descriptor/data/hidden_service_duckduckgo b/test/unit/descriptor/data/hidden_service_duckduckgo new file mode 100644 index 00000000..f3f4868b --- /dev/null +++ b/test/unit/descriptor/data/hidden_service_duckduckgo @@ -0,0 +1,60 @@ +@type hidden-service-descriptor 1.0 +rendezvous-service-descriptor y3olqqblqw2gbh6phimfuiroechjjafa +version 2 +permanent-key +-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBAJ/SzzgrXPxTlFrKVhXh3buCWv2QfcNgncUpDpKouLn3AtPH5Ocys0jE +aZSKdvaiQ62md2gOwj4x61cFNdi05tdQjS+2thHKEm/KsB9BGLSLBNJYY356bupg +I5gQozM65ENelfxYlysBjJ52xSDBd8C4f/p9umdzaaaCmzXG/nhzAgMBAAE= +-----END RSA PUBLIC KEY----- +secret-id-part e24kgecavwsznj7gpbktqsiwgvngsf4e +publication-time 2015-02-23 20:00:00 +protocol-versions 2,3 +introduction-points +-----BEGIN MESSAGE----- +aW50cm9kdWN0aW9uLXBvaW50IGl3a2k3N3h0YnZwNnF2ZWRmcndkem5jeHMzY2th +eWV1CmlwLWFkZHJlc3MgMTc4LjYyLjIyMi4xMjkKb25pb24tcG9ydCA0NDMKb25p +b24ta2V5Ci0tLS0tQkVHSU4gUlNBIFBVQkxJQyBLRVktLS0tLQpNSUdKQW9HQkFL +OTRCRVlJSFo0S2RFa2V5UGhiTENwUlc1RVNnKzJXUFFock00eXVLWUd1cTh3Rldn +dW1aWVI5CmsvV0EvL0ZZWE1CejBiQitja3Vacy9ZdTluSytITHpwR2FwVjBjbHN0 +NEdVTWNCSW5VQ3pDY3BqSlRRc1FEZ20KMy9ZM2NxaDBXNTVnT0NGaG9tUTQvMVdP +WWc3WUNqazRYWUhKRTIwT2RHMkxsNXpvdEs2ZkFnTUJBQUU9Ci0tLS0tRU5EIFJT +QSBQVUJMSUMgS0VZLS0tLS0Kc2VydmljZS1rZXkKLS0tLS1CRUdJTiBSU0EgUFVC +TElDIEtFWS0tLS0tCk1JR0pBb0dCQUpYbUpiOGxTeWRNTXFDZ0NnZmd2bEIyRTVy +cGQ1N2t6L0FxZzcvZDFIS2MzK2w1UW9Vdkh5dXkKWnNBbHlrYThFdTUzNGhsNDFv +cUVLcEFLWWNNbjFUTTB2cEpFR05WT2MrMDVCSW54STloOWYwTWcwMVBEMHRZdQpH +Y0xIWWdCemNyZkVtS3dNdE04V0VtY01KZDduMnVmZmFBdko4NDZXdWJiZVY3TVcx +WWVoQWdNQkFBRT0KLS0tLS1FTkQgUlNBIFBVQkxJQyBLRVktLS0tLQppbnRyb2R1 +Y3Rpb24tcG9pbnQgZW00Z2prNmVpaXVhbGhtbHlpaWZyemM3bGJ0cnNiaXAKaXAt +YWRkcmVzcyA0Ni40LjE3NC41Mgpvbmlvbi1wb3J0IDQ0Mwpvbmlvbi1rZXkKLS0t +LS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JR0pBb0dCQUxCbWhkRjV3SHhI +cnBMU21qQVpvdHR4MjIwKzk5NUZkTU9PdFpOalJ3MURCU3ByVVpacXR4V2EKUDhU +S3BIS3p3R0pLQ1ZZSUlqN2xvaGJ2OVQ5dXJtbGZURTA1VVJHZW5ab2lmT0ZOejNZ +d01KVFhTY1FFQkoxMAo5aVdOTERUc2tMekRLQ0FiR2hibi9NS3dPZllHQmhOVGxq +ZHlUbU5ZNUVDUmJSempldjl2QWdNQkFBRT0KLS0tLS1FTkQgUlNBIFBVQkxJQyBL +RVktLS0tLQpzZXJ2aWNlLWtleQotLS0tLUJFR0lOIFJTQSBQVUJMSUMgS0VZLS0t +LS0KTUlHSkFvR0JBTXhNSG9BbXJiVU1zeGlJQ3AzaVRQWWdobjBZdWVLSHgyMTl3 +dThPL1E1MVF5Y1ZWTHBYMjdkMQpoSlhrUEIzM1hRQlhzQlM3U3hzU3NTQ1EzR0V1 +clFKN0d1QkxwWUlSL3Zxc2FrRS9sOHdjMkNKQzVXVWh5RkZrCisxVFdJVUk1dHhu +WEx5V0NSY0tEVXJqcWRvc0RhRG9zZ0hGZzIzTW54K3hYY2FRL2ZyQi9BZ01CQUFF +PQotLS0tLUVORCBSU0EgUFVCTElDIEtFWS0tLS0tCmludHJvZHVjdGlvbi1wb2lu +dCBqcWhmbDM2NHgzdXBlNmxxbnhpem9sZXdsZnJzdzJ6eQppcC1hZGRyZXNzIDYy +LjIxMC44Mi4xNjkKb25pb24tcG9ydCA0NDMKb25pb24ta2V5Ci0tLS0tQkVHSU4g +UlNBIFBVQkxJQyBLRVktLS0tLQpNSUdKQW9HQkFQVWtxeGdmWWR3MFBtL2c2TWJo +bVZzR0tsdWppZm1raGRmb0VldXpnbyt3bkVzR3Z3VWVienJ6CmZaSlJ0MGNhWEZo +bkNHZ1FEMklnbWFyVWFVdlAyNGZYby80bVl6TGNQZUk3Z1puZXVBUUpZdm05OFl2 +OXZPSGwKTmFNL1d2RGtDc0ozR1ZOSjFIM3dMUFFSSTN2N0tiTnVjOXRDT1lsL3Iw +OU9oVmFXa3phakFnTUJBQUU9Ci0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K +c2VydmljZS1rZXkKLS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JR0pB +b0dCQUxieDhMZXFSb1Avcjl3OWhqd0Q0MVlVbTdQbzY5N3hSdHl0RjBNY3lMQ1M3 +R1JpVVluamk3S1kKZmVwWGR2Ti9KbDVxUUtISUJiNjAya3VPVGwwcE44UStZZUZV +U0lJRGNtUEJMcEJEaEgzUHZyUU1jR1ZhaU9XSAo4dzBITVpDeGd3QWNDQzUxdzVW +d2l1bXhFSk5CVmNac094MG16TjFDbG95KzkwcTBsRlhMQWdNQkFBRT0KLS0tLS1F +TkQgUlNBIFBVQkxJQyBLRVktLS0tLQoK +-----END MESSAGE----- +signature +-----BEGIN SIGNATURE----- +VKMmsDIUUFOrpqvcQroIZjDZTKxqNs88a4M9Te8cR/ZvS7H2nffv6iQs0tom5X4D +4Dy4iZiy+pwYxdHfaOxmdpgMCRvgPb34MExWr5YemH0QuGtnlp5Wxr8GYaAQVuZX +cZjQLW0juUYCbgIGdxVEBnlEt2rgBSM9+1oR7EAfV1U= +-----END SIGNATURE----- diff --git a/test/unit/descriptor/data/hidden_service_facebook b/test/unit/descriptor/data/hidden_service_facebook new file mode 100644 index 00000000..705c7761 --- /dev/null +++ b/test/unit/descriptor/data/hidden_service_facebook @@ -0,0 +1,60 @@ +@type hidden-service-descriptor 1.0 +rendezvous-service-descriptor utjk4arxqg6s6zzo7n6cjnq6ot34udhr +version 2 +permanent-key +-----BEGIN RSA PUBLIC KEY----- +MIGKAoGBAL7zXRnwSycHlKSqK+B8PFvy7RhkQ+OytYtxBwOLzhD82oY7zbpjSHY4 +BZ+hsnceXVjB+f1mXGjvLY6pnYxuufV4wsMsk7a58aJOqUvZFFI2vXhJtnLICxoZ +AEHWzajz4ULagahB1Vi62loQE84OEcuFBekTvnHca1ZTxwp16aZtAgQvoiLZ +-----END RSA PUBLIC KEY----- +secret-id-part 6355jaerje3bqozopwq2qmpf4iviizdn +publication-time 2014-10-31 23:00:00 +protocol-versions 2,3 +introduction-points +-----BEGIN MESSAGE----- +aW50cm9kdWN0aW9uLXBvaW50IHJ6Y3V3am5jbDdpYXpkbm41NXV5cmEybWVlamxz +eXVoCmlwLWFkZHJlc3MgMTkzLjExLjExNC40Nwpvbmlvbi1wb3J0IDkwMDQKb25p +b24ta2V5Ci0tLS0tQkVHSU4gUlNBIFBVQkxJQyBLRVktLS0tLQpNSUdKQW9HQkFM +dndQc012TkhqMTY3YlJyLzlLbi9iVGtPUTN0VC9qOFdSOWV5NTlZU2NBVEQ4TXVz +OEV6ZWpNCjc0RjdhTGR1VjZuRk15djlFYmhEbVNIZDZRMmhwNkYwb2FGODB0MHMv +bThXYmVWTUF1aTRvVWRSU1ZRb0drY20KTDJXTlViNy84UWYwb2hFKzZ1K1pTL0FO +U2NEc0FDT2hwNkliMWxHWjhaZGxWZFJzeWxtckFnTUJBQUU9Ci0tLS0tRU5EIFJT +QSBQVUJMSUMgS0VZLS0tLS0Kc2VydmljZS1rZXkKLS0tLS1CRUdJTiBSU0EgUFVC +TElDIEtFWS0tLS0tCk1JR0pBb0dCQU1YVUJRMVd0cXJSZHRkaTdhODkvWU9EQUJ6 +d2U2L0JSS3dEcGFBUENyMC9CZlppVnFadXFFMzcKYmlxbW1pODBPVm5uQzd6eis3 +cDZYQ1QycitEemxtSmFCOGdzTjZCZlU2ek45Wklwd3ptcm1XTy94SGNoZ1BkOQpK +SUNrSUxHWEJlQnorNmtnSTZiZDcrbFZEYmxJYXNRNHkrUjhMWWxTeCtRMk9mK1gy +eDFwQWdNQkFBRT0KLS0tLS1FTkQgUlNBIFBVQkxJQyBLRVktLS0tLQppbnRyb2R1 +Y3Rpb24tcG9pbnQgcXE0YXZmdnZhcHljMnhyNmNpYmpuaXFoNTdpY2FkMnEKaXAt +YWRkcmVzcyAxODguNDAuODMuMjA4Cm9uaW9uLXBvcnQgNDQ0Cm9uaW9uLWtleQot +LS0tLUJFR0lOIFJTQSBQVUJMSUMgS0VZLS0tLS0KTUlHSkFvR0JBTWZySUJ1TlVF +MGZxa3ZySDlEZzJkNkM5SFUvNWR2TGxQL0NGVGFFYlVhMURvUHhyQm5iQ1BEcQp5 +bHhwNGs3M04yVDM3aXJrN3FacmVZQ3B6aUVPQ3paRU4xdGlkM0t1c0tFczgzL0FQ +aDFreTVRRmpkRmpBU0NxCmovRDl0VXNYYkVaa1FEN203SUlmWU40SG52KzROM0dV +WjVqNkhFMWJvYmxsbURyNkd2U0xBZ01CQUFFPQotLS0tLUVORCBSU0EgUFVCTElD +IEtFWS0tLS0tCnNlcnZpY2Uta2V5Ci0tLS0tQkVHSU4gUlNBIFBVQkxJQyBLRVkt +LS0tLQpNSUdKQW9HQkFNRXMyc2tRLzJsVjRFUGpRTDFrZk0zZjExUzRNZUdPcDFC +aWgrcm1ESGhic09yWDRoZk1GV1NuCjY1dTlZUE9zY1hldUlHbFp4YmFyUUY2Nzkz +bGMrbk9KbjVyQ016a0pYNWt0OHdobkZoVlFXN0ZoejUxRmdMNFkKNUFKKzdkaW9l +WTNKdjVNVE1rMUF4Qk1hbkJsR2YxazZrU05kZWxtMCs2bWtZVmVOcVJzckFnTUJB +QUU9Ci0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0KaW50cm9kdWN0aW9uLXBv +aW50IGI1ZHNpY29oZ2Fqb3Vhc3VxZ3lyNWtwNXIzdzdpaWYyCmlwLWFkZHJlc3Mg +MTg1LjUuNTMuNzIKb25pb24tcG9ydCA5MDAxCm9uaW9uLWtleQotLS0tLUJFR0lO +IFJTQSBQVUJMSUMgS0VZLS0tLS0KTUlHSkFvR0JBTWVUc0VqS1ZMeGsyL3o4bnRG +T2xWU09ZTWFjWTZBeFJTTXdQdkxoT2FsajVkNkRraitnSVA5agpsWDFoekRpZ0VP +b2FBV3FwWjRjRENLa3p6UGdzWEc3ZXVnSmFxTGQydUliZmh4WEdISTJDNWtWQWZZ +VnVyL1N3CnBXNFRRbmhKUytvbG1BWG84b1hvT2JNWGt1YURUdmlLSXZLa3lQWCs0 +dGJ4a1RxWWt5ZURBZ01CQUFFPQotLS0tLUVORCBSU0EgUFVCTElDIEtFWS0tLS0t +CnNlcnZpY2Uta2V5Ci0tLS0tQkVHSU4gUlNBIFBVQkxJQyBLRVktLS0tLQpNSUdK +QW9HQkFPM05SSGMwTld5MFo5aFQrZEUxZ3hKRnFsb0dhdmdLYzZJZ0E1UU5ORDNO +RHJJakdBYmNsRVh4Ck1tY1BBR2hhc1VlNnJ2aFN0VlRkZEVvN1V0SElCM3F2YXNL +d0NPem82Z2dLSHZtVVRFOWRNTk9LSEcyLy9xTi8KVkducGdJUkRoeDRwbE9DOXV6 +NGFKU0RZdlhiSXVtZmtxdGJzb1BacUZMYUFqdGFWR015VkFnTUJBQUU9Ci0tLS0t +RU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0KCg== +-----END MESSAGE----- +signature +-----BEGIN SIGNATURE----- +tFCNLEOFeqeDZOruKt4SBGvJ2Y2lLo29XUzd09FlIalnwb71TKcKSWi1JVB0vDqF +ftGlP4M+nZGh6YGxdEriV0ikFbcW+F4XOpWR5NEPV5cCoQAMd1a8mqivGjM/lQwe +41p+e4XCex2Brsl3o+pSyHy/U+p+xdQkmNPqrHpXVK8= +-----END SIGNATURE----- diff --git a/test/unit/descriptor/data/hidden_service_stealth_auth b/test/unit/descriptor/data/hidden_service_stealth_auth new file mode 100644 index 00000000..df25c832 --- /dev/null +++ b/test/unit/descriptor/data/hidden_service_stealth_auth @@ -0,0 +1,60 @@ +@type hidden-service-descriptor 1.0 +rendezvous-service-descriptor ubf3xeibzlfil6s4larq6y5peup2z3oj +version 2 +permanent-key +-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBAL1f7GdEObH+xMhf4GsaTCtfVH7ZpanegC65jn0/Kz9wlkpF+SQdIvTu +Ha2iZB34GDT2PvTy98chSxz+E3Kv2h45pQWbrwLN3Fj4qa+klclIXWcIa7GT4Pct +YZzAvHIh9t9EAe6ejYu8I+h4yL6QNAA2bYOi1d66+qCNCAFBgAqpAgMBAAE= +-----END RSA PUBLIC KEY----- +secret-id-part jczvydhzetbpdiylj3d5nsnjvaigs7xm +publication-time 2015-02-24 20:00:00 +protocol-versions 2,3 +introduction-points +-----BEGIN MESSAGE----- +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 +-----END MESSAGE----- +signature +-----BEGIN SIGNATURE----- +c8HgXcZesCwAzgDlE3kRYsq059yCIE7MH7r2jBHqJVYPRrtm/HF/mTUykwFPzwsY +ulcuoNlPfgGMKS8qBL4kFVZ9uR2Y6P4zLchoVS6wjL+cNYOQfeQs3sNZkiIrOjbb +590tr1/yrt0qUtITGhUGhBZVs9gvkuqaThTIXleEseI= +-----END SIGNATURE----- diff --git a/test/unit/descriptor/hidden_service_descriptor.py b/test/unit/descriptor/hidden_service_descriptor.py new file mode 100644 index 00000000..911e9fa1 --- /dev/null +++ b/test/unit/descriptor/hidden_service_descriptor.py @@ -0,0 +1,499 @@ +""" +Unit tests for stem.descriptor.hidden_service_descriptor. +""" + +import datetime +import unittest + +import stem.descriptor + +from test.mocking import CRYPTO_BLOB, get_hidden_service_descriptor +from test.unit.descriptor import get_resource + +from stem.descriptor.hidden_service_descriptor import ( + REQUIRED_FIELDS, + DecryptionFailure, + HiddenServiceDescriptor, +) + +MESSAGE_BLOCK = """ +-----BEGIN MESSAGE----- +%s +-----END MESSAGE-----\ +""" + +EXPECTED_DDG_PERMANENT_KEY = """\ +-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBAJ/SzzgrXPxTlFrKVhXh3buCWv2QfcNgncUpDpKouLn3AtPH5Ocys0jE +aZSKdvaiQ62md2gOwj4x61cFNdi05tdQjS+2thHKEm/KsB9BGLSLBNJYY356bupg +I5gQozM65ENelfxYlysBjJ52xSDBd8C4f/p9umdzaaaCmzXG/nhzAgMBAAE= +-----END RSA PUBLIC KEY-----\ +""" + +EXPECTED_DDG_INTRODUCTION_POINTS_ENCODED = """\ +-----BEGIN MESSAGE----- +aW50cm9kdWN0aW9uLXBvaW50IGl3a2k3N3h0YnZwNnF2ZWRmcndkem5jeHMzY2th +eWV1CmlwLWFkZHJlc3MgMTc4LjYyLjIyMi4xMjkKb25pb24tcG9ydCA0NDMKb25p +b24ta2V5Ci0tLS0tQkVHSU4gUlNBIFBVQkxJQyBLRVktLS0tLQpNSUdKQW9HQkFL +OTRCRVlJSFo0S2RFa2V5UGhiTENwUlc1RVNnKzJXUFFock00eXVLWUd1cTh3Rldn +dW1aWVI5CmsvV0EvL0ZZWE1CejBiQitja3Vacy9ZdTluSytITHpwR2FwVjBjbHN0 +NEdVTWNCSW5VQ3pDY3BqSlRRc1FEZ20KMy9ZM2NxaDBXNTVnT0NGaG9tUTQvMVdP +WWc3WUNqazRYWUhKRTIwT2RHMkxsNXpvdEs2ZkFnTUJBQUU9Ci0tLS0tRU5EIFJT +QSBQVUJMSUMgS0VZLS0tLS0Kc2VydmljZS1rZXkKLS0tLS1CRUdJTiBSU0EgUFVC +TElDIEtFWS0tLS0tCk1JR0pBb0dCQUpYbUpiOGxTeWRNTXFDZ0NnZmd2bEIyRTVy +cGQ1N2t6L0FxZzcvZDFIS2MzK2w1UW9Vdkh5dXkKWnNBbHlrYThFdTUzNGhsNDFv +cUVLcEFLWWNNbjFUTTB2cEpFR05WT2MrMDVCSW54STloOWYwTWcwMVBEMHRZdQpH +Y0xIWWdCemNyZkVtS3dNdE04V0VtY01KZDduMnVmZmFBdko4NDZXdWJiZVY3TVcx +WWVoQWdNQkFBRT0KLS0tLS1FTkQgUlNBIFBVQkxJQyBLRVktLS0tLQppbnRyb2R1 +Y3Rpb24tcG9pbnQgZW00Z2prNmVpaXVhbGhtbHlpaWZyemM3bGJ0cnNiaXAKaXAt +YWRkcmVzcyA0Ni40LjE3NC41Mgpvbmlvbi1wb3J0IDQ0Mwpvbmlvbi1rZXkKLS0t +LS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JR0pBb0dCQUxCbWhkRjV3SHhI +cnBMU21qQVpvdHR4MjIwKzk5NUZkTU9PdFpOalJ3MURCU3ByVVpacXR4V2EKUDhU +S3BIS3p3R0pLQ1ZZSUlqN2xvaGJ2OVQ5dXJtbGZURTA1VVJHZW5ab2lmT0ZOejNZ +d01KVFhTY1FFQkoxMAo5aVdOTERUc2tMekRLQ0FiR2hibi9NS3dPZllHQmhOVGxq +ZHlUbU5ZNUVDUmJSempldjl2QWdNQkFBRT0KLS0tLS1FTkQgUlNBIFBVQkxJQyBL +RVktLS0tLQpzZXJ2aWNlLWtleQotLS0tLUJFR0lOIFJTQSBQVUJMSUMgS0VZLS0t +LS0KTUlHSkFvR0JBTXhNSG9BbXJiVU1zeGlJQ3AzaVRQWWdobjBZdWVLSHgyMTl3 +dThPL1E1MVF5Y1ZWTHBYMjdkMQpoSlhrUEIzM1hRQlhzQlM3U3hzU3NTQ1EzR0V1 +clFKN0d1QkxwWUlSL3Zxc2FrRS9sOHdjMkNKQzVXVWh5RkZrCisxVFdJVUk1dHhu +WEx5V0NSY0tEVXJqcWRvc0RhRG9zZ0hGZzIzTW54K3hYY2FRL2ZyQi9BZ01CQUFF +PQotLS0tLUVORCBSU0EgUFVCTElDIEtFWS0tLS0tCmludHJvZHVjdGlvbi1wb2lu +dCBqcWhmbDM2NHgzdXBlNmxxbnhpem9sZXdsZnJzdzJ6eQppcC1hZGRyZXNzIDYy +LjIxMC44Mi4xNjkKb25pb24tcG9ydCA0NDMKb25pb24ta2V5Ci0tLS0tQkVHSU4g +UlNBIFBVQkxJQyBLRVktLS0tLQpNSUdKQW9HQkFQVWtxeGdmWWR3MFBtL2c2TWJo +bVZzR0tsdWppZm1raGRmb0VldXpnbyt3bkVzR3Z3VWVienJ6CmZaSlJ0MGNhWEZo +bkNHZ1FEMklnbWFyVWFVdlAyNGZYby80bVl6TGNQZUk3Z1puZXVBUUpZdm05OFl2 +OXZPSGwKTmFNL1d2RGtDc0ozR1ZOSjFIM3dMUFFSSTN2N0tiTnVjOXRDT1lsL3Iw +OU9oVmFXa3phakFnTUJBQUU9Ci0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K +c2VydmljZS1rZXkKLS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JR0pB +b0dCQUxieDhMZXFSb1Avcjl3OWhqd0Q0MVlVbTdQbzY5N3hSdHl0RjBNY3lMQ1M3 +R1JpVVluamk3S1kKZmVwWGR2Ti9KbDVxUUtISUJiNjAya3VPVGwwcE44UStZZUZV +U0lJRGNtUEJMcEJEaEgzUHZyUU1jR1ZhaU9XSAo4dzBITVpDeGd3QWNDQzUxdzVW +d2l1bXhFSk5CVmNac094MG16TjFDbG95KzkwcTBsRlhMQWdNQkFBRT0KLS0tLS1F +TkQgUlNBIFBVQkxJQyBLRVktLS0tLQoK +-----END MESSAGE-----\ +""" + +EXPECTED_DDG_INTRODUCTION_POINTS_CONTENT = b"""\ +introduction-point iwki77xtbvp6qvedfrwdzncxs3ckayeu +ip-address 178.62.222.129 +onion-port 443 +onion-key +-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBAK94BEYIHZ4KdEkeyPhbLCpRW5ESg+2WPQhrM4yuKYGuq8wFWgumZYR9 +k/WA//FYXMBz0bB+ckuZs/Yu9nK+HLzpGapV0clst4GUMcBInUCzCcpjJTQsQDgm +3/Y3cqh0W55gOCFhomQ4/1WOYg7YCjk4XYHJE20OdG2Ll5zotK6fAgMBAAE= +-----END RSA PUBLIC KEY----- +service-key +-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBAJXmJb8lSydMMqCgCgfgvlB2E5rpd57kz/Aqg7/d1HKc3+l5QoUvHyuy +ZsAlyka8Eu534hl41oqEKpAKYcMn1TM0vpJEGNVOc+05BInxI9h9f0Mg01PD0tYu +GcLHYgBzcrfEmKwMtM8WEmcMJd7n2uffaAvJ846WubbeV7MW1YehAgMBAAE= +-----END RSA PUBLIC KEY----- +introduction-point em4gjk6eiiualhmlyiifrzc7lbtrsbip +ip-address 46.4.174.52 +onion-port 443 +onion-key +-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBALBmhdF5wHxHrpLSmjAZottx220+995FdMOOtZNjRw1DBSprUZZqtxWa +P8TKpHKzwGJKCVYIIj7lohbv9T9urmlfTE05URGenZoifOFNz3YwMJTXScQEBJ10 +9iWNLDTskLzDKCAbGhbn/MKwOfYGBhNTljdyTmNY5ECRbRzjev9vAgMBAAE= +-----END RSA PUBLIC KEY----- +service-key +-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBAMxMHoAmrbUMsxiICp3iTPYghn0YueKHx219wu8O/Q51QycVVLpX27d1 +hJXkPB33XQBXsBS7SxsSsSCQ3GEurQJ7GuBLpYIR/vqsakE/l8wc2CJC5WUhyFFk ++1TWIUI5txnXLyWCRcKDUrjqdosDaDosgHFg23Mnx+xXcaQ/frB/AgMBAAE= +-----END RSA PUBLIC KEY----- +introduction-point jqhfl364x3upe6lqnxizolewlfrsw2zy +ip-address 62.210.82.169 +onion-port 443 +onion-key +-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBAPUkqxgfYdw0Pm/g6MbhmVsGKlujifmkhdfoEeuzgo+wnEsGvwUebzrz +fZJRt0caXFhnCGgQD2IgmarUaUvP24fXo/4mYzLcPeI7gZneuAQJYvm98Yv9vOHl +NaM/WvDkCsJ3GVNJ1H3wLPQRI3v7KbNuc9tCOYl/r09OhVaWkzajAgMBAAE= +-----END RSA PUBLIC KEY----- +service-key +-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBALbx8LeqRoP/r9w9hjwD41YUm7Po697xRtytF0McyLCS7GRiUYnji7KY +fepXdvN/Jl5qQKHIBb602kuOTl0pN8Q+YeFUSIIDcmPBLpBDhH3PvrQMcGVaiOWH +8w0HMZCxgwAcCC51w5VwiumxEJNBVcZsOx0mzN1Cloy+90q0lFXLAgMBAAE= +-----END RSA PUBLIC KEY----- + +""" + +EXPECTED_DDG_SIGNATURE = """\ +-----BEGIN SIGNATURE----- +VKMmsDIUUFOrpqvcQroIZjDZTKxqNs88a4M9Te8cR/ZvS7H2nffv6iQs0tom5X4D +4Dy4iZiy+pwYxdHfaOxmdpgMCRvgPb34MExWr5YemH0QuGtnlp5Wxr8GYaAQVuZX +cZjQLW0juUYCbgIGdxVEBnlEt2rgBSM9+1oR7EAfV1U= +-----END SIGNATURE-----\ +""" + +EXPECT_POINT_1_ONION_KEY = """\ +-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBAK94BEYIHZ4KdEkeyPhbLCpRW5ESg+2WPQhrM4yuKYGuq8wFWgumZYR9 +k/WA//FYXMBz0bB+ckuZs/Yu9nK+HLzpGapV0clst4GUMcBInUCzCcpjJTQsQDgm +3/Y3cqh0W55gOCFhomQ4/1WOYg7YCjk4XYHJE20OdG2Ll5zotK6fAgMBAAE= +-----END RSA PUBLIC KEY-----\ +""" + +EXPECT_POINT_1_SERVICE_KEY = """\ +-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBAJXmJb8lSydMMqCgCgfgvlB2E5rpd57kz/Aqg7/d1HKc3+l5QoUvHyuy +ZsAlyka8Eu534hl41oqEKpAKYcMn1TM0vpJEGNVOc+05BInxI9h9f0Mg01PD0tYu +GcLHYgBzcrfEmKwMtM8WEmcMJd7n2uffaAvJ846WubbeV7MW1YehAgMBAAE= +-----END RSA PUBLIC KEY-----\ +""" + +EXPECT_POINT_2_ONION_KEY = """\ +-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBALBmhdF5wHxHrpLSmjAZottx220+995FdMOOtZNjRw1DBSprUZZqtxWa +P8TKpHKzwGJKCVYIIj7lohbv9T9urmlfTE05URGenZoifOFNz3YwMJTXScQEBJ10 +9iWNLDTskLzDKCAbGhbn/MKwOfYGBhNTljdyTmNY5ECRbRzjev9vAgMBAAE= +-----END RSA PUBLIC KEY-----\ +""" + +EXPECT_POINT_2_SERVICE_KEY = """\ +-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBAMxMHoAmrbUMsxiICp3iTPYghn0YueKHx219wu8O/Q51QycVVLpX27d1 +hJXkPB33XQBXsBS7SxsSsSCQ3GEurQJ7GuBLpYIR/vqsakE/l8wc2CJC5WUhyFFk ++1TWIUI5txnXLyWCRcKDUrjqdosDaDosgHFg23Mnx+xXcaQ/frB/AgMBAAE= +-----END RSA PUBLIC KEY-----\ +""" + +EXPECT_POINT_3_ONION_KEY = """\ +-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBAPUkqxgfYdw0Pm/g6MbhmVsGKlujifmkhdfoEeuzgo+wnEsGvwUebzrz +fZJRt0caXFhnCGgQD2IgmarUaUvP24fXo/4mYzLcPeI7gZneuAQJYvm98Yv9vOHl +NaM/WvDkCsJ3GVNJ1H3wLPQRI3v7KbNuc9tCOYl/r09OhVaWkzajAgMBAAE= +-----END RSA PUBLIC KEY-----\ +""" + +EXPECT_POINT_3_SERVICE_KEY = """\ +-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBALbx8LeqRoP/r9w9hjwD41YUm7Po697xRtytF0McyLCS7GRiUYnji7KY +fepXdvN/Jl5qQKHIBb602kuOTl0pN8Q+YeFUSIIDcmPBLpBDhH3PvrQMcGVaiOWH +8w0HMZCxgwAcCC51w5VwiumxEJNBVcZsOx0mzN1Cloy+90q0lFXLAgMBAAE= +-----END RSA PUBLIC KEY-----\ +""" + +EXPECTED_BASIC_AUTH_INTRODUCTION_POINTS_ENCODED = """\ +-----BEGIN MESSAGE----- +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 +-----END MESSAGE-----\ +""" + + +class TestHiddenServiceDescriptor(unittest.TestCase): + 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) + + 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) + + def test_for_facebook(self): + """ + Parse facebook's descriptor. + """ + + descriptor_file = open(get_resource('hidden_service_facebook'), 'rb') + + 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) + self.assertEqual(datetime.datetime(2014, 10, 31, 23, 0, 0), desc.published) + self.assertEqual([2, 3], desc.protocol_versions) + + def test_with_basic_auth(self): + """ + Parse a descriptor with introduction-points encrypted with basic auth. + """ + + descriptor_file = open(get_resource('hidden_service_basic_auth'), 'rb') + + 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) + self.assertEqual(datetime.datetime(2015, 2, 24, 20, 0, 0), desc.published) + self.assertEqual([2, 3], desc.protocol_versions) + self.assertEqual(EXPECTED_BASIC_AUTH_INTRODUCTION_POINTS_ENCODED, desc.introduction_points_encoded) + self.assertEqual([], desc.introduction_points_auth) + + self.assertRaises(DecryptionFailure, desc.introduction_points) + + def test_with_stealth_auth(self): + """ + Parse a descriptor with introduction-points encrypted with stealth auth. + """ + + descriptor_file = open(get_resource('hidden_service_stealth_auth'), 'rb') + + 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) + self.assertEqual(datetime.datetime(2015, 2, 24, 20, 0, 0), desc.published) + self.assertEqual([2, 3], desc.protocol_versions) + self.assertEqual([], desc.introduction_points_auth) + + self.assertRaises(DecryptionFailure, desc.introduction_points) + + def _assert_matches_duckduckgo(self, desc): + self.assertEqual('y3olqqblqw2gbh6phimfuiroechjjafa', desc.descriptor_id) + self.assertEqual(2, desc.version) + self.assertEqual(EXPECTED_DDG_PERMANENT_KEY, desc.permanent_key) + self.assertEqual('e24kgecavwsznj7gpbktqsiwgvngsf4e', desc.secret_id_part) + self.assertEqual(datetime.datetime(2015, 2, 23, 20, 0, 0), desc.published) + self.assertEqual([2, 3], desc.protocol_versions) + self.assertEqual(EXPECTED_DDG_INTRODUCTION_POINTS_ENCODED, desc.introduction_points_encoded) + self.assertEqual([], desc.introduction_points_auth) + self.assertEqual(EXPECTED_DDG_INTRODUCTION_POINTS_CONTENT, desc.introduction_points_content) + self.assertEqual(EXPECTED_DDG_SIGNATURE, desc.signature) + + introduction_points = desc.introduction_points() + self.assertEqual(3, len(introduction_points)) + + point = introduction_points[0] + self.assertEqual('iwki77xtbvp6qvedfrwdzncxs3ckayeu', point.identifier) + self.assertEqual('178.62.222.129', point.address) + self.assertEqual(443, point.port) + self.assertEqual(EXPECT_POINT_1_ONION_KEY, point.onion_key) + self.assertEqual(EXPECT_POINT_1_SERVICE_KEY, point.service_key) + self.assertEqual([], point.intro_authentication) + + point = introduction_points[1] + self.assertEqual('em4gjk6eiiualhmlyiifrzc7lbtrsbip', point.identifier) + self.assertEqual('46.4.174.52', point.address) + self.assertEqual(443, point.port) + self.assertEqual(EXPECT_POINT_2_ONION_KEY, point.onion_key) + self.assertEqual(EXPECT_POINT_2_SERVICE_KEY, point.service_key) + self.assertEqual([], point.intro_authentication) + + point = introduction_points[2] + self.assertEqual('jqhfl364x3upe6lqnxizolewlfrsw2zy', point.identifier) + self.assertEqual('62.210.82.169', point.address) + self.assertEqual(443, point.port) + self.assertEqual(EXPECT_POINT_3_ONION_KEY, point.onion_key) + self.assertEqual(EXPECT_POINT_3_SERVICE_KEY, point.service_key) + self.assertEqual([], point.intro_authentication) + + def test_minimal_hidden_service_descriptor(self): + """ + Basic sanity check that we can parse a hidden service descriptor with minimal attributes. + """ + + desc = get_hidden_service_descriptor() + + self.assertEqual('y3olqqblqw2gbh6phimfuiroechjjafa', desc.descriptor_id) + self.assertEqual(2, desc.version) + self.assertTrue(CRYPTO_BLOB in desc.permanent_key) + self.assertEqual('e24kgecavwsznj7gpbktqsiwgvngsf4e', desc.secret_id_part) + self.assertEqual(datetime.datetime(2015, 2, 23, 20, 0, 0), desc.published) + self.assertEqual([2, 3], desc.protocol_versions) + self.assertEqual('-----BEGIN MESSAGE-----\n-----END MESSAGE-----', desc.introduction_points_encoded) + self.assertEqual([], desc.introduction_points_auth) + self.assertEqual(b'', desc.introduction_points_content) + self.assertTrue(CRYPTO_BLOB in desc.signature) + self.assertEqual([], desc.introduction_points()) + + def test_unrecognized_line(self): + """ + Includes unrecognized content in the descriptor. + """ + + desc = get_hidden_service_descriptor({'pepperjack': 'is oh so tasty!'}) + self.assertEqual(['pepperjack is oh so tasty!'], desc.get_unrecognized_lines()) + + def test_proceeding_line(self): + """ + Includes a line prior to the 'rendezvous-service-descriptor' entry. + """ + + desc_text = b'hibernate 1\n' + get_hidden_service_descriptor(content = True) + self._expect_invalid_attr(desc_text) + + def test_trailing_line(self): + """ + Includes a line after the 'router-signature' entry. + """ + + desc_text = get_hidden_service_descriptor(content = True) + b'\nhibernate 1' + self._expect_invalid_attr(desc_text) + + def test_required_fields(self): + """ + Check that we require the mandatory fields. + """ + + line_to_attr = { + 'rendezvous-service-descriptor': 'descriptor_id', + 'version': 'version', + 'permanent-key': 'permanent_key', + 'secret-id-part': 'secret_id_part', + 'publication-time': 'published', + 'introduction-points': 'introduction_points_encoded', + 'protocol-versions': 'protocol_versions', + 'signature': 'signature', + } + + for line in REQUIRED_FIELDS: + desc_text = get_hidden_service_descriptor(content = True, exclude = (line,)) + + expected = [] if line == 'protocol-versions' else None + self._expect_invalid_attr(desc_text, line_to_attr[line], expected) + + def test_invalid_version(self): + """ + Checks that our version field expects a numeric value. + """ + + test_values = ( + '', + '-10', + 'hello', + ) + + for test_value in test_values: + desc_text = get_hidden_service_descriptor({'version': test_value}, content = True) + self._expect_invalid_attr(desc_text, 'version') + + def test_invalid_protocol_versions(self): + """ + Checks that our protocol-versions field expects comma separated numeric + values. + """ + + test_values = ( + '', + '-10', + 'hello', + '10,', + ',10', + '10,-10', + '10,hello', + ) + + for test_value in test_values: + desc_text = get_hidden_service_descriptor({'protocol-versions': test_value}, content = True) + self._expect_invalid_attr(desc_text, 'protocol_versions', []) + + def test_introduction_points_when_empty(self): + """ + It's valid to advertise zero introduciton points. I'm not clear if this + would mean an empty protocol-versions field or that it's omitted but either + are valid according to the spec. + """ + + missing_field_desc = get_hidden_service_descriptor(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 = get_hidden_service_descriptor({'introduction-points': MESSAGE_BLOCK % ''}) + + self.assertEqual((MESSAGE_BLOCK % '').strip(), empty_field_desc.introduction_points_encoded) + self.assertEqual([], empty_field_desc.introduction_points_auth) + self.assertEqual(b'', empty_field_desc.introduction_points_content) + self.assertEqual([], empty_field_desc.introduction_points()) + + def test_introduction_points_when_not_base64(self): + """ + Checks the introduction-points field when the content isn't base64 encoded. + """ + + test_values = ( + MESSAGE_BLOCK % '12345', + MESSAGE_BLOCK % 'hello', + ) + + for test_value in test_values: + desc_text = get_hidden_service_descriptor({'introduction-points': test_value}, content = True) + + desc = self._expect_invalid_attr(desc_text, 'introduction_points_encoded', test_value.strip()) + self.assertEqual([], desc.introduction_points_auth) + self.assertEqual(None, desc.introduction_points_content) + self.assertEqual([], desc.introduction_points()) + + def _expect_invalid_attr(self, desc_text, attr = None, expected_value = None): + """ + Asserts that construction will fail due to desc_text having a malformed + attribute. If an attr is provided then we check that it matches an expected + value when we're constructed without validation. + """ + + self.assertRaises(ValueError, HiddenServiceDescriptor, desc_text, True) + desc = HiddenServiceDescriptor(desc_text, validate = False) + + if attr: + # check that the invalid attribute matches the expected value when + # constructed without validation + + self.assertEqual(expected_value, getattr(desc, attr)) + else: + # check a default attribute + self.assertEqual('y3olqqblqw2gbh6phimfuiroechjjafa', desc.descriptor_id) + + return desc diff --git a/test/unit/descriptor/server_descriptor.py b/test/unit/descriptor/server_descriptor.py index 00d2bb34..fd0c0326 100644 --- a/test/unit/descriptor/server_descriptor.py +++ b/test/unit/descriptor/server_descriptor.py @@ -452,7 +452,7 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4= desc_text = get_relay_server_descriptor({'opt': 'protocols Link 1 2'}, content = True) self._expect_invalid_attr(desc_text, 'circuit_protocols') - @patch('stem.descriptor.server_descriptor.RelayDescriptor._verify_digest', Mock()) + @patch('stem.prereq.is_crypto_available', Mock(return_value = False)) def test_published_leap_year(self): """ Constructs with a published entry for a leap year, and when the date is @@ -508,7 +508,7 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4= self.assertEqual(900, desc.read_history_interval) self.assertEqual([], desc.read_history_values) - @patch('stem.descriptor.server_descriptor.RelayDescriptor._verify_digest', Mock()) + @patch('stem.prereq.is_crypto_available', Mock(return_value = False)) def test_annotations(self): """ Checks that content before a descriptor are parsed as annotations. diff --git a/test/unit/tutorial.py b/test/unit/tutorial.py index 55a170b9..7d204378 100644 --- a/test/unit/tutorial.py +++ b/test/unit/tutorial.py @@ -201,7 +201,7 @@ class TestTutorial(unittest.TestCase): @patch('sys.stdout', new_callable = StringIO) @patch('stem.descriptor.remote.DescriptorDownloader') - @patch('stem.descriptor.server_descriptor.RelayDescriptor._verify_digest', Mock()) + @patch('stem.prereq.is_crypto_available', Mock(return_value = False)) def test_mirror_mirror_on_the_wall_5(self, downloader_mock, stdout_mock): def tutorial_example(): from stem.descriptor.remote import DescriptorDownloader |
