commit 80def07ebfdbb2fe00b85a6721fd3cbb2b4220b2 Author: Damian Johnson atagar@torproject.org Date: Sat Oct 5 18:41:45 2019 -0700
Merge hsv3_crypto into hidden_service.py --- stem/client/datatype.py | 4 +- stem/descriptor/hidden_service.py | 88 +++++++++++++++----- stem/descriptor/hsv3_crypto.py | 128 ------------------------------ test/unit/descriptor/hidden_service_v3.py | 8 +- 4 files changed, 76 insertions(+), 152 deletions(-)
diff --git a/stem/client/datatype.py b/stem/client/datatype.py index 1f49388b..7e33e353 100644 --- a/stem/client/datatype.py +++ b/stem/client/datatype.py @@ -628,7 +628,7 @@ class LinkByFingerprint(LinkSpecifier): 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 + self.fingerprint = stem.util.str_tools._to_unicode(value)
class LinkByEd25519(LinkSpecifier): @@ -644,7 +644,7 @@ class LinkByEd25519(LinkSpecifier): 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 + self.fingerprint = stem.util.str_tools._to_unicode(value)
class KDF(collections.namedtuple('KDF', ['key_hash', 'forward_digest', 'backward_digest', 'forward_key', 'backward_key'])): diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py index e4ee13e3..601348d9 100644 --- a/stem/descriptor/hidden_service.py +++ b/stem/descriptor/hidden_service.py @@ -30,9 +30,9 @@ import binascii import collections import hashlib import io +import struct
import stem.client.datatype -import stem.descriptor.hsv3_crypto import stem.prereq import stem.util.connection import stem.util.str_tools @@ -105,6 +105,12 @@ BASIC_AUTH = 1 STEALTH_AUTH = 2 CHECKSUM_CONSTANT = b'.onion checksum'
+SALT_LEN = 16 +MAC_LEN = 32 + +S_KEY_LEN = 32 +S_IV_LEN = 16 +
class DecryptionFailure(Exception): """ @@ -132,6 +138,8 @@ class IntroductionPointV3(collections.namedtuple('IntroductionPointV3', ['link_s """ Introduction point for a v3 hidden service.
+ .. versionadded:: 1.8.0 + :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 @@ -194,6 +202,46 @@ def _parse_file(descriptor_file, desc_type = None, validate = False, **kwargs): break # done parsing file
+def _decrypt_layer(encrypted_block, constant, revision_counter, subcredential, blinded_key): + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + from cryptography.hazmat.backends import default_backend + + def pack(val): + return struct.pack('>Q', val) + + if encrypted_block.startswith('-----BEGIN MESSAGE-----\n') and encrypted_block.endswith('\n-----END MESSAGE-----'): + encrypted_block = encrypted_block[24:-22] + + try: + encrypted = base64.b64decode(encrypted_block) + except: + raise ValueError('Unable to decode encrypted block as base64') + + if len(encrypted) < SALT_LEN + MAC_LEN: + raise ValueError('Encrypted block malformed (only %i bytes)' % len(encrypted)) + + salt = encrypted[:SALT_LEN] + ciphertext = encrypted[SALT_LEN:-MAC_LEN] + expected_mac = encrypted[-MAC_LEN:] + + kdf = hashlib.shake_256(blinded_key + subcredential + pack(revision_counter) + salt + constant) + keys = kdf.digest(S_KEY_LEN + S_IV_LEN + MAC_LEN) + + secret_key = keys[:S_KEY_LEN] + secret_iv = keys[S_KEY_LEN:S_KEY_LEN + S_IV_LEN] + mac_key = keys[S_KEY_LEN + S_IV_LEN:] + + mac = hashlib.sha3_256(pack(len(mac_key)) + mac_key + pack(len(salt)) + salt + ciphertext).digest() + + if mac != expected_mac: + raise ValueError('Malformed mac (expected %s, but was %s)' % (expected_mac, mac)) + + cipher = Cipher(algorithms.AES(secret_key), modes.CTR(secret_iv), default_backend()) + decryptor = cipher.decryptor() + + return stem.util.str_tools._to_unicode(decryptor.update(ciphertext) + decryptor.finalize()) + + def _parse_protocol_versions_line(descriptor, entries): value = _value('protocol-versions', entries)
@@ -693,14 +741,13 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor): else: self._entries = entries
- def decrypt(self, onion_address, validate = False): + def decrypt(self, onion_address): """ Decrypt this descriptor. Hidden serice descriptors contain two encryption layers (:class:`~stem.descriptor.hidden_service.OuterLayer` and :class:`~stem.descriptor.hidden_service.InnerLayer`).
:param str onion_address: hidden service address this descriptor is from - :param bool validate: perform validation checks on decrypted content
:returns: :class:`~stem.descriptor.hidden_service.InnerLayer` with our decrypted content @@ -721,19 +768,15 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor): if not blinded_key: raise ValueError('No signing key is present')
- identity_public_key = HiddenServiceDescriptorV3._public_key_from_address(onion_address) - # credential = H('credential' | public-identity-key) # subcredential = H('subcredential' | credential | blinded-public-key)
+ identity_public_key = HiddenServiceDescriptorV3._public_key_from_address(onion_address) credential = hashlib.sha3_256(b'credential%s' % (identity_public_key)).digest() subcredential = hashlib.sha3_256(b'subcredential%s%s' % (credential, blinded_key)).digest()
- outer_layer = OuterLayer(stem.descriptor.hsv3_crypto.decrypt_outter_layer(self.superencrypted, self.revision_counter, blinded_key, subcredential), validate) - - inner_layer_plaintext = stem.descriptor.hsv3_crypto.decrypt_inner_layer(outer_layer.encrypted, self.revision_counter, blinded_key, subcredential) - - self._inner_layer = InnerLayer(inner_layer_plaintext, validate, outer_layer) + outer_layer = OuterLayer._decrypt(self.superencrypted, self.revision_counter, subcredential, blinded_key) + self._inner_layer = InnerLayer._decrypt(outer_layer, self.revision_counter, subcredential, blinded_key)
return self._inner_layer
@@ -753,16 +796,16 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor): decoded_address = base64.b32decode(onion_address.upper())
pubkey = decoded_address[:32] - checksum = decoded_address[32:34] + expected_checksum = decoded_address[32:34] version = decoded_address[34:35]
- # validate our address checksum + checksum = hashlib.sha3_256(CHECKSUM_CONSTANT + pubkey + version).digest()[:2]
- my_checksum_body = b'%s%s%s' % (CHECKSUM_CONSTANT, pubkey, version) - my_checksum = hashlib.sha3_256(my_checksum_body).digest()[:2] + if expected_checksum != checksum: + checksum_str = stem.util.str_tools._to_unicode(binascii.hexlify(checksum)) + expected_checksum_str = stem.util.str_tools._to_unicode(binascii.hexlify(expected_checksum))
- if (checksum != my_checksum): - raise ValueError('Bad checksum (expected %s but was %s)' % (binascii.hexlify(checksum), binascii.hexlify(my_checksum))) + raise ValueError('Bad checksum (expected %s but was %s)' % (expected_checksum_str, checksum_str))
return pubkey
@@ -798,8 +841,13 @@ class OuterLayer(Descriptor): 'encrypted': _parse_v3_outer_encrypted, }
+ @staticmethod + def _decrypt(encrypted, revision_counter, subcredential, blinded_key): + plaintext = _decrypt_layer(encrypted, b'hsdir-superencrypted-data', revision_counter, subcredential, blinded_key) + return OuterLayer(plaintext) + def __init__(self, content, validate = False): - content = content.rstrip(b'\x00') # strip null byte padding + content = content.rstrip('\x00') # strip null byte padding
super(OuterLayer, self).__init__(content, lazy_load = not validate) entries = _descriptor_components(content, validate) @@ -841,9 +889,13 @@ class InnerLayer(Descriptor): 'single-onion-service': _parse_v3_inner_single_service, }
+ @staticmethod + def _decrypt(outer_layer, revision_counter, subcredential, blinded_key): + plaintext = _decrypt_layer(outer_layer.encrypted, b'hsdir-encrypted-data', revision_counter, subcredential, blinded_key) + return InnerLayer(plaintext, outer_layer = outer_layer) + def __init__(self, content, validate = False, outer_layer = None): super(InnerLayer, self).__init__(content, lazy_load = not validate) - self.outer = outer_layer
# inner layer begins with a few header fields, followed by multiple any diff --git a/stem/descriptor/hsv3_crypto.py b/stem/descriptor/hsv3_crypto.py deleted file mode 100644 index 9acb5242..00000000 --- a/stem/descriptor/hsv3_crypto.py +++ /dev/null @@ -1,128 +0,0 @@ -import base64 -import hashlib -import struct - -""" -Onion addresses - - onion_address = base32(PUBKEY | CHECKSUM | VERSION) + '.onion' - CHECKSUM = H('.onion checksum' | PUBKEY | VERSION)[:2] - - - PUBKEY is the 32 bytes ed25519 master pubkey of the hidden service. - - VERSION is an one byte version field (default value '\x03') - - '.onion checksum' is a constant string - - CHECKSUM is truncated to two bytes before inserting it in onion_address -""" - - -""" -Blinded key stuff - - Now wrt SRVs, if a client is in the time segment between a new time period - and a new SRV (i.e. the segments drawn with '-') it uses the current SRV, - else if the client is in a time segment between a new SRV and a new time - period (i.e. the segments drawn with '='), it uses the previous SRV. -""" - -pass - -""" -Basic descriptor logic: - - SALT = 16 bytes from H(random), changes each time we rebuld the - descriptor even if the content of the descriptor hasn't changed. - (So that we don't leak whether the intro point list etc. changed) - - secret_input = SECRET_DATA | subcredential | INT_8(revision_counter) - - keys = KDF(secret_input | salt | STRING_CONSTANT, S_KEY_LEN + S_IV_LEN + MAC_KEY_LEN) - - SECRET_KEY = first S_KEY_LEN bytes of keys - SECRET_IV = next S_IV_LEN bytes of keys - MAC_KEY = last MAC_KEY_LEN bytes of keys - - -Layer data: - - 2.5.1.1. First layer encryption logic - SECRET_DATA = blinded-public-key - STRING_CONSTANT = 'hsdir-superencrypted-data' - - 2.5.2.1. Second layer encryption keys - SECRET_DATA = blinded-public-key | descriptor_cookie - STRING_CONSTANT = 'hsdir-encrypted-data' -""" - -SALT_LEN = 16 -MAC_LEN = 32 - -S_KEY_LEN = 32 -S_IV_LEN = 16 -MAC_KEY_LEN = 32 - - -def _ciphertext_mac_is_valid(key, salt, ciphertext, mac): - """ - Instantiate MAC(key=k, message=m) with H(k_len | k | m), where k_len is - htonll(len(k)). - - XXX spec: H(mac_key_len | mac_key | salt_len | salt | encrypted) - """ - - # Construct our own MAC first - key_len = struct.pack('>Q', len(key)) - salt_len = struct.pack('>Q', len(salt)) - - my_mac_body = b'%s%s%s%s%s' % (key_len, key, salt_len, salt, ciphertext) - my_mac = hashlib.sha3_256(my_mac_body).digest() - - # Compare the two MACs - return my_mac == mac - - -def _decrypt_descriptor_layer(ciphertext_blob_b64, revision_counter, subcredential, secret_data, string_constant): - 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) - - if (len(ciphertext_blob) < SALT_LEN + MAC_LEN): - raise ValueError('bad encrypted blob') - - salt = ciphertext_blob[:16] - ciphertext = ciphertext_blob[16:-32] - mac = ciphertext_blob[-32:] - - # INT_8(revision_counter) - rev_counter_int_8 = struct.pack('>Q', revision_counter) - secret_input = b'%s%s%s' % (secret_data, subcredential, rev_counter_int_8) - - kdf = hashlib.shake_256(b'%s%s%s' % (secret_input, salt, string_constant)) - keys = kdf.digest(S_KEY_LEN + S_IV_LEN + MAC_KEY_LEN) - - secret_key = keys[:S_KEY_LEN] - secret_iv = keys[S_KEY_LEN:S_KEY_LEN + S_IV_LEN] - mac_key = keys[S_KEY_LEN + S_IV_LEN:] - - # Now time to decrypt descriptor - cipher = Cipher(algorithms.AES(secret_key), modes.CTR(secret_iv), default_backend()) - decryptor = cipher.decryptor() - decrypted = decryptor.update(ciphertext) + decryptor.finalize() - - # validate mac (the mac validates the two fields before the mac) - if not _ciphertext_mac_is_valid(mac_key, salt, ciphertext, mac): - raise ValueError('Bad MAC!!!') - - return decrypted - - -def decrypt_outter_layer(superencrypted_blob_b64, revision_counter, blinded_key, subcredential): - return _decrypt_descriptor_layer(superencrypted_blob_b64, revision_counter, subcredential, blinded_key, b'hsdir-superencrypted-data') - - -def decrypt_inner_layer(encrypted_blob_b64, revision_counter, blinded_key, subcredential): - return _decrypt_descriptor_layer(encrypted_blob_b64, revision_counter, 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 093d5cb6..37781b5f 100644 --- a/test/unit/descriptor/hidden_service_v3.py +++ b/test/unit/descriptor/hidden_service_v3.py @@ -35,13 +35,13 @@ BDwQZ8rhp05oCqhhY3oFHqG9KS7HGzv9g2v1/PrVJMbkfpwu1YK4b3zIZAk= -----END ED25519 CERT-----\ """
-with open(get_resource('hidden_service_v3'), 'rb') as descriptor_file: +with open(get_resource('hidden_service_v3')) as descriptor_file: HS_DESC_STR = descriptor_file.read()
-with open(get_resource('hidden_service_v3_outer_layer'), 'rb') as outer_layer_file: +with open(get_resource('hidden_service_v3_outer_layer')) as outer_layer_file: OUTER_LAYER_STR = outer_layer_file.read()
-with open(get_resource('hidden_service_v3_inner_layer'), 'rb') as inner_layer_file: +with open(get_resource('hidden_service_v3_inner_layer')) as inner_layer_file: INNER_LAYER_STR = inner_layer_file.read()
@@ -79,7 +79,7 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase): inner_layer = desc.decrypt(HS_ADDRESS)
self.assertEqual(INNER_LAYER_STR, str(inner_layer)) - self.assertEqual(OUTER_LAYER_STR.rstrip(b'\x00'), str(inner_layer.outer)) + self.assertEqual(OUTER_LAYER_STR.rstrip('\x00'), str(inner_layer.outer))
def test_outer_layer(self): """