[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[tor-commits] [stem/master] Parse descriptor inner layer



commit 56290e08c48e9dc57d807ba2f4ced9096eed0054
Author: Damian Johnson <atagar@xxxxxxxxxxxxxx>
Date:   Fri Oct 4 16:42:08 2019 -0700

    Parse descriptor inner layer
    
    The introduction points made the inner layer a bit tricker than the outer
    (duplicate descriptor lines are highly unusual). Think this'll do the trick,
    though test data of a 'legacy-key' and 'legacy-key-cert' would be nice.
---
 stem/descriptor/hidden_service.py         | 159 ++++++++++++++++++++++++++++++
 test/unit/descriptor/hidden_service_v3.py |  35 +++++++
 2 files changed, 194 insertions(+)

diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py
index 314e623a..6911fa3c 100644
--- a/stem/descriptor/hidden_service.py
+++ b/stem/descriptor/hidden_service.py
@@ -31,6 +31,7 @@ import collections
 import hashlib
 import io
 
+import stem.client.datatype
 import stem.descriptor.hsv3_crypto
 import stem.prereq
 import stem.util.connection
@@ -49,6 +50,7 @@ from stem.descriptor import (
   _value,
   _values,
   _parse_simple_line,
+  _parse_if_present,
   _parse_int_line,
   _parse_timestamp_line,
   _parse_key_block,
@@ -110,8 +112,12 @@ class DecryptionFailure(Exception):
   """
 
 
+# TODO: rename in stem 2.x (add 'V2' and drop plural)
+
 class IntroductionPoints(collections.namedtuple('IntroductionPoints', INTRODUCTION_POINTS_ATTR.keys())):
   """
+  Introduction point for a v2 hidden service.
+
   :var str identifier: hash of this introduction point's identity key
   :var str address: address of this introduction point
   :var int port: port where this introduction point is listening
@@ -122,6 +128,20 @@ class IntroductionPoints(collections.namedtuple('IntroductionPoints', INTRODUCTI
   """
 
 
+class IntroductionPointV3(collections.namedtuple('IntroductionPointV3', ['link_specifiers', 'onion_key', 'auth_key', 'enc_key', 'enc_key_cert', 'legacy_key', 'legacy_key_cert'])):
+  """
+  Introduction point for a v3 hidden service.
+
+  :var list link_specifiers: :class:`~stem.client.datatype.LinkSpecifier` where this service is reachable
+  :var str onion_key: ntor introduction point public key
+  :var str auth_key: cross-certifier of the signing key
+  :var str enc_key: introduction request encryption key
+  :var str enc_key_cert: cross-certifier of the signing key by the encryption key
+  :var str legacy_key: legacy introduction point RSA public key
+  :var str legacy_key_cert: cross-certifier of the signing key by the legacy key
+  """
+
+
 class AuthorizedClient(collections.namedtuple('AuthorizedClient', ['id', 'iv', 'cookie'])):
   """
   Client authorized to use a v3 hidden service.
@@ -220,6 +240,90 @@ def _parse_v3_outer_clients(descriptor, entries):
   descriptor.clients = clients
 
 
+def _parse_v3_inner_formats(descriptor, entries):
+  value, formats = _value('create2-formats', entries), []
+
+  for entry in value.split(' '):
+    if not entry.isdigit():
+      raise ValueError("create2-formats should only contain integers, but was '%s'" % value)
+
+    formats.append(int(entry))
+
+  descriptor.formats = formats
+
+
+def _parse_v3_introduction_points(descriptor, entries):
+  if hasattr(descriptor, '_unparsed_introduction_points'):
+    introduction_points = []
+    remaining = descriptor._unparsed_introduction_points
+
+    while remaining:
+      div = remaining.find('\nintroduction-point ', 10)
+
+      if div == -1:
+        intro_point_str = remaining
+        remaining = ''
+      else:
+        intro_point_str = remaining[:div]
+        remaining = remaining[div + 1:]
+
+      entry = _descriptor_components(intro_point_str, False)
+      link_specifiers = _parse_link_specifiers(_value('introduction-point', entry))
+
+      onion_key_line = _value('onion-key', entry)
+      onion_key = onion_key_line[5:] if onion_key_line.startswith('ntor ') else None
+
+      _, block_type, auth_key = entry['auth-key'][0]
+
+      if block_type != 'ED25519 CERT':
+        raise ValueError('Expected auth-key to have an ed25519 certificate, but was %s' % block_type)
+
+      enc_key_line = _value('enc-key', entry)
+      enc_key = enc_key_line[5:] if enc_key_line.startswith('ntor ') else None
+
+      _, block_type, enc_key_cert = entry['enc-key-cert'][0]
+
+      if block_type != 'ED25519 CERT':
+        raise ValueError('Expected enc-key-cert to have an ed25519 certificate, but was %s' % block_type)
+
+      legacy_key = entry['legacy-key'][0][2] if 'legacy-key' in entry else None
+      legacy_key_cert = entry['legacy-key-cert'][0][2] if 'legacy-key-cert' in entry else None
+
+      introduction_points.append(
+        IntroductionPointV3(
+          link_specifiers,
+          onion_key,
+          auth_key,
+          enc_key,
+          enc_key_cert,
+          legacy_key,
+          legacy_key_cert,
+        )
+      )
+
+    descriptor.introduction_points = introduction_points
+    del descriptor._unparsed_introduction_points
+
+
+def _parse_link_specifiers(val):
+  try:
+    val = base64.b64decode(val)
+  except Exception as exc:
+    raise ValueError('Unable to base64 decode introduction point (%s): %s' % (exc, val))
+
+  link_specifiers = []
+  count, val = stem.client.datatype.Size.CHAR.pop(val)
+
+  for i in range(count):
+    link_specifier, val = stem.client.datatype.LinkSpecifier.pop(val)
+    link_specifiers.append(link_specifier)
+
+  if val:
+    raise ValueError('Introduction point had excessive data (%s)' % val)
+
+  return link_specifiers
+
+
 _parse_v2_version_line = _parse_int_line('version', 'version', allow_negative = False)
 _parse_rendezvous_service_descriptor_line = _parse_simple_line('rendezvous-service-descriptor', 'descriptor_id')
 _parse_permanent_key_line = _parse_key_block('permanent-key', 'permanent_key', 'RSA PUBLIC KEY')
@@ -238,6 +342,9 @@ _parse_v3_outer_auth_type = _parse_simple_line('desc-auth-type', 'auth_type')
 _parse_v3_outer_ephemeral_key = _parse_simple_line('desc-auth-ephemeral-key', 'ephemeral_key')
 _parse_v3_outer_encrypted = _parse_key_block('encrypted', 'encrypted', 'MESSAGE')
 
+_parse_v3_inner_intro_auth = _parse_simple_line('intro-auth-required', 'intro_auth', func = lambda v: v.split(' '))
+_parse_v3_inner_single_service = _parse_if_present('single-onion-service', 'is_single_service')
+
 
 class BaseHiddenServiceDescriptor(Descriptor):
   """
@@ -691,6 +798,58 @@ class OuterLayer(Descriptor):
       self._entries = entries
 
 
+class InnerLayer(Descriptor):
+  """
+  Second encryped layer of a hidden service v3 descriptor (`spec
+  <https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt#n1308>`_).
+
+  .. versionadded:: 1.8.0
+
+  :var list formats: **\\*** recognized CREATE2 cell formats
+  :var list intro_auth: **\\*** introduction-layer authentication types
+  :var bool is_single_service: **\\*** **True** if this is a `single onion service <https://gitweb.torproject.org/torspec.git/tree/proposals/260-rend-single-onion.txt>`_, **False** otherwise
+  :var list introduction_points: :class:`~stem.descriptor.hidden_service.IntroductionPointV3` where this service is reachable
+
+  **\\*** attribute is either required when we're parsed with validation or has
+  a default value, others are left as **None** if undefined
+  """
+
+  ATTRIBUTES = {
+    'formats': ([], _parse_v3_inner_formats),
+    'intro_auth': ([], _parse_v3_inner_intro_auth),
+    'is_single_service': (False, _parse_v3_inner_single_service),
+    'introduction_points': ([], _parse_v3_introduction_points),
+  }
+
+  PARSER_FOR_LINE = {
+    'create2-formats': _parse_v3_inner_formats,
+    'intro-auth-required': _parse_v3_inner_intro_auth,
+    'single-onion-service': _parse_v3_inner_single_service,
+  }
+
+  def __init__(self, content, validate = False):
+    super(InnerLayer, self).__init__(content, lazy_load = not validate)
+
+    # inner layer begins with a few header fields, followed by multiple any
+    # number of introduction-points
+
+    div = content.find('\nintroduction-point ')
+
+    if div != -1:
+      self._unparsed_introduction_points = content[div + 1:]
+      content = content[:div]
+    else:
+      self._unparsed_introduction_points = None
+
+    entries = _descriptor_components(content, validate)
+
+    if validate:
+      self._parse(entries, validate)
+      _parse_v3_introduction_points(self, entries)
+    else:
+      self._entries = entries
+
+
 # TODO: drop this alias in stem 2.x
 
 HiddenServiceDescriptor = HiddenServiceDescriptorV2
diff --git a/test/unit/descriptor/hidden_service_v3.py b/test/unit/descriptor/hidden_service_v3.py
index 3140d193..36b85f7c 100644
--- a/test/unit/descriptor/hidden_service_v3.py
+++ b/test/unit/descriptor/hidden_service_v3.py
@@ -5,6 +5,7 @@ Unit tests for stem.descriptor.hidden_service for version 3.
 import functools
 import unittest
 
+import stem.client.datatype
 import stem.descriptor
 import stem.prereq
 
@@ -12,6 +13,7 @@ from stem.descriptor.hidden_service import (
   REQUIRED_V3_FIELDS,
   HiddenServiceDescriptorV3,
   OuterLayer,
+  InnerLayer,
 )
 
 from test.unit.descriptor import (
@@ -78,6 +80,39 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase):
     self.assertEqual('or3nS3ScSPYfLJuP9osGiQ', client.iv)
     self.assertEqual('B40RdIWhw7kdA7lt3KJPvQ', client.cookie)
 
+  def test_inner_layer(self):
+    """
+    Parse the inner layer of our test descriptor.
+    """
+
+    with open(get_resource('hidden_service_v3_inner_layer'), 'rb') as descriptor_file:
+      desc = InnerLayer(descriptor_file.read())
+
+    self.assertEqual([2], desc.formats)
+    self.assertEqual(['ed25519'], desc.intro_auth)
+    self.assertEqual(True, desc.is_single_service)
+    self.assertEqual(4, len(desc.introduction_points))
+
+    intro_point = desc.introduction_points[0]
+
+    self.assertEqual(2, len(intro_point.link_specifiers))
+
+    link_specifier = intro_point.link_specifiers[0]
+    self.assertEqual(stem.client.datatype.LinkByFingerprint, type(link_specifier))
+    self.assertEqual('CCCCCCCCCCCCCCCCCCCC', link_specifier.fingerprint)
+
+    link_specifier = intro_point.link_specifiers[1]
+    self.assertEqual(stem.client.datatype.LinkByIPv4, type(link_specifier))
+    self.assertEqual('1.2.3.4', link_specifier.address)
+    self.assertEqual(9001, link_specifier.port)
+
+    self.assertEqual('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', intro_point.onion_key)
+    self.assertTrue('ID2l9EFNrp' in intro_point.auth_key)
+    self.assertEqual('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', intro_point.enc_key)
+    self.assertTrue('ZvjPt5IfeQ', intro_point.enc_key_cert)
+    self.assertEqual(None, intro_point.legacy_key)
+    self.assertEqual(None, intro_point.legacy_key_cert)
+
   def test_required_fields(self):
     """
     Check that we require the mandatory fields.



_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits