[tor-commits] [stem/master] Refactor IntroductionPointV3 to be able to facilitate encoding/decoding.

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


commit 1475f12d5afa60d21b9b688e2324bca754a60077
Author: George Kadianakis <desnacked at riseup.net>
Date:   Thu Oct 10 20:46:10 2019 +0300

    Refactor IntroductionPointV3 to be able to facilitate encoding/decoding.
    
    - Changed the attributes of IntroductionPointV3 to be the actual crypto objects
      stored instead of strings.
    
    - This allows us to pass introduction point objects from onionbalance to stem,
      so that stem can encode them in the descriptor.
    
    - Implement encoding of introduction points.
    
    - Refactor the intro point parsing to support the new attributes.
---
 stem/descriptor/certificate.py    |  17 +++++
 stem/descriptor/hidden_service.py | 153 +++++++++++++++++++++++++++++++++-----
 2 files changed, 152 insertions(+), 18 deletions(-)

diff --git a/stem/descriptor/certificate.py b/stem/descriptor/certificate.py
index ac6d455f..cdc44e53 100644
--- a/stem/descriptor/certificate.py
+++ b/stem/descriptor/certificate.py
@@ -253,6 +253,23 @@ class Ed25519CertificateV1(Ed25519Certificate):
 
     return datetime.datetime.now() > self.expiration
 
+  def certified_ed25519_key(self):
+    """
+    Provide this certificate's certified ed25519 key (the one that got signed)
+
+    :returns: **Ed25519PublicKey**
+
+    :raises: **ValueError** if it's not an ed25519 cert
+    """
+    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
+
+    # Make sure it's an ed25519 cert
+    if (self.key_type != 1):
+      raise ValueError("Certificate is not an ed25519 cert (%d)" % key_type)
+
+    ed_key = Ed25519PublicKey.from_public_bytes(self.key)
+    return ed_key
+
   def signing_key(self):
     """
     Provides this certificate's signing key.
diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py
index 3277e183..4a75bc88 100644
--- a/stem/descriptor/hidden_service.py
+++ b/stem/descriptor/hidden_service.py
@@ -138,24 +138,132 @@ class IntroductionPoints(collections.namedtuple('IntroductionPoints', INTRODUCTI
   """
 
 
-class IntroductionPointV3(collections.namedtuple('IntroductionPointV3', ['link_specifiers', 'onion_key', 'auth_key', 'enc_key', 'enc_key_cert', 'legacy_key', 'legacy_key_cert'])):
+class IntroductionPointV3(object):
   """
   Introduction point for a v3 hidden service.
 
+  We want this class to satisfy two use cases:
+
+  - Parsing introduction points directly from the HSv3 descriptor and saving
+    their data here.
+
+  - Creating introduction points for inclusion to an HSv3 descriptor at a point
+    where a descriptor signing key is not yet known (because the descriptor is
+    not yet made). In which case, the certificates cannot be created yet and
+    hence need to be created at encoding time.
+
   .. 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
-  :var str enc_key: introduction request encryption key
-  :var str enc_key_cert: cross-certifier of the signing key by the encryption key
-  :var str legacy_key: legacy introduction point RSA public key
-  :var str legacy_key_cert: cross-certifier of the signing key by the legacy key
+  :var X25519PublicKey onion_key: ntor introduction point public key
+  :var Ed25519PublicKey auth_key: ed25519 authentication key for this intro point
+  :var stem.certificate.Ed25519Certificate auth_key_cert: cross-certifier of the signing key with the auth key
+  :var X25519PublicKey enc_key: introduction request encryption key
+  :var stem.certificate.Ed25519Certificate enc_key_cert: cross-certifier of the signing key by the encryption key
+  :var XXX legacy_key: legacy introduction point RSA public key
+  :var stem.certificate.Ed25519Certificate legacy_key_cert: cross-certifier of the signing key by the legacy key
+
+  :var Ed25519Certificate descriptor_signing_key: hsv3 descriptor signing key (needed to encode the intro point)
   """
+  def __init__(self, link_specifiers, onion_key, enc_key,
+               auth_key=None, auth_key_cert=None, legacy_key=None, enc_key_cert=None, legacy_key_cert=None):
+    """
+    Initialize this intro point.
+
+    While not all attributes are mandatory, at the very least the link
+    specifiers, the auth key, the onion key and the encryption key need to be
+    provided.
 
+    The certificates can be left out (for example in the case of creating a new
+    intro point), and they will be created at encode time when the
+    descriptor_signing_key is provided.
+    """
+    if not link_specifiers or not onion_key or not enc_key:
+      raise ValueError("Introduction point missing essential keys")
+
+    if not auth_key and not auth_key_cert:
+      raise ValueError("Either auth key or auth key cert needs to be provided")
+
+    # If we have an auth key cert but not an auth key, extract the key
+    if auth_key_cert and not auth_key:
+      auth_key = auth_key_cert.certified_ed25519_key()
+
+    self.link_specifiers = link_specifiers
+    self.onion_key = enc_key
+    self.enc_key = enc_key
+    self.legacy_key = legacy_key
+    self.auth_key = auth_key
+    self.auth_key_cert = auth_key_cert
+    self.enc_key_cert = enc_key_cert
+    self.legacy_key_cert = legacy_key_cert
+
+  def _encode_link_specifier_block(self):
+    """
+    See BUILDING-BLOCKS in rend-spec-v3.txt
+
+         NSPEC      (Number of link specifiers)   [1 byte]
+         NSPEC times:
+           LSTYPE (Link specifier type)           [1 byte]
+           LSLEN  (Link specifier length)         [1 byte]
+           LSPEC  (Link specifier)                [LSLEN bytes]
+    """
+    ls_block = b""
+    ls_block += bytes([len(self.link_specifiers)])
+    for ls in self.link_specifiers:
+      ls_block += ls.encode()
+
+    return base64.b64encode(ls_block)
+
+  def encode(self, descriptor_signing_privkey):
+    """
+    Encode this introduction point into bytes
+    """
+    if not descriptor_signing_privkey:
+      raise ValueError("Cannot encode: Descriptor signing key not provided")
+
+    cert_expiration_date = datetime.datetime.utcnow() + datetime.timedelta(hours=54)
+
+    body = b""
+
+    body += b"introduction-point %s\n" % (self._encode_link_specifier_block())
+
+    # Onion key
+    onion_key_bytes = self.onion_key.public_bytes(encoding=serialization.Encoding.Raw,
+                                                 format=serialization.PublicFormat.Raw)
+    body += b"onion-key ntor %s\n" % (base64.b64encode(onion_key_bytes))
+
+    # Build auth key certificate
+    auth_key_cert = stem.descriptor.certificate.MyED25519Certificate(cert_type=CertType.HS_V3_INTRO_AUTH,
+                                                                     expiration_date=cert_expiration_date,
+                                                                     cert_key_type=1, certified_pub_key=self.auth_key,
+                                                                     signing_priv_key=descriptor_signing_privkey,
+                                                                     include_signing_key=True)
+    auth_key_cert_b64_blob = auth_key_cert.encode_for_descriptor()
+    body += b"auth-key\n%s\n" % (auth_key_cert_b64_blob)
+
+    # Build enc key line
+    enc_key_bytes = self.enc_key.public_bytes(encoding=serialization.Encoding.Raw,
+                                              format=serialization.PublicFormat.Raw)
+    body += b"enc-key ntor %s\n" % (base64.b64encode(enc_key_bytes))
+
+    # Build enc key cert (this does not actually need to certify anything because of #29583)
+    enc_key_cert = stem.descriptor.certificate.MyED25519Certificate(cert_type=CertType.HS_V3_INTRO_ENC,
+                                                                    expiration_date=cert_expiration_date,
+                                                                    cert_key_type=1, certified_pub_key=self.auth_key,
+                                                                    signing_priv_key=descriptor_signing_privkey,
+                                                                    include_signing_key=True)
+    enc_key_cert_b64_blob = enc_key_cert.encode_for_descriptor()
+    body += b"enc-key-cert\n%s\n" % (enc_key_cert_b64_blob)
+
+    # We are called to encode legacy key, but we don't know how
+    # TODO do legacy keys!
+    if self.legacy_key or self.legacy_key_cert:
+      raise NotImplementedError
+
+    return body
 
 class AuthorizedClient(collections.namedtuple('AuthorizedClient', ['id', 'iv', 'cookie'])):
-  """
+    """
   Client authorized to use a v3 hidden service.
 
   .. versionadded:: 1.8.0
@@ -305,6 +413,8 @@ def _parse_v3_inner_formats(descriptor, entries):
 
 
 def _parse_v3_introduction_points(descriptor, entries):
+  from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
+
   if hasattr(descriptor, '_unparsed_introduction_points'):
     introduction_points = []
     remaining = descriptor._unparsed_introduction_points
@@ -323,33 +433,40 @@ def _parse_v3_introduction_points(descriptor, entries):
       link_specifiers = _parse_link_specifiers(_value('introduction-point', entry))
 
       onion_key_line = _value('onion-key', entry)
-      onion_key = onion_key_line[5:] if onion_key_line.startswith('ntor ') else None
+      onion_key_b64 = onion_key_line[5:] if onion_key_line.startswith('ntor ') else None
+      onion_key = X25519PublicKey.from_public_bytes(base64.b64decode(onion_key_b64))
 
-      _, block_type, auth_key = entry['auth-key'][0]
+      _, block_type, auth_key_cert = entry['auth-key'][0]
+      auth_key_cert = Ed25519Certificate.parse(auth_key_cert)
 
       if block_type != 'ED25519 CERT':
         raise ValueError('Expected auth-key to have an ed25519 certificate, but was %s' % block_type)
 
       enc_key_line = _value('enc-key', entry)
-      enc_key = enc_key_line[5:] if enc_key_line.startswith('ntor ') else None
+      enc_key_b64 = enc_key_line[5:] if enc_key_line.startswith('ntor ') else None
+      enc_key = X25519PublicKey.from_public_bytes(base64.b64decode(enc_key_b64))
 
       _, block_type, enc_key_cert = entry['enc-key-cert'][0]
+      enc_key_cert = Ed25519Certificate.parse(enc_key_cert)
 
       if block_type != 'ED25519 CERT':
         raise ValueError('Expected enc-key-cert to have an ed25519 certificate, but was %s' % block_type)
 
       legacy_key = entry['legacy-key'][0][2] if 'legacy-key' in entry else None
       legacy_key_cert = entry['legacy-key-cert'][0][2] if 'legacy-key-cert' in entry else None
+#      if legacy_key_cert:
+#      legacy_key_cert = Ed25519Certificate.parse(legacy_key_cert)
+
 
       introduction_points.append(
         IntroductionPointV3(
-          link_specifiers,
-          onion_key,
-          auth_key,
-          enc_key,
-          enc_key_cert,
-          legacy_key,
-          legacy_key_cert,
+          link_specifiers=link_specifiers,
+          onion_key=onion_key,
+          auth_key_cert=auth_key_cert,
+          enc_key=enc_key,
+          enc_key_cert=enc_key_cert,
+          legacy_key=legacy_key,
+          legacy_key_cert=legacy_key_cert,
         )
       )
 





More information about the tor-commits mailing list