[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