tor-commits
Threads by month
- ----- 2025 -----
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
October 2019
- 22 participants
- 4141 discussions
commit 56290e08c48e9dc57d807ba2f4ced9096eed0054
Author: Damian Johnson <atagar(a)torproject.org>
Date: Fri Oct 4 16:42:08 2019 -0700
Parse descriptor inner layer
The introduction points made the inner layer a bit tricker than the outer
(duplicate descriptor lines are highly unusual). Think this'll do the trick,
though test data of a 'legacy-key' and 'legacy-key-cert' would be nice.
---
stem/descriptor/hidden_service.py | 159 ++++++++++++++++++++++++++++++
test/unit/descriptor/hidden_service_v3.py | 35 +++++++
2 files changed, 194 insertions(+)
diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py
index 314e623a..6911fa3c 100644
--- a/stem/descriptor/hidden_service.py
+++ b/stem/descriptor/hidden_service.py
@@ -31,6 +31,7 @@ import collections
import hashlib
import io
+import stem.client.datatype
import stem.descriptor.hsv3_crypto
import stem.prereq
import stem.util.connection
@@ -49,6 +50,7 @@ from stem.descriptor import (
_value,
_values,
_parse_simple_line,
+ _parse_if_present,
_parse_int_line,
_parse_timestamp_line,
_parse_key_block,
@@ -110,8 +112,12 @@ class DecryptionFailure(Exception):
"""
+# TODO: rename in stem 2.x (add 'V2' and drop plural)
+
class IntroductionPoints(collections.namedtuple('IntroductionPoints', INTRODUCTION_POINTS_ATTR.keys())):
"""
+ Introduction point for a v2 hidden service.
+
:var str identifier: hash of this introduction point's identity key
:var str address: address of this introduction point
:var int port: port where this introduction point is listening
@@ -122,6 +128,20 @@ 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'])):
+ """
+ Introduction point for a v3 hidden service.
+
+ :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
+ """
+
+
class AuthorizedClient(collections.namedtuple('AuthorizedClient', ['id', 'iv', 'cookie'])):
"""
Client authorized to use a v3 hidden service.
@@ -220,6 +240,90 @@ def _parse_v3_outer_clients(descriptor, entries):
descriptor.clients = clients
+def _parse_v3_inner_formats(descriptor, entries):
+ value, formats = _value('create2-formats', entries), []
+
+ for entry in value.split(' '):
+ if not entry.isdigit():
+ raise ValueError("create2-formats should only contain integers, but was '%s'" % value)
+
+ formats.append(int(entry))
+
+ descriptor.formats = formats
+
+
+def _parse_v3_introduction_points(descriptor, entries):
+ if hasattr(descriptor, '_unparsed_introduction_points'):
+ introduction_points = []
+ remaining = descriptor._unparsed_introduction_points
+
+ while remaining:
+ div = remaining.find('\nintroduction-point ', 10)
+
+ if div == -1:
+ intro_point_str = remaining
+ remaining = ''
+ else:
+ intro_point_str = remaining[:div]
+ remaining = remaining[div + 1:]
+
+ entry = _descriptor_components(intro_point_str, False)
+ 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
+
+ _, block_type, auth_key = entry['auth-key'][0]
+
+ 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
+
+ _, block_type, enc_key_cert = entry['enc-key-cert'][0]
+
+ 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
+
+ introduction_points.append(
+ IntroductionPointV3(
+ link_specifiers,
+ onion_key,
+ auth_key,
+ enc_key,
+ enc_key_cert,
+ legacy_key,
+ legacy_key_cert,
+ )
+ )
+
+ descriptor.introduction_points = introduction_points
+ del descriptor._unparsed_introduction_points
+
+
+def _parse_link_specifiers(val):
+ try:
+ val = base64.b64decode(val)
+ except Exception as exc:
+ raise ValueError('Unable to base64 decode introduction point (%s): %s' % (exc, val))
+
+ link_specifiers = []
+ count, val = stem.client.datatype.Size.CHAR.pop(val)
+
+ for i in range(count):
+ link_specifier, val = stem.client.datatype.LinkSpecifier.pop(val)
+ link_specifiers.append(link_specifier)
+
+ if val:
+ raise ValueError('Introduction point had excessive data (%s)' % val)
+
+ return link_specifiers
+
+
_parse_v2_version_line = _parse_int_line('version', 'version', allow_negative = False)
_parse_rendezvous_service_descriptor_line = _parse_simple_line('rendezvous-service-descriptor', 'descriptor_id')
_parse_permanent_key_line = _parse_key_block('permanent-key', 'permanent_key', 'RSA PUBLIC KEY')
@@ -238,6 +342,9 @@ _parse_v3_outer_auth_type = _parse_simple_line('desc-auth-type', 'auth_type')
_parse_v3_outer_ephemeral_key = _parse_simple_line('desc-auth-ephemeral-key', 'ephemeral_key')
_parse_v3_outer_encrypted = _parse_key_block('encrypted', 'encrypted', 'MESSAGE')
+_parse_v3_inner_intro_auth = _parse_simple_line('intro-auth-required', 'intro_auth', func = lambda v: v.split(' '))
+_parse_v3_inner_single_service = _parse_if_present('single-onion-service', 'is_single_service')
+
class BaseHiddenServiceDescriptor(Descriptor):
"""
@@ -691,6 +798,58 @@ class OuterLayer(Descriptor):
self._entries = entries
+class InnerLayer(Descriptor):
+ """
+ Second encryped layer of a hidden service v3 descriptor (`spec
+ <https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt#n1308>`_).
+
+ .. versionadded:: 1.8.0
+
+ :var list formats: **\\*** recognized CREATE2 cell formats
+ :var list intro_auth: **\\*** introduction-layer authentication types
+ :var bool is_single_service: **\\*** **True** if this is a `single onion service <https://gitweb.torproject.org/torspec.git/tree/proposals/260-rend-single-on…>`_, **False** otherwise
+ :var list introduction_points: :class:`~stem.descriptor.hidden_service.IntroductionPointV3` where this service is reachable
+
+ **\\*** attribute is either required when we're parsed with validation or has
+ a default value, others are left as **None** if undefined
+ """
+
+ ATTRIBUTES = {
+ 'formats': ([], _parse_v3_inner_formats),
+ 'intro_auth': ([], _parse_v3_inner_intro_auth),
+ 'is_single_service': (False, _parse_v3_inner_single_service),
+ 'introduction_points': ([], _parse_v3_introduction_points),
+ }
+
+ PARSER_FOR_LINE = {
+ 'create2-formats': _parse_v3_inner_formats,
+ 'intro-auth-required': _parse_v3_inner_intro_auth,
+ 'single-onion-service': _parse_v3_inner_single_service,
+ }
+
+ def __init__(self, content, validate = False):
+ super(InnerLayer, self).__init__(content, lazy_load = not validate)
+
+ # inner layer begins with a few header fields, followed by multiple any
+ # number of introduction-points
+
+ div = content.find('\nintroduction-point ')
+
+ if div != -1:
+ self._unparsed_introduction_points = content[div + 1:]
+ content = content[:div]
+ else:
+ self._unparsed_introduction_points = None
+
+ entries = _descriptor_components(content, validate)
+
+ if validate:
+ self._parse(entries, validate)
+ _parse_v3_introduction_points(self, entries)
+ else:
+ self._entries = entries
+
+
# TODO: drop this alias in stem 2.x
HiddenServiceDescriptor = HiddenServiceDescriptorV2
diff --git a/test/unit/descriptor/hidden_service_v3.py b/test/unit/descriptor/hidden_service_v3.py
index 3140d193..36b85f7c 100644
--- a/test/unit/descriptor/hidden_service_v3.py
+++ b/test/unit/descriptor/hidden_service_v3.py
@@ -5,6 +5,7 @@ Unit tests for stem.descriptor.hidden_service for version 3.
import functools
import unittest
+import stem.client.datatype
import stem.descriptor
import stem.prereq
@@ -12,6 +13,7 @@ from stem.descriptor.hidden_service import (
REQUIRED_V3_FIELDS,
HiddenServiceDescriptorV3,
OuterLayer,
+ InnerLayer,
)
from test.unit.descriptor import (
@@ -78,6 +80,39 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase):
self.assertEqual('or3nS3ScSPYfLJuP9osGiQ', client.iv)
self.assertEqual('B40RdIWhw7kdA7lt3KJPvQ', client.cookie)
+ def test_inner_layer(self):
+ """
+ Parse the inner layer of our test descriptor.
+ """
+
+ with open(get_resource('hidden_service_v3_inner_layer'), 'rb') as descriptor_file:
+ desc = InnerLayer(descriptor_file.read())
+
+ self.assertEqual([2], desc.formats)
+ self.assertEqual(['ed25519'], desc.intro_auth)
+ self.assertEqual(True, desc.is_single_service)
+ self.assertEqual(4, len(desc.introduction_points))
+
+ intro_point = desc.introduction_points[0]
+
+ self.assertEqual(2, len(intro_point.link_specifiers))
+
+ link_specifier = intro_point.link_specifiers[0]
+ self.assertEqual(stem.client.datatype.LinkByFingerprint, type(link_specifier))
+ self.assertEqual('CCCCCCCCCCCCCCCCCCCC', link_specifier.fingerprint)
+
+ link_specifier = intro_point.link_specifiers[1]
+ self.assertEqual(stem.client.datatype.LinkByIPv4, type(link_specifier))
+ self.assertEqual('1.2.3.4', link_specifier.address)
+ self.assertEqual(9001, link_specifier.port)
+
+ self.assertEqual('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', intro_point.onion_key)
+ self.assertTrue('ID2l9EFNrp' in intro_point.auth_key)
+ self.assertEqual('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', intro_point.enc_key)
+ self.assertTrue('ZvjPt5IfeQ', intro_point.enc_key_cert)
+ self.assertEqual(None, intro_point.legacy_key)
+ self.assertEqual(None, intro_point.legacy_key_cert)
+
def test_required_fields(self):
"""
Check that we require the mandatory fields.
1
0
commit ceb691b7d33f942c487934d7c81e5d4d5bbd4b0d
Author: Damian Johnson <atagar(a)torproject.org>
Date: Fri Oct 4 13:54:34 2019 -0700
Parse link specifiers
Hidden service v3 introductory points begin with link specifiers, which are
used by EXTEND cells as well. We'll need this when expanding client
functionality, so might as well bite the bullet and do proper parsing
of these fields.
---
stem/client/datatype.py | 136 ++++++++++++++++++++++++++++++++++++-
stem/util/connection.py | 3 +
test/settings.cfg | 1 +
test/unit/client/link_specifier.py | 59 ++++++++++++++++
4 files changed, 197 insertions(+), 2 deletions(-)
diff --git a/stem/client/datatype.py b/stem/client/datatype.py
index fa393d30..1f49388b 100644
--- a/stem/client/datatype.py
+++ b/stem/client/datatype.py
@@ -25,6 +25,12 @@ users.** See our :class:`~stem.client.Relay` the API you probably want.
|- unpack - decodes content
+- pop - decodes content with remainder
+ LinkSpecifier - Communication method relays in a circuit.
+ |- LinkByIPv4 - TLS connection to an IPv4 address.
+ |- LinkByIPv6 - TLS connection to an IPv6 address.
+ |- LinkByFingerprint - SHA1 identity fingerprint.
+ +- LinkByEd25519 - Ed25519 identity fingerprint.
+
KDF - KDF-TOR derivatived attributes
+- from_value - parses key material
@@ -112,6 +118,7 @@ users.** See our :class:`~stem.client.Relay` the API you probably want.
===================== ===========
"""
+import binascii
import collections
import hashlib
import struct
@@ -449,7 +456,7 @@ class Address(Field):
if len(value) != 4:
raise ValueError('Packed IPv4 addresses should be four bytes, but was: %s' % repr(value))
- self.value = '.'.join([str(Size.CHAR.unpack(value[i:i + 1])) for i in range(4)])
+ self.value = _unpack_ipv4_address(value)
self.value_bin = value
elif self.type == AddrType.IPv6:
if stem.util.connection.is_valid_ipv6_address(value):
@@ -459,7 +466,7 @@ class Address(Field):
if len(value) != 16:
raise ValueError('Packed IPv6 addresses should be sixteen bytes, but was: %s' % repr(value))
- self.value = ':'.join(['%04x' % Size.SHORT.unpack(value[i * 2:(i + 1) * 2]) for i in range(8)])
+ self.value = _unpack_ipv6_address(value)
self.value_bin = value
else:
# The spec doesn't really tell us what form to expect errors to be. For
@@ -527,6 +534,119 @@ class Certificate(Field):
return stem.util._hash_attr(self, 'type_int', 'value')
+class LinkSpecifier(object):
+ """
+ Method of communicating with a circuit's relay. Recognized link specification
+ types are an instantiation of a subclass. For more information see the
+ `EXTEND cell specification
+ <https://gitweb.torproject.org/torspec.git/tree/tor-spec.txt#n975>`_.
+
+ :var int type: numeric identifier of our type
+ :var bytes value: encoded link specification destination
+ """
+
+ def __init__(self, link_type, value):
+ self.type = link_type
+ self.value = value
+
+ @staticmethod
+ def pop(content):
+ # LSTYPE (Link specifier type) [1 byte]
+ # LSLEN (Link specifier length) [1 byte]
+ # LSPEC (Link specifier) [LSLEN bytes]
+
+ link_type, content = Size.CHAR.pop(content)
+ value_size, content = Size.CHAR.pop(content)
+
+ if value_size > len(content):
+ raise ValueError('Link specifier should have %i bytes, but only had %i remaining' % (value_size, len(content)))
+
+ value, content = split(content, value_size)
+
+ if link_type == 0:
+ return LinkByIPv4(value), content
+ elif link_type == 1:
+ return LinkByIPv6(value), content
+ elif link_type == 2:
+ return LinkByFingerprint(value), content
+ elif link_type == 3:
+ return LinkByEd25519(value), content
+ else:
+ return LinkSpecifier(link_type, value), content # unrecognized type
+
+
+class LinkByIPv4(LinkSpecifier):
+ """
+ TLS connection to an IPv4 address.
+
+ :var str address: relay IPv4 address
+ :var int port: relay ORPort
+ """
+
+ def __init__(self, value):
+ super(LinkByIPv4, self).__init__(0, value)
+
+ if len(value) != 6:
+ raise ValueError('IPv4 link specifiers should be six bytes, but was %i instead: %s' % (len(value), binascii.hexlify(value)))
+
+ address_bin, value = split(value, 4)
+ self.address = _unpack_ipv4_address(address_bin)
+
+ self.port, _ = Size.SHORT.pop(value)
+
+
+class LinkByIPv6(LinkSpecifier):
+ """
+ TLS connection to an IPv6 address.
+
+ :var str address: relay IPv6 address
+ :var int port: relay ORPort
+ """
+
+ def __init__(self, value):
+ super(LinkByIPv6, self).__init__(1, value)
+
+ if len(value) != 18:
+ raise ValueError('IPv6 link specifiers should be eighteen bytes, but was %i instead: %s' % (len(value), binascii.hexlify(value)))
+
+ address_bin, value = split(value, 16)
+ self.address = _unpack_ipv6_address(address_bin)
+
+ self.port, _ = Size.SHORT.pop(value)
+
+
+class LinkByFingerprint(LinkSpecifier):
+ """
+ Connection to a SHA1 identity fingerprint.
+
+ :var str fingerprint: relay sha1 fingerprint
+ """
+
+ def __init__(self, value):
+ super(LinkByFingerprint, self).__init__(2, value)
+
+ if len(value) != 20:
+ raise ValueError('Fingerprint link specifiers should be twenty bytes, but was %i instead: %s' % (len(value), binascii.hexlify(value)))
+
+ self.fingerprint = value
+
+
+class LinkByEd25519(LinkSpecifier):
+ """
+ Connection to a Ed25519 identity fingerprint.
+
+ :var str fingerprint: relay ed25519 fingerprint
+ """
+
+ def __init__(self, value):
+ super(LinkByEd25519, self).__init__(3, value)
+
+ if len(value) != 32:
+ raise ValueError('Fingerprint link specifiers should be thirty two bytes, but was %i instead: %s' % (len(value), binascii.hexlify(value)))
+
+ self.fingerprint = value
+
+
class KDF(collections.namedtuple('KDF', ['key_hash', 'forward_digest', 'backward_digest', 'forward_key', 'backward_key'])):
"""
Computed KDF-TOR derived values for TAP, CREATE_FAST handshakes, and hidden
@@ -561,6 +681,18 @@ class KDF(collections.namedtuple('KDF', ['key_hash', 'forward_digest', 'backward
return KDF(key_hash, forward_digest, backward_digest, forward_key, backward_key)
+def _unpack_ipv4_address(value):
+ # convert bytes to a standard IPv4 address
+
+ return '.'.join([str(Size.CHAR.unpack(value[i:i + 1])) for i in range(4)])
+
+
+def _unpack_ipv6_address(value):
+ # convert bytes to a standard IPv6 address
+
+ return ':'.join(['%04x' % Size.SHORT.unpack(value[i * 2:(i + 1) * 2]) for i in range(8)])
+
+
setattr(Size, 'CHAR', Size('CHAR', 1, '!B'))
setattr(Size, 'SHORT', Size('SHORT', 2, '!H'))
setattr(Size, 'LONG', Size('LONG', 4, '!L'))
diff --git a/stem/util/connection.py b/stem/util/connection.py
index 421eb9a3..5bfb1024 100644
--- a/stem/util/connection.py
+++ b/stem/util/connection.py
@@ -769,6 +769,9 @@ def _get_binary(value, bits):
return ''.join([str((value >> y) & 1) for y in range(bits - 1, -1, -1)])
+# TODO: In stem 2.x we should consider unifying this with
+# stem.client.datatype's _unpack_ipv4_address() and _unpack_ipv6_address().
+
def _address_to_binary(address):
"""
Provides the binary value for an IPv4 or IPv6 address.
diff --git a/test/settings.cfg b/test/settings.cfg
index eca719df..8fe79d2a 100644
--- a/test/settings.cfg
+++ b/test/settings.cfg
@@ -287,6 +287,7 @@ test.unit_tests
|test.unit.client.address.TestAddress
|test.unit.client.link_protocol.TestLinkProtocol
|test.unit.client.certificate.TestCertificate
+|test.unit.client.link_specifier.TestLinkSpecifier
|test.unit.client.kdf.TestKDF
|test.unit.client.cell.TestCell
|test.unit.connection.authentication.TestAuthenticate
diff --git a/test/unit/client/link_specifier.py b/test/unit/client/link_specifier.py
new file mode 100644
index 00000000..470ee276
--- /dev/null
+++ b/test/unit/client/link_specifier.py
@@ -0,0 +1,59 @@
+"""
+Unit tests for stem.client.datatype.LinkSpecifier and subclasses.
+"""
+
+import unittest
+
+from stem.client.datatype import (
+ LinkSpecifier,
+ LinkByIPv4,
+ LinkByIPv6,
+ LinkByFingerprint,
+ LinkByEd25519,
+)
+
+
+class TestLinkSpecifier(unittest.TestCase):
+ def test_link_by_ipv4_address(self):
+ destination, _ = LinkSpecifier.pop(b'\x00\x06\x01\x02\x03\x04#)')
+
+ self.assertEqual(LinkByIPv4, type(destination))
+ self.assertEqual(0, destination.type)
+ self.assertEqual(b'\x01\x02\x03\x04#)', destination.value)
+ self.assertEqual('1.2.3.4', destination.address)
+ self.assertEqual(9001, destination.port)
+
+ def test_link_by_ipv6_address(self):
+ destination, _ = LinkSpecifier.pop(b'\x01\x12&\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01#)')
+
+ self.assertEqual(LinkByIPv6, type(destination))
+ self.assertEqual(1, destination.type)
+ self.assertEqual(b'&\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01#)', destination.value)
+ self.assertEqual('2600:0000:0000:0000:0000:0000:0000:0001', destination.address)
+ self.assertEqual(9001, destination.port)
+
+ def test_link_by_fingerprint(self):
+ destination, _ = LinkSpecifier.pop(b'\x02\x14CCCCCCCCCCCCCCCCCCCC')
+
+ self.assertEqual(LinkByFingerprint, type(destination))
+ self.assertEqual(2, destination.type)
+ self.assertEqual(b'CCCCCCCCCCCCCCCCCCCC', destination.value)
+ self.assertEqual('CCCCCCCCCCCCCCCCCCCC', destination.fingerprint)
+
+ def test_link_by_ed25519fingerprint(self):
+ destination, _ = LinkSpecifier.pop(b'\x03\x20CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC')
+
+ self.assertEqual(LinkByEd25519, type(destination))
+ self.assertEqual(3, destination.type)
+ self.assertEqual(b'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC', destination.value)
+ self.assertEqual('CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC', destination.fingerprint)
+
+ def test_unrecognized_type(self):
+ destination, _ = LinkSpecifier.pop(b'\x04\x20CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC')
+
+ self.assertEqual(LinkSpecifier, type(destination))
+ self.assertEqual(4, destination.type)
+ self.assertEqual(b'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC', destination.value)
+
+ def test_wrong_size(self):
+ self.assertRaisesWith(ValueError, 'Link specifier should have 32 bytes, but only had 7 remaining', LinkSpecifier.pop, b'\x04\x20CCCCCCC')
1
0
commit 3498ebedccdcd59cfc9072e2f24f5c9eb0c3daae
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sat Oct 5 15:06:27 2019 -0700
Public decryption method
After considering a few APIs descided to keep this simple, and simply return
the InnerLayer which references the OuterLayer.
---
stem/descriptor/hidden_service.py | 58 ++++++++++++++++++++-----------
test/unit/descriptor/hidden_service_v3.py | 36 ++++++++++++++-----
2 files changed, 64 insertions(+), 30 deletions(-)
diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py
index 6911fa3c..e4ee13e3 100644
--- a/stem/descriptor/hidden_service.py
+++ b/stem/descriptor/hidden_service.py
@@ -671,8 +671,10 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
def create(cls, attr = None, exclude = (), validate = True, sign = False):
return cls(cls.content(attr, exclude, sign), validate = validate, skip_crypto_validation = not sign)
- def __init__(self, raw_contents, validate = False, skip_crypto_validation = False):
+ def __init__(self, raw_contents, validate = False):
super(HiddenServiceDescriptorV3, self).__init__(raw_contents, lazy_load = not validate)
+
+ self._inner_layer = None
entries = _descriptor_components(raw_contents, validate)
if validate:
@@ -691,39 +693,49 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
else:
self._entries = entries
- # TODO: The following is only marked as private because it is a work in
- # progress. This will probably become something like "body()" which decrypts
- # and parses the internal descriptor content.
+ def decrypt(self, onion_address, validate = False):
+ """
+ Decrypt this descriptor. Hidden serice descriptors contain two encryption
+ layers (:class:`~stem.descriptor.hidden_service.OuterLayer` and
+ :class:`~stem.descriptor.hidden_service.InnerLayer`).
+
+ :param str onion_address: hidden service address this descriptor is from
+ :param bool validate: perform validation checks on decrypted content
+
+ :returns: :class:`~stem.descriptor.hidden_service.InnerLayer` with our
+ decrypted content
+
+ :raises:
+ * **ImportError** if required cryptography or sha3 module is unavailable
+ * **ValueError** if unable to decrypt or validation fails
+ """
- def _decrypt(self, onion_address, outer_layer = False):
if not stem.prereq.is_crypto_available(ed25519 = True):
raise ImportError('Hidden service descriptor decryption requires cryptography version 2.6')
elif not stem.prereq._is_sha3_available():
raise ImportError('Hidden service descriptor decryption requires python 3.6+ or the pysha3 module (https://pypi.org/project/pysha3/)')
- blinded_key = self.signing_cert.signing_key()
-
- if not blinded_key:
- raise ValueError('No signing key is present')
+ if self._inner_layer is None:
+ blinded_key = self.signing_cert.signing_key()
- identity_public_key = HiddenServiceDescriptorV3._public_key_from_address(onion_address)
+ if not blinded_key:
+ raise ValueError('No signing key is present')
- # credential = H('credential' | public-identity-key)
- # subcredential = H('subcredential' | credential | blinded-public-key)
+ identity_public_key = HiddenServiceDescriptorV3._public_key_from_address(onion_address)
- credential = hashlib.sha3_256(b'credential%s' % (identity_public_key)).digest()
- subcredential = hashlib.sha3_256(b'subcredential%s%s' % (credential, blinded_key)).digest()
+ # credential = H('credential' | public-identity-key)
+ # subcredential = H('subcredential' | credential | blinded-public-key)
- outter_layer_plaintext = stem.descriptor.hsv3_crypto.decrypt_outter_layer(self.superencrypted, self.revision_counter, blinded_key, subcredential)
+ credential = hashlib.sha3_256(b'credential%s' % (identity_public_key)).digest()
+ subcredential = hashlib.sha3_256(b'subcredential%s%s' % (credential, blinded_key)).digest()
- if outer_layer:
- return outter_layer_plaintext
+ outer_layer = OuterLayer(stem.descriptor.hsv3_crypto.decrypt_outter_layer(self.superencrypted, self.revision_counter, blinded_key, subcredential), validate)
- inner_layer_ciphertext = OuterLayer(outter_layer_plaintext).encrypted
+ inner_layer_plaintext = stem.descriptor.hsv3_crypto.decrypt_inner_layer(outer_layer.encrypted, self.revision_counter, blinded_key, subcredential)
- inner_layer_plaintext = stem.descriptor.hsv3_crypto.decrypt_inner_layer(inner_layer_ciphertext, self.revision_counter, blinded_key, subcredential)
+ self._inner_layer = InnerLayer(inner_layer_plaintext, validate, outer_layer)
- return inner_layer_plaintext
+ return self._inner_layer
@staticmethod
def _public_key_from_address(onion_address):
@@ -805,6 +817,8 @@ class InnerLayer(Descriptor):
.. versionadded:: 1.8.0
+ :var stem.descriptor.hidden_service.OuterLayer outer: enclosing encryption layer
+
:var list formats: **\\*** recognized CREATE2 cell formats
:var list intro_auth: **\\*** introduction-layer authentication types
:var bool is_single_service: **\\*** **True** if this is a `single onion service <https://gitweb.torproject.org/torspec.git/tree/proposals/260-rend-single-on…>`_, **False** otherwise
@@ -827,9 +841,11 @@ class InnerLayer(Descriptor):
'single-onion-service': _parse_v3_inner_single_service,
}
- def __init__(self, content, validate = False):
+ def __init__(self, content, validate = False, outer_layer = None):
super(InnerLayer, self).__init__(content, lazy_load = not validate)
+ self.outer = outer_layer
+
# inner layer begins with a few header fields, followed by multiple any
# number of introduction-points
diff --git a/test/unit/descriptor/hidden_service_v3.py b/test/unit/descriptor/hidden_service_v3.py
index 36b85f7c..093d5cb6 100644
--- a/test/unit/descriptor/hidden_service_v3.py
+++ b/test/unit/descriptor/hidden_service_v3.py
@@ -35,6 +35,15 @@ BDwQZ8rhp05oCqhhY3oFHqG9KS7HGzv9g2v1/PrVJMbkfpwu1YK4b3zIZAk=
-----END ED25519 CERT-----\
"""
+with open(get_resource('hidden_service_v3'), 'rb') as descriptor_file:
+ HS_DESC_STR = descriptor_file.read()
+
+with open(get_resource('hidden_service_v3_outer_layer'), 'rb') as outer_layer_file:
+ OUTER_LAYER_STR = outer_layer_file.read()
+
+with open(get_resource('hidden_service_v3_inner_layer'), 'rb') as inner_layer_file:
+ INNER_LAYER_STR = inner_layer_file.read()
+
class TestHiddenServiceDescriptorV3(unittest.TestCase):
def test_real_descriptor(self):
@@ -54,20 +63,30 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase):
self.assertTrue('eaH8VdaTKS' in desc.superencrypted)
self.assertEqual('aglChCQF+lbzKgyxJJTpYGVShV/GMDRJ4+cRGCp+a2y/yX/tLSh7hzqI7rVZrUoGj74Xr1CLMYO3fXYCS+DPDQ', desc.signature)
- if stem.prereq.is_crypto_available(ed25519 = True) and stem.prereq._is_sha3_available():
- with open(get_resource('hidden_service_v3_outer_layer'), 'rb') as outer_layer_file:
- self.assertEqual(outer_layer_file.read(), desc._decrypt(HS_ADDRESS, outer_layer = True))
+ def test_decryption(self):
+ """
+ Decrypt our descriptor and validate its content.
+ """
+
+ if not stem.prereq.is_crypto_available(ed25519 = True):
+ self.skipTest('(requires cryptography ed25519 support)')
+ return
+ elif not stem.prereq._is_sha3_available():
+ self.skipTest('(requires sha3 support)')
+ return
+
+ desc = HiddenServiceDescriptorV3.from_str(HS_DESC_STR)
+ inner_layer = desc.decrypt(HS_ADDRESS)
- with open(get_resource('hidden_service_v3_inner_layer'), 'rb') as outer_layer_file:
- self.assertEqual(outer_layer_file.read(), desc._decrypt(HS_ADDRESS, outer_layer = False))
+ self.assertEqual(INNER_LAYER_STR, str(inner_layer))
+ self.assertEqual(OUTER_LAYER_STR.rstrip(b'\x00'), str(inner_layer.outer))
def test_outer_layer(self):
"""
Parse the outer layer of our test descriptor.
"""
- with open(get_resource('hidden_service_v3_outer_layer'), 'rb') as descriptor_file:
- desc = OuterLayer(descriptor_file.read())
+ desc = OuterLayer(OUTER_LAYER_STR)
self.assertEqual('x25519', desc.auth_type)
self.assertEqual('WjZCU9sV1oxkxaPcd7/YozeZgq0lEs6DhWyrdYRNJR4=', desc.ephemeral_key)
@@ -85,8 +104,7 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase):
Parse the inner layer of our test descriptor.
"""
- with open(get_resource('hidden_service_v3_inner_layer'), 'rb') as descriptor_file:
- desc = InnerLayer(descriptor_file.read())
+ desc = InnerLayer(INNER_LAYER_STR)
self.assertEqual([2], desc.formats)
self.assertEqual(['ed25519'], desc.intro_auth)
1
0

06 Oct '19
commit 80def07ebfdbb2fe00b85a6721fd3cbb2b4220b2
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sat Oct 5 18:41:45 2019 -0700
Merge hsv3_crypto into hidden_service.py
---
stem/client/datatype.py | 4 +-
stem/descriptor/hidden_service.py | 88 +++++++++++++++-----
stem/descriptor/hsv3_crypto.py | 128 ------------------------------
test/unit/descriptor/hidden_service_v3.py | 8 +-
4 files changed, 76 insertions(+), 152 deletions(-)
diff --git a/stem/client/datatype.py b/stem/client/datatype.py
index 1f49388b..7e33e353 100644
--- a/stem/client/datatype.py
+++ b/stem/client/datatype.py
@@ -628,7 +628,7 @@ class LinkByFingerprint(LinkSpecifier):
if len(value) != 20:
raise ValueError('Fingerprint link specifiers should be twenty bytes, but was %i instead: %s' % (len(value), binascii.hexlify(value)))
- self.fingerprint = value
+ self.fingerprint = stem.util.str_tools._to_unicode(value)
class LinkByEd25519(LinkSpecifier):
@@ -644,7 +644,7 @@ class LinkByEd25519(LinkSpecifier):
if len(value) != 32:
raise ValueError('Fingerprint link specifiers should be thirty two bytes, but was %i instead: %s' % (len(value), binascii.hexlify(value)))
- self.fingerprint = value
+ self.fingerprint = stem.util.str_tools._to_unicode(value)
class KDF(collections.namedtuple('KDF', ['key_hash', 'forward_digest', 'backward_digest', 'forward_key', 'backward_key'])):
diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py
index e4ee13e3..601348d9 100644
--- a/stem/descriptor/hidden_service.py
+++ b/stem/descriptor/hidden_service.py
@@ -30,9 +30,9 @@ import binascii
import collections
import hashlib
import io
+import struct
import stem.client.datatype
-import stem.descriptor.hsv3_crypto
import stem.prereq
import stem.util.connection
import stem.util.str_tools
@@ -105,6 +105,12 @@ BASIC_AUTH = 1
STEALTH_AUTH = 2
CHECKSUM_CONSTANT = b'.onion checksum'
+SALT_LEN = 16
+MAC_LEN = 32
+
+S_KEY_LEN = 32
+S_IV_LEN = 16
+
class DecryptionFailure(Exception):
"""
@@ -132,6 +138,8 @@ class IntroductionPointV3(collections.namedtuple('IntroductionPointV3', ['link_s
"""
Introduction point for a v3 hidden service.
+ .. 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
@@ -194,6 +202,46 @@ def _parse_file(descriptor_file, desc_type = None, validate = False, **kwargs):
break # done parsing file
+def _decrypt_layer(encrypted_block, constant, revision_counter, subcredential, blinded_key):
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+ from cryptography.hazmat.backends import default_backend
+
+ def pack(val):
+ return struct.pack('>Q', val)
+
+ if encrypted_block.startswith('-----BEGIN MESSAGE-----\n') and encrypted_block.endswith('\n-----END MESSAGE-----'):
+ encrypted_block = encrypted_block[24:-22]
+
+ try:
+ encrypted = base64.b64decode(encrypted_block)
+ except:
+ raise ValueError('Unable to decode encrypted block as base64')
+
+ if len(encrypted) < SALT_LEN + MAC_LEN:
+ raise ValueError('Encrypted block malformed (only %i bytes)' % len(encrypted))
+
+ salt = encrypted[:SALT_LEN]
+ ciphertext = encrypted[SALT_LEN:-MAC_LEN]
+ expected_mac = encrypted[-MAC_LEN:]
+
+ kdf = hashlib.shake_256(blinded_key + subcredential + pack(revision_counter) + salt + constant)
+ keys = kdf.digest(S_KEY_LEN + S_IV_LEN + MAC_LEN)
+
+ secret_key = keys[:S_KEY_LEN]
+ secret_iv = keys[S_KEY_LEN:S_KEY_LEN + S_IV_LEN]
+ mac_key = keys[S_KEY_LEN + S_IV_LEN:]
+
+ mac = hashlib.sha3_256(pack(len(mac_key)) + mac_key + pack(len(salt)) + salt + ciphertext).digest()
+
+ if mac != expected_mac:
+ raise ValueError('Malformed mac (expected %s, but was %s)' % (expected_mac, mac))
+
+ cipher = Cipher(algorithms.AES(secret_key), modes.CTR(secret_iv), default_backend())
+ decryptor = cipher.decryptor()
+
+ return stem.util.str_tools._to_unicode(decryptor.update(ciphertext) + decryptor.finalize())
+
+
def _parse_protocol_versions_line(descriptor, entries):
value = _value('protocol-versions', entries)
@@ -693,14 +741,13 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
else:
self._entries = entries
- def decrypt(self, onion_address, validate = False):
+ def decrypt(self, onion_address):
"""
Decrypt this descriptor. Hidden serice descriptors contain two encryption
layers (:class:`~stem.descriptor.hidden_service.OuterLayer` and
:class:`~stem.descriptor.hidden_service.InnerLayer`).
:param str onion_address: hidden service address this descriptor is from
- :param bool validate: perform validation checks on decrypted content
:returns: :class:`~stem.descriptor.hidden_service.InnerLayer` with our
decrypted content
@@ -721,19 +768,15 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
if not blinded_key:
raise ValueError('No signing key is present')
- identity_public_key = HiddenServiceDescriptorV3._public_key_from_address(onion_address)
-
# credential = H('credential' | public-identity-key)
# subcredential = H('subcredential' | credential | blinded-public-key)
+ identity_public_key = HiddenServiceDescriptorV3._public_key_from_address(onion_address)
credential = hashlib.sha3_256(b'credential%s' % (identity_public_key)).digest()
subcredential = hashlib.sha3_256(b'subcredential%s%s' % (credential, blinded_key)).digest()
- outer_layer = OuterLayer(stem.descriptor.hsv3_crypto.decrypt_outter_layer(self.superencrypted, self.revision_counter, blinded_key, subcredential), validate)
-
- inner_layer_plaintext = stem.descriptor.hsv3_crypto.decrypt_inner_layer(outer_layer.encrypted, self.revision_counter, blinded_key, subcredential)
-
- self._inner_layer = InnerLayer(inner_layer_plaintext, validate, outer_layer)
+ outer_layer = OuterLayer._decrypt(self.superencrypted, self.revision_counter, subcredential, blinded_key)
+ self._inner_layer = InnerLayer._decrypt(outer_layer, self.revision_counter, subcredential, blinded_key)
return self._inner_layer
@@ -753,16 +796,16 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
decoded_address = base64.b32decode(onion_address.upper())
pubkey = decoded_address[:32]
- checksum = decoded_address[32:34]
+ expected_checksum = decoded_address[32:34]
version = decoded_address[34:35]
- # validate our address checksum
+ checksum = hashlib.sha3_256(CHECKSUM_CONSTANT + pubkey + version).digest()[:2]
- my_checksum_body = b'%s%s%s' % (CHECKSUM_CONSTANT, pubkey, version)
- my_checksum = hashlib.sha3_256(my_checksum_body).digest()[:2]
+ if expected_checksum != checksum:
+ checksum_str = stem.util.str_tools._to_unicode(binascii.hexlify(checksum))
+ expected_checksum_str = stem.util.str_tools._to_unicode(binascii.hexlify(expected_checksum))
- if (checksum != my_checksum):
- raise ValueError('Bad checksum (expected %s but was %s)' % (binascii.hexlify(checksum), binascii.hexlify(my_checksum)))
+ raise ValueError('Bad checksum (expected %s but was %s)' % (expected_checksum_str, checksum_str))
return pubkey
@@ -798,8 +841,13 @@ class OuterLayer(Descriptor):
'encrypted': _parse_v3_outer_encrypted,
}
+ @staticmethod
+ def _decrypt(encrypted, revision_counter, subcredential, blinded_key):
+ plaintext = _decrypt_layer(encrypted, b'hsdir-superencrypted-data', revision_counter, subcredential, blinded_key)
+ return OuterLayer(plaintext)
+
def __init__(self, content, validate = False):
- content = content.rstrip(b'\x00') # strip null byte padding
+ content = content.rstrip('\x00') # strip null byte padding
super(OuterLayer, self).__init__(content, lazy_load = not validate)
entries = _descriptor_components(content, validate)
@@ -841,9 +889,13 @@ class InnerLayer(Descriptor):
'single-onion-service': _parse_v3_inner_single_service,
}
+ @staticmethod
+ def _decrypt(outer_layer, revision_counter, subcredential, blinded_key):
+ plaintext = _decrypt_layer(outer_layer.encrypted, b'hsdir-encrypted-data', revision_counter, subcredential, blinded_key)
+ return InnerLayer(plaintext, outer_layer = outer_layer)
+
def __init__(self, content, validate = False, outer_layer = None):
super(InnerLayer, self).__init__(content, lazy_load = not validate)
-
self.outer = outer_layer
# inner layer begins with a few header fields, followed by multiple any
diff --git a/stem/descriptor/hsv3_crypto.py b/stem/descriptor/hsv3_crypto.py
deleted file mode 100644
index 9acb5242..00000000
--- a/stem/descriptor/hsv3_crypto.py
+++ /dev/null
@@ -1,128 +0,0 @@
-import base64
-import hashlib
-import struct
-
-"""
-Onion addresses
-
- onion_address = base32(PUBKEY | CHECKSUM | VERSION) + '.onion'
- CHECKSUM = H('.onion checksum' | PUBKEY | VERSION)[:2]
-
- - PUBKEY is the 32 bytes ed25519 master pubkey of the hidden service.
- - VERSION is an one byte version field (default value '\x03')
- - '.onion checksum' is a constant string
- - CHECKSUM is truncated to two bytes before inserting it in onion_address
-"""
-
-
-"""
-Blinded key stuff
-
- Now wrt SRVs, if a client is in the time segment between a new time period
- and a new SRV (i.e. the segments drawn with '-') it uses the current SRV,
- else if the client is in a time segment between a new SRV and a new time
- period (i.e. the segments drawn with '='), it uses the previous SRV.
-"""
-
-pass
-
-"""
-Basic descriptor logic:
-
- SALT = 16 bytes from H(random), changes each time we rebuld the
- descriptor even if the content of the descriptor hasn't changed.
- (So that we don't leak whether the intro point list etc. changed)
-
- secret_input = SECRET_DATA | subcredential | INT_8(revision_counter)
-
- keys = KDF(secret_input | salt | STRING_CONSTANT, S_KEY_LEN + S_IV_LEN + MAC_KEY_LEN)
-
- SECRET_KEY = first S_KEY_LEN bytes of keys
- SECRET_IV = next S_IV_LEN bytes of keys
- MAC_KEY = last MAC_KEY_LEN bytes of keys
-
-
-Layer data:
-
- 2.5.1.1. First layer encryption logic
- SECRET_DATA = blinded-public-key
- STRING_CONSTANT = 'hsdir-superencrypted-data'
-
- 2.5.2.1. Second layer encryption keys
- SECRET_DATA = blinded-public-key | descriptor_cookie
- STRING_CONSTANT = 'hsdir-encrypted-data'
-"""
-
-SALT_LEN = 16
-MAC_LEN = 32
-
-S_KEY_LEN = 32
-S_IV_LEN = 16
-MAC_KEY_LEN = 32
-
-
-def _ciphertext_mac_is_valid(key, salt, ciphertext, mac):
- """
- Instantiate MAC(key=k, message=m) with H(k_len | k | m), where k_len is
- htonll(len(k)).
-
- XXX spec: H(mac_key_len | mac_key | salt_len | salt | encrypted)
- """
-
- # Construct our own MAC first
- key_len = struct.pack('>Q', len(key))
- salt_len = struct.pack('>Q', len(salt))
-
- my_mac_body = b'%s%s%s%s%s' % (key_len, key, salt_len, salt, ciphertext)
- my_mac = hashlib.sha3_256(my_mac_body).digest()
-
- # Compare the two MACs
- return my_mac == mac
-
-
-def _decrypt_descriptor_layer(ciphertext_blob_b64, revision_counter, subcredential, secret_data, string_constant):
- from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
- from cryptography.hazmat.backends import default_backend
-
- if ciphertext_blob_b64.startswith('-----BEGIN MESSAGE-----\n') and ciphertext_blob_b64.endswith('\n-----END MESSAGE-----'):
- ciphertext_blob_b64 = ciphertext_blob_b64[24:-22]
-
- # decode the thing
- ciphertext_blob = base64.b64decode(ciphertext_blob_b64)
-
- if (len(ciphertext_blob) < SALT_LEN + MAC_LEN):
- raise ValueError('bad encrypted blob')
-
- salt = ciphertext_blob[:16]
- ciphertext = ciphertext_blob[16:-32]
- mac = ciphertext_blob[-32:]
-
- # INT_8(revision_counter)
- rev_counter_int_8 = struct.pack('>Q', revision_counter)
- secret_input = b'%s%s%s' % (secret_data, subcredential, rev_counter_int_8)
-
- kdf = hashlib.shake_256(b'%s%s%s' % (secret_input, salt, string_constant))
- keys = kdf.digest(S_KEY_LEN + S_IV_LEN + MAC_KEY_LEN)
-
- secret_key = keys[:S_KEY_LEN]
- secret_iv = keys[S_KEY_LEN:S_KEY_LEN + S_IV_LEN]
- mac_key = keys[S_KEY_LEN + S_IV_LEN:]
-
- # Now time to decrypt descriptor
- cipher = Cipher(algorithms.AES(secret_key), modes.CTR(secret_iv), default_backend())
- decryptor = cipher.decryptor()
- decrypted = decryptor.update(ciphertext) + decryptor.finalize()
-
- # validate mac (the mac validates the two fields before the mac)
- if not _ciphertext_mac_is_valid(mac_key, salt, ciphertext, mac):
- raise ValueError('Bad MAC!!!')
-
- return decrypted
-
-
-def decrypt_outter_layer(superencrypted_blob_b64, revision_counter, blinded_key, subcredential):
- return _decrypt_descriptor_layer(superencrypted_blob_b64, revision_counter, subcredential, blinded_key, b'hsdir-superencrypted-data')
-
-
-def decrypt_inner_layer(encrypted_blob_b64, revision_counter, blinded_key, subcredential):
- return _decrypt_descriptor_layer(encrypted_blob_b64, revision_counter, subcredential, blinded_key, b'hsdir-encrypted-data')
diff --git a/test/unit/descriptor/hidden_service_v3.py b/test/unit/descriptor/hidden_service_v3.py
index 093d5cb6..37781b5f 100644
--- a/test/unit/descriptor/hidden_service_v3.py
+++ b/test/unit/descriptor/hidden_service_v3.py
@@ -35,13 +35,13 @@ BDwQZ8rhp05oCqhhY3oFHqG9KS7HGzv9g2v1/PrVJMbkfpwu1YK4b3zIZAk=
-----END ED25519 CERT-----\
"""
-with open(get_resource('hidden_service_v3'), 'rb') as descriptor_file:
+with open(get_resource('hidden_service_v3')) as descriptor_file:
HS_DESC_STR = descriptor_file.read()
-with open(get_resource('hidden_service_v3_outer_layer'), 'rb') as outer_layer_file:
+with open(get_resource('hidden_service_v3_outer_layer')) as outer_layer_file:
OUTER_LAYER_STR = outer_layer_file.read()
-with open(get_resource('hidden_service_v3_inner_layer'), 'rb') as inner_layer_file:
+with open(get_resource('hidden_service_v3_inner_layer')) as inner_layer_file:
INNER_LAYER_STR = inner_layer_file.read()
@@ -79,7 +79,7 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase):
inner_layer = desc.decrypt(HS_ADDRESS)
self.assertEqual(INNER_LAYER_STR, str(inner_layer))
- self.assertEqual(OUTER_LAYER_STR.rstrip(b'\x00'), str(inner_layer.outer))
+ self.assertEqual(OUTER_LAYER_STR.rstrip('\x00'), str(inner_layer.outer))
def test_outer_layer(self):
"""
1
0

[stem/master] Add initial support for decrypting both layers of the descriptor.
by atagar@torproject.org 06 Oct '19
by atagar@torproject.org 06 Oct '19
06 Oct '19
commit a0238b7da59529f0cbcd8ab13eac426ac1ca10b0
Author: George Kadianakis <desnacked(a)riseup.net>
Date: Tue Sep 3 15:45:27 2019 +0300
Add initial support for decrypting both layers of the descriptor.
---
stem/descriptor/hidden_service.py | 40 +++++++++
stem/descriptor/hsv3_crypto.py | 185 ++++++++++++++++++++++++++++++++++++++
2 files changed, 225 insertions(+)
diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py
index e6809306..5397d057 100644
--- a/stem/descriptor/hidden_service.py
+++ b/stem/descriptor/hidden_service.py
@@ -470,6 +470,9 @@ class HiddenServiceDescriptorV2(BaseHiddenServiceDescriptor):
return introduction_points
+import stem.descriptor.certificate
+import stem.descriptor.hsv3_crypto as hsv3_crypto
+from cryptography.hazmat.primitives import serialization
class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
"""
@@ -538,6 +541,7 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
raise ValueError("The onion address MUST be provided to parse a V3 descriptor")
self.onion_address = onion_address
+ # XXX Do this parsing in its own function
if validate:
for keyword in REQUIRED_V3_FIELDS:
if keyword not in entries:
@@ -554,6 +558,42 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
else:
self._entries = entries
+ # ATAGAR XXX need to do this cert extraction in the parsing handler
+ assert(self.signing_cert)
+ cert_lines = self.signing_cert.split('\n')
+ assert(cert_lines[0] == '-----BEGIN ED25519 CERT-----' and cert_lines[-1] == '-----END ED25519 CERT-----')
+ desc_signing_cert = stem.descriptor.certificate.Ed25519Certificate.parse(''.join(cert_lines[1:-1]))
+
+ # crypto validation (check skip_crypto_validation)
+ # ASN XXX need to verify descriptor signing certificate (for now we trust Tor to do it)
+ # ASN XXX need to verify descriptor signature (for now we trust Tor to do it)
+
+ plaintext = self.decrypt_descriptor(desc_signing_cert)
+
+ def decrypt_descriptor(self, desc_signing_cert):
+ # Get crypto material.
+ # ASN XXX Extract to its own function and assign them to class variables
+ blinded_key_bytes = desc_signing_cert.get_signing_key()
+ identity_public_key = hsv3_crypto.decode_address(self.onion_address)
+ identity_public_key_bytes = identity_public_key.public_bytes(encoding=serialization.Encoding.Raw,
+ format=serialization.PublicFormat.Raw)
+ assert(len(identity_public_key_bytes) == 32)
+ assert(len(blinded_key_bytes) == 32)
+
+ subcredential_bytes = hsv3_crypto.get_subcredential(identity_public_key_bytes, blinded_key_bytes)
+
+ ####################################### Do the decryption ###################################
+
+ outter_layer_plaintext = hsv3_crypto.decrypt_outter_layer(self.superencrypted, self.revision_counter,
+ identity_public_key_bytes, blinded_key_bytes, subcredential_bytes)
+
+ # ATAGAR XXX this parsing function is a hack. need to replace it with some stem parsing.
+ inner_layer_ciphertext = hsv3_crypto.parse_superencrypted_plaintext(outter_layer_plaintext)
+
+ inner_layer_plaintext = hsv3_crypto.decrypt_inner_layer(inner_layer_ciphertext, self.revision_counter,
+ identity_public_key_bytes, blinded_key_bytes, subcredential_bytes)
+
+ print(inner_layer_plaintext)
# TODO: drop this alias in stem 2.x
diff --git a/stem/descriptor/hsv3_crypto.py b/stem/descriptor/hsv3_crypto.py
index 0feb0fe0..de88b7ac 100644
--- a/stem/descriptor/hsv3_crypto.py
+++ b/stem/descriptor/hsv3_crypto.py
@@ -55,3 +55,188 @@ def decode_address(onion_address_str):
raise ValueError("Bad checksum")
return Ed25519PublicKey.from_public_bytes(pubkey)
+
+"""
+Blinded key stuff
+
+ Now wrt SRVs, if a client is in the time segment between a new time period
+ and a new SRV (i.e. the segments drawn with "-") it uses the current SRV,
+ else if the client is in a time segment between a new SRV and a new time
+ period (i.e. the segments drawn with "="), it uses the previous SRV.
+"""
+
+pass
+
+"""
+Subcredential:
+
+ subcredential = H("subcredential" | credential | blinded-public-key
+ credential = H("credential" | public-identity-key)
+
+Both keys are in bytes
+"""
+def get_subcredential(public_identity_key, blinded_key):
+ cred_bytes_constant = "credential".encode()
+ subcred_bytes_constant = "subcredential".encode()
+
+ credential = hashlib.sha3_256(b"%s%s" % (cred_bytes_constant, public_identity_key)).digest()
+ subcredential = hashlib.sha3_256(b"%s%s%s" % (subcred_bytes_constant, credential, blinded_key)).digest()
+
+ print("public_identity_key: %s" % (public_identity_key.hex()))
+ print("credential: %s" % (credential.hex()))
+ print("blinded_key: %s" % (blinded_key.hex()))
+ print("subcredential: %s" % (subcredential.hex()))
+
+ print("===")
+
+ return subcredential
+
+"""
+Basic descriptor logic:
+
+ SALT = 16 bytes from H(random), changes each time we rebuld the
+ descriptor even if the content of the descriptor hasn't changed.
+ (So that we don't leak whether the intro point list etc. changed)
+
+ secret_input = SECRET_DATA | subcredential | INT_8(revision_counter)
+
+ keys = KDF(secret_input | salt | STRING_CONSTANT, S_KEY_LEN + S_IV_LEN + MAC_KEY_LEN)
+
+ SECRET_KEY = first S_KEY_LEN bytes of keys
+ SECRET_IV = next S_IV_LEN bytes of keys
+ MAC_KEY = last MAC_KEY_LEN bytes of keys
+
+
+Layer data:
+
+ 2.5.1.1. First layer encryption logic
+ SECRET_DATA = blinded-public-key
+ STRING_CONSTANT = "hsdir-superencrypted-data"
+
+ 2.5.2.1. Second layer encryption keys
+ SECRET_DATA = blinded-public-key | descriptor_cookie
+ STRING_CONSTANT = "hsdir-encrypted-data"
+"""
+
+SALT_LEN = 16
+MAC_LEN = 32
+
+S_KEY_LEN = 32
+S_IV_LEN = 16
+MAC_KEY_LEN = 32
+
+def _ciphertext_mac_is_valid(key, salt, ciphertext, mac):
+ """
+ Instantiate MAC(key=k, message=m) with H(k_len | k | m), where k_len is
+ htonll(len(k)).
+
+ XXX spec: H(mac_key_len | mac_key | salt_len | salt | encrypted)
+ """
+ # Construct our own MAC first
+ key_len = len(key).to_bytes(8, 'big')
+ salt_len = len(salt).to_bytes(8, 'big')
+
+ my_mac_body = b"%s%s%s%s%s" % (key_len, key, salt_len, salt, ciphertext)
+ my_mac = hashlib.sha3_256(my_mac_body).digest()
+
+ print("===")
+ print("my mac: %s" % my_mac.hex())
+ print("their mac: %s" % mac.hex())
+
+ # Compare the two MACs
+ return my_mac == mac
+
+def _decrypt_descriptor_layer(ciphertext_blob_b64, revision_counter,
+ public_identity_key, subcredential,
+ secret_data, string_constant):
+ # decode the thing
+ ciphertext_blob = base64.b64decode(ciphertext_blob_b64)
+
+ if (len(ciphertext_blob) < SALT_LEN + MAC_LEN):
+ raise ValueError("bad encrypted blob")
+
+ salt = ciphertext_blob[:16]
+ ciphertext = ciphertext_blob[16:-32]
+ mac = ciphertext_blob[-32:]
+
+ print("encrypted blob lenth :%s" % len(ciphertext_blob))
+ print("salt: %s" % salt.hex())
+ print("ciphertext length: %s" % len(ciphertext))
+ print("mac: %s" % mac.hex())
+ print("===")
+
+ # INT_8(revision_counter)
+ rev_counter_int_8 = revision_counter.to_bytes(8, 'big')
+ secret_input = b"%s%s%s" % (secret_data, subcredential, rev_counter_int_8)
+ secret_input = secret_input
+
+ print("secret_data (%d): %s" % (len(secret_data), secret_data.hex()))
+ print("subcredential (%d): %s" % (len(subcredential), subcredential.hex()))
+ print("rev counter int 8 (%d): %s" % (len(rev_counter_int_8), rev_counter_int_8.hex()))
+ print("secret_input (%s): %s" % (len(secret_input), secret_input.hex()))
+ print("===")
+
+ kdf = hashlib.shake_256(b"%s%s%s" % (secret_input, salt, string_constant))
+ keys = kdf.digest(S_KEY_LEN+S_IV_LEN+MAC_KEY_LEN)
+
+ secret_key = keys[:S_KEY_LEN]
+ secret_iv = keys[S_KEY_LEN:S_KEY_LEN+S_IV_LEN]
+ mac_key = keys[S_KEY_LEN+S_IV_LEN:]
+
+ print("secret_key: %s" % secret_key.hex())
+ print("secret_iv: %s" % secret_iv.hex())
+ print("mac_key: %s" % mac_key.hex())
+
+ # Now time to decrypt descriptor
+ cipher = Cipher(algorithms.AES(secret_key), modes.CTR(secret_iv), default_backend())
+ decryptor = cipher.decryptor()
+ decrypted = decryptor.update(ciphertext) + decryptor.finalize()
+
+ # validate mac (the mac validates the two fields before the mac)
+ if not _ciphertext_mac_is_valid(mac_key, salt, ciphertext, mac):
+ raise ValueError("Bad MAC!!!")
+
+ return decrypted
+
+def decrypt_outter_layer(superencrypted_blob_b64, revision_counter,
+ public_identity_key, blinded_key, subcredential):
+ secret_data = blinded_key
+ string_constant = b"hsdir-superencrypted-data"
+
+ # XXX Remove the BEGIN MESSSAGE around the thing
+ superencrypted_blob_b64_lines = superencrypted_blob_b64.split('\n')
+ assert(superencrypted_blob_b64_lines[0] == '-----BEGIN MESSAGE-----')
+ assert(superencrypted_blob_b64_lines[-1] == '-----END MESSAGE-----')
+ superencrypted_blob_b64 = ''.join(superencrypted_blob_b64_lines[1:-1])
+
+ print("====== Decrypting outter layer =======")
+
+ return _decrypt_descriptor_layer(superencrypted_blob_b64, revision_counter,
+ public_identity_key, subcredential,
+ secret_data, string_constant)
+
+def decrypt_inner_layer(encrypted_blob_b64, revision_counter,
+ public_identity_key, blinded_key, subcredential):
+ secret_data = blinded_key
+ string_constant = b"hsdir-encrypted-data"
+
+ print("====== Decrypting inner layer =======")
+
+ return _decrypt_descriptor_layer(encrypted_blob_b64, revision_counter,
+ public_identity_key, subcredential,
+ secret_data, string_constant)
+
+def parse_superencrypted_plaintext(outter_layer_plaintext):
+ """Super hacky function to parse the superencrypted plaintext. This will need to be replaced by proper stem code."""
+ import re
+
+ START_CONSTANT = b'-----BEGIN MESSAGE-----\n'
+ END_CONSTANT = b'\n-----END MESSAGE-----'
+
+ start = outter_layer_plaintext.find(START_CONSTANT)
+ end = outter_layer_plaintext.find(END_CONSTANT)
+
+ start = start + len(START_CONSTANT)
+
+ return outter_layer_plaintext[start:end]
+
1
0
commit 3625cb184da353f0ae895a1b65e22d9e50dd5e14
Author: George Kadianakis <desnacked(a)riseup.net>
Date: Mon Aug 26 14:48:29 2019 +0300
Add support for v3 ed25519 certs.
---
stem/descriptor/certificate.py | 58 +++++++++++++++++++++++++++++++++++-------
1 file changed, 49 insertions(+), 9 deletions(-)
diff --git a/stem/descriptor/certificate.py b/stem/descriptor/certificate.py
index 449e106c..470fa623 100644
--- a/stem/descriptor/certificate.py
+++ b/stem/descriptor/certificate.py
@@ -26,13 +26,16 @@ used to validate the key used to sign server descriptors.
Purpose of Ed25519 certificate. As new certificate versions are added this
enumeration will expand.
- ============== ===========
- CertType Description
- ============== ===========
- **SIGNING** signing a signing key with an identity key
- **LINK_CERT** TLS link certificate signed with ed25519 signing key
- **AUTH** authentication key signed with ed25519 signing key
- ============== ===========
+ ============== ===========
+ CertType Description
+ ============== ===========
+ **SIGNING** signing a signing key with an identity key
+ **LINK_CERT** TLS link certificate signed with ed25519 signing key
+ **AUTH** authentication key signed with ed25519 signing key
+ **HS_V3_DESC_SIGNING_KEY** onion service v3 descriptor signing key cert (see rend-spec-v3.txt)
+ **HS_V3_INTRO_POINT_AUTH_KEY** onion service v3 intro point authentication key cert (see rend-spec-v3.txt)
+ **HS_V3_INTRO_POINT_ENC_KEY** onion service v3 intro point encryption key cert (see rend-spec-v3.txt)
+ ============== ===========
.. data:: ExtensionType (enum)
@@ -70,7 +73,8 @@ ED25519_HEADER_LENGTH = 40
ED25519_SIGNATURE_LENGTH = 64
ED25519_ROUTER_SIGNATURE_PREFIX = b'Tor router descriptor signature v1'
-CertType = stem.util.enum.UppercaseEnum('SIGNING', 'LINK_CERT', 'AUTH')
+CertType = stem.util.enum.UppercaseEnum('SIGNING', 'LINK_CERT', 'AUTH',
+ "HS_V3_DESC_SIGNING_KEY", "HS_V3_INTRO_POINT_AUTH_KEY", "HS_V3_INTRO_POINT_ENC_KEY")
ExtensionType = stem.util.enum.Enum(('HAS_SIGNING_KEY', 4),)
ExtensionFlag = stem.util.enum.UppercaseEnum('AFFECTS_VALIDATION', 'UNKNOWN')
@@ -158,8 +162,14 @@ class Ed25519CertificateV1(Ed25519Certificate):
self.type = CertType.AUTH
elif cert_type == 7:
raise ValueError('Ed25519 certificate cannot have a type of 7. This is reserved for RSA identity cross-certification.')
+ elif cert_type == 8: # see rend-spec-v3.txt appendix E for these defintions
+ self.type = CertType.HS_V3_DESC_SIGNING_KEY
+ elif cert_type == 9:
+ self.type = CertType.HS_V3_INTRO_POINT_AUTH_KEY
+ elif cert_type == 0x0B:
+ self.type = CertType.HS_V3_INTRO_POINT_ENC_KEY
else:
- raise ValueError("BUG: Ed25519 certificate type is decoded from one byte. It shouldn't be possible to have a value of %i." % cert_type)
+ raise ValueError("Ed25519 certificate type is an unknown value %i." % cert_type)
# expiration time is in hours since epoch
try:
@@ -214,6 +224,9 @@ class Ed25519CertificateV1(Ed25519Certificate):
return datetime.datetime.now() > self.expiration
+ # ATAGAR XXX certificates are generic and not just for descriptor, however
+ # this function assumes they are. this needs to be moved to the descriptor
+ # module. the new verify() function is more generic and should be used.
def validate(self, server_descriptor):
"""
Validates our signing key and that the given descriptor content matches its
@@ -269,3 +282,30 @@ class Ed25519CertificateV1(Ed25519Certificate):
verify_key.verify(signature_bytes, descriptor_sha256_digest)
except InvalidSignature:
raise ValueError('Descriptor Ed25519 certificate signature invalid (Signature was forged or corrupt)')
+
+ def get_signing_key(self):
+ """
+ Get the signing key for this certificate. This is included in the extensions.
+ WARNING: This is the key that signed the certificate, not the key that got
+ certified.
+
+ :returns: Raw bytes of an ed25519 key.
+
+ :raises: **ValueError** if the signing key cannot be found.
+ """
+ signing_key_extension = None
+
+ for extension in self.extensions:
+ if extension.type == ExtensionType.HAS_SIGNING_KEY:
+ signing_key_extension = extension
+ break
+
+ if not signing_key_extension:
+ raise ValueError('Signing key extension could not be found')
+
+ if (len(signing_key_extension.data) != 32):
+ raise ValueError('Signing key extension has malformed key')
+
+ return signing_key_extension.data
+
+
1
0

06 Oct '19
commit f815223fc2e7d2e460c18a90d66d0b4128eb9f23
Author: George Kadianakis <desnacked(a)riseup.net>
Date: Tue Sep 3 14:03:04 2019 +0300
Add initial support for parsing v3 addresses.
---
stem/descriptor/hidden_service.py | 10 ++++++-
stem/descriptor/hsv3_crypto.py | 57 +++++++++++++++++++++++++++++++++++++++
2 files changed, 66 insertions(+), 1 deletion(-)
diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py
index 52e1b0b1..e6809306 100644
--- a/stem/descriptor/hidden_service.py
+++ b/stem/descriptor/hidden_service.py
@@ -526,10 +526,18 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
def create(cls, attr = None, exclude = (), validate = True, sign = False):
return cls(cls.content(attr, exclude, sign), validate = validate, skip_crypto_validation = not sign)
- def __init__(self, raw_contents, validate = False, skip_crypto_validation = False):
+ def __init__(self, raw_contents, validate = False, onion_address = None, skip_crypto_validation = False):
+ """
+ The onion_address is needed so that we can decrypt the descriptor, which is
+ impossible without the full onion address.
+ """
super(HiddenServiceDescriptorV3, self).__init__(raw_contents, lazy_load = not validate)
entries = _descriptor_components(raw_contents, validate)
+ if onion_address == None:
+ raise ValueError("The onion address MUST be provided to parse a V3 descriptor")
+ self.onion_address = onion_address
+
if validate:
for keyword in REQUIRED_V3_FIELDS:
if keyword not in entries:
diff --git a/stem/descriptor/hsv3_crypto.py b/stem/descriptor/hsv3_crypto.py
new file mode 100644
index 00000000..0feb0fe0
--- /dev/null
+++ b/stem/descriptor/hsv3_crypto.py
@@ -0,0 +1,57 @@
+import base64
+import hashlib
+
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
+from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+from cryptography.hazmat.backends import default_backend
+
+
+"""
+Onion addresses
+
+ onion_address = base32(PUBKEY | CHECKSUM | VERSION) + ".onion"
+ CHECKSUM = H(".onion checksum" | PUBKEY | VERSION)[:2]
+
+ - PUBKEY is the 32 bytes ed25519 master pubkey of the hidden service.
+ - VERSION is an one byte version field (default value '\x03')
+ - ".onion checksum" is a constant string
+ - CHECKSUM is truncated to two bytes before inserting it in onion_address
+
+"""
+
+CHECKSUM_CONSTANT = b".onion checksum"
+
+def decode_address(onion_address_str):
+ """
+ Parse onion_address_str and return the pubkey.
+
+ onion_address = base32(PUBKEY | CHECKSUM | VERSION) + ".onion"
+ CHECKSUM = H(".onion checksum" | PUBKEY | VERSION)[:2]
+
+ :return: Ed25519PublicKey
+
+ :raises: ValueError
+ """
+ if (len(onion_address_str) != 56 + len(".onion")):
+ raise ValueError("Wrong address length")
+
+ # drop the '.onion'
+ onion_address = onion_address_str[:56]
+
+ # base32 decode the addr (convert to uppercase since that's what python expects)
+ onion_address = base64.b32decode(onion_address.upper())
+ assert(len(onion_address) == 35)
+
+ # extract pieces of information
+ pubkey = onion_address[:32]
+ checksum = onion_address[32:34]
+ version = onion_address[34]
+
+ # Do checksum validation
+ my_checksum_body = b"%s%s%s" % (CHECKSUM_CONSTANT, pubkey, bytes([version]))
+ my_checksum = hashlib.sha3_256(my_checksum_body).digest()
+
+ if (checksum != my_checksum[:2]):
+ raise ValueError("Bad checksum")
+
+ return Ed25519PublicKey.from_public_bytes(pubkey)
1
0

06 Oct '19
commit ec4d6f99e55b2ca62b7def152d31364de605c2fd
Author: Damian Johnson <atagar(a)torproject.org>
Date: Wed Sep 4 17:34:00 2019 -0700
Only import crypto modules if available
Stem has a soft dependency on the cryptography module. If unavailable we should
gracefully degrade or explicitly error with a message saying that cryptography
is required.
Also, cryptography's ed25519 class is pretty new, so folks will have outdated
module versions for a while...
======================================================================
ERROR: test_for_decrypt
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/atagar/Desktop/stem/test/unit/descriptor/hidden_service_v3.py", line 43, in test_for_decrypt
onion_address="sltib6sxkuxh2scmtuvd5w2g7pahnzkovefxpo4e4ptnkzl5kkq5h2ad.onion"))
File "/home/atagar/Desktop/stem/stem/descriptor/__init__.py", line 442, in parse_file
for desc in parse(descriptor_file):
File "/home/atagar/Desktop/stem/stem/descriptor/__init__.py", line 545, in _parse_metrics_file
for desc in stem.descriptor.hidden_service._parse_file(descriptor_file, desc_type, validate = validate, **kwargs):
File "/home/atagar/Desktop/stem/stem/descriptor/hidden_service.py", line 156, in _parse_file
yield desc_type(bytes.join(b'', descriptor_content), validate, **kwargs)
File "/home/atagar/Desktop/stem/stem/descriptor/hidden_service.py", line 571, in __init__
plaintext = self.decrypt_descriptor(desc_signing_cert)
File "/home/atagar/Desktop/stem/stem/descriptor/hidden_service.py", line 579, in decrypt_descriptor
identity_public_key = stem.descriptor.hsv3_crypto.decode_address(self.onion_address)
File "/home/atagar/Desktop/stem/stem/descriptor/hsv3_crypto.py", line 30, in decode_address
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
ImportError: No module named ed25519
---
stem/descriptor/hidden_service.py | 20 +++++++++++---------
stem/descriptor/hsv3_crypto.py | 16 ++++++++++++----
stem/prereq.py | 10 +++++++++-
3 files changed, 32 insertions(+), 14 deletions(-)
diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py
index 5397d057..150ba368 100644
--- a/stem/descriptor/hidden_service.py
+++ b/stem/descriptor/hidden_service.py
@@ -31,6 +31,8 @@ import collections
import hashlib
import io
+import stem.descriptor.certificate
+import stem.descriptor.hsv3_crypto
import stem.prereq
import stem.util.connection
import stem.util.str_tools
@@ -470,9 +472,6 @@ class HiddenServiceDescriptorV2(BaseHiddenServiceDescriptor):
return introduction_points
-import stem.descriptor.certificate
-import stem.descriptor.hsv3_crypto as hsv3_crypto
-from cryptography.hazmat.primitives import serialization
class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
"""
@@ -568,29 +567,32 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
# ASN XXX need to verify descriptor signing certificate (for now we trust Tor to do it)
# ASN XXX need to verify descriptor signature (for now we trust Tor to do it)
- plaintext = self.decrypt_descriptor(desc_signing_cert)
+ if not skip_crypto_validation and stem.prereq.is_crypto_available():
+ plaintext = self.decrypt_descriptor(desc_signing_cert)
def decrypt_descriptor(self, desc_signing_cert):
# Get crypto material.
# ASN XXX Extract to its own function and assign them to class variables
+ from cryptography.hazmat.primitives import serialization
+
blinded_key_bytes = desc_signing_cert.get_signing_key()
- identity_public_key = hsv3_crypto.decode_address(self.onion_address)
+ identity_public_key = stem.descriptor.hsv3_crypto.decode_address(self.onion_address)
identity_public_key_bytes = identity_public_key.public_bytes(encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw)
assert(len(identity_public_key_bytes) == 32)
assert(len(blinded_key_bytes) == 32)
- subcredential_bytes = hsv3_crypto.get_subcredential(identity_public_key_bytes, blinded_key_bytes)
+ subcredential_bytes = stem.descriptor.hsv3_crypto.get_subcredential(identity_public_key_bytes, blinded_key_bytes)
####################################### Do the decryption ###################################
- outter_layer_plaintext = hsv3_crypto.decrypt_outter_layer(self.superencrypted, self.revision_counter,
+ outter_layer_plaintext = stem.descriptor.hsv3_crypto.decrypt_outter_layer(self.superencrypted, self.revision_counter,
identity_public_key_bytes, blinded_key_bytes, subcredential_bytes)
# ATAGAR XXX this parsing function is a hack. need to replace it with some stem parsing.
- inner_layer_ciphertext = hsv3_crypto.parse_superencrypted_plaintext(outter_layer_plaintext)
+ inner_layer_ciphertext = stem.descriptor.hsv3_crypto.parse_superencrypted_plaintext(outter_layer_plaintext)
- inner_layer_plaintext = hsv3_crypto.decrypt_inner_layer(inner_layer_ciphertext, self.revision_counter,
+ inner_layer_plaintext = stem.descriptor.hsv3_crypto.decrypt_inner_layer(inner_layer_ciphertext, self.revision_counter,
identity_public_key_bytes, blinded_key_bytes, subcredential_bytes)
print(inner_layer_plaintext)
diff --git a/stem/descriptor/hsv3_crypto.py b/stem/descriptor/hsv3_crypto.py
index de88b7ac..8f304c52 100644
--- a/stem/descriptor/hsv3_crypto.py
+++ b/stem/descriptor/hsv3_crypto.py
@@ -1,10 +1,7 @@
import base64
import hashlib
-from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
-from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
-from cryptography.hazmat.backends import default_backend
-
+import stem.prereq
"""
Onion addresses
@@ -32,6 +29,14 @@ def decode_address(onion_address_str):
:raises: ValueError
"""
+
+ # TODO: note the module version
+
+ if not stem.prereq.is_crypto_available(ed25519 = True):
+ raise ImportError('Onion address decoding requires cryptography version XXX')
+
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
+
if (len(onion_address_str) != 56 + len(".onion")):
raise ValueError("Wrong address length")
@@ -149,6 +154,9 @@ def _ciphertext_mac_is_valid(key, salt, ciphertext, mac):
def _decrypt_descriptor_layer(ciphertext_blob_b64, revision_counter,
public_identity_key, subcredential,
secret_data, string_constant):
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+ from cryptography.hazmat.backends import default_backend
+
# decode the thing
ciphertext_blob = base64.b64decode(ciphertext_blob_b64)
diff --git a/stem/prereq.py b/stem/prereq.py
index 4cb51113..0aadb35a 100644
--- a/stem/prereq.py
+++ b/stem/prereq.py
@@ -114,11 +114,13 @@ def is_sqlite_available():
return False
-def is_crypto_available():
+def is_crypto_available(ed25519 = False):
"""
Checks if the cryptography functions we use are available. This is used for
verifying relay descriptor signatures.
+ :param bool ed25519: check for ed25519 support
+
:returns: **True** if we can use the cryptography module and **False**
otherwise
"""
@@ -134,6 +136,12 @@ def is_crypto_available():
if not hasattr(rsa.RSAPrivateKey, 'sign'):
raise ImportError()
+ # TODO: Check when the cryptography module's ed25519 class was added
+ # (it's not present in 2.0.3).
+
+ if ed25519:
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
+
return True
except ImportError:
from stem.util import log
1
0

[stem/master] Add a test descriptor to be parsed while coding v3 support.
by atagar@torproject.org 06 Oct '19
by atagar@torproject.org 06 Oct '19
06 Oct '19
commit c1b793a9f4d6ac7e2465c1c712c8427e73ffcb76
Author: George Kadianakis <desnacked(a)riseup.net>
Date: Tue Sep 3 14:03:40 2019 +0300
Add a test descriptor to be parsed while coding v3 support.
---
test/unit/descriptor/data/hidden_service_v3_test | 223 +++++++++++++++++++++++
test/unit/descriptor/hidden_service_v3.py | 15 ++
2 files changed, 238 insertions(+)
diff --git a/test/unit/descriptor/data/hidden_service_v3_test b/test/unit/descriptor/data/hidden_service_v3_test
new file mode 100644
index 00000000..12ddcc69
--- /dev/null
+++ b/test/unit/descriptor/data/hidden_service_v3_test
@@ -0,0 +1,223 @@
+hs-descriptor 3
+descriptor-lifetime 180
+descriptor-signing-key-cert
+-----BEGIN ED25519 CERT-----
+AQgABl5/AZLmgPpXVS59SEydKj7bRvvAduVOqQt3u4Tj5tVlfVKhAQAgBABUhpfe
+/Wd3p/M74DphsGcIMee/npQ9BTzkzCyTyVmDbykek2EciWaOTCVZJVyiKPErngfW
+BDwQZ8rhp05oCqhhY3oFHqG9KS7HGzv9g2v1/PrVJMbkfpwu1YK4b3zIZAk=
+-----END ED25519 CERT-----
+revision-counter 42
+superencrypted
+-----BEGIN MESSAGE-----
+Jmu66WXn0+CDLXVM02n85rj84Fv4ynLcjFFWPoLNm6Op+S14CAm0H2qfMj8OO/jw
+NJiNxY/L/8SeY5ZlvqPHzI8jBqKW7nT5CN7xLUEvzdFhG3AnWC48r8fp2E+TQ8gb
+Fw00gDEIPT8q1nfKHNEnErS03KbW25kPGv8iX5v8XxpkwBFexR1BPEJGi2U7sI18
+SyuJ21hbc03khpj/PilVgNeY359/Aoa6sER5kz46YHR+xpFt7fufybpCcFQQSYkX
+fXZ+dHTMpshCSUtbyKJZtO1P9PPaDZ2NpNTGrCXf8T45q/OlnTg2YO4OuBX87HA3
+CT4jyc1dmduXirAucJB2f9DQkvWTIz1dMtE8itjiH2fnuawJMFFXS+prbMU0VAwT
+yGIqCPLc6xkChuG/NWS3Pd5UsM/xTmBulDt7SCZ7OMOIHqn1uFbMip/HuGzAo60o
+oXgdkPqx1RZnmpfD4xeaH8VdaTKS97xRP5R860PgTaGJ/jyzkKGcpuThsTqbYSel
+P7rFVhdZNIyDb6lSGsqvoxuL+E3fz+Nb/vSxXd2B+iQ6gjB81s5wiAnycmhvruz5
+8hLpYGISyYw6XEG3k74+lbbFAmXTViUPV2cL/On6ROgNAix/7L7iREnWfWGuEaIc
+4IbH2QPaOTtOMeghZQKsb3StB1fVA640RV0eCw62i7IZ2KQazcb6UnACw0PypBZq
+EzE2BsydtMWid3Hk1mFmXt0tY5i7XzXfdxaTQXIH4GVSPt3QJg8JMMP2+6TCghDe
+wZjScrfRcwRTDNEVSrHkd09ZFSMJUijtyFWDkY/VW0YxII0lgFBA/gaqfBcTa9D0
+ag81vIQxozq6b1mVVmUcJC8SSw2bHKmjnBlY7EO1CnBGzMxJilR02jfQqqOkUxnv
+6PFyzMCTG5iNm/Ob+BS61WseUW5rmgg+2EzwuugAKD5fAZGW9V/fV0nKNJPwW45R
+XJsDiaFu1bB2vFclL99DpOkgUMYVCq7jNN7XWBTl3FR6AlPR4x6Y0hOT0vqyEeak
+vR1cVTqBdHoV1VeIQvk9FzRL/JRI5cXFUy0zfzBX23bt01FmZqKv2ObnTjkuiV6k
+CA1gfWiFQXd59c/roeH5DWbGyfmUn8b8X0hTBsuRBHPrdEDUAco+WFhhA2sAG7yM
+7UOxUTrDqc15CZvK55rDRRBE2crSJ/odTCvBXr7eODD+3XcfbhGlq+frYTHN2/z9
+eZm9wiU/ik98R7fbT8RQio0sba6wyFSevbJ3NlI40MdFgqD+S0Dr46FbgLd/6L6r
+tGG1mmeXv5U3TiO/99n9s+RXCMnoU/FXdldP1X7qVlelNxlFzMIuy2N6ZPGGm5dX
+/TX459ujKpIF9JHhA2+rzSfqy/aV+0mbNJ+/7GK94zMOm1xzg9Koa94Dim5tvvq7
+6vvB/HEfoi3tT24o6eoWBvHE4FB+toMEqVmq6wVTbWInWZtXeJ8mxY04ChkllJ/Q
+/SvtJcApd9UCDVNY98w97KYQx1qAl9f67+pkNvjABtwKhTc2x8FSuO51QFzZ6LD+
+ksth/ccyKeS0CrQz0SVGB0lerI14db1nasrQAE3gXfC1a/2hR+LweftowES4dUMB
+4zW1CQMP5mK7lsdGrnjUqMjiCku0gPeSQMLIFcfLDMqECQ4X5aM3JS2unA2Lyq1n
+JiL/bpa6AvmSvmVKnCAoWYS1HXhqRuj2OQmtNEKtsUvmfb9RJYhvisD408jPYqbj
+k1O6ykQiqm6oqmg0bWlJXYzJ4aXa7E9rtI9kAFV27yODXGitT53PCi173kxadONS
+/XFsfZoxLVkQdRRUDWpPyVe5xn38rDY8fvoG683xxwbuaOHk+H2Dxcwbwn1RLT9Z
+nsgHQ3MW3xcAmfKn1d0cQhHkHaLWP4HBQmiQq9nJtdQYbOmT64EMajSt+FxzPM5M
+0joHs/wCCFYsJzAW9UZ1/Y8l7Y18wM76VPLoK43cwqHjwKjKoYL8QzKaBfbdyWz1
+irGrRZLDmQaFj1IiPjni6nis0+yb09bdz7p48p3r+SEej1HO2kuggixolEa4tT0B
+az9GMBFN9ydsEN8n+A3xIXHzlg8RNDTa5MYUAW0d8punj5NaDg08yMN5OAoisndy
+J4yeNJAdkncRFtcGyX8QyYPnFNybqfmT7epl89Ax9gdHQ1FQhbWbmBthgGgGs80P
+0kHZLRGDUEGQkhSS/imKOInINJVgcbcRZTqf9g5gVEy5P3TkVQDEv/KDcE9Sjke7
+CMrtpsiU/O/9so+0ltVtGOrbzQIebCbvnQNbO9i1kniTtidrAlmyaF4bjIDoAvwo
+/nih+f/J7w3KdVs4DdFqCRpYIibJC6FQqHpbizliN6PfPtOeEnLDxyrcp7BxSs6i
+6otmxeHRbLF2vio6ek2HHGvQegbVLRyQ2gAbuKfaJyOpxFekde70S+HngjBvZ10J
+QX8MHBmwlQABzFLe6Exd/Oi5Wlf7XlyJXOjJ3q+YnOx/bQKNhA3JqpfTDJMhciqr
+zgMfWVJN3FrJwBMyNh9IjIpUyHCDDof3YOxL9xW0zxn8RlAQgYEUHGmPTIN0qc19
+aWbYdwf4U5Hsw78CVZvTe/UdMRTSa5YbrVViDbYBH1AGO60rtmZ4sqLV7hUe9sIH
+fBXlUCaAyYS3BCd0qIdzWMlZD3s8PBWfwTOsGZPGw2iVTjV7IioKGAD8onCnSToT
+rmTSJWcMx0P+XIkvsMqf4EWqPeaMWv3eY2JtqaslPIasy4m9h6CCin08fHPl+s80
+f0h6820c5HCuEPRcH9VuRMm3MOdjOG3K+SVdBnovoJxllSp/N3aii2D5vrQgV/rq
+fJXx+OL6xNATcJYydFOlN4UkkyHvhEnK2nqmYQFnnzqgS1Glg/ZpheJ/9Ib9ugFI
+0qJf/1VZNHaASnAVFBxG67AkMyPotlU++pmFk8tlH0C9lDdsXLZ+TAkhzvmO+4PR
+WjRASGli1QiEIQY9AtrNbyV001acR0qJZD7FC3oftYT8GcnzxBbDxhjqDiOTP6TC
+cqvSzCy+e59lLh8oNf0aaYs0WoLawva7vxG7TEWPF6xoU/O++SOj9eJXjlIwV5E4
+IklE94jhKZ0Cyi3B7x9H2TYn1qDlvzAr3nYCXFQMP2dO7tWr9ItuZsvMm3KEa6ap
+GXvWML6wwRRWZeLPr1nuiU4n60EETY/ZBJFH7Gzwa1/b0qhcIk2coUCcrPdH8CFK
+6HuEhxqqBzr3BqijoWbhwsLrZL8WR6ouRRZ8iGgzt5utWDq3nz30PhmSKWZt22ym
+nY0MrWS/oMWYkftDQbDSv+76UC8pbvF43vKCGNIHnFBWNi48CPhSgwqSEeXiOFpV
++MjkhaVP7LwHFqmWg+yrZMeWU4GDmGQV6poixIJRzPThBhsFgJNd+UX5e35WjXDg
+JBsdUGneT4aEzVi3KrSc7PdJN1f3X3F9lb7K+2IEg3vezxmR2DuGJMWf7QtfEJH5
+1ezAn7B+4cRkKzVxhgeKKjo6Lk6CVVUfHKFz74Dar04BhwkdKvyLRm3vwDDpEYKX
+sxqAt1irNyNwbbUC9Ldw+Iyx+I2VgQ6aLHkpvBKYgXsJeXBeuRriHw7Umep8Hwu9
+piWt6b2lHB0qH5P9l0LPK2n64vzpT4S+1vPcsQ91DB+QynssBLSgilPUtyn/0/LO
+MtIECHSkgJznFY1l3jJ1OV6snC0mFfuYio4gf7WYMHwWjoZ99rudtsrYpHmJoyq8
+V9Mbl1GRGPlGWGe8lMmU7Wb/ZFsX6GPUsBUOUN0NA6u16Z565dPoxnHWbjoVlMbK
+LzI/RZFWOOOc4AIZTCPBNjZ1j5tlRnptG1+O0BCyD6cSOfq/4zHAHsY+fcbgkFQF
+hOok2PV44vETV3JH9Oihpr1YeQ3swTWMzh53+IYOxdFx/5i8SjvjULmYTmiKXt1H
+Q3dA5eFNVv9y0Et6iimLadMEBf0x9WUsL5aofG10RhDdciO41patUmdwxo+sq+38
+87qP2RPGxybqC3JUvR7zDINzGgCNkwDzcSV2lOdvOT7u08fumYo2PkVhub4o9QpJ
+yQlIMxyn/glkayMmBwgOLEwCCEkq39Zi8zhvUnAwsMDNJTd2fUE1DlQQ/V0S4XHf
+XrMA9LcJZj4gH1vauNsVdd5zBJ92g4RhEu5w9tAya1LdFCBChWv5JUrSKWB4Uw2P
+7q2Dg92MbDJLXj3hsbuKpbreLGFnCoh0hWjp0HTKcKys6Ec5+aslA+6TVo1j01mT
+5rXJKQsPbqUmXeh9zfqtFzhPXTHnAZTdIYpJWVaxwMkacNJAgJ6NGg4NR3uc7Xjv
+CV64CKk7KZViCiaAogpqveIyCgjkqtWfMkbqRFhCBHGB2Tm1G9m8+yFsllaUjBgo
+fm/HFBx7w7ho/2iwv4tmEtrV5QqzYFOcpxSnUzqiMlCKSwqHfFg4Q0ajNIhs7Yqg
+XKfeWIg/hs8MKc5h20o3d8e/esgXMoTIMyNZoiA+ACtS8A4N1ve31NEspoANX4n/
+W0ZuRHuT3aa5BERjQFD9xfy7UAU1ZrPSEfIxC0RBXP2MmXJOdIWjW2xiEXDlLnal
+pCKQJ3Sy/gUw7iaQ/PnROqKyK9eMsQ7xA5O98FTDC9wonymVU0WymupRyvJTYk4l
+LvaHL8YPTnHvjFSy/FltfxIrJBtsNLkq8MrDIJaxrTW+8dwEUH68b6j4ZQTo2Xvm
+mkBXojwOG8I1BsMY89/uphia+NW4uSh/ZrsV+lF5Jc9fPDdo7AfM38gY/+45HL5U
+wo9kdP9Exff1vQHTFcP48nG097AeZAWbEMn57yWrK28OWFP2xJCG+DfslHaZUL8y
+SbDc9l8faLYCRPa07Qd+CTxPqrdRixKygunNPJyZErEBqxw/ftxWictgEoaHHEmU
+h5Ge3pJ1+wdZrrs0EOhc0iBEIhxuifUhBxtwH0ohkYYv/14UnULVca48ZeE9OwjI
+R10G48Lz5XQbxBMU4xwSlOchyCBagS9PrRtcoMzpv0Dyb1Q11ndtRaj8LKa3hX6u
+kkoa3LEORDkXMichUcfOQVSZIXXvyDaXJDbaeZx0tqeRx0ZPBUFNe4Fbu45gXxUj
+m/uo1bBzAtLq91uR/txhwNuLrxbv+JwvY3ng/Cs3Y53GK8M6i6/iswELb9eRz7Bt
+cm/J+RmfcAMQZeQRDkIRBc8DM2tNnfMDkdaqpvdDUY6jZNQgbKuQpbskJGguI+yJ
+sx53hpZfBF+E0YHUV9s9R1Qm8l/4imzgRbAenSCDcYc56YYk1v5CC3MK2VCCwQ9h
+ZJp7n3LQIrRCVZb1teCTtwQ2aOc7qLeceaeNTpjayfGBVACeokK8H39xf7Qc5HL3
+fIFADtH3Rj86IjtjfSSo6OuXZlOKyBZMP+7lRC8gVknRxLwTpU4CIPm0R7Qk7LSx
+mWKwvqb0sYXpqmBngkHI2N5pInd3yzDq//ZcXLwpB8pxhBTmCkPQGFXhXgrby6+B
+b9BiYGoUaS8+5TXVB1EJcvzTv18CLtqv7b8zblrsdeYc81T/w0fl/dnfTaSEOfXa
+ggoG5hJpPuM20BdZNVvJDhQNU0ffAW2TKzLtjqa64XyEhKmvnBd9Y3m21gaJBdUp
+reDjLHTEyGFX6cyXU8T4dnYcvIq4pkU43xNSSUhpPFA0CDa5hjbpFhWd+4nARPqx
+T3q362ERag+MFUdCyo9Ufzn+YbmagEAOdmWmmga8wYIY+xBfOugV4t4DFEFqRVZI
+bCSmcGHi19jAkmOWm6Ik8/H5mW8XqsRLiXUvDRAj/f1LH7/tuWQ/+JQmIAEBllIh
+IEdVErWlZCm30iOC4d3g05SYIQbLi73zWxtuUHArKoyRm5YWxjh/fphYHtq2gscM
+97OX3JvG5C+4KDbojgaPHPcGfC64sL6+T9eImHb7KsV+KLXBn8TUTlS1nvHHC9fp
+xsiHo32xCvcA5KUHzYAqzjfs9xd1uxHEcHADko0wfzh6XVvCqTNTcghwJ7IfvWUL
+hXrJeKqw6eeqwUISwAsUrYfDfoi89ju73YrlnAMxNAt7MolrjK3hvgqEgVbCWqi1
+8RmPNReNmB0N+7OBxk+Rxm6wv64WfWu1hgSnq3AFhhqe3sX05JUcABBVKAXQ6ZNv
+Ax34oYr+NdCP8DnTEDoaqDf7xLjfjdJB8ZQuaSpYFKAMxUNsiwvG424vA0vlYLy+
+UO3RQRYx85HnAIx+lrp2BlN+AMR/b38df6G20bovzuWPK+PH/kMqVDdpfRHL70iK
+bT28zdg9KsRWCA8rTs0UkO1yWuByOGX6EzbDaHXmx9qzOzokRZoRNfj/Sxjs4g1l
+r60B74TvJqN/ps0OcsZGOF7di55fius8GnHjvFi5qBRbdL/KjCdkDiis4M3t6r6E
+e2cki/zm3UwU+fH1M1AemeNm7X8Jo63bryN9TLHvSmXLN7kO3APY5rJZd66ko3Av
+s/wjOcqZVedARglSyjPXC0zl5xezEinA0hof7JItcpTEw6GS8NS6cIy906F+0xc9
+bEzrYx5kIQhJ/iM0vZzTz7554w8Fd6FAL0f6gw8+HK26mV+6QnD0Jwb7QwWDBFxF
+MBRoWA7/0YrOI5w/To0STU8pIUcIbKBbD4NGS/6LnevwQHgom/N8xDYB9Q9pjLyj
+i3O0Gy4zw8Wem+OCzHr9ElOdos4KEVocGwBOQYzNZMQINIeoxoARZ5JklIe6xT1D
+jW72Lg9w4Waekwr7Nh/yBN2iatiXiqK5781jjXU36dw9SWqYJTLuDic0u66GQj4u
+JDLMfc4oKQ+LPGI/1aUvPMdtvxTG+uE5kQLv6SS3jwuoRws9aiBtM05vgAO1gbtc
+S8kTjy1C1QMVvTulMXN2H/Z2m1NX+xfsrbXfl384/wDM2U4sIsFqLbEcddmlj84J
+SjbtWuZHBnbEd03Cp6Scaw66AXKIhWkRVwiHFkQKVn8Jj3d9/mEn9jl/ktMgsspD
+fP6utlNDv5abZ5Dj/jN/ODHBlZaCxMCVl4xLEq89SYGLgIbxD7+QyOItGr8Y1m5E
+TIdKINUExwVw6/Fw169DZWX6JAIiL01PjZtejT3jrqmGhiT/X/XF1K+QwJYRnPFK
+9OkwGX6MtgnRzHMxjLqLbsrvfZKcybd1MOBXjNK1tYoEDzrAZJLfViOX1VA1mDqe
+TL7GEam59WwuMPmELBICM5fqeiu6/YW/1+B7wesImxP04KXztWRTXZr5hodQlBs3
+w5A89HEmoRLCWyDJNcpaCBP9m6mp4LHDjeFcdkHGDC16P09r0fn+1oRVzjhFue+l
+WjShPRFJhUgkh8y+JimAhjpCm+a2O9/pJcMmIyCN39FTUKemS6QVuzZwwa4PYrT5
+r9GJ4mp6n1bBI3snDue2CxgTDk4CtWOXgS0rnDDgNwENTN56H3guCAAqr9mHjLGu
+SOr/UxScVBxto4YSCn0z4SbUw2O4nF0IR0bLKJEfpl0WszgRCve7+T0RmLv5klyl
+g67tIC6lsEz44DePOpTOZo6blpNnFbmk1Awm8wkb+ycv6uSaiKAsq7KnlBnaRmst
+d2dsBOLcMNZEvCrUR6ce6o/aDmB23Tz4N2o+bqlbVlcPChSwOM8TKxKzreUzrrPo
+4zp1C8rVEBg3SAOv5LmC2V0GwT0ms85xBQ0zjFoM9SUUcA0WKWOln8C6CyQK0mMv
+ZCxzvnfUrtfpGvfAtXhIIDTU6wm22aX6fH0eQ7wicEbuTSEe8WPOZXHLqqqWerwz
+ZKZAvUvDLtM+lHql+EhI4jIs3YvCO8pQGtGeSLn9o50M24d6a8zvsv0PNd1pNyxH
+HyCJqilc02hbkjUsRiN8JaTaC6uwO3VWHb6SuNcf8MXxTGhri8j3bsn7QdYddSvK
+GBUTpsUgecLVIqxfpq3y1T08IMGEYOPypN+3tUfKLn1TXU1iJCyzffFsNxt4CpJS
+KUG/ytJFT8XoxQwf5uRQ6pm6OFZKD7AycAaHa/GjDggqNU7YgYAOyrdbCx5XRi6l
+yD8hr759L1mOn1b7RGS/d7Z5Vvhnh7m7CuxUvC6/KJNmKJL4PiMGjfFPfcrUTD25
+3ZvHQFsKAe2zi+Iqs5g81m7X24XVm1UsRcdMqPZw8QrGsuneUBzfs5P5snsRkmOu
+b5/AC8zVH66GdlIZ0PygrO1e3RE1OqGbRH+QXHEd4v209QDmBlXinnqi1IouxXZK
+Cm0UjeR9y1uGlLknJsMIFe7FgFRYUxghZhc+99yBr/u5mNuc4v+8hTYsLtBgoPQ4
+CNz+67dOfqKMDJz8pd2qiF0x6oJuO9EzAkfYfCUJjK4JS4dHI0hw50/UfJShRNsv
+rprtj5dGSaVn8HAI4+ugMsD0If1dTMWFOvySmnayOfug5JhlKZk8whHJWz616o8b
+qJMbIqXQOFCeznb/WFtq66eXIlac9OPZ1oe5vApKNlkpqBfXHDLXwG9vKD76J/M3
+9JwHUSZRz8KIvYaUkiqWTy8u7o/DwTDaYSzIfvQfG7mCY+lGgrCbpzaHbxLndU1B
+RVNbd4GhxlQSzlqzP8awLJH3MWOaw09x9C2gg3cB9gLtwT7OIdjWXc8/O8Otgldq
+6/1wuavW+rGG3eMKpS7N5OE6tyUQCndTjDvpthvuaD6aPP3y9cGwe9Ujo1XMAZcx
+oW8+gMVxeURzftJm490j1UbfWSW+5XRr0JNI80sqRDaedmJ04MwxMP++TJY9Fswy
+F8xS7UFUCookBdcFFf8jHWqYj0hLp3Mq78MJTtGZgOI0Tt7XfVqX1MH5lRPpRWFx
+/4CgNf3L5GpUgdiIvCdJSk9J7ivWYeIMzxQ7eLv2EBVdTkosxrTI7bsZEqRm076m
+W9cALrsVc/0yuSo/ZPtB6rZ/L3BwRNmIcVMQlVZa3J8cILJDwpZ5xoTGd+qLkbA7
+UzM/uXsuqOsn6AmrgKrG07Dlm9RC4N9jjKuBuA/mvvYp90KaXcHU+4S1ZacfA0Ad
+4FtvBDwDMBs+tCcs7olAFXpKci6itL6QWfujaNtuEBmjtdizO7b13fkQI4UWp9Ud
+k4wT7Z53mdmgwnI8ufDi+Kn3eKcsjFMPneKa7qx3mM7LJd/sRr3aToZj7uwextDY
+BWVuFY8iA1HU33Bp2/DKYfR/UYcxAwNxPxOF9vOk1cqH0r60N0WDaEVuLJa9SVNv
+0ikCwjL0LLSv1otSsornwUCgp8QjzRT0XcunaJNVaWmQu50x8ZfNOINBXVg/4XK6
+Kyy3sqfhf0ZdtRwITZ7UiGvNbwOzsrCu5gfJjC/8waVUK61qIAyl3MHNxc/e60L1
+7XHf6iKtlxoPM2oNu7wMQiDYKcczRZe0kLkmBfNz//84U7Bu+uXrMfuG0NKsZJ81
+bxSvj7Kn8PSbY+n2OxuQyGYoRwUDUMQq8zCgdYInP4KX4KAbn83DeydEnPSz5XPW
+j21zD5MWXtDiVBgBxPr1C1msGaenem98nu3S50iBxN9M5ZEl473fxjs3WaFofWMZ
+5NVd1L5AkJIiMAIHAdHQDIEegoKkbeYUrvLd0vYvpXrsF2+7+hgKqefU0NgCWTzM
+glhn4wMP2QrLteA2EcC9IqiCazukHmO7oG476AByxeZj63WrLSNvw5I4NURBo2wR
+m3ZFJiTOSN3LYSIU49HT99jT/Y0Wn8qBvRNSr9ywXI+EvNMplerDDkuKjGCYs47l
+lUUMcQjsUL9hDSDb0JjGagHtd6O9jw34v27QG9ws+DrvGOvoNKn8auPTfzSUDkZt
+ztCLo09b1yT7wq7tT+kZW6DqOtfTbCN8NJRU5tVTJCtCtc0JEE5kK8yGovM96rnR
+qVY3zasA7+S87zWXyqhj8MxpHW/ZSJlQzuiolAjZoOaxtq4e8i4p+uQvZcE06g5b
+3bXBdw10xlbzInUG5b1YaJFtr1AVOZjQeduzCMBw0CFE7VAL0qA7MAoleMXcMMcN
+SEyh5eIhbD6hhmmJ3LMB1j7WXtd7tU5lfrIdPefwSd8rJNMx5Nhhc/Td7+u3ulFZ
+DgqMGr8YieaysasTHjTU28e+JRFzVyUAYWsVZXElbvhzkZS3iHATeFRYDMrteDYu
+XbosIx8Boyrcc6ow8eh372t2IRyn2Aj0Yfpvw6a+I9iGmD4RjbjHNDWSRGBt1kgU
+BNkq5DUapn1NNt4XIKnu4hQcyq0HldEAMVw3/VwFMeSQEpCVnwx6D77kGm65AYZo
+d4ESqFw+91UX1dYfyCJxflI3eHhIg/p72BZ7VHWlHk/cMJ8ie6BXBqXpUVAQ4LD+
+78vJeKYBAI+OoC86sPPzykoU+Pjmju0FCVbTxESbwGUrpHHxjQpGEQfOYA4jUSwm
+/163aUubZIjTCPKZa+euO2bBJ1diOPOS1+ZvzSzUlRJeFq5P60sROT2JtNdleNRI
+ZDulqCreiUZmhGCFk1MQQFESV6E9mNoVdML7mAbj/rPmghTnCryuev8IhP2P5ZA1
+BMOtQO1ZFsUvry//Xk7TBzApdsHtlQvI4CwFxnlYdwoqwXERbKKI6J5J23aIU+k2
+rMwuOg50ALDO1zmkucFU7hwhIYSccVQ3kru+Di/+YsSVU/62WgJwDYAH1ChOJu1/
+lvLpZnce7/+F2jxt0hJHYxPf9lIDvEbSyn6c8/ElAveIrusbMgLk8GqpAnnsXokv
+o+zxR48AXM0FsGjQ8WG59WVDUYbe/e86LH7PpNdw27qc1PI5mLw4Fof/S+1IDJm+
+oIAXJ3D0G2+mypD6n4RHX8Nem07lNSAOFRA9rkVxQ9dENs6sBiyetDtHLWeIy52M
+XqG9D2vb1jaingrDocLeLW8vEFDTjYB7TP/sjqNeYE+7v5sKA2YZ+V6ws1HkX3Rf
+HdMkLrz1B+DCulfjqg9u4G9IfE2t2GVYS97Vx/O/MjIoMTYU2e/5j67+PZvCY8Zf
+jtbIfJANX7v5/iLoWbbncyE0j6yNjazkrGLaiA45UmJDqLrev2P9UPRgjQUb+vgM
+KrQmCC6/C5z2KPiWVD0FGSOZr4G7HhGByawc6R1xFaXI8CbJ8sSPK5q+iNtR7hDb
+FAn/kbMrep8H1syLv0uAl2+uRPWkutdazbBAK1P85/yjfHTOxWOaqYB/TsHA7FdH
+Ps3b0QPwUtFsUJ75z4koEnVe0q6CNRMrrJitH6+i1jEmNnUXEq/ZkvA1NXblJGyu
+KMK8kOgxlZWPVMkKR+4TrHoKtOQ5hSrpH+e1S1sutWDEAzqW4wc8r3Bjpx4m6Zw8
+uLwkkqUZG2ZYXReAf1JPrzlA4KIiAjlL/hnn/x2owGx77LKGnVrgaGZBtBHTL4Kc
+UbOdRa0EPJ5qDQWQKAa71fy7Oj7wcF4roXarPcAvDsxUB4vvmCcecdE3QGf1zaF6
+siXMn26ldevLV9H4v7mehL//smO7FtFHtm/P5Kx+E1IWB4CXyuuC1qk7g9mn2oeq
+ixqSOrr2ud0LyETsYrQPiSqys3rww+SQLPSb3tmv/PFmpjFl3C0SaloSpOMggfxy
+a56maNVWwuNvKKMX5Hh4jujdq+wWO8Xfac0+ExedUMamJWcjlDoGDnbcsd0vwcYC
+SWbZZJIyqXSHfwxkWkAozq8hG+QNI5w/IyUZWg4Srdkhs6yoErqhy/ueF9nvNemO
+NUM7y6UsoEu0gvACds6ilkptfauk3Q1buXUEazSTL7oMjGa3HwfVnazx4J2HLTEb
+rfWed5Zb5HoVeFIhIE2kXGy9CDJWp+BdC/PUpTCM9UX38dUjFVxJj1Id+YRJ1o0c
+PW5FJhcOShCq/UkRHG9/OCsqDISOVMxbfkNOKV1WM6CHA2Y2Dm+Nu9wYLX2pwiBm
+uFmv6hHrLKNrEI2FfR+vIOov0OUbQRgCyQ1+e0qNZbq9byMyOm8sxf0Vb+zhg2yv
+6ZiQ3nJKZdvZgdHcyA1aR2aQxP3RZ3iIOZa/PjaMdV5cM+7J/pbBDb67jKtdbBDf
+WLGRHh8omhqXwAOcQQ7oNiZed5pNg2ZNNZGwguGm2B+l8V4zmgvxuD4i+VeqEO5T
+mPfP1AJOeVQ7f6+3dnqfDKM6ySIvzwiIc04hHkfzSgq3sGwmOdyJYxfRJphR/AH1
+j/J0Vzm+pgBswTdZtc7BemGt8s2/4CJp/eIwxT60Yucp9zEKPsedg0TkRFAOMQ22
+0ckKfRW8FQQDWxkRgrrAg+o/QNSF16dyHg7hnKNXjo0gR2f9HvzzLoyFLi/KxTxz
+EZxtz4VmwZcA808NxrkM2gomdNuBsp+iK5wsPiJe3CpQSirb1eyhX/dKbHD16ns2
+iDHX9WA3jIidNVvJFl3MzX9La6R6Efi+PCmcK6zlKd54CGxzT7wcFhUYRxcdNCBx
+FNqCJIlc9LNgxJ5KLXLCXThSJ8S1K0k0SP6VYQ6N6TObc/d0t4+XpHTZYG9ItMrT
+uQxwVbtsLqPxdLOz9WGTtbyhTSiVWe9+174cHUSeMfmT8CVeqEIn5tb8t52Cx3en
+oACa2ybPqR2McpVCSNwZkd6P+04JNu7fxZ5mf6M7YDwQiRq/JL6+StcKonhn9mg+
+0j6pIkz8piUYruHj67E647zItvwJf8mcdkCZtlZaAQQYZvUL1FFf/nLZSic6NhUo
+A09cnhVibf00QEhPjJ2PZyZ+32V+Oo9tYJJmCC6rpG85i0aYQc9cIv1Yku+bp2jD
+y14LioDF7+3C3XG00uganXzM/ZRvDMb0JenHhaB/EQAfW7jYbJkSsT74+IFpL2pQ
+oIkiC+f8MnticqXmnyXREBBq3hZaM3s6Nt/TNleX7TUYsHTA7QDluRqHnuAo/kMB
+pNF1aKQy+IHEz/duPiGp3nMj1CzXQvP6Ng5s5zF2aCWwMdqAYfhIx2YDUGK/U0I8
+nLCcSVNspS44jRV2fuY8AFMdwRLugrNgGDi/GwlkP545ff03KjKnO/vdPk/lUn9O
+ish3lNhvFDFnfjx/TArL0Yl0zdYqYydAxygJMynXyMvv0+MKv25L4uVIPqcUBUFp
+HvYuVZ1pFut4505wzKxs2Y1XBwZvQWoXbud7ndBknt8TS3SXbbjWcguJJIfUJMrG
+70CFoJPWDggJkMRifwACqgbmTXpfnWNypYu++McymhyYBRXvf2gtnSOmghnq++MD
+K8yNYD7bnFsLEedGIopPJYDCVvyZrfR1hcHyNVj5Gjxfmqrowe9SpGfLWAGuKNFf
+9NRgx22lW2PHPk/FUEcTlzWAsZuqNGWha2+MPCVDB39i/cb1Uxl2nJKD7J/fEAWM
+ck4GXColNtWPwI9JUGV6d1YOEi678gwhDQau4ErcAezncH2kjU9d0OGd4qn9IQNb
+o22bew9awlh6PT/jvO6ngYiZY6v5X2m3OMR4KLF2GEZRPIxf+vrBU8r8bueQaDXk
+3Fn+19aENaeSYdjGp54m4macm0OyECgWHGq//f8LOrHkRq36rapiaMzbY2G83oHK
+cAdbZsQ41m1cj887LP2tJcldWGFWhQ+QstpQaBOURGM9JqmPkuYOmGkvNRUraIum
+bqoJQIB8KxcD5uZqa4wtow==
+-----END MESSAGE-----
+signature aglChCQF+lbzKgyxJJTpYGVShV/GMDRJ4+cRGCp+a2y/yX/tLSh7hzqI7rVZrUoGj74Xr1CLMYO3fXYCS+DPDQ
diff --git a/test/unit/descriptor/hidden_service_v3.py b/test/unit/descriptor/hidden_service_v3.py
index f6407623..97763a39 100644
--- a/test/unit/descriptor/hidden_service_v3.py
+++ b/test/unit/descriptor/hidden_service_v3.py
@@ -31,6 +31,21 @@ oDrFE1+ztSxzN8sApKOb5UuDtoe/E03DxZU5+r/K5AV6G0hYn21V7Xbu2pZHvIkT
class TestHiddenServiceDescriptorV3(unittest.TestCase):
+ def test_for_decrypt(self):
+ """
+ Parse a test descriptor (used while making the v3 decode function)...
+
+ sltib6sxkuxh2scmtuvd5w2g7pahnzkovefxpo4e4ptnkzl5kkq5h2ad.onion
+ """
+
+ with open(get_resource('hidden_service_v3_test'), 'rb') as descriptor_file:
+ desc = next(stem.descriptor.parse_file(descriptor_file, 'hidden-service-descriptor-3 1.0', validate = True,
+ onion_address="sltib6sxkuxh2scmtuvd5w2g7pahnzkovefxpo4e4ptnkzl5kkq5h2ad.onion"))
+
+ self.assertEqual(3, desc.version)
+ self.assertEqual(180, desc.lifetime)
+ self.assertEqual(42, desc.revision_counter)
+
def test_for_riseup(self):
"""
Parse riseup's descriptor...
1
0
commit c90f397d6da23b15fc5896f83f5237e4517d7e2a
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sun Sep 22 12:44:16 2019 -0700
Shorten certificate type enumerations
Possible step back on my part, but we're attempting to keep enumeration names
as short as possible (since stem users need to type these). Just minor tweaks.
---
stem/descriptor/certificate.py | 37 +++++++++++++++++++++----------------
1 file changed, 21 insertions(+), 16 deletions(-)
diff --git a/stem/descriptor/certificate.py b/stem/descriptor/certificate.py
index cdde0c24..5895790f 100644
--- a/stem/descriptor/certificate.py
+++ b/stem/descriptor/certificate.py
@@ -26,16 +26,21 @@ used to validate the key used to sign server descriptors.
Purpose of Ed25519 certificate. As new certificate versions are added this
enumeration will expand.
- ============== ===========
- CertType Description
- ============== ===========
- **SIGNING** signing a signing key with an identity key
- **LINK_CERT** TLS link certificate signed with ed25519 signing key
- **AUTH** authentication key signed with ed25519 signing key
- **HS_V3_DESC_SIGNING_KEY** onion service v3 descriptor signing key cert (see rend-spec-v3.txt)
- **HS_V3_INTRO_POINT_AUTH_KEY** onion service v3 intro point authentication key cert (see rend-spec-v3.txt)
- **HS_V3_INTRO_POINT_ENC_KEY** onion service v3 intro point encryption key cert (see rend-spec-v3.txt)
- ============== ===========
+ For more information see...
+
+ * `cert-spec.txt <https://gitweb.torproject.org/torspec.git/tree/cert-spec.txt>`_ section A.1
+ * `rend-spec-v3.txt <https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt>`_ appendix E
+
+ ======================== ===========
+ CertType Description
+ ======================== ===========
+ **SIGNING** signing key with an identity key
+ **LINK_CERT** TLS link certificate signed with ed25519 signing key
+ **AUTH** authentication key signed with ed25519 signing key
+ **HS_V3_DESC_SIGNING** hidden service v3 short-term descriptor signing key
+ **HS_V3_INTRO_AUTH** hidden service v3 introductory point authentication key
+ **HS_V3_INTRO_ENCRYPT** hidden service v3 introductory point encryption key
+ ======================== ===========
.. data:: ExtensionType (enum)
@@ -77,9 +82,9 @@ CertType = stem.util.enum.UppercaseEnum(
'SIGNING',
'LINK_CERT',
'AUTH',
- 'HS_V3_DESC_SIGNING_KEY',
- 'HS_V3_INTRO_POINT_AUTH_KEY',
- 'HS_V3_INTRO_POINT_ENC_KEY',
+ 'HS_V3_DESC_SIGNING',
+ 'HS_V3_INTRO_AUTH',
+ 'HS_V3_INTRO_ENCRYPT',
)
ExtensionType = stem.util.enum.Enum(('HAS_SIGNING_KEY', 4),)
@@ -171,11 +176,11 @@ class Ed25519CertificateV1(Ed25519Certificate):
raise ValueError('Ed25519 certificate cannot have a type of 7. This is reserved for RSA identity cross-certification.')
elif cert_type == 8:
# see rend-spec-v3.txt appendix E for these defintions
- self.type = CertType.HS_V3_DESC_SIGNING_KEY
+ self.type = CertType.HS_V3_DESC_SIGNING
elif cert_type == 9:
- self.type = CertType.HS_V3_INTRO_POINT_AUTH_KEY
+ self.type = CertType.HS_V3_INTRO_AUTH
elif cert_type == 0x0B:
- self.type = CertType.HS_V3_INTRO_POINT_ENC_KEY
+ self.type = CertType.HS_V3_INTRO_ENCRYPT
else:
raise ValueError('Ed25519 certificate type %i is unrecognized' % cert_type)
1
0