Unverified Commit 3ce66316 authored by Patrick O'Doherty's avatar Patrick O'Doherty
Browse files

Add Ed25519 certificate extension support

Parse the identity-ed25519 certificate block and validate the extension
contents in accordance with prop #220. Validates the router-sig-ed25519
signature against the provided certified key.

Start of work to verify onion-key-crosscert blocks
parent 67252eb7
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -3,3 +3,4 @@ pyflakes
pycodestyle
tox
cryptography
pynacl
+11 −1
Original line number Diff line number Diff line
@@ -636,7 +636,17 @@ class Descriptor(object):
      raise ValueError("Digest is for the range ending with '%s' but that isn't in our descriptor" % end)

    digest_content = raw_descriptor[start_index:end_index + len(end)]
    digest_hash = hashlib.sha1(stem.util.str_tools._to_bytes(digest_content))
    return self._digest_for_bytes(digest_content)

  def _digest_for_bytes(self, bytes_to_sign):
    """
    Provides a digest of the provided bytes

    :param bytes bytes_to_sign: the bytes for which we should generate a digest

    :returns: the digest string encoded in uppercase hex
    """
    digest_hash = hashlib.sha1(bytes_to_sign)
    return stem.util.str_tools._to_unicode(digest_hash.hexdigest().upper())

  def __getattr__(self, name):
+198 −0
Original line number Diff line number Diff line
# Copyright 2016, Patrick O'Doherty and The Tor Project
# See LICENSE for licensing information

"""
Parsing for the Tor server descriptor Ed25519 Certificates, which is used to
validate the Ed25519 key used to sign the relay descriptor.

Certificates can optionally contain CertificateExtension objects depending on their type and purpose. Currently Ed25519KeyCertificate certificates will contain one SignedWithEd25519KeyCertificateExtensio


**Module Overview:**

::

  Certificate - Tor Certificate
    |- Ed25519KeyCertificate - Certificate for Ed25519 signing key
    +- +- verify_descriptor_signature - verify a relay descriptor against a signature


  CertificateExtension - Certificate extension
  +- - SignedWithEd25519KeyCertificateExtension - Ed25519 signing key extension
"""

import base64
import hashlib
import time
from collections import OrderedDict

import stem.util.str_tools

import nacl.signing
from nacl.exceptions import BadSignatureError


SIGNATURE_LENGTH = 64
STANDARD_ATTRIBUTES_LENGTH = 40
CERTIFICATE_FLAGS_LENGTH = 4
ED25519_ROUTER_SIGNATURE_PREFIX = 'Tor router descriptor signature v1'


def _bytes_to_long(b):
  return long(b.encode('hex'), 16)


def _parse_long_offset(offset, length):
  def _parse(raw_contents):
    return _bytes_to_long(raw_contents[offset:(offset + length)])

  return _parse


def _parse_offset(offset, length):
  def _parse(raw_contents):
    return raw_contents[offset:(offset + length)]

  return _parse


def _parse_certificate(raw_contents, master_key_bytes, validate = False):
  version, cert_type = raw_contents[0:2]

  if version == '\x01':
    if cert_type == '\x04':
      return Ed25519KeyCertificate(raw_contents, master_key_bytes, validate = validate)
    elif cert_type == '\x05':
      # TLS link certificated signed with ed25519 signing key
      pass
    elif cert_type == '\x06':
      # Ed25519 authentication signed with ed25519 signing key
      pass
    else:
      raise ValueError("Unknown Certificate type %s" % cert_type.encode('hex'))
  else:
    raise ValueError("Unknown Certificate version %s" % version.encode('hex'))


def _parse_extensions(raw_contents):
  n_extensions = _bytes_to_long(raw_contents[39:40])
  if n_extensions == 0:
    return []

  extensions = []
  extension_bytes = raw_contents[STANDARD_ATTRIBUTES_LENGTH:-SIGNATURE_LENGTH]
  while len(extension_bytes) > 0:
    ext_length = _bytes_to_long(extension_bytes[0:2])
    ext_type, ext_flags = extension_bytes[2:CERTIFICATE_FLAGS_LENGTH]
    try:
      ext_data = extension_bytes[CERTIFICATE_FLAGS_LENGTH:(CERTIFICATE_FLAGS_LENGTH + ext_length)]
    except:
      raise ValueError('Certificate contained truncated extension')

    if ext_type == SignedWithEd25519KeyCertificateExtension.TYPE:
      extension = SignedWithEd25519KeyCertificateExtension(ext_type, ext_flags, ext_data)
    else:
      raise ValueError('Invalid certificate extension type: %s' % ext_type.encode('hex'))

    extensions.append(extension)
    extension_bytes = extension_bytes[CERTIFICATE_FLAGS_LENGTH + ext_length:]

  if len(extensions) != n_extensions:
    raise ValueError('n_extensions was %d but parsed %d' % (n_extensions, len(extensions)))

  return extensions


def _parse_signature(cert):
  return cert[-SIGNATURE_LENGTH:]


class Certificate(object):
  """
  See proposal #220 <https://gitweb.torproject.org/torspec.git/tree/proposals/220-ecc-id-keys.txt>
  """

  ATTRIBUTES = {
    'version': _parse_offset(0, 1),
    'cert_type': _parse_offset(1, 1),
    'expiration_date': _parse_long_offset(2, 4),
    'cert_key_type': _parse_offset(6, 1),
    'certified_key': _parse_offset(7, 32),
    'n_extensions': _parse_long_offset(39, 1),
    'extensions': _parse_extensions,
    'signature': _parse_signature
  }

  def __init__(self, raw_contents, identity_key, validate = False):
    self.certificate_bytes = raw_contents
    self.identity_key = identity_key

    self.__set_certificate_entries(raw_contents)

  def __set_certificate_entries(self, raw_contents):
    entries = OrderedDict()
    for key, func in Certificate.ATTRIBUTES.iteritems():
      try:
        entries[key] = func(raw_contents)
      except IndexError:
        raise ValueError('Unable to get bytes for %s from certificate' % key)

    for key, value in entries.iteritems():
      setattr(self, key, value)


class Ed25519KeyCertificate(Certificate):
  def __init__(self, raw_contents, identity_key, validate = False):
    super(Ed25519KeyCertificate, self).__init__(raw_contents, identity_key, validate = False)

    if validate:
      if len(self.extensions) == 0:
        raise ValueError('Ed25519KeyCertificate missing SignedWithEd25519KeyCertificateExtension extension')

      self._verify_signature()

      if (self.expiration_date * 3600) < int(time.time()):
        raise ValueError('Expired Ed25519KeyCertificate')

  def verify_descriptor_signature(self, descriptor, signature):
    missing_padding = len(signature) % 4
    signature_bytes = base64.b64decode(stem.util.str_tools._to_bytes(signature) + b'=' * missing_padding)
    verify_key = nacl.signing.VerifyKey(self.certified_key)

    signed_part = descriptor[:descriptor.index('router-sig-ed25519 ') + len('router-sig-ed25519 ')]
    descriptor_with_prefix = ED25519_ROUTER_SIGNATURE_PREFIX + signed_part
    descriptor_sha256_digest = hashlib.sha256(descriptor_with_prefix).digest()
    verify_key.verify(descriptor_sha256_digest, signature_bytes)

  def _verify_signature(self):
    if self.identity_key:
      verify_key = nacl.signing.VerifyKey(base64.b64decode(self.identity_key + '='))
    else:
      verify_key = nacl.singing.VerifyKey(self.extensions[0].ext_data)

    try:
      verify_key.verify(self.certificate_bytes[:-SIGNATURE_LENGTH], self.signature)
    except BadSignatureError:
      raise ValueError('Ed25519KeyCertificate signature invalid')


class CertificateExtension(object):
  KNOWN_TYPES = ['\x04']

  def __init__(self, ext_type, ext_flags, ext_data):
    self.ext_type = ext_type
    self.ext_flags = ext_flags
    self.ext_data = ext_data

  def is_known_type(self):
    return self.ext_type in CertificateExtension.KNOWN_TYPES

  def affects_validation(self):
    return self.ext_flags == '\x01'


class SignedWithEd25519KeyCertificateExtension(CertificateExtension):
  TYPE = '\x04'

  def __init__(self, ext_type, ext_flags, ext_data):
    super(SignedWithEd25519KeyCertificateExtension, self).__init__(ext_type, ext_flags, ext_data)
+29 −0
Original line number Diff line number Diff line
@@ -62,6 +62,8 @@ from stem.descriptor import (
  _parse_key_block,
)

from stem.descriptor.certificate import _parse_certificate

try:
  # added in python 3.2
  from functools import lru_cache
@@ -662,6 +664,14 @@ class ServerDescriptor(Descriptor):
    if expected_last_keyword and expected_last_keyword != list(entries.keys())[-1]:
      raise ValueError("Descriptor must end with a '%s' entry" % expected_last_keyword)

    if 'identity-ed25519' in entries.keys():
      if not 'router-sig-ed25519' in entries.keys():
        raise ValueError("Descriptor must have router-sig-ed25519 entry to accompany identity-ed25519")

      if 'router-sig-ed25519' != list(entries.keys())[-2]:
        if 'router-sig-ed25519' != list(entries.keys())[-1]:
          raise ValueError("Descriptor must end with a 'router-sig-ed25519' entry")

    if not self.exit_policy:
      raise ValueError("Descriptor must have at least one 'accept' or 'reject' entry")

@@ -750,6 +760,25 @@ class RelayDescriptor(ServerDescriptor):
        if signed_digest != self.digest():
          raise ValueError('Decrypted digest does not match local digest (calculated: %s, local: %s)' % (signed_digest, self.digest()))

      if stem.prereq.is_nacl_available() and self.ed25519_certificate:
        self.certificate = _parse_certificate(_bytes_for_block(self.ed25519_certificate),
                                              self.ed25519_master_key,
                                              validate)

        if self.ed25519_master_key is not None:
          if self.certificate.identity_key != self.ed25519_master_key:
            raise ValueError("master-key-ed25519 does not match ed25519 certificate identity key")

        self.certificate.verify_descriptor_signature(stem.util.str_tools._to_unicode(raw_contents),
                                                     self.ed25519_signature)

        onion_key_bytes = _bytes_for_block(self.onion_key)
        from Crypto.Util import asn1
        seq = asn1.DerSequence()
        seq.decode(onion_key_bytes)
        self._digest_for_signature(self.onion_key, self.onion_key_crosscert)


  @lru_cache()
  def digest(self):
    """
+21 −0
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@ Checks for stem dependencies. We require python 2.6 or greater (including the
  check_requirements - checks for minimum requirements for running stem
  is_python_3 - checks if python 3.0 or later is available
  is_crypto_available - checks if the cryptography module is available
  is_nacl_available - checks if the pynacl module is available
"""

import inspect
@@ -27,6 +28,7 @@ except ImportError:
  from stem.util.lru_cache import lru_cache

CRYPTO_UNAVAILABLE = "Unable to import the cryptography module. Because of this we'll be unable to verify descriptor signature integrity. You can get cryptography from: https://pypi.python.org/pypi/cryptography"
NACL_UNAVAILABLE = "Unable to import the pynacl module. Because of this we'll be unable to verify descriptor ed25519 certificate integrity. You can get pynacl from https://github.com/pyca/pynacl/"


def check_requirements():
@@ -146,3 +148,22 @@ def is_mock_available():
    return True
  except ImportError:
    return False

@lru_cache()
def is_nacl_available():
  """
  Checks if the pynacl functions we use are available. This is used for
  verifying ed25519 certificates in relay descriptor signatures.

  :returns: **True** if we can use pynacl and **False** otherwise
  """

  from stem.util import log

  try:
    from nacl import encoding
    from nacl import signing
    return True
  except ImportError:
    log.log_once('stem.prereq.is_nacl_available', log.INFO, NACL_UNAVAILABLE)
    return False
Loading