commit 0881dcc003ef29272f514778af71b368de5be82e Author: Damian Johnson atagar@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):