[tor-commits] [stem/master] Rewrite HSv3 descriptor construction

atagar at torproject.org atagar at torproject.org
Sun Nov 17 23:40:39 UTC 2019


commit a8c2a431c1c2a6f38bbf1678c05aebf0a5e217d6
Author: Damian Johnson <atagar at torproject.org>
Date:   Wed Nov 13 17:06:12 2019 -0800

    Rewrite HSv3 descriptor construction
    
    Now that we have all the building blocks in place we can tackle HSv3 descriptor
    creation itself. Two main changes are...
    
      1. More flexibility. All parameters are now customizable.
    
      2. Simpler. All parameters have defaults so simply calling
         HiddenServiceDescriptorV3.create() once again works.
---
 stem/descriptor/__init__.py               |   2 +
 stem/descriptor/certificate.py            |  14 ++-
 stem/descriptor/hidden_service.py         | 151 +++++++++++++++---------------
 test/unit/descriptor/hidden_service_v3.py |   2 +-
 4 files changed, 85 insertions(+), 84 deletions(-)

diff --git a/stem/descriptor/__init__.py b/stem/descriptor/__init__.py
index 862d5aca..49a4d4b5 100644
--- a/stem/descriptor/__init__.py
+++ b/stem/descriptor/__init__.py
@@ -895,6 +895,8 @@ class Descriptor(object):
       * **NotImplementedError** if not implemented for this descriptor type
     """
 
+    # TODO: drop the 'sign' argument in stem 2.x (only a few subclasses use this)
+
     raise NotImplementedError("The create and content methods haven't been implemented for %s" % cls.__name__)
 
   @classmethod
diff --git a/stem/descriptor/certificate.py b/stem/descriptor/certificate.py
index f9ab8f80..10518364 100644
--- a/stem/descriptor/certificate.py
+++ b/stem/descriptor/certificate.py
@@ -294,17 +294,21 @@ class Ed25519CertificateV1(Ed25519Certificate):
   :param cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey: certificate signing key
   """
 
-  def __init__(self, cert_type, expiration, key_type, key, extensions, signature = None, signing_key = None):
+  def __init__(self, cert_type = None, expiration = None, key_type = None, key = None, extensions = None, signature = None, signing_key = None):
     super(Ed25519CertificateV1, self).__init__(1)
 
     if not signature and not signing_key:
       raise ValueError('Certificate signature or signing key is required')
+    elif cert_type is None:
+      raise ValueError('Certificate type is required')
+    elif key is None:
+      raise ValueError('Certificate key is required')
 
     self.type, self.type_int = ClientCertType.get(cert_type)
-    self.expiration = expiration
-    self.key_type = key_type
-    self.key = key
-    self.extensions = extensions
+    self.expiration = expiration if expiration else datetime.datetime.utcnow() + datetime.timedelta(hours = DEFAULT_EXPIRATION_HOURS)
+    self.key_type = key_type if key_type else 1
+    self.key = key if isinstance(key, str) else stem.util._pubkey_bytes(key)
+    self.extensions = extensions if extensions else []
     self.signature = signature
 
     if signing_key:
diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py
index 05c8267b..604c3849 100644
--- a/stem/descriptor/hidden_service.py
+++ b/stem/descriptor/hidden_service.py
@@ -903,95 +903,95 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
   }
 
   @classmethod
-  def content(cls, attr = None, exclude = (), sign = False, ed25519_private_identity_key = None, intro_points = None, blinding_param = None):
+  def content(cls, attr = None, exclude = (), sign = False, inner_layer = None, outer_layer = None, identity_key = None, signing_key = None, signing_cert = None, revision_counter = None, blinding_param = None):
     """
-    'ed25519_private_identity_key' is the Ed25519PrivateKey  of the onion service
+    Hidden service v3 descriptors consist of three parts:
+
+      * InnerLayer, which most notably contain introduction points where the
+        service can be reached.
+
+      * OuterLayer, which encrypts the InnerLayer among other paremters.
+
+      * HiddenServiceDescriptorV3, which contains the OuterLayer and plaintext
+        parameters.
+
+    Construction through this method can supply any or none of these, with
+    omitted parameters populated with randomized defaults.
+
+    :param dict attr: keyword/value mappings to be included in plaintext descriptor
+    :param list exclude: mandatory keywords to exclude from the descriptor, this
+      results in an invalid descriptor
+    :param bool sign: includes cryptographic signatures and digests if True
+    :param stem.descriptor.hidden_service.InnerLayer inner_layer: inner
+      encrypted layer
+    :param stem.descriptor.hidden_service.OuterLayer outer_layer: outer
+      encrypted layer
+    :param cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey
+      identity_key: service identity key
+    :param cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey
+      signing_key: service signing key
+    :param stem.descriptor.Ed25519CertificateV1 signing_cert: certificate
+      signing this descriptor
+    :param int revision_counter: descriptor revision number
+    :param bytes blinding_param: 32 byte blinding factor to derive the blinding key
+
+    :returns: **str** with the content of a descriptor
 
-    'intro_points' is a list of IntroductionPointV3 objects
-
-    'blinding_param' is a 32 byte blinding factor that should be used to derive
-    the blinded key from the identity key
+    :raises:
+      * **ValueError** if parameters are malformed
+      * **ImportError** if cryptography is unavailable
     """
 
-    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+    if not stem.prereq.is_crypto_available(ed25519 = True):
+      raise ImportError('Hidden service descriptor creation requires cryptography version 2.6')
+    elif not stem.prereq._is_sha3_available():
+      raise ImportError('Hidden service descriptor creation requires python 3.6+ or the pysha3 module (https://pypi.org/project/pysha3/)')
 
-    if sign:
-      raise NotImplementedError('Signing of %s not implemented' % cls.__name__)
+    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
 
-    # TODO: When these additional arguments are not present simply constructing
-    # a descriptor that looks reasonable. This might not be the fallback we use
-    # later on.
-
-    if not ed25519_private_identity_key or not intro_points or not blinding_param:
-      return _descriptor_content(attr, exclude, (
-        ('hs-descriptor', '3'),
-        ('descriptor-lifetime', '180'),
-        ('descriptor-signing-key-cert', _random_crypto_blob('ED25519 CERT')),
-        ('revision-counter', '15'),
-        ('superencrypted', _random_crypto_blob('MESSAGE')),
-        ('signature', 'wdc7ffr+dPZJ/mIQ1l4WYqNABcmsm6SHW/NL3M3wG7bjjqOJWoPR5TimUXxH52n5Zk0Gc7hl/hz3YYmAx5MvAg'),
-      ), ())
-
-    # We need an private identity key for the onion service to create its
-    # descriptor. We could make a new one on the spot, but we also need to
-    # return it to the caller, otherwise the caller will have no way to decode
-    # the descriptor without knowing the private key or the onion address, so
-    # for now we consider it a mandatory argument.
-
-    if not ed25519_private_identity_key:
-      raise ValueError('Need to provide a private ed25519 identity key to create a descriptor')
-
-    if not intro_points:
-      raise ValueError('Need to provide the introduction points for this descriptor')
-
-    if not blinding_param:
-      raise ValueError('Need to provide a blinding param for this descriptor')
-
-    # 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_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(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,
-    )
-
-    expiration = datetime.datetime.utcnow() + datetime.timedelta(hours = stem.descriptor.certificate.DEFAULT_EXPIRATION_HOURS)
-    extensions = [Ed25519Extension(ExtensionType.HAS_SIGNING_KEY, None, blinded_privkey.blinded_pubkey)]
-
-    signing_cert = Ed25519CertificateV1(CertType.HS_V3_DESC_SIGNING, expiration, 1, stem.util._pubkey_bytes(descriptor_signing_public_key), extensions, signing_key = blinded_privkey)
+    inner_layer = inner_layer if inner_layer else InnerLayer.create(exclude = exclude)
+    identity_key = identity_key if identity_key else Ed25519PrivateKey.generate()
+    signing_key = signing_key if signing_key else Ed25519PrivateKey.generate()
+    revision_counter = revision_counter if revision_counter else int(time.time())
+    blinding_param = blinding_param if blinding_param else os.urandom(32)
+
+    blinded_key = stem.descriptor.hsv3_crypto.HSv3PrivateBlindedKey(identity_key, blinding_param = blinding_param)
+    subcredential = HiddenServiceDescriptorV3._subcredential(identity_key, blinded_key.blinded_pubkey)
+
+    if not outer_layer:
+      outer_layer = OuterLayer.create(
+        exclude = exclude,
+        inner_layer = inner_layer,
+        revision_counter = revision_counter,
+        subcredential = subcredential,
+        blinded_key = blinded_key.blinded_pubkey,
+      )
+
+    if not signing_cert:
+      signing_cert = Ed25519CertificateV1(
+        cert_type = CertType.HS_V3_DESC_SIGNING,
+        key = signing_key,
+        extensions = [Ed25519Extension(ExtensionType.HAS_SIGNING_KEY, None, blinded_key.blinded_pubkey)],
+        signing_key = blinded_key,
+      )
 
     desc_content = _descriptor_content(attr, exclude, (
       ('hs-descriptor', '3'),
       ('descriptor-lifetime', '180'),
       ('descriptor-signing-key-cert', '\n' + signing_cert.to_base64(pem = True)),
       ('revision-counter', str(revision_counter)),
-      ('superencrypted', b'\n' + outer_layer._encrypt(revision_counter, subcredential, blinded_key)),
-    ), ())
-
-    # Add a final newline before the signature block
-    desc_content += b'\n'
+      ('superencrypted', b'\n' + outer_layer._encrypt(revision_counter, subcredential, blinded_key.blinded_pubkey)),
+    ), ()) + b'\n'
 
-    sig_content = stem.descriptor.certificate.SIG_PREFIX_HS_V3 + desc_content
-    signature = b'signature %s' % base64.b64encode(signing_key.sign(sig_content)).rstrip(b'=')
+    if 'signature' not in exclude:
+      sig_content = stem.descriptor.certificate.SIG_PREFIX_HS_V3 + desc_content
+      desc_content += b'signature %s' % base64.b64encode(signing_key.sign(sig_content)).rstrip(b'=')
 
-    return desc_content + signature
+    return desc_content
 
   @classmethod
-  def create(cls, attr = None, exclude = (), validate = True, sign = False):
-    return cls(cls.content(attr, exclude, sign), validate = validate, skip_crypto_validation = not sign)
+  def create(cls, attr = None, exclude = (), validate = True, sign = False, inner_layer = None, outer_layer = None, identity_key = None, signing_key = None, signing_cert = None, revision_counter = None, blinding_param = None):
+    return cls(cls.content(attr, exclude, sign, inner_layer, outer_layer, identity_key, signing_key, signing_cert, revision_counter, blinding_param), validate = validate, skip_crypto_validation = not sign)
 
   def __init__(self, raw_contents, validate = False):
     super(HiddenServiceDescriptorV3, self).__init__(raw_contents, lazy_load = not validate)
@@ -1176,8 +1176,6 @@ class OuterLayer(Descriptor):
   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
 
@@ -1252,9 +1250,6 @@ class InnerLayer(Descriptor):
 
   @classmethod
   def content(cls, attr = None, exclude = (), sign = False, introduction_points = None):
-    if sign:
-      raise NotImplementedError('Signing of %s not implemented' % cls.__name__)
-
     if introduction_points:
       suffix = '\n' + '\n'.join(map(IntroductionPointV3.encode, introduction_points))
     else:
diff --git a/test/unit/descriptor/hidden_service_v3.py b/test/unit/descriptor/hidden_service_v3.py
index 73f3e2c3..81e04a0f 100644
--- a/test/unit/descriptor/hidden_service_v3.py
+++ b/test/unit/descriptor/hidden_service_v3.py
@@ -439,7 +439,7 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase):
     blind_param = bytearray.fromhex('677776AE42464CAAB0DF0BF1E68A5FB651A390A6A8243CF4B60EE73A6AC2E4E3')
 
     # Build the descriptor
-    desc_string = HiddenServiceDescriptorV3.content(ed25519_private_identity_key = private_identity_key, intro_points = intro_points, blinding_param = blind_param)
+    desc_string = HiddenServiceDescriptorV3.content(identity_key = private_identity_key, inner_layer = InnerLayer.create(introduction_points = intro_points), blinding_param = blind_param)
 
     # Parse the descriptor
     desc = HiddenServiceDescriptorV3.from_str(desc_string)





More information about the tor-commits mailing list