commit 01b81dca033dbcaa75ed13e85acd57864dd5f9fb Author: Damian Johnson atagar@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.
tor-commits@lists.torproject.org