Stem Docs

stem.descriptor.server_descriptor

Source code for stem.descriptor.server_descriptor

# Copyright 2012-2014, Damian Johnson and The Tor Project
# See LICENSE for licensing information

"""
Parsing for Tor server descriptors, which contains the infrequently changing
information about a Tor relay (contact information, exit policy, public keys,
etc). This information is provided from a few sources...

* control port via 'GETINFO desc/\*' queries
* the 'cached-descriptors' file in tor's data directory
* tor metrics, at https://metrics.torproject.org/data.html
* directory authorities and mirrors via their DirPort

**Module Overview:**

::

  ServerDescriptor - Tor server descriptor.
    |- RelayDescriptor - Server descriptor for a relay.
    |
    |- BridgeDescriptor - Scrubbed server descriptor for a bridge.
    |  |- is_scrubbed - checks if our content has been properly scrubbed
    |  +- get_scrubbing_issues - description of issues with our scrubbing
    |
    |- digest - calculates the upper-case hex digest value for our content
    |- get_unrecognized_lines - lines with unrecognized content
    |- get_annotations - dictionary of content prior to the descriptor entry
    +- get_annotation_lines - lines that provided the annotations
"""

import base64
import codecs
import datetime
import hashlib
import re

import stem.descriptor.extrainfo_descriptor
import stem.exit_policy
import stem.prereq
import stem.util.connection
import stem.util.str_tools
import stem.util.tor_tools
import stem.version

from stem.util import log

from stem.descriptor import (
  PGP_BLOCK_END,
  Descriptor,
  _get_bytes_field,
  _get_descriptor_components,
  _read_until_keywords,
)

try:
  # added in python 3.2
  from functools import lru_cache
except ImportError:
  from stem.util.lru_cache import lru_cache

# relay descriptors must have exactly one of the following
REQUIRED_FIELDS = (
  'router',
  'bandwidth',
  'published',
  'onion-key',
  'signing-key',
  'router-signature',
)

# optional entries that can appear at most once
SINGLE_FIELDS = (
  'platform',
  'fingerprint',
  'hibernating',
  'uptime',
  'contact',
  'read-history',
  'write-history',
  'eventdns',
  'family',
  'caches-extra-info',
  'extra-info-digest',
  'hidden-service-dir',
  'protocols',
  'allow-single-hop-exits',
  'ntor-onion-key',
)

DEFAULT_IPV6_EXIT_POLICY = stem.exit_policy.MicroExitPolicy('reject 1-65535')
REJECT_ALL_POLICY = stem.exit_policy.ExitPolicy('reject *:*')


def _parse_file(descriptor_file, is_bridge = False, validate = True, **kwargs):
  """
  Iterates over the server descriptors in a file.

  :param file descriptor_file: file with descriptor content
  :param bool is_bridge: parses the file as being a bridge descriptor
  :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 ServerDescriptor instances in the file

  :raises:
    * **ValueError** if the contents is malformed and validate is True
    * **IOError** if the file can't be read
  """

  # Handler for relay descriptors
  #
  # Cached descriptors consist of annotations followed by the descriptor
  # itself. For instance...
  #
  #   @downloaded-at 2012-03-14 16:31:05
  #   @source "145.53.65.130"
  #   router caerSidi 71.35.143.157 9001 0 0
  #   platform Tor 0.2.1.30 on Linux x86_64
  #   <rest of the descriptor content>
  #   router-signature
  #   -----BEGIN SIGNATURE-----
  #   <signature for the above descriptor>
  #   -----END SIGNATURE-----
  #
  # Metrics descriptor files are the same, but lack any annotations. The
  # following simply does the following...
  #
  #   - parse as annotations until we get to 'router'
  #   - parse as descriptor content until we get to 'router-signature' followed
  #     by the end of the signature block
  #   - construct a descriptor and provide it back to the caller
  #
  # Any annotations after the last server descriptor is ignored (never provided
  # to the caller).

  while True:
    annotations = _read_until_keywords('router', descriptor_file)
    descriptor_content = _read_until_keywords('router-signature', descriptor_file)

    # we've reached the 'router-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:]

      # strip newlines from annotations
      annotations = map(bytes.strip, annotations)

      descriptor_text = bytes.join(b'', descriptor_content)

      if is_bridge:
        yield BridgeDescriptor(descriptor_text, validate, annotations, **kwargs)
      else:
        yield RelayDescriptor(descriptor_text, validate, annotations, **kwargs)
    else:
      if validate and annotations:
        orphaned_annotations = stem.util.str_tools._to_unicode(b'\n'.join(annotations))
        raise ValueError('Content conform to being a server descriptor:\n%s' % orphaned_annotations)

      break  # done parsing descriptors


[docs]class ServerDescriptor(Descriptor): """ Common parent for server descriptors. :var str nickname: **\*** relay's nickname :var str fingerprint: identity key fingerprint :var datetime published: **\*** time in UTC when this descriptor was made :var str address: **\*** IPv4 address of the relay :var int or_port: **\*** port used for relaying :var int socks_port: **\*** port used as client (deprecated, always **None**) :var int dir_port: **\*** port used for descriptor mirroring :var bytes platform: line with operating system and tor version :var stem.version.Version tor_version: version of tor :var str operating_system: operating system :var int uptime: uptime when published in seconds :var bytes contact: contact information :var stem.exit_policy.ExitPolicy exit_policy: **\*** stated exit policy :var stem.exit_policy.MicroExitPolicy exit_policy_v6: **\*** exit policy for IPv6 :var set family: **\*** nicknames or fingerprints of declared family :var int average_bandwidth: **\*** average rate it's willing to relay in bytes/s :var int burst_bandwidth: **\*** burst rate it's willing to relay in bytes/s :var int observed_bandwidth: **\*** estimated capacity based on usage in bytes/s :var list link_protocols: link protocols supported by the relay :var list circuit_protocols: circuit protocols supported by the relay :var bool hibernating: **\*** hibernating when published :var bool allow_single_hop_exits: **\*** flag if single hop exiting is allowed :var bool extra_info_cache: **\*** flag if a mirror for extra-info documents :var str extra_info_digest: upper-case hex encoded digest of our extra-info document :var bool eventdns: flag for evdns backend (deprecated, always unset) :var list or_addresses: **\*** alternative for our address/or_port attributes, each entry is a tuple of the form (address (**str**), port (**int**), is_ipv6 (**bool**)) Deprecated, moved to extra-info descriptor... :var datetime read_history_end: end of the sampling interval :var int read_history_interval: seconds per interval :var list read_history_values: bytes read during each interval :var datetime write_history_end: end of the sampling interval :var int write_history_interval: seconds per interval :var list write_history_values: bytes written during each interval **\*** attribute is either required when we're parsed with validation or has a default value, others are left as **None** if undefined """ def __init__(self, raw_contents, validate = True, annotations = None): """ Server descriptor constructor, created from an individual relay's descriptor content (as provided by 'GETINFO desc/*', cached descriptors, and metrics). By default this validates the descriptor's content as it's parsed. This validation can be disables to either improve performance or be accepting of malformed data. :param str raw_contents: descriptor content provided by the relay :param bool validate: checks the validity of the descriptor's content if **True**, skips these checks otherwise :param list annotations: lines that appeared prior to the descriptor :raises: **ValueError** if the contents is malformed and validate is True """ super(ServerDescriptor, self).__init__(raw_contents) # Only a few things can be arbitrary bytes according to the dir-spec, so # parsing them separately. self.platform = _get_bytes_field('platform', raw_contents) self.contact = _get_bytes_field('contact', raw_contents) raw_contents = stem.util.str_tools._to_unicode(raw_contents) self.nickname = None self.fingerprint = None self.published = None self.address = None self.or_port = None self.socks_port = None self.dir_port = None self.tor_version = None self.operating_system = None self.uptime = None self.exit_policy = None self.exit_policy_v6 = DEFAULT_IPV6_EXIT_POLICY self.family = set() self.average_bandwidth = None self.burst_bandwidth = None self.observed_bandwidth = None self.link_protocols = None self.circuit_protocols = None self.hibernating = False self.allow_single_hop_exits = False self.extra_info_cache = False self.extra_info_digest = None self.hidden_service_dir = None self.eventdns = None self.or_addresses = [] self.read_history_end = None self.read_history_interval = None self.read_history_values = None self.write_history_end = None self.write_history_interval = None self.write_history_values = None self._unrecognized_lines = [] self._annotation_lines = annotations if annotations else [] # A descriptor contains a series of 'keyword lines' which are simply a # keyword followed by an optional value. Lines can also be followed by a # signature block. # # We care about the ordering of 'accept' and 'reject' entries because this # influences the resulting exit policy, but for everything else the order # does not matter so breaking it into key / value pairs. entries, policy = _get_descriptor_components(raw_contents, validate, ('accept', 'reject')) if policy == [u'reject *:*']: self.exit_policy = REJECT_ALL_POLICY else: self.exit_policy = stem.exit_policy.ExitPolicy(*policy) self._parse(entries, validate) if validate: self._check_constraints(entries)
[docs] def digest(self): """ Provides the hex encoded sha1 of our content. This value is part of the network status entry for this relay. :returns: **unicode** with the upper-case hex digest value for this server descriptor """ raise NotImplementedError('Unsupported Operation: this should be implemented by the ServerDescriptor subclass')
[docs] def get_unrecognized_lines(self): return list(self._unrecognized_lines)
@lru_cache()
[docs] def get_annotations(self): """ Provides content that appeared prior to the descriptor. If this comes from the cached-descriptors file then this commonly contains content like... :: @downloaded-at 2012-03-18 21:18:29 @source "173.254.216.66" :returns: **dict** with the key/value pairs in our annotations """ annotation_dict = {} for line in self._annotation_lines: if b' ' in line: key, value = line.split(b' ', 1) annotation_dict[key] = value else: annotation_dict[line] = None return annotation_dict
[docs] def get_annotation_lines(self): """ Provides the lines of content that appeared prior to the descriptor. This is the same as the :func:`~stem.descriptor.server_descriptor.ServerDescriptor.get_annotations` results, but with the unparsed lines and ordering retained. :returns: **list** with the lines of annotation that came before this descriptor """ return self._annotation_lines
def _parse(self, entries, validate): """ Parses a series of 'keyword => (value, pgp block)' mappings and applies them as attributes. :param dict entries: descriptor contents to be applied :param bool validate: checks the validity of descriptor content if **True** :raises: **ValueError** if an error occurs in validation """ for keyword, values in entries.items(): # most just work with the first (and only) value value, block_contents = values[0] line = '%s %s' % (keyword, value) # original line if block_contents: line += '\n%s' % block_contents if keyword == 'router': # "router" nickname address ORPort SocksPort DirPort router_comp = value.split() if len(router_comp) < 5: if not validate: continue raise ValueError('Router line must have five values: %s' % line) if validate: if not stem.util.tor_tools.is_valid_nickname(router_comp[0]): raise ValueError("Router line entry isn't a valid nickname: %s" % router_comp[0]) elif not stem.util.connection.is_valid_ipv4_address(router_comp[1]): raise ValueError("Router line entry isn't a valid IPv4 address: %s" % router_comp[1]) elif not stem.util.connection.is_valid_port(router_comp[2], allow_zero = True): raise ValueError("Router line's ORPort is invalid: %s" % router_comp[2]) elif not stem.util.connection.is_valid_port(router_comp[3], allow_zero = True): raise ValueError("Router line's SocksPort is invalid: %s" % router_comp[3]) elif not stem.util.connection.is_valid_port(router_comp[4], allow_zero = True): raise ValueError("Router line's DirPort is invalid: %s" % router_comp[4]) elif not (router_comp[2].isdigit() and router_comp[3].isdigit() and router_comp[4].isdigit()): continue self.nickname = router_comp[0] self.address = router_comp[1] self.or_port = int(router_comp[2]) self.socks_port = None if router_comp[3] == '0' else int(router_comp[3]) self.dir_port = None if router_comp[4] == '0' else int(router_comp[4]) elif keyword == 'bandwidth': # "bandwidth" bandwidth-avg bandwidth-burst bandwidth-observed bandwidth_comp = value.split() if len(bandwidth_comp) < 3: if not validate: continue raise ValueError('Bandwidth line must have three values: %s' % line) elif not bandwidth_comp[0].isdigit(): if not validate: continue raise ValueError("Bandwidth line's average rate isn't numeric: %s" % bandwidth_comp[0]) elif not bandwidth_comp[1].isdigit(): if not validate: continue raise ValueError("Bandwidth line's burst rate isn't numeric: %s" % bandwidth_comp[1]) elif not bandwidth_comp[2].isdigit(): if not validate: continue raise ValueError("Bandwidth line's observed rate isn't numeric: %s" % bandwidth_comp[2]) self.average_bandwidth = int(bandwidth_comp[0]) self.burst_bandwidth = int(bandwidth_comp[1]) self.observed_bandwidth = int(bandwidth_comp[2]) elif keyword == 'platform': # "platform" string # The platform attribute was set earlier. This line can contain any # arbitrary data, but tor seems to report its version followed by the # os like the following... # # platform Tor 0.2.2.35 (git-73ff13ab3cc9570d) on Linux x86_64 # # There's no guarantee that we'll be able to pick these out the # version, but might as well try to save our caller the effort. platform_match = re.match('^(?:node-)?Tor (\S*).* on (.*)$', value) if platform_match: version_str, self.operating_system = platform_match.groups() try: self.tor_version = stem.version._get_version(version_str) except ValueError: pass elif keyword == 'published': # "published" YYYY-MM-DD HH:MM:SS try: self.published = datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S') except ValueError: if validate: raise ValueError("Published line's time wasn't parsable: %s" % line) elif keyword == 'fingerprint': # This is forty hex digits split into space separated groups of four. # Checking that we match this pattern. fingerprint = value.replace(' ', '') if validate: for grouping in value.split(' '): if len(grouping) != 4: raise ValueError('Fingerprint line should have groupings of four hex digits: %s' % value) if not stem.util.tor_tools.is_valid_fingerprint(fingerprint): raise ValueError('Tor relay fingerprints consist of forty hex digits: %s' % value) self.fingerprint = fingerprint elif keyword == 'hibernating': # "hibernating" 0|1 (in practice only set if one) if validate and not value in ('0', '1'): raise ValueError('Hibernating line had an invalid value, must be zero or one: %s' % value) self.hibernating = value == '1' elif keyword == 'allow-single-hop-exits': self.allow_single_hop_exits = True elif keyword == 'caches-extra-info': self.extra_info_cache = True elif keyword == 'extra-info-digest': # this is forty hex digits which just so happens to be the same a # fingerprint if validate and not stem.util.tor_tools.is_valid_fingerprint(value): raise ValueError('Extra-info digests should consist of forty hex digits: %s' % value) self.extra_info_digest = value elif keyword == 'hidden-service-dir': if value: self.hidden_service_dir = value.split(' ') else: self.hidden_service_dir = ['2'] elif keyword == 'uptime': # 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. try: self.uptime = int(value) except ValueError: if not validate: continue raise ValueError('Uptime line must have an integer value: %s' % value) elif keyword == 'contact': pass # parsed as a bytes field earlier elif keyword == 'protocols': protocols_match = re.match('^Link (.*) Circuit (.*)$', value) if protocols_match: link_versions, circuit_versions = protocols_match.groups() self.link_protocols = link_versions.split(' ') self.circuit_protocols = circuit_versions.split(' ') elif validate: raise ValueError('Protocols line did not match the expected pattern: %s' % line) elif keyword == 'family': self.family = set(value.split(' ')) elif keyword == 'eventdns': self.eventdns = value == '1' elif keyword == 'ipv6-policy': self.exit_policy_v6 = stem.exit_policy.MicroExitPolicy(value) elif keyword == 'or-address': or_address_entries = [value for (value, _) in values] for entry in or_address_entries: line = '%s %s' % (keyword, entry) if not ':' in entry: if not validate: continue else: raise ValueError('or-address line missing a colon: %s' % line) address, port = entry.rsplit(':', 1) is_ipv6 = address.startswith('[') and address.endswith(']') if is_ipv6: address = address[1:-1] # remove brackets if not ((not is_ipv6 and stem.util.connection.is_valid_ipv4_address(address)) or (is_ipv6 and stem.util.connection.is_valid_ipv6_address(address))): if not validate: continue else: raise ValueError('or-address line has a malformed address: %s' % line) if stem.util.connection.is_valid_port(port): self.or_addresses.append((address, int(port), is_ipv6)) elif validate: raise ValueError('or-address line has a malformed port: %s' % line) elif keyword in ('read-history', 'write-history'): try: timestamp, interval, remainder = \ stem.descriptor.extrainfo_descriptor._parse_timestamp_and_interval(keyword, value) try: if remainder: history_values = [int(entry) for entry in remainder.split(',')] else: history_values = [] except ValueError: raise ValueError('%s line has non-numeric values: %s' % (keyword, line)) if keyword == 'read-history': self.read_history_end = timestamp self.read_history_interval = interval self.read_history_values = history_values else: self.write_history_end = timestamp self.write_history_interval = interval self.write_history_values = history_values except ValueError as exc: if validate: raise exc else: self._unrecognized_lines.append(line) # if we have a negative uptime and a tor version that shouldn't exhibit # this bug then fail validation if validate and self.uptime and self.tor_version: if self.uptime < 0 and self.tor_version >= stem.version.Version('0.1.2.7'): raise ValueError("Descriptor for version '%s' had a negative uptime value: %i" % (self.tor_version, self.uptime)) def _check_constraints(self, entries): """ Does a basic check that the entries conform to this descriptor type's constraints. :param dict entries: keyword => (value, pgp key) entries :raises: **ValueError** if an issue arises in validation """ for keyword in self._required_fields(): if not keyword in entries: raise ValueError("Descriptor must have a '%s' entry" % keyword) for keyword in self._single_fields(): if keyword in entries and len(entries[keyword]) > 1: raise ValueError("The '%s' entry can only appear once in a descriptor" % keyword) expected_first_keyword = self._first_keyword() if expected_first_keyword and expected_first_keyword != entries.keys()[0]: raise ValueError("Descriptor must start with a '%s' entry" % expected_first_keyword) expected_last_keyword = self._last_keyword() if expected_last_keyword and expected_last_keyword != entries.keys()[-1]: raise ValueError("Descriptor must end with a '%s' entry" % expected_last_keyword) if not self.exit_policy: raise ValueError("Descriptor must have at least one 'accept' or 'reject' entry") # Constraints that the descriptor must meet to be valid. These can be None if # not applicable. def _required_fields(self): return REQUIRED_FIELDS def _single_fields(self): return REQUIRED_FIELDS + SINGLE_FIELDS def _first_keyword(self): return 'router' def _last_keyword(self): return 'router-signature'
[docs]class RelayDescriptor(ServerDescriptor): """ Server descriptor (`descriptor specification <https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt>`_) :var str onion_key: **\*** key used to encrypt EXTEND cells :var str ntor_onion_key: base64 key used to encrypt EXTEND in the ntor protocol :var str signing_key: **\*** relay's long-term identity key :var str signature: **\*** signature for this descriptor **\*** attribute is required when we're parsed with validation """ def __init__(self, raw_contents, validate = True, annotations = None): self.onion_key = None self.ntor_onion_key = None self.signing_key = None self.signature = None super(RelayDescriptor, self).__init__(raw_contents, validate, annotations) # validate the descriptor if required if validate: self._validate_content() @lru_cache()
[docs] def digest(self): """ Provides the digest of our descriptor's content. :returns: the digest string encoded in uppercase hex :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)) def _parse(self, entries, validate): entries = dict(entries) # shallow copy since we're destructive # handles fields only in server descriptors for keyword, values in entries.items(): value, block_contents = values[0] line = '%s %s' % (keyword, value) if keyword == 'onion-key': if validate and not block_contents: raise ValueError('Onion key line must be followed by a public key: %s' % line) self.onion_key = block_contents del entries['onion-key'] elif keyword == 'ntor-onion-key': self.ntor_onion_key = value del entries['ntor-onion-key'] elif keyword == 'signing-key': if validate and not block_contents: raise ValueError('Signing key line must be followed by a public key: %s' % line) self.signing_key = block_contents del entries['signing-key'] elif keyword == 'router-signature': if validate and not block_contents: raise ValueError('Router signature line must be followed by a signature block: %s' % line) self.signature = block_contents del entries['router-signature'] ServerDescriptor._parse(self, entries, validate) def _compare(self, other, method): if not isinstance(other, RelayDescriptor): return False return method(str(self).strip(), str(other).strip()) def __hash__(self): return hash(str(self).strip()) def __eq__(self, other): return self._compare(other, lambda s, o: s == o) def __lt__(self, other): return self._compare(other, lambda s, o: s < o) 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
[docs]class BridgeDescriptor(ServerDescriptor): """ Bridge descriptor (`bridge descriptor specification <https://metrics.torproject.org/formats.html#bridgedesc>`_) """ def __init__(self, raw_contents, validate = True, annotations = None): self._digest = None super(BridgeDescriptor, self).__init__(raw_contents, validate, annotations)
[docs] def digest(self): return self._digest
def _parse(self, entries, validate): entries = dict(entries) # handles fields only in bridge descriptors for keyword, values in entries.items(): value, block_contents = values[0] line = '%s %s' % (keyword, value) if keyword == 'router-digest': if validate and not stem.util.tor_tools.is_hex_digits(value, 40): raise ValueError('Router digest line had an invalid sha1 digest: %s' % line) self._digest = stem.util.str_tools._to_unicode(value) del entries['router-digest'] ServerDescriptor._parse(self, entries, validate)
[docs] def is_scrubbed(self): """ Checks if we've been properly scrubbed in accordance with the `bridge descriptor specification <https://metrics.torproject.org/formats.html#bridgedesc>`_. Validation is a moving target so this may not be fully up to date. :returns: **True** if we're scrubbed, **False** otherwise """ return self.get_scrubbing_issues() == []
@lru_cache()
[docs] def get_scrubbing_issues(self): """ Provides issues with our scrubbing. :returns: **list** of strings which describe issues we have with our scrubbing, this list is empty if we're properly scrubbed """ issues = [] if not self.address.startswith('10.'): issues.append("Router line's address should be scrubbed to be '10.x.x.x': %s" % self.address) if self.contact and self.contact != "somebody": issues.append("Contact line should be scrubbed to be 'somebody', but instead had '%s'" % self.contact) for address, _, is_ipv6 in self.or_addresses: if not is_ipv6 and not address.startswith('10.'): issues.append("or-address line's address should be scrubbed to be '10.x.x.x': %s" % address) elif is_ipv6 and not address.startswith('fd9f:2e19:3bcf::'): # TODO: this check isn't quite right because we aren't checking that # the next grouping of hex digits contains 1-2 digits issues.append("or-address line's address should be scrubbed to be 'fd9f:2e19:3bcf::xx:xxxx': %s" % address) for line in self.get_unrecognized_lines(): if line.startswith('onion-key '): issues.append('Bridge descriptors should have their onion-key scrubbed: %s' % line) elif line.startswith('signing-key '): issues.append('Bridge descriptors should have their signing-key scrubbed: %s' % line) elif line.startswith('router-signature '): issues.append('Bridge descriptors should have their signature scrubbed: %s' % line) return issues
def _required_fields(self): # bridge required fields are the same as a relay descriptor, minus items # excluded according to the format page excluded_fields = [ 'onion-key', 'signing-key', 'router-signature', ] included_fields = [ 'router-digest', ] return tuple(included_fields + [f for f in REQUIRED_FIELDS if not f in excluded_fields]) def _single_fields(self): return self._required_fields() + SINGLE_FIELDS def _last_keyword(self): return None def _compare(self, other, method): if not isinstance(other, BridgeDescriptor): return False return method(str(self).strip(), str(other).strip()) def __hash__(self): return hash(str(self).strip()) def __eq__(self, other): return self._compare(other, lambda s, o: s == o) def __lt__(self, other): return self._compare(other, lambda s, o: s < o) def __le__(self, other): return self._compare(other, lambda s, o: s <= o)