[tor-commits] [stem/master] Outer layer creation and encryption
atagar at torproject.org
atagar at torproject.org
Sun Nov 17 23:40:39 UTC 2019
commit 04747884a33c5a0874a0143376010a4793245ec8
Author: Damian Johnson <atagar at torproject.org>
Date: Tue Nov 12 16:50:30 2019 -0800
Outer layer creation and encryption
Similar approach as the inner layer. This will provide more flexibility once we
propagate the attribute up HiddenServiceDescriptorV3.content().
---
stem/descriptor/hidden_service.py | 143 ++++++++++++------------------
test/unit/descriptor/hidden_service_v3.py | 70 +++++++++++++++
2 files changed, 126 insertions(+), 87 deletions(-)
diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py
index fcd29032..56ba4d22 100644
--- a/stem/descriptor/hidden_service.py
+++ b/stem/descriptor/hidden_service.py
@@ -883,62 +883,6 @@ def _get_descriptor_signing_cert(descriptor_signing_public_key, blinded_priv_key
return '\n' + desc_signing_cert.to_base64(pem = True)
-def _get_fake_clients_bytes():
- """
- Generate fake client authorization data for the middle layer
- """
-
- final_bytes = b''
- num_fake_clients = 16 # default for when client auth is disabled
-
- for _ in range(num_fake_clients):
- client_id = base64.b64encode(os.urandom(8)).rstrip(b'=')
- client_iv = base64.b64encode(os.urandom(16)).rstrip(b'=')
- descriptor_cookie = base64.b64encode(os.urandom(16)).rstrip(b'=')
-
- final_bytes += b'%s %s %s %s\n' % (b'auth-client', client_id, client_iv, descriptor_cookie)
-
- return final_bytes
-
-
-def _get_middle_descriptor_layer_body(encrypted):
- """
- Get the middle descriptor layer as bytes
- (It's just fake client auth data since client auth is disabled)
- """
-
- from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
-
- fake_pub_key = X25519PrivateKey.generate().public_key()
- fake_pub_key_bytes = stem.util._pubkey_bytes(fake_pub_key)
- fake_pub_key_bytes_b64 = base64.b64encode(fake_pub_key_bytes)
- fake_clients = _get_fake_clients_bytes()
-
- return b'desc-auth-type x25519\n' \
- b'desc-auth-ephemeral-key %s\n' \
- b'%s' \
- b'%s' % (fake_pub_key_bytes_b64, fake_clients, encrypted)
-
-
-def _get_superencrypted_blob(intro_points, revision_counter, blinded_key, subcredential):
- """
- Get the superencrypted blob (which also includes the encrypted blob) that
- should be attached to the descriptor
- """
-
- inner_layer = InnerLayer.create(introduction_points = intro_points)
- inner_ciphertext_b64 = b'encrypted\n' + inner_layer.encrypt(revision_counter, blinded_key, subcredential)
-
- middle_descriptor_layer = _get_middle_descriptor_layer_body(inner_ciphertext_b64)
-
- # Spec mandated padding: "Before encryption the plaintext is padded with NUL
- # bytes to the nearest multiple of 10k bytes."
-
- middle_descriptor_layer = middle_descriptor_layer + b'\x00' * (len(middle_descriptor_layer) % 10000)
-
- return b'\n' + _encrypt_layer(middle_descriptor_layer, b'hsdir-superencrypted-data', revision_counter, subcredential, blinded_key)
-
-
class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
"""
Version 3 hidden service descriptor.
@@ -1023,33 +967,33 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
if not blinding_param:
raise ValueError('Need to provide a blinding param for this descriptor')
- # Get the identity public key
- public_identity_key_bytes = stem.util._pubkey_bytes(ed25519_private_identity_key)
-
# Blind the identity key to get ephemeral blinded key
blinded_privkey = stem.descriptor.hsv3_crypto.HSv3PrivateBlindedKey(ed25519_private_identity_key, blinding_param = blinding_param)
- blinded_pubkey_bytes = blinded_privkey.blinded_pubkey
+ blinded_key = blinded_privkey.blinded_pubkey
# Generate descriptor signing key
signing_key = Ed25519PrivateKey.generate()
descriptor_signing_public_key = signing_key.public_key()
# Get the main encrypted descriptor body
- revision_counter_int = int(time.time())
- subcredential = HiddenServiceDescriptorV3._subcredential(public_identity_key_bytes, blinded_pubkey_bytes)
-
- # XXX It would be more elegant to have all the above variables attached to
- # this descriptor object so that we don't have to carry them around
- # functions and instead we could use e.g. self.descriptor_signing_public_key
- # But because this is a @classmethod this is not possible :/
- superencrypted_blob = _get_superencrypted_blob(intro_points, revision_counter_int, blinded_pubkey_bytes, subcredential)
+ revision_counter = int(time.time())
+ subcredential = HiddenServiceDescriptorV3._subcredential(ed25519_private_identity_key, blinded_key)
+
+ outer_layer = OuterLayer.create(
+ inner_layer = InnerLayer.create(
+ introduction_points = intro_points,
+ ),
+ revision_counter = revision_counter,
+ subcredential = subcredential,
+ blinded_key = blinded_key,
+ )
desc_content = _descriptor_content(attr, exclude, (
('hs-descriptor', '3'),
('descriptor-lifetime', '180'),
('descriptor-signing-key-cert', _get_descriptor_signing_cert(descriptor_signing_public_key, blinded_privkey)),
- ('revision-counter', str(revision_counter_int)),
- ('superencrypted', superencrypted_blob),
+ ('revision-counter', str(revision_counter)),
+ ('superencrypted', b'\n' + outer_layer._encrypt(revision_counter, subcredential, blinded_key)),
), ())
# Add a final newline before the signature block
@@ -1189,11 +1133,11 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
return pubkey
@staticmethod
- def _subcredential(public_key, blinded_key):
+ def _subcredential(identity_key, blinded_key):
# credential = H('credential' | public-identity-key)
# subcredential = H('subcredential' | credential | blinded-public-key)
- credential = hashlib.sha3_256(b'credential%s' % public_key).digest()
+ credential = hashlib.sha3_256(b'credential%s' % stem.util._pubkey_bytes(identity_key)).digest()
return hashlib.sha3_256(b'subcredential%s%s' % (credential, blinded_key)).digest()
@@ -1233,6 +1177,41 @@ class OuterLayer(Descriptor):
plaintext = _decrypt_layer(encrypted, b'hsdir-superencrypted-data', revision_counter, subcredential, blinded_key)
return OuterLayer(plaintext)
+ def _encrypt(self, revision_counter, subcredential, blinded_key):
+ # Spec mandated padding: "Before encryption the plaintext is padded with
+ # NUL bytes to the nearest multiple of 10k bytes."
+
+ content = self.get_bytes() + b'\x00' * (len(self.get_bytes()) % 10000)
+
+ # encrypt back into a hidden service descriptor's 'superencrypted' field
+
+ return _encrypt_layer(content, b'hsdir-superencrypted-data', revision_counter, subcredential, blinded_key)
+
+ @classmethod
+ def content(cls, attr = None, exclude = (), validate = True, sign = False, inner_layer = None, revision_counter = None, subcredential = None, blinded_key = None):
+ if not stem.prereq.is_crypto_available(ed25519 = True):
+ raise ImportError('Hidden service layer creation requires cryptography version 2.6')
+ elif sign:
+ raise NotImplementedError('Signing of %s not implemented' % cls.__name__)
+
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+
+ inner_layer = inner_layer if inner_layer else InnerLayer.create()
+ revision_counter = revision_counter if revision_counter else 1
+ blinded_key = blinded_key if blinded_key else stem.util._pubkey_bytes(Ed25519PrivateKey.generate())
+ subcredential = subcredential if subcredential else HiddenServiceDescriptorV3._subcredential(Ed25519PrivateKey.generate(), blinded_key)
+
+ return _descriptor_content(attr, exclude, (
+ ('desc-auth-type', 'x25519'),
+ ('desc-auth-ephemeral-key', base64.b64encode(os.urandom(32))),
+ ), (
+ ('encrypted', b'\n' + inner_layer._encrypt(revision_counter, subcredential, blinded_key)),
+ ))
+
+ @classmethod
+ def create(cls, attr = None, exclude = (), validate = True, sign = False, inner_layer = None, revision_counter = None, subcredential = None, blinded_key = None):
+ return cls(cls.content(attr, exclude, validate, sign, inner_layer, revision_counter, subcredential, blinded_key), validate = validate)
+
def __init__(self, content, validate = False):
content = content.rstrip('\x00') # strip null byte padding
@@ -1281,6 +1260,11 @@ class InnerLayer(Descriptor):
plaintext = _decrypt_layer(outer_layer.encrypted, b'hsdir-encrypted-data', revision_counter, subcredential, blinded_key)
return InnerLayer(plaintext, validate = True, outer_layer = outer_layer)
+ def _encrypt(self, revision_counter, subcredential, blinded_key):
+ # encrypt back into an outer layer's 'encrypted' field
+
+ return _encrypt_layer(self.get_bytes(), b'hsdir-encrypted-data', revision_counter, subcredential, blinded_key)
+
@classmethod
def content(cls, attr = None, exclude = (), sign = False, introduction_points = None):
if sign:
@@ -1322,21 +1306,6 @@ class InnerLayer(Descriptor):
else:
self._entries = entries
- def encrypt(self, revision_counter, blinded_key, subcredential):
- """
- Encrypts into the content contained within the OuterLayer.
-
- :param int revision_counter: descriptor revision number
- :param bytes blinded_key: descriptor signing key
- :param bytes subcredential: public key hash
-
- :returns: base64 encoded content of the outer layer's 'encrypted' field
- """
-
- if not stem.prereq.is_crypto_available(ed25519 = True):
- raise ImportError('Hidden service descriptor encryption requires cryptography version 2.6')
-
- return _encrypt_layer(self.get_bytes(), b'hsdir-encrypted-data', revision_counter, subcredential, blinded_key)
# TODO: drop this alias in stem 2.x
diff --git a/test/unit/descriptor/hidden_service_v3.py b/test/unit/descriptor/hidden_service_v3.py
index f6173768..73f3e2c3 100644
--- a/test/unit/descriptor/hidden_service_v3.py
+++ b/test/unit/descriptor/hidden_service_v3.py
@@ -17,6 +17,7 @@ import test.require
from stem.descriptor.hidden_service import (
IntroductionPointV3,
HiddenServiceDescriptorV3,
+ AuthorizedClient,
OuterLayer,
InnerLayer,
)
@@ -56,6 +57,17 @@ BDwQZ8rhp05oCqhhY3oFHqG9KS7HGzv9g2v1/PrVJMbkfpwu1YK4b3zIZAk=
-----END ED25519 CERT-----\
"""
+EXPECTED_OUTER_LAYER = """\
+desc-auth-type foo
+desc-auth-ephemeral-key bar
+auth-client JNil86N07AA epkaL79NtajmgME/egi8oA qosYH4rXisxda3X7p9b6fw
+auth-client 1D8VBAh9hdM 6K/uO3sRqBp6URrKC7GB6Q ElwRj5+6SN9kb8bRhiiQvA
+encrypted
+-----BEGIN MESSAGE-----
+malformed block
+-----END MESSAGE-----\
+"""
+
with open(get_resource('hidden_service_v3')) as descriptor_file:
HS_DESC_STR = descriptor_file.read()
@@ -333,6 +345,64 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase):
]).startswith('create2-formats 2\nintroduction-point AQAGAQEBASMp'))
@test.require.ed25519_support
+ def test_outer_layer_creation(self):
+ """
+ Outer layer creation.
+ """
+
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+
+ # minimal layer
+
+ self.assertTrue(OuterLayer.content().startswith('desc-auth-type x25519\ndesc-auth-ephemeral-key '))
+ self.assertEqual('x25519', OuterLayer.create().auth_type)
+
+ # specify the parameters
+
+ desc = OuterLayer.create({
+ 'desc-auth-type': 'foo',
+ 'desc-auth-ephemeral-key': 'bar',
+ 'auth-client': [
+ 'JNil86N07AA epkaL79NtajmgME/egi8oA qosYH4rXisxda3X7p9b6fw',
+ '1D8VBAh9hdM 6K/uO3sRqBp6URrKC7GB6Q ElwRj5+6SN9kb8bRhiiQvA',
+ ],
+ 'encrypted': '\n-----BEGIN MESSAGE-----\nmalformed block\n-----END MESSAGE-----',
+ })
+
+ self.assertEqual('foo', desc.auth_type)
+ self.assertEqual('bar', desc.ephemeral_key)
+ self.assertEqual('-----BEGIN MESSAGE-----\nmalformed block\n-----END MESSAGE-----', desc.encrypted)
+
+ self.assertEqual({
+ '1D8VBAh9hdM': AuthorizedClient(id = '1D8VBAh9hdM', iv = '6K/uO3sRqBp6URrKC7GB6Q', cookie = 'ElwRj5+6SN9kb8bRhiiQvA'),
+ 'JNil86N07AA': AuthorizedClient(id = 'JNil86N07AA', iv = 'epkaL79NtajmgME/egi8oA', cookie = 'qosYH4rXisxda3X7p9b6fw'),
+ }, desc.clients)
+
+ self.assertEqual(EXPECTED_OUTER_LAYER, str(desc))
+
+ # create an inner layer then decrypt it
+
+ revision_counter = 5
+ blinded_key = stem.util._pubkey_bytes(Ed25519PrivateKey.generate())
+ subcredential = HiddenServiceDescriptorV3._subcredential(Ed25519PrivateKey.generate(), blinded_key)
+
+ outer_layer = OuterLayer.create(
+ inner_layer = InnerLayer.create(
+ introduction_points = [
+ IntroductionPointV3.create('1.1.1.1', 9001),
+ ]
+ ),
+ revision_counter = revision_counter,
+ subcredential = subcredential,
+ blinded_key = blinded_key,
+ )
+
+ inner_layer = InnerLayer._decrypt(outer_layer, revision_counter, subcredential, blinded_key)
+
+ self.assertEqual(1, len(inner_layer.introduction_points))
+ self.assertEqual('1.1.1.1', inner_layer.introduction_points[0].link_specifiers[0].address)
+
+ @test.require.ed25519_support
def test_encode_decode_descriptor(self):
"""
Encode an HSv3 descriptor and then decode it and make sure you get the intended results.
More information about the tor-commits
mailing list