[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [stem/master] Add Ed25519 certificate extension support
commit 3ce6631670cf3a0ffb5d44736bd426c3eaa82b5d
Author: Patrick O'Doherty <p@xxxxxxxxxxx>
Date: Sun Sep 18 17:24:02 2016 -0700
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
---
requirements.txt | 1 +
stem/descriptor/__init__.py | 12 +-
stem/descriptor/certificate.py | 198 ++++++++++++++++++++++++++++++
stem/descriptor/server_descriptor.py | 29 +++++
stem/prereq.py | 21 ++++
test/unit/descriptor/server_descriptor.py | 12 ++
6 files changed, 272 insertions(+), 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index 6dc054c..3cd7160 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,3 +3,4 @@ pyflakes
pycodestyle
tox
cryptography
+pynacl
diff --git a/stem/descriptor/__init__.py b/stem/descriptor/__init__.py
index 6e8c5a4..4e90889 100644
--- a/stem/descriptor/__init__.py
+++ b/stem/descriptor/__init__.py
@@ -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):
diff --git a/stem/descriptor/certificate.py b/stem/descriptor/certificate.py
new file mode 100644
index 0000000..e26992f
--- /dev/null
+++ b/stem/descriptor/certificate.py
@@ -0,0 +1,198 @@
+# 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)
diff --git a/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py
index 3aa5fb0..c5bcaad 100644
--- a/stem/descriptor/server_descriptor.py
+++ b/stem/descriptor/server_descriptor.py
@@ -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):
"""
diff --git a/stem/prereq.py b/stem/prereq.py
index 585b619..e8769a3 100644
--- a/stem/prereq.py
+++ b/stem/prereq.py
@@ -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
diff --git a/test/unit/descriptor/server_descriptor.py b/test/unit/descriptor/server_descriptor.py
index ba8271d..c4e9b67 100644
--- a/test/unit/descriptor/server_descriptor.py
+++ b/test/unit/descriptor/server_descriptor.py
@@ -3,6 +3,7 @@ Unit tests for stem.descriptor.server_descriptor.
"""
import datetime
+import time
import io
import pickle
import tarfile
@@ -245,6 +246,7 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4=
self.assertTrue(isinstance(str(desc), str))
+ @patch('time.time', Mock(return_value = time.mktime(datetime.date(2010, 1, 1).timetuple())))
def test_with_ed25519(self):
"""
Parses a descriptor with a ed25519 identity key, as added by proposal 228
@@ -299,6 +301,16 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4=
self.assertEqual('B5E441051D139CCD84BC765D130B01E44DAC29AD', desc.digest())
self.assertEqual([], desc.get_unrecognized_lines())
+ @patch('time.time', Mock(return_value = time.mktime(datetime.date(2020, 1, 1).timetuple())))
+ def test_with_ed25519_expired_cert(self):
+ """
+ Parses a server descriptor with an expired ed25519 certificate
+ """
+ desc_text = open(get_resource('bridge_descriptor_with_ed25519'), 'rb').read()
+ desc_iter = stem.descriptor.server_descriptor._parse_file(io.BytesIO(desc_text), validate = True)
+ self.assertRaises(ValueError, list, desc_iter)
+
+
def test_bridge_with_ed25519(self):
"""
Parses a bridge descriptor with ed25519.
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits