[tor-commits] [stem/master] Move HSv3 certificate validation

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


commit 0881dcc003ef29272f514778af71b368de5be82e
Author: Damian Johnson <atagar at torproject.org>
Date:   Tue Oct 15 18:29:37 2019 -0700

    Move HSv3 certificate validation
    
    Adding HSv3 cert validation to our Ed25519CertificateV1's validate() method.
    Pretty similar to server descriptor validation.
---
 stem/descriptor/certificate.py         | 83 ++++++++++++++++++++++------------
 stem/descriptor/hidden_service.py      | 25 ++--------
 stem/descriptor/router_status_entry.py |  7 +--
 stem/util/str_tools.py                 | 12 +++++
 test/unit/descriptor/certificate.py    |  2 +-
 5 files changed, 72 insertions(+), 57 deletions(-)

diff --git a/stem/descriptor/certificate.py b/stem/descriptor/certificate.py
index 514f3155..05e0eefb 100644
--- a/stem/descriptor/certificate.py
+++ b/stem/descriptor/certificate.py
@@ -73,8 +73,10 @@ import binascii
 import collections
 import datetime
 import hashlib
+import re
 
 import stem.prereq
+import stem.descriptor.hidden_service
 import stem.descriptor.server_descriptor
 import stem.util.enum
 import stem.util.str_tools
@@ -85,7 +87,6 @@ from cryptography.hazmat.primitives import serialization
 
 ED25519_HEADER_LENGTH = 40
 ED25519_SIGNATURE_LENGTH = 64
-ED25519_ROUTER_SIGNATURE_PREFIX = b'Tor router descriptor signature v1'
 
 CertType = stem.util.enum.UppercaseEnum(
   'RESERVED_0', 'RESERVED_1', 'RESERVED_2', 'RESERVED_3',
@@ -272,59 +273,83 @@ class Ed25519CertificateV1(Ed25519Certificate):
 
   def validate(self, descriptor):
     """
-    Validates our signing key and that the given descriptor content matches its
-    Ed25519 signature. Supported descriptor types include...
+    Validate our descriptor content matches its ed25519 signature. Supported
+    descriptor types include...
 
-      * server descriptors
+      * :class:`~stem.descriptor.server_descriptor.RelayDescriptor`
+      * :class:`~stem.descriptor.hidden_service.HiddenServiceDescriptorV3`
 
     :param stem.descriptor.__init__.Descriptor descriptor: descriptor to validate
 
     :raises:
       * **ValueError** if signing key or descriptor are invalid
-      * **ImportError** if cryptography module is unavailable or ed25519 is
-        unsupported
+      * **TypeError** if descriptor type is unsupported
+      * **ImportError** if cryptography module or ed25519 support unavailable
     """
 
     if not stem.prereq._is_crypto_ed25519_supported():
       raise ImportError('Certificate validation requires the cryptography module and ed25519 support')
 
-    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
-    from cryptography.exceptions import InvalidSignature
+    if isinstance(descriptor, stem.descriptor.server_descriptor.RelayDescriptor):
+      signed_content = hashlib.sha256(Ed25519CertificateV1._signed_content(descriptor)).digest()
+      signature = stem.util.str_tools._decode_b64(descriptor.ed25519_signature)
 
-    if not isinstance(descriptor, stem.descriptor.server_descriptor.RelayDescriptor):
-      raise ValueError('Certificate validation only supported for server descriptors, not %s' % type(descriptor).__name__)
-
-    if descriptor.ed25519_master_key:
-      signing_key = base64.b64decode(stem.util.str_tools._to_bytes(descriptor.ed25519_master_key) + b'=')
+      self._validate_server_desc_signing_key(descriptor)
+    elif isinstance(descriptor, stem.descriptor.hidden_service.HiddenServiceDescriptorV3):
+      signed_content = Ed25519CertificateV1._signed_content(descriptor)
+      signature = stem.util.str_tools._decode_b64(descriptor.signature)
     else:
-      signing_key = self.signing_key()
+      raise TypeError('Certificate validation only supported for server and hidden service descriptors, not %s' % type(descriptor).__name__)
 
-    if not signing_key:
-      raise ValueError('Server descriptor missing an ed25519 signing key')
+    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
+    from cryptography.exceptions import InvalidSignature
 
     try:
-      Ed25519PublicKey.from_public_bytes(signing_key).verify(self.signature, base64.b64decode(stem.util.str_tools._to_bytes(self.encoded))[:-ED25519_SIGNATURE_LENGTH])
+      key = Ed25519PublicKey.from_public_bytes(self.key)
+      key.verify(signature, signed_content)
     except InvalidSignature:
-      raise ValueError('Ed25519KeyCertificate signing key is invalid (Signature was forged or corrupt)')
+      raise ValueError('Descriptor Ed25519 certificate signature invalid (signature forged or corrupt)')
 
-    # ed25519 signature validates descriptor content up until the signature itself
+  @staticmethod
+  def _signed_content(descriptor):
+    """
+    Provides this descriptor's signing constant, appended with the portion of
+    the descriptor that's signed.
+    """
 
-    descriptor_content = descriptor.get_bytes()
+    if isinstance(descriptor, stem.descriptor.server_descriptor.RelayDescriptor):
+      prefix = b'Tor router descriptor signature v1'
+      regex = '(.+router-sig-ed25519 )'
+    elif isinstance(descriptor, stem.descriptor.hidden_service.HiddenServiceDescriptorV3):
+      prefix = b'Tor onion service descriptor sig v3'
+      regex = '(.+)signature '
+    else:
+      raise ValueError('BUG: %s type unexpected' % type(descriptor).__name__)
+
+    match = re.search(regex, descriptor.get_bytes(), re.DOTALL)
+
+    if not match:
+      raise ValueError('Malformed descriptor missing signature line')
 
-    if b'router-sig-ed25519 ' not in descriptor_content:
-      raise ValueError("Descriptor doesn't have a router-sig-ed25519 entry.")
+    return prefix + match.group(1)
+
+  def _validate_server_desc_signing_key(self, descriptor):
+    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
+    from cryptography.exceptions import InvalidSignature
 
-    signed_content = descriptor_content[:descriptor_content.index(b'router-sig-ed25519 ') + 19]
-    descriptor_sha256_digest = hashlib.sha256(ED25519_ROUTER_SIGNATURE_PREFIX + signed_content).digest()
+    if descriptor.ed25519_master_key:
+      signing_key = base64.b64decode(stem.util.str_tools._to_bytes(descriptor.ed25519_master_key) + b'=')
+    else:
+      signing_key = self.signing_key()
 
-    missing_padding = len(descriptor.ed25519_signature) % 4
-    signature_bytes = base64.b64decode(stem.util.str_tools._to_bytes(descriptor.ed25519_signature) + b'=' * missing_padding)
+    if not signing_key:
+      raise ValueError('Server descriptor missing an ed25519 signing key')
 
     try:
-      verify_key = Ed25519PublicKey.from_public_bytes(self.key)
-      verify_key.verify(signature_bytes, descriptor_sha256_digest)
+      key = Ed25519PublicKey.from_public_bytes(signing_key)
+      key.verify(self.signature, base64.b64decode(stem.util.str_tools._to_bytes(self.encoded))[:-ED25519_SIGNATURE_LENGTH])
     except InvalidSignature:
-      raise ValueError('Descriptor Ed25519 certificate signature invalid (Signature was forged or corrupt)')
+      raise ValueError('Ed25519KeyCertificate signing key is invalid (signature forged or corrupt)')
 
 
 class MyED25519Certificate(object):
diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py
index 7c279629..49009a6a 100644
--- a/stem/descriptor/hidden_service.py
+++ b/stem/descriptor/hidden_service.py
@@ -655,8 +655,7 @@ class HiddenServiceDescriptorV2(BaseHiddenServiceDescriptor):
         raise DecryptionFailure('Decrypting introduction-points requires the cryptography module')
 
       try:
-        missing_padding = len(authentication_cookie) % 4
-        authentication_cookie = base64.b64decode(stem.util.str_tools._to_bytes(authentication_cookie) + b'=' * missing_padding)
+        authentication_cookie = stem.util.str_tools._decode_b64(authentication_cookie)
       except TypeError as exc:
         raise DecryptionFailure('authentication_cookie must be a base64 encoded string (%s)' % exc)
 
@@ -1054,28 +1053,12 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
         raise ValueError("Hidden service descriptor must end with a 'signature' entry")
 
       self._parse(entries, validate)
+
+      if self.signing_cert:
+        self.signing_cert.validate(self)
     else:
       self._entries = entries
 
-    from cryptography.hazmat.backends.openssl.backend import backend
-
-    if backend.x25519_supported() and self.signing_cert:
-      from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
-
-      # Verify the signature!
-      # First compute the body that was signed
-
-      descriptor_signing_key = Ed25519PublicKey.from_public_bytes(self.signing_cert.key)
-      descriptor_body = raw_contents.split(b'signature')[0]  # everything before the signature
-      signature_body = b'Tor onion service descriptor sig v3' + descriptor_body
-
-      # Decode base64 signature
-      missing_padding = len(self.signature) % 4
-      signature = base64.b64decode(self.signature + '=' * missing_padding)
-
-      # Verify signature
-      descriptor_signing_key.verify(signature, signature_body)
-
   def decrypt(self, onion_address):
     """
     Decrypt this descriptor. Hidden serice descriptors contain two encryption
diff --git a/stem/descriptor/router_status_entry.py b/stem/descriptor/router_status_entry.py
index ce662b40..91f2c146 100644
--- a/stem/descriptor/router_status_entry.py
+++ b/stem/descriptor/router_status_entry.py
@@ -21,7 +21,6 @@ sources...
     +- RouterStatusEntryMicroV3 - Entry for a microdescriptor flavored v3 document
 """
 
-import base64
 import binascii
 import io
 
@@ -369,12 +368,8 @@ def _base64_to_hex(identity, check_if_fingerprint = True):
   :raises: **ValueError** if the result isn't a valid fingerprint
   """
 
-  # trailing equal signs were stripped from the identity
-  missing_padding = len(identity) % 4
-  identity += '=' * missing_padding
-
   try:
-    identity_decoded = base64.b64decode(stem.util.str_tools._to_bytes(identity))
+    identity_decoded = stem.util.str_tools._decode_b64(stem.util.str_tools._to_bytes(identity))
   except (TypeError, binascii.Error):
     raise ValueError("Unable to decode identity string '%s'" % identity)
 
diff --git a/stem/util/str_tools.py b/stem/util/str_tools.py
index aba619b9..869f46b6 100644
--- a/stem/util/str_tools.py
+++ b/stem/util/str_tools.py
@@ -21,6 +21,7 @@ Toolkit for various string activity.
   parse_short_time_label - seconds represented by a short time label
 """
 
+import base64
 import codecs
 import datetime
 import re
@@ -116,6 +117,17 @@ def _to_unicode(msg):
   return _to_unicode_impl(msg)
 
 
+def _decode_b64(msg):
+  """
+  Base64 decode, without padding concerns.
+  """
+
+  missing_padding = len(msg) % 4
+  padding_chr = b'=' if isinstance(msg, bytes) else '='
+
+  return base64.b64decode(msg + padding_chr * missing_padding)
+
+
 def _to_int(msg):
   """
   Serializes a string to a number.
diff --git a/test/unit/descriptor/certificate.py b/test/unit/descriptor/certificate.py
index d86c169a..6d2fd378 100644
--- a/test/unit/descriptor/certificate.py
+++ b/test/unit/descriptor/certificate.py
@@ -194,7 +194,7 @@ class TestEd25519Certificate(unittest.TestCase):
       desc = next(stem.descriptor.parse_file(descriptor_file, validate = False))
 
     cert = Ed25519Certificate.parse(certificate())
-    self.assertRaisesWith(ValueError, 'Ed25519KeyCertificate signing key is invalid (Signature was forged or corrupt)', cert.validate, desc)
+    self.assertRaisesWith(ValueError, 'Ed25519KeyCertificate signing key is invalid (signature forged or corrupt)', cert.validate, desc)
 
   @test.require.ed25519_support
   def test_encode_decode_certificate(self):





More information about the tor-commits mailing list