[tor-commits] [stem/master] Parse link specifiers

atagar at torproject.org atagar at torproject.org
Sun Oct 6 02:07:35 UTC 2019


commit ceb691b7d33f942c487934d7c81e5d4d5bbd4b0d
Author: Damian Johnson <atagar at torproject.org>
Date:   Fri Oct 4 13:54:34 2019 -0700

    Parse link specifiers
    
    Hidden service v3 introductory points begin with link specifiers, which are
    used by EXTEND cells as well. We'll need this when expanding client
    functionality, so might as well bite the bullet and do proper parsing
    of these fields.
---
 stem/client/datatype.py            | 136 ++++++++++++++++++++++++++++++++++++-
 stem/util/connection.py            |   3 +
 test/settings.cfg                  |   1 +
 test/unit/client/link_specifier.py |  59 ++++++++++++++++
 4 files changed, 197 insertions(+), 2 deletions(-)

diff --git a/stem/client/datatype.py b/stem/client/datatype.py
index fa393d30..1f49388b 100644
--- a/stem/client/datatype.py
+++ b/stem/client/datatype.py
@@ -25,6 +25,12 @@ users.** See our :class:`~stem.client.Relay` the API you probably want.
     |- unpack - decodes content
     +- pop - decodes content with remainder
 
+  LinkSpecifier - Communication method relays in a circuit.
+    |- LinkByIPv4 - TLS connection to an IPv4 address.
+    |- LinkByIPv6 - TLS connection to an IPv6 address.
+    |- LinkByFingerprint - SHA1 identity fingerprint.
+    +- LinkByEd25519 - Ed25519 identity fingerprint.
+
   KDF - KDF-TOR derivatived attributes
     +- from_value - parses key material
 
@@ -112,6 +118,7 @@ users.** See our :class:`~stem.client.Relay` the API you probably want.
   ===================== ===========
 """
 
+import binascii
 import collections
 import hashlib
 import struct
@@ -449,7 +456,7 @@ class Address(Field):
         if len(value) != 4:
           raise ValueError('Packed IPv4 addresses should be four bytes, but was: %s' % repr(value))
 
-        self.value = '.'.join([str(Size.CHAR.unpack(value[i:i + 1])) for i in range(4)])
+        self.value = _unpack_ipv4_address(value)
         self.value_bin = value
     elif self.type == AddrType.IPv6:
       if stem.util.connection.is_valid_ipv6_address(value):
@@ -459,7 +466,7 @@ class Address(Field):
         if len(value) != 16:
           raise ValueError('Packed IPv6 addresses should be sixteen bytes, but was: %s' % repr(value))
 
-        self.value = ':'.join(['%04x' % Size.SHORT.unpack(value[i * 2:(i + 1) * 2]) for i in range(8)])
+        self.value = _unpack_ipv6_address(value)
         self.value_bin = value
     else:
       # The spec doesn't really tell us what form to expect errors to be. For
@@ -527,6 +534,119 @@ class Certificate(Field):
     return stem.util._hash_attr(self, 'type_int', 'value')
 
 
+class LinkSpecifier(object):
+  """
+  Method of communicating with a circuit's relay. Recognized link specification
+  types are an instantiation of a subclass. For more information see the
+  `EXTEND cell specification
+  <https://gitweb.torproject.org/torspec.git/tree/tor-spec.txt#n975>`_.
+
+  :var int type: numeric identifier of our type
+  :var bytes value: encoded link specification destination
+  """
+
+  def __init__(self, link_type, value):
+    self.type = link_type
+    self.value = value
+
+  @staticmethod
+  def pop(content):
+    # LSTYPE (Link specifier type)           [1 byte]
+    # LSLEN  (Link specifier length)         [1 byte]
+    # LSPEC  (Link specifier)                [LSLEN bytes]
+
+    link_type, content = Size.CHAR.pop(content)
+    value_size, content = Size.CHAR.pop(content)
+
+    if value_size > len(content):
+      raise ValueError('Link specifier should have %i bytes, but only had %i remaining' % (value_size, len(content)))
+
+    value, content = split(content, value_size)
+
+    if link_type == 0:
+      return LinkByIPv4(value), content
+    elif link_type == 1:
+      return LinkByIPv6(value), content
+    elif link_type == 2:
+      return LinkByFingerprint(value), content
+    elif link_type == 3:
+      return LinkByEd25519(value), content
+    else:
+      return LinkSpecifier(link_type, value), content  # unrecognized type
+
+
+class LinkByIPv4(LinkSpecifier):
+  """
+  TLS connection to an IPv4 address.
+
+  :var str address: relay IPv4 address
+  :var int port: relay ORPort
+  """
+
+  def __init__(self, value):
+    super(LinkByIPv4, self).__init__(0, value)
+
+    if len(value) != 6:
+      raise ValueError('IPv4 link specifiers should be six bytes, but was %i instead: %s' % (len(value), binascii.hexlify(value)))
+
+    address_bin, value = split(value, 4)
+    self.address = _unpack_ipv4_address(address_bin)
+
+    self.port, _ = Size.SHORT.pop(value)
+
+
+class LinkByIPv6(LinkSpecifier):
+  """
+  TLS connection to an IPv6 address.
+
+  :var str address: relay IPv6 address
+  :var int port: relay ORPort
+  """
+
+  def __init__(self, value):
+    super(LinkByIPv6, self).__init__(1, value)
+
+    if len(value) != 18:
+      raise ValueError('IPv6 link specifiers should be eighteen bytes, but was %i instead: %s' % (len(value), binascii.hexlify(value)))
+
+    address_bin, value = split(value, 16)
+    self.address = _unpack_ipv6_address(address_bin)
+
+    self.port, _ = Size.SHORT.pop(value)
+
+
+class LinkByFingerprint(LinkSpecifier):
+  """
+  Connection to a SHA1 identity fingerprint.
+
+  :var str fingerprint: relay sha1 fingerprint
+  """
+
+  def __init__(self, value):
+    super(LinkByFingerprint, self).__init__(2, value)
+
+    if len(value) != 20:
+      raise ValueError('Fingerprint link specifiers should be twenty bytes, but was %i instead: %s' % (len(value), binascii.hexlify(value)))
+
+    self.fingerprint = value
+
+
+class LinkByEd25519(LinkSpecifier):
+  """
+  Connection to a Ed25519 identity fingerprint.
+
+  :var str fingerprint: relay ed25519 fingerprint
+  """
+
+  def __init__(self, value):
+    super(LinkByEd25519, self).__init__(3, value)
+
+    if len(value) != 32:
+      raise ValueError('Fingerprint link specifiers should be thirty two bytes, but was %i instead: %s' % (len(value), binascii.hexlify(value)))
+
+    self.fingerprint = value
+
+
 class KDF(collections.namedtuple('KDF', ['key_hash', 'forward_digest', 'backward_digest', 'forward_key', 'backward_key'])):
   """
   Computed KDF-TOR derived values for TAP, CREATE_FAST handshakes, and hidden
@@ -561,6 +681,18 @@ class KDF(collections.namedtuple('KDF', ['key_hash', 'forward_digest', 'backward
     return KDF(key_hash, forward_digest, backward_digest, forward_key, backward_key)
 
 
+def _unpack_ipv4_address(value):
+  # convert bytes to a standard IPv4 address
+
+  return '.'.join([str(Size.CHAR.unpack(value[i:i + 1])) for i in range(4)])
+
+
+def _unpack_ipv6_address(value):
+  # convert bytes to a standard IPv6 address
+
+  return ':'.join(['%04x' % Size.SHORT.unpack(value[i * 2:(i + 1) * 2]) for i in range(8)])
+
+
 setattr(Size, 'CHAR', Size('CHAR', 1, '!B'))
 setattr(Size, 'SHORT', Size('SHORT', 2, '!H'))
 setattr(Size, 'LONG', Size('LONG', 4, '!L'))
diff --git a/stem/util/connection.py b/stem/util/connection.py
index 421eb9a3..5bfb1024 100644
--- a/stem/util/connection.py
+++ b/stem/util/connection.py
@@ -769,6 +769,9 @@ def _get_binary(value, bits):
   return ''.join([str((value >> y) & 1) for y in range(bits - 1, -1, -1)])
 
 
+# TODO: In stem 2.x we should consider unifying this with
+# stem.client.datatype's _unpack_ipv4_address() and _unpack_ipv6_address().
+
 def _address_to_binary(address):
   """
   Provides the binary value for an IPv4 or IPv6 address.
diff --git a/test/settings.cfg b/test/settings.cfg
index eca719df..8fe79d2a 100644
--- a/test/settings.cfg
+++ b/test/settings.cfg
@@ -287,6 +287,7 @@ test.unit_tests
 |test.unit.client.address.TestAddress
 |test.unit.client.link_protocol.TestLinkProtocol
 |test.unit.client.certificate.TestCertificate
+|test.unit.client.link_specifier.TestLinkSpecifier
 |test.unit.client.kdf.TestKDF
 |test.unit.client.cell.TestCell
 |test.unit.connection.authentication.TestAuthenticate
diff --git a/test/unit/client/link_specifier.py b/test/unit/client/link_specifier.py
new file mode 100644
index 00000000..470ee276
--- /dev/null
+++ b/test/unit/client/link_specifier.py
@@ -0,0 +1,59 @@
+"""
+Unit tests for stem.client.datatype.LinkSpecifier and subclasses.
+"""
+
+import unittest
+
+from stem.client.datatype import (
+  LinkSpecifier,
+  LinkByIPv4,
+  LinkByIPv6,
+  LinkByFingerprint,
+  LinkByEd25519,
+)
+
+
+class TestLinkSpecifier(unittest.TestCase):
+  def test_link_by_ipv4_address(self):
+    destination, _ = LinkSpecifier.pop(b'\x00\x06\x01\x02\x03\x04#)')
+
+    self.assertEqual(LinkByIPv4, type(destination))
+    self.assertEqual(0, destination.type)
+    self.assertEqual(b'\x01\x02\x03\x04#)', destination.value)
+    self.assertEqual('1.2.3.4', destination.address)
+    self.assertEqual(9001, destination.port)
+
+  def test_link_by_ipv6_address(self):
+    destination, _ = LinkSpecifier.pop(b'\x01\x12&\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01#)')
+
+    self.assertEqual(LinkByIPv6, type(destination))
+    self.assertEqual(1, destination.type)
+    self.assertEqual(b'&\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01#)', destination.value)
+    self.assertEqual('2600:0000:0000:0000:0000:0000:0000:0001', destination.address)
+    self.assertEqual(9001, destination.port)
+
+  def test_link_by_fingerprint(self):
+    destination, _ = LinkSpecifier.pop(b'\x02\x14CCCCCCCCCCCCCCCCCCCC')
+
+    self.assertEqual(LinkByFingerprint, type(destination))
+    self.assertEqual(2, destination.type)
+    self.assertEqual(b'CCCCCCCCCCCCCCCCCCCC', destination.value)
+    self.assertEqual('CCCCCCCCCCCCCCCCCCCC', destination.fingerprint)
+
+  def test_link_by_ed25519fingerprint(self):
+    destination, _ = LinkSpecifier.pop(b'\x03\x20CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC')
+
+    self.assertEqual(LinkByEd25519, type(destination))
+    self.assertEqual(3, destination.type)
+    self.assertEqual(b'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC', destination.value)
+    self.assertEqual('CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC', destination.fingerprint)
+
+  def test_unrecognized_type(self):
+    destination, _ = LinkSpecifier.pop(b'\x04\x20CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC')
+
+    self.assertEqual(LinkSpecifier, type(destination))
+    self.assertEqual(4, destination.type)
+    self.assertEqual(b'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC', destination.value)
+
+  def test_wrong_size(self):
+    self.assertRaisesWith(ValueError, 'Link specifier should have 32 bytes, but only had 7 remaining', LinkSpecifier.pop, b'\x04\x20CCCCCCC')





More information about the tor-commits mailing list