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

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


commit 01b81dca033dbcaa75ed13e85acd57864dd5f9fb
Author: Damian Johnson <atagar at torproject.org>
Date:   Thu Oct 3 16:01:54 2019 -0700

    Parse descriptor outer layer
    
    Quick and dirty parser for the outer layer of hidden service descriptors.
---
 stem/descriptor/hidden_service.py         | 82 +++++++++++++++++++++++++++++--
 stem/descriptor/hsv3_crypto.py            | 31 ++----------
 test/unit/descriptor/hidden_service_v3.py | 20 ++++++++
 3 files changed, 104 insertions(+), 29 deletions(-)

diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py
index e162079b..a20d853a 100644
--- a/stem/descriptor/hidden_service.py
+++ b/stem/descriptor/hidden_service.py
@@ -47,6 +47,7 @@ from stem.descriptor import (
   _read_until_keywords,
   _bytes_for_block,
   _value,
+  _values,
   _parse_simple_line,
   _parse_int_line,
   _parse_timestamp_line,
@@ -103,6 +104,12 @@ STEALTH_AUTH = 2
 CHECKSUM_CONSTANT = b'.onion checksum'
 
 
+class DecryptionFailure(Exception):
+  """
+  Failure to decrypt the hidden service descriptor's introduction-points.
+  """
+
+
 class IntroductionPoints(collections.namedtuple('IntroductionPoints', INTRODUCTION_POINTS_ATTR.keys())):
   """
   :var str identifier: hash of this introduction point's identity key
@@ -115,9 +122,15 @@ class IntroductionPoints(collections.namedtuple('IntroductionPoints', INTRODUCTI
   """
 
 
-class DecryptionFailure(Exception):
+class AuthorizedClient(collections.namedtuple('AuthorizedClient', ['id', 'iv', 'cookie'])):
   """
-  Failure to decrypt the hidden service descriptor's introduction-points.
+  Client authorized to use a v3 hidden service.
+
+  .. versionadded:: 1.8.0
+
+  :var str id: base64 encoded client id
+  :var str iv: base64 encoded randomized initialization vector
+  :var str cookie: base64 encoded authentication cookie
   """
 
 
@@ -191,6 +204,22 @@ def _parse_introduction_points_line(descriptor, entries):
     raise ValueError("'introduction-points' isn't base64 encoded content:\n%s" % block_contents)
 
 
+def _parse_v3_outer_clients(descriptor, entries):
+  # "auth-client" client-id iv encrypted-cookie
+
+  clients = {}
+
+  for value in _values('auth-client', entries):
+    value_comp = value.split()
+
+    if len(value_comp) < 3:
+      raise ValueError('auth-client should have a client-id, iv, and cookie: auth-client %s' % value)
+
+    clients[value_comp[0]] = AuthorizedClient(value_comp[0], value_comp[1], value_comp[2])
+
+  descriptor.clients = clients
+
+
 _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')
@@ -205,6 +234,10 @@ _parse_revision_counter_line = _parse_int_line('revision-counter', 'revision_cou
 _parse_superencrypted_line = _parse_key_block('superencrypted', 'superencrypted', 'MESSAGE')
 _parse_v3_signature_line = _parse_simple_line('signature', 'signature')
 
+_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')
+
 
 class BaseHiddenServiceDescriptor(Descriptor):
   """
@@ -579,7 +612,7 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
     if outer_layer:
       return outter_layer_plaintext
 
-    inner_layer_ciphertext = stem.descriptor.hsv3_crypto.parse_superencrypted_plaintext(outter_layer_plaintext)
+    inner_layer_ciphertext = OuterLayer(outter_layer_plaintext).encrypted
 
     inner_layer_plaintext = stem.descriptor.hsv3_crypto.decrypt_inner_layer(inner_layer_ciphertext, self.revision_counter, identity_public_key, blinded_key, subcredential)
 
@@ -615,6 +648,49 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
     return pubkey
 
 
+class OuterLayer(Descriptor):
+  """
+  Initial encryped layer of a hidden service v3 descriptor (`spec
+  <https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt#n1154>`_).
+
+  .. versionadded:: 1.8.0
+
+  :var str auth_type: **\\*** encryption scheme used for descriptor authorization
+  :var str ephemeral_key: **\\*** base64 encoded x25519 public key
+  :var dict clients: **\\*** mapping of authorized client ids to their
+    :class:`~stem.descriptor.hidden_service.AuthorizedClient`
+  :var str encrypted: **\\*** encrypted descriptor inner layer
+
+  **\\*** attribute is either required when we're parsed with validation or has
+  a default value, others are left as **None** if undefined
+  """
+
+  ATTRIBUTES = {
+    'auth_type': (None, _parse_v3_outer_auth_type),
+    'ephemeral_key': (None, _parse_v3_outer_ephemeral_key),
+    'clients': ({}, _parse_v3_outer_clients),
+    'encrypted': (None, _parse_v3_outer_encrypted),
+  }
+
+  PARSER_FOR_LINE = {
+    'desc-auth-type': _parse_v3_outer_auth_type,
+    'desc-auth-ephemeral-key': _parse_v3_outer_ephemeral_key,
+    'auth-client': _parse_v3_outer_clients,
+    'encrypted': _parse_v3_outer_encrypted,
+  }
+
+  def __init__(self, content, validate = False):
+    content = content.rstrip(b'\x00')  # strip null byte padding
+
+    super(OuterLayer, self).__init__(content, lazy_load = not validate)
+    entries = _descriptor_components(content, validate)
+
+    if validate:
+      self._parse(entries, validate)
+    else:
+      self._entries = entries
+
+
 # TODO: drop this alias in stem 2.x
 
 HiddenServiceDescriptor = HiddenServiceDescriptorV2
diff --git a/stem/descriptor/hsv3_crypto.py b/stem/descriptor/hsv3_crypto.py
index 2f9f2d66..078d71b4 100644
--- a/stem/descriptor/hsv3_crypto.py
+++ b/stem/descriptor/hsv3_crypto.py
@@ -84,6 +84,9 @@ def _decrypt_descriptor_layer(ciphertext_blob_b64, revision_counter, public_iden
   from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
   from cryptography.hazmat.backends import default_backend
 
+  if ciphertext_blob_b64.startswith('-----BEGIN MESSAGE-----\n') and ciphertext_blob_b64.endswith('\n-----END MESSAGE-----'):
+    ciphertext_blob_b64 = ciphertext_blob_b64[24:-22]
+
   # decode the thing
   ciphertext_blob = base64.b64decode(ciphertext_blob_b64)
 
@@ -119,32 +122,8 @@ def _decrypt_descriptor_layer(ciphertext_blob_b64, revision_counter, public_iden
 
 
 def decrypt_outter_layer(superencrypted_blob_b64, revision_counter, public_identity_key, blinded_key, subcredential):
-  secret_data = blinded_key
-  string_constant = b'hsdir-superencrypted-data'
-
-  # XXX Remove the BEGIN MESSSAGE around the thing
-  superencrypted_blob_b64_lines = superencrypted_blob_b64.split('\n')
-  superencrypted_blob_b64 = ''.join(superencrypted_blob_b64_lines[1:-1])
-
-  return _decrypt_descriptor_layer(superencrypted_blob_b64, revision_counter, public_identity_key, subcredential, secret_data, string_constant)
+  return _decrypt_descriptor_layer(superencrypted_blob_b64, revision_counter, public_identity_key, subcredential, blinded_key, b'hsdir-superencrypted-data')
 
 
 def decrypt_inner_layer(encrypted_blob_b64, revision_counter, public_identity_key, blinded_key, subcredential):
-  secret_data = blinded_key
-  string_constant = b'hsdir-encrypted-data'
-
-  return _decrypt_descriptor_layer(encrypted_blob_b64, revision_counter, public_identity_key, subcredential, secret_data, string_constant)
-
-
-def parse_superencrypted_plaintext(outter_layer_plaintext):
-  """Super hacky function to parse the superencrypted plaintext. This will need to be replaced by proper stem code."""
-
-  START_CONSTANT = b'-----BEGIN MESSAGE-----\n'
-  END_CONSTANT = b'\n-----END MESSAGE-----'
-
-  start = outter_layer_plaintext.find(START_CONSTANT)
-  end = outter_layer_plaintext.find(END_CONSTANT)
-
-  start = start + len(START_CONSTANT)
-
-  return outter_layer_plaintext[start:end]
+  return _decrypt_descriptor_layer(encrypted_blob_b64, revision_counter, public_identity_key, subcredential, blinded_key, b'hsdir-encrypted-data')
diff --git a/test/unit/descriptor/hidden_service_v3.py b/test/unit/descriptor/hidden_service_v3.py
index 3824c8a6..3140d193 100644
--- a/test/unit/descriptor/hidden_service_v3.py
+++ b/test/unit/descriptor/hidden_service_v3.py
@@ -11,6 +11,7 @@ import stem.prereq
 from stem.descriptor.hidden_service import (
   REQUIRED_V3_FIELDS,
   HiddenServiceDescriptorV3,
+  OuterLayer,
 )
 
 from test.unit.descriptor import (
@@ -58,6 +59,25 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase):
       with open(get_resource('hidden_service_v3_inner_layer'), 'rb') as outer_layer_file:
         self.assertEqual(outer_layer_file.read(), desc._decrypt(HS_ADDRESS, outer_layer = False))
 
+  def test_outer_layer(self):
+    """
+    Parse the outer layer of our test descriptor.
+    """
+
+    with open(get_resource('hidden_service_v3_outer_layer'), 'rb') as descriptor_file:
+      desc = OuterLayer(descriptor_file.read())
+
+    self.assertEqual('x25519', desc.auth_type)
+    self.assertEqual('WjZCU9sV1oxkxaPcd7/YozeZgq0lEs6DhWyrdYRNJR4=', desc.ephemeral_key)
+    self.assertTrue('BsRYMH/No+LgetIFv' in desc.encrypted)
+
+    client = desc.clients['D0Bz0OlEMCg']
+
+    self.assertEqual(16, len(desc.clients))
+    self.assertEqual('D0Bz0OlEMCg', client.id)
+    self.assertEqual('or3nS3ScSPYfLJuP9osGiQ', client.iv)
+    self.assertEqual('B40RdIWhw7kdA7lt3KJPvQ', client.cookie)
+
   def test_required_fields(self):
     """
     Check that we require the mandatory fields.





More information about the tor-commits mailing list