summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDamian Johnson <atagar@torproject.org>2019-11-17 15:32:40 -0800
committerDamian Johnson <atagar@torproject.org>2019-11-17 15:40:21 -0800
commit91973b00077f9aba0d3064355838c8f8ace6e206 (patch)
tree8ee0a4df3b58ddb2d872adba6701a25ed5a6f9e2
parenteff0664caed44c3858a3aeb6cf9bbfc64c764501 (diff)
parent860afdb8f6edccdb31a9e4e13399b178645554b3 (diff)
HSv3 descriptor creation support
Cryptographically valid support for hidden service creation... https://trac.torproject.org/projects/tor/ticket/31823 HSv3 descriptors consist of three parts: an inner layer, outer layer, and the descriptor itself. Callers of HiddenServiceDescriptorV3's create() and content() methods can supply these to specify that layer's parameters. For example, to supply custom introduction points with random key material simply call... HiddenServiceDescriptorV3.content( inner_layer = InnerLayer.create( introduction_points = [ IntroductionPointV3.create('1.1.1.1', 9001), IntroductionPointV3.create('2.2.2.2', 9001), IntroductionPointV3.create('3.3.3.3', 9001), ], ), )
-rw-r--r--stem/client/datatype.py146
-rw-r--r--stem/descriptor/__init__.py4
-rw-r--r--stem/descriptor/certificate.py358
-rw-r--r--stem/descriptor/hidden_service.py638
-rw-r--r--stem/descriptor/router_status_entry.py7
-rw-r--r--stem/descriptor/server_descriptor.py4
-rw-r--r--stem/prereq.py37
-rw-r--r--stem/util/__init__.py31
-rw-r--r--stem/util/slow_ed25519.py158
-rw-r--r--stem/util/str_tools.py12
-rw-r--r--test/require.py3
-rw-r--r--test/settings.cfg2
-rw-r--r--test/unit/client/cell.py6
-rw-r--r--test/unit/client/certificate.py2
-rw-r--r--test/unit/client/link_specifier.py22
-rw-r--r--test/unit/descriptor/certificate.py70
-rw-r--r--test/unit/descriptor/data/hidden_service_v3_intro_point28
-rw-r--r--test/unit/descriptor/hidden_service_v3.py302
-rw-r--r--test/unit/descriptor/server_descriptor.py5
19 files changed, 1484 insertions, 351 deletions
diff --git a/stem/client/datatype.py b/stem/client/datatype.py
index 7e33e353..ac417b83 100644
--- a/stem/client/datatype.py
+++ b/stem/client/datatype.py
@@ -17,6 +17,12 @@ users.** See our :class:`~stem.client.Relay` the API you probably want.
LinkProtocol - ORPort protocol version.
Field - Packable and unpackable datatype.
+ |- 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.
+ |
|- Size - Field of a static size.
|- Address - Relay address.
|- Certificate - Relay certificate.
@@ -25,12 +31,6 @@ 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
@@ -83,16 +83,33 @@ users.** See our :class:`~stem.client.Relay` the API you probably want.
.. data:: CertType (enum)
- Relay certificate type.
-
- ===================== ===========
- CertType Description
- ===================== ===========
- **LINK** link key certificate certified by RSA1024 identity
- **IDENTITY** RSA1024 Identity certificate
- **AUTHENTICATE** RSA1024 AUTHENTICATE cell link certificate
- **UNKNOWN** unrecognized certificate type
- ===================== ===========
+ Certificate purpose. For more information see...
+
+ * `tor-spec.txt <https://gitweb.torproject.org/torspec.git/tree/tor-spec.txt>`_ section 4.2
+ * `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
+
+ .. versionchanged:: 1.8.0
+ Added the ED25519_SIGNING, LINK_CERT, ED25519_AUTHENTICATE,
+ ED25519_IDENTITY, HS_V3_DESC_SIGNING, HS_V3_INTRO_AUTH, NTOR_ONION_KEY,
+ and HS_V3_NTOR_ENC certificate types.
+
+ ========================= ===========
+ CertType Description
+ ========================= ===========
+ **LINK** link key certificate certified by RSA1024 identity
+ **IDENTITY** RSA1024 Identity certificate
+ **AUTHENTICATE** RSA1024 AUTHENTICATE cell link certificate
+ **ED25519_SIGNING** Ed25519 signing key, signed with identity key
+ **LINK_CERT** TLS link certificate, signed with ed25519 signing key
+ **ED25519_AUTHENTICATE** Ed25519 AUTHENTICATE cell key, signed with ed25519 signing key
+ **ED25519_IDENTITY** Ed25519 identity, signed with RSA identity
+ **HS_V3_DESC_SIGNING** hidden service v3 short-term descriptor signing key
+ **HS_V3_INTRO_AUTH** hidden service v3 introduction point authentication key
+ **NTOR_ONION_KEY** ntor onion key cross-certifying ed25519 identity key
+ **HS_V3_NTOR_ENC** hidden service v3 ntor-extra encryption key
+ **UNKNOWN** unrecognized certificate type
+ ========================= ===========
.. data:: CloseReason (enum)
@@ -201,9 +218,17 @@ RelayCommand = _IntegerEnum(
)
CertType = _IntegerEnum(
- ('LINK', 1),
- ('IDENTITY', 2),
- ('AUTHENTICATE', 3),
+ ('LINK', 1), # (tor-spec.txt section 4.2)
+ ('IDENTITY', 2), # (tor-spec.txt section 4.2)
+ ('AUTHENTICATE', 3), # (tor-spec.txt section 4.2)
+ ('ED25519_SIGNING', 4), # (prop220 section 4.2)
+ ('LINK_CERT', 5), # (prop220 section 4.2)
+ ('ED25519_AUTHENTICATE', 6), # (prop220 section 4.2)
+ ('ED25519_IDENTITY', 7), # (prop220 section 4.2)
+ ('HS_V3_DESC_SIGNING', 8), # (rend-spec-v3.txt, "DESC_OUTER" description)
+ ('HS_V3_INTRO_AUTH', 9), # (rend-spec-v3.txt, "auth-key" description)
+ ('NTOR_ONION_KEY', 10), # (dir-spec.txt, "ntor-onion-key-crosscert" description)
+ ('HS_V3_NTOR_ENC', 11), # (rend-spec-v3.txt, "enc-key-cert" description)
)
CloseReason = _IntegerEnum(
@@ -534,13 +559,15 @@ class Certificate(Field):
return stem.util._hash_attr(self, 'type_int', 'value')
-class LinkSpecifier(object):
+class LinkSpecifier(Field):
"""
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>`_.
+ .. versionadded:: 1.8.0
+
:var int type: numeric identifier of our type
:var bytes value: encoded link specification destination
"""
@@ -550,75 +577,94 @@ class LinkSpecifier(object):
self.value = value
@staticmethod
- def pop(content):
+ def pop(packed):
# 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)
+ link_type, packed = Size.CHAR.pop(packed)
+ value_size, packed = Size.CHAR.pop(packed)
- if value_size > len(content):
- raise ValueError('Link specifier should have %i bytes, but only had %i remaining' % (value_size, len(content)))
+ if value_size > len(packed):
+ raise ValueError('Link specifier should have %i bytes, but only had %i remaining' % (value_size, len(packed)))
- value, content = split(content, value_size)
+ value, packed = split(packed, value_size)
if link_type == 0:
- return LinkByIPv4(value), content
+ return LinkByIPv4.unpack(value), packed
elif link_type == 1:
- return LinkByIPv6(value), content
+ return LinkByIPv6.unpack(value), packed
elif link_type == 2:
- return LinkByFingerprint(value), content
+ return LinkByFingerprint(value), packed
elif link_type == 3:
- return LinkByEd25519(value), content
+ return LinkByEd25519(value), packed
else:
- return LinkSpecifier(link_type, value), content # unrecognized type
+ return LinkSpecifier(link_type, value), packed # unrecognized type
+
+ def pack(self):
+ cell = bytearray()
+ cell += Size.CHAR.pack(self.type)
+ cell += Size.CHAR.pack(len(self.value))
+ cell += self.value
+ return bytes(cell)
class LinkByIPv4(LinkSpecifier):
"""
TLS connection to an IPv4 address.
+ .. versionadded:: 1.8.0
+
:var str address: relay IPv4 address
:var int port: relay ORPort
"""
- def __init__(self, value):
- super(LinkByIPv4, self).__init__(0, value)
+ def __init__(self, address, port):
+ super(LinkByIPv4, self).__init__(0, _pack_ipv4_address(address) + Size.SHORT.pack(port))
+ self.address = address
+ self.port = port
+
+ @staticmethod
+ def unpack(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)
+ addr, port = split(value, 4)
+ return LinkByIPv4(_unpack_ipv4_address(addr), Size.SHORT.unpack(port))
class LinkByIPv6(LinkSpecifier):
"""
TLS connection to an IPv6 address.
+ .. versionadded:: 1.8.0
+
:var str address: relay IPv6 address
:var int port: relay ORPort
"""
- def __init__(self, value):
- super(LinkByIPv6, self).__init__(1, value)
+ def __init__(self, address, port):
+ super(LinkByIPv6, self).__init__(1, _pack_ipv6_address(address) + Size.SHORT.pack(port))
+
+ self.address = address
+ self.port = port
+ @staticmethod
+ def unpack(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)
+ addr, port = split(value, 16)
+ return LinkByIPv6(_unpack_ipv6_address(addr), Size.SHORT.unpack(port))
class LinkByFingerprint(LinkSpecifier):
"""
Connection to a SHA1 identity fingerprint.
+ .. versionadded:: 1.8.0
+
:var str fingerprint: relay sha1 fingerprint
"""
@@ -635,6 +681,8 @@ class LinkByEd25519(LinkSpecifier):
"""
Connection to a Ed25519 identity fingerprint.
+ .. versionadded:: 1.8.0
+
:var str fingerprint: relay ed25519 fingerprint
"""
@@ -681,15 +729,19 @@ 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
+def _pack_ipv4_address(address):
+ return b''.join([Size.CHAR.pack(int(v)) for v in address.split('.')])
+
+def _unpack_ipv4_address(value):
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
+def _pack_ipv6_address(address):
+ return b''.join([Size.SHORT.pack(int(v, 16)) for v in address.split(':')])
+
+def _unpack_ipv6_address(value):
return ':'.join(['%04x' % Size.SHORT.unpack(value[i * 2:(i + 1) * 2]) for i in range(8)])
diff --git a/stem/descriptor/__init__.py b/stem/descriptor/__init__.py
index 3a3d1838..49a4d4b5 100644
--- a/stem/descriptor/__init__.py
+++ b/stem/descriptor/__init__.py
@@ -895,6 +895,8 @@ class Descriptor(object):
* **NotImplementedError** if not implemented for this descriptor type
"""
+ # TODO: drop the 'sign' argument in stem 2.x (only a few subclasses use this)
+
raise NotImplementedError("The create and content methods haven't been implemented for %s" % cls.__name__)
@classmethod
@@ -975,7 +977,7 @@ class Descriptor(object):
:returns: **bytes** for the descriptor's contents
"""
- return self._raw_contents
+ return stem.util.str_tools._to_bytes(self._raw_contents)
def get_unrecognized_lines(self):
"""
diff --git a/stem/descriptor/certificate.py b/stem/descriptor/certificate.py
index 6ba92ee7..fe94b52d 100644
--- a/stem/descriptor/certificate.py
+++ b/stem/descriptor/certificate.py
@@ -19,22 +19,27 @@ used to for a variety of purposes...
Ed25519Certificate - Ed25519 signing key certificate
| +- Ed25519CertificateV1 - version 1 Ed25519 certificate
| |- is_expired - checks if certificate is presently expired
- | +- validate - validates signature of a server descriptor
+ | |- signing_key - certificate signing key
+ | +- validate - validates a descriptor's signature
|
- +- parse - reads base64 encoded certificate data
+ |- from_base64 - decodes a base64 encoded certificate
+ |- to_base64 - base64 encoding of this certificate
+ |
+ |- unpack - decodes a byte encoded certificate
+ +- pack - byte encoding of this certificate
Ed25519Extension - extension included within an Ed25519Certificate
.. data:: CertType (enum)
- Purpose of Ed25519 certificate. As new certificate versions are added this
- enumeration will expand.
-
- For more information see...
+ Purpose of Ed25519 certificate. 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
+ .. deprecated:: 1.8.0
+ Replaced with :data:`stem.client.datatype.CertType`
+
======================== ===========
CertType Description
======================== ===========
@@ -70,18 +75,32 @@ used to for a variety of purposes...
import base64
import binascii
-import collections
import datetime
import hashlib
+import re
-import stem.prereq
+import stem.descriptor.hidden_service
import stem.descriptor.server_descriptor
+import stem.prereq
+import stem.util
import stem.util.enum
import stem.util.str_tools
+from stem.client.datatype import Field, Size, split
+
+# TODO: Importing under an alternate name until we can deprecate our redundant
+# CertType enum in Stem 2.x.
+
+from stem.client.datatype import CertType as ClientCertType
+
+ED25519_KEY_LENGTH = 32
ED25519_HEADER_LENGTH = 40
ED25519_SIGNATURE_LENGTH = 64
-ED25519_ROUTER_SIGNATURE_PREFIX = b'Tor router descriptor signature v1'
+
+SIG_PREFIX_SERVER_DESC = b'Tor router descriptor signature v1'
+SIG_PREFIX_HS_V3 = b'Tor onion service descriptor sig v3'
+
+DEFAULT_EXPIRATION_HOURS = 54 # HSv3 certificate expiration of tor
CertType = stem.util.enum.UppercaseEnum(
'SIGNING',
@@ -96,16 +115,58 @@ ExtensionType = stem.util.enum.Enum(('HAS_SIGNING_KEY', 4),)
ExtensionFlag = stem.util.enum.UppercaseEnum('AFFECTS_VALIDATION', 'UNKNOWN')
-class Ed25519Extension(collections.namedtuple('Ed25519Extension', ['type', 'flags', 'flag_int', 'data'])):
+class Ed25519Extension(Field):
"""
Extension within an Ed25519 certificate.
- :var int type: extension type
+ :var stem.descriptor.certificate.ExtensionType type: extension type
:var list flags: extension attribute flags
:var int flag_int: integer encoding of the extension attribute flags
:var bytes data: data the extension concerns
"""
+ def __init__(self, ext_type, flag_val, data):
+ self.type = ext_type
+ self.flags = []
+ self.flag_int = flag_val if flag_val else 0
+ self.data = data
+
+ if flag_val and flag_val % 2 == 1:
+ self.flags.append(ExtensionFlag.AFFECTS_VALIDATION)
+ flag_val -= 1
+
+ if flag_val:
+ self.flags.append(ExtensionFlag.UNKNOWN)
+
+ if ext_type == ExtensionType.HAS_SIGNING_KEY and len(data) != 32:
+ raise ValueError('Ed25519 HAS_SIGNING_KEY extension must be 32 bytes, but was %i.' % len(data))
+
+ def pack(self):
+ encoded = bytearray()
+ encoded += Size.SHORT.pack(len(self.data))
+ encoded += Size.CHAR.pack(self.type)
+ encoded += Size.CHAR.pack(self.flag_int)
+ encoded += self.data
+ return bytes(encoded)
+
+ @staticmethod
+ def pop(content):
+ if len(content) < 4:
+ raise ValueError('Ed25519 extension is missing header fields')
+
+ data_size, content = Size.SHORT.pop(content)
+ ext_type, content = Size.CHAR.pop(content)
+ flags, content = Size.CHAR.pop(content)
+ data, content = split(content, data_size)
+
+ if len(data) != data_size:
+ raise ValueError("Ed25519 extension is truncated. It should have %i bytes of data but there's only %i." % (data_size, len(data)))
+
+ return Ed25519Extension(ext_type, flags, data), content
+
+ def __hash__(self):
+ return stem.util._hash_attr(self, 'type', 'flag_int', 'data', cache = True)
+
class Ed25519Certificate(object):
"""
@@ -115,14 +176,34 @@ class Ed25519Certificate(object):
:var unicode encoded: base64 encoded ed25519 certificate
"""
- def __init__(self, version, encoded):
+ def __init__(self, version):
self.version = version
- self.encoded = encoded
+ self.encoded = None # TODO: remove in stem 2.x
@staticmethod
- def parse(content):
+ def unpack(content):
+ """
+ Parses a byte encoded ED25519 certificate.
+
+ :param bytes content: encoded certificate
+
+ :returns: :class:`~stem.descriptor.certificate.Ed25519Certificate` subclsss
+ for the given certificate
+
+ :raises: **ValueError** if certificate is malformed
"""
- Parses the given base64 encoded data as an Ed25519 certificate.
+
+ version = Size.CHAR.pop(content)[0]
+
+ if version == 1:
+ return Ed25519CertificateV1.unpack(content)
+ else:
+ raise ValueError('Ed25519 certificate is version %i. Parser presently only supports version 1.' % version)
+
+ @staticmethod
+ def from_base64(content):
+ """
+ Parses a base64 encoded ED25519 certificate.
:param str content: base64 encoded certificate
@@ -142,15 +223,39 @@ class Ed25519Certificate(object):
if not decoded:
raise TypeError('empty')
+
+ instance = Ed25519Certificate.unpack(decoded)
+ instance.encoded = content
+ return instance
except (TypeError, binascii.Error) as exc:
raise ValueError("Ed25519 certificate wasn't propoerly base64 encoded (%s):\n%s" % (exc, content))
- version = stem.util.str_tools._to_int(decoded[0:1])
+ def pack(self):
+ """
+ Encoded byte representation of our certificate.
- if version == 1:
- return Ed25519CertificateV1(version, content, decoded)
- else:
- raise ValueError('Ed25519 certificate is version %i. Parser presently only supports version 1.' % version)
+ :returns: **bytes** for our encoded certificate representation
+ """
+
+ raise NotImplementedError('Certificate encoding has not been implemented for %s' % type(self).__name__)
+
+ def to_base64(self, pem = False):
+ """
+ Base64 encoded certificate data.
+
+ :param bool pem: include `PEM header/footer
+ <https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail>`_, for more
+ information see `RFC 7468 <https://tools.ietf.org/html/rfc7468>`_
+
+ :returns: **unicode** for our encoded certificate representation
+ """
+
+ encoded = b'\n'.join(stem.util.str_tools._split_by_length(base64.b64encode(self.pack()), 64))
+
+ if pem:
+ encoded = b'-----BEGIN ED25519 CERT-----\n%s\n-----END ED25519 CERT-----' % encoded
+
+ return stem.util.str_tools._to_unicode(encoded)
@staticmethod
def _from_descriptor(keyword, attribute):
@@ -160,12 +265,16 @@ class Ed25519Certificate(object):
if not block_contents or block_type != 'ED25519 CERT':
raise ValueError("'%s' should be followed by a ED25519 CERT block, but was a %s" % (keyword, block_type))
- setattr(descriptor, attribute, Ed25519Certificate.parse(block_contents))
+ setattr(descriptor, attribute, Ed25519Certificate.from_base64(block_contents))
return _parse
def __str__(self):
- return '-----BEGIN ED25519 CERT-----\n%s\n-----END ED25519 CERT-----' % self.encoded
+ return self.to_base64(pem = True)
+
+ @staticmethod
+ def parse(content):
+ return Ed25519Certificate.from_base64(content) # TODO: drop this alias in stem 2.x
class Ed25519CertificateV1(Ed25519Certificate):
@@ -173,85 +282,94 @@ class Ed25519CertificateV1(Ed25519Certificate):
Version 1 Ed25519 certificate, which are used for signing tor server
descriptors.
- :var CertType type: certificate purpose
+ :var stem.client.datatype.CertType type: certificate purpose
+ :var int type_int: integer value of the certificate purpose
:var datetime expiration: expiration of the certificate
:var int key_type: format of the key
:var bytes key: key content
:var list extensions: :class:`~stem.descriptor.certificate.Ed25519Extension` in this certificate
:var bytes signature: certificate signature
+
+ :param bytes signature: pre-calculated certificate signature
+ :param cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey signing_key: certificate signing key
"""
- def __init__(self, version, encoded, decoded):
- super(Ed25519CertificateV1, self).__init__(version, encoded)
+ def __init__(self, cert_type = None, expiration = None, key_type = None, key = None, extensions = None, signature = None, signing_key = None):
+ super(Ed25519CertificateV1, self).__init__(1)
- if len(decoded) < ED25519_HEADER_LENGTH + ED25519_SIGNATURE_LENGTH:
- raise ValueError('Ed25519 certificate was %i bytes, but should be at least %i' % (len(decoded), ED25519_HEADER_LENGTH + ED25519_SIGNATURE_LENGTH))
+ if cert_type is None:
+ raise ValueError('Certificate type is required')
+ elif key is None:
+ raise ValueError('Certificate key is required')
- cert_type = stem.util.str_tools._to_int(decoded[1:2])
+ self.type, self.type_int = ClientCertType.get(cert_type)
+ self.expiration = expiration if expiration else datetime.datetime.utcnow() + datetime.timedelta(hours = DEFAULT_EXPIRATION_HOURS)
+ self.key_type = key_type if key_type else 1
+ self.key = stem.util._pubkey_bytes(key)
+ self.extensions = extensions if extensions else []
+ self.signature = signature
- if cert_type in (0, 1, 2, 3):
- raise ValueError('Ed25519 certificate cannot have a type of %i. This is reserved to avoid conflicts with tor CERTS cells.' % cert_type)
- elif cert_type == 4:
- self.type = CertType.SIGNING
- elif cert_type == 5:
- self.type = CertType.LINK_CERT
- elif cert_type == 6:
- 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 definitions
- self.type = CertType.HS_V3_DESC_SIGNING
- elif cert_type == 9:
- self.type = CertType.HS_V3_INTRO_AUTH
- elif cert_type == 0x0B:
- self.type = CertType.HS_V3_INTRO_ENCRYPT
- else:
- raise ValueError('Ed25519 certificate type %i is unrecognized' % cert_type)
+ if signing_key:
+ calculated_sig = signing_key.sign(self.pack())
- # expiration time is in hours since epoch
- try:
- self.expiration = datetime.datetime.utcfromtimestamp(stem.util.str_tools._to_int(decoded[2:6]) * 3600)
- except ValueError as exc:
- raise ValueError('Invalid expiration timestamp (%s): %s' % (exc, stem.util.str_tools._to_int(decoded[2:6]) * 3600))
+ # if caller provides both signing key *and* signature then ensure they match
- self.key_type = stem.util.str_tools._to_int(decoded[6:7])
- self.key = decoded[7:39]
- self.signature = decoded[-ED25519_SIGNATURE_LENGTH:]
+ if self.signature and self.signature != calculated_sig:
+ raise ValueError("Signature calculated from its key (%s) mismatches '%s'" % (calculated_sig, self.signature))
- self.extensions = []
- extension_count = stem.util.str_tools._to_int(decoded[39:40])
- remaining_data = decoded[40:-ED25519_SIGNATURE_LENGTH]
+ self.signature = calculated_sig
- for i in range(extension_count):
- if len(remaining_data) < 4:
- raise ValueError('Ed25519 extension is missing header field data')
+ if self.type in (ClientCertType.LINK, ClientCertType.IDENTITY, ClientCertType.AUTHENTICATE):
+ raise ValueError('Ed25519 certificate cannot have a type of %i. This is reserved for CERTS cells.' % self.type_int)
+ elif self.type == ClientCertType.ED25519_IDENTITY:
+ raise ValueError('Ed25519 certificate cannot have a type of 7. This is reserved for RSA identity cross-certification.')
+ elif self.type == ClientCertType.UNKNOWN:
+ raise ValueError('Ed25519 certificate type %i is unrecognized' % self.type_int)
+
+ def pack(self):
+ encoded = bytearray()
+ encoded += Size.CHAR.pack(self.version)
+ encoded += Size.CHAR.pack(self.type_int)
+ encoded += Size.LONG.pack(int(stem.util.datetime_to_unix(self.expiration) / 3600))
+ encoded += Size.CHAR.pack(self.key_type)
+ encoded += self.key
+ encoded += Size.CHAR.pack(len(self.extensions))
+
+ for extension in self.extensions:
+ encoded += extension.pack()
- extension_length = stem.util.str_tools._to_int(remaining_data[:2])
- extension_type = stem.util.str_tools._to_int(remaining_data[2:3])
- extension_flags = stem.util.str_tools._to_int(remaining_data[3:4])
- extension_data = remaining_data[4:4 + extension_length]
+ if self.signature:
+ encoded += self.signature
- if extension_length != len(extension_data):
- raise ValueError("Ed25519 extension is truncated. It should have %i bytes of data but there's only %i." % (extension_length, len(extension_data)))
+ return bytes(encoded)
- flags, remaining_flags = [], extension_flags
+ @staticmethod
+ def unpack(content):
+ if len(content) < ED25519_HEADER_LENGTH + ED25519_SIGNATURE_LENGTH:
+ raise ValueError('Ed25519 certificate was %i bytes, but should be at least %i' % (len(content), ED25519_HEADER_LENGTH + ED25519_SIGNATURE_LENGTH))
- if remaining_flags % 2 == 1:
- flags.append(ExtensionFlag.AFFECTS_VALIDATION)
- remaining_flags -= 1
+ header, signature = split(content, len(content) - ED25519_SIGNATURE_LENGTH)
- if remaining_flags:
- flags.append(ExtensionFlag.UNKNOWN)
+ version, header = Size.CHAR.pop(header)
+ cert_type, header = Size.CHAR.pop(header)
+ expiration_hours, header = Size.LONG.pop(header)
+ key_type, header = Size.CHAR.pop(header)
+ key, header = split(header, ED25519_KEY_LENGTH)
+ extension_count, extension_data = Size.CHAR.pop(header)
- if extension_type == ExtensionType.HAS_SIGNING_KEY and len(extension_data) != 32:
- raise ValueError('Ed25519 HAS_SIGNING_KEY extension must be 32 bytes, but was %i.' % len(extension_data))
+ if version != 1:
+ raise ValueError('Ed25519 v1 parser cannot read version %i certificates' % version)
- self.extensions.append(Ed25519Extension(extension_type, flags, extension_flags, extension_data))
- remaining_data = remaining_data[4 + extension_length:]
+ extensions = []
- if remaining_data:
- raise ValueError('Ed25519 certificate had %i bytes of unused extension data' % len(remaining_data))
+ for i in range(extension_count):
+ extension, extension_data = Ed25519Extension.pop(extension_data)
+ extensions.append(extension)
+
+ if extension_data:
+ raise ValueError('Ed25519 certificate had %i bytes of unused extension data' % len(extension_data))
+
+ return Ed25519CertificateV1(cert_type, datetime.datetime.utcfromtimestamp(expiration_hours * 3600), key_type, key, extensions, signature)
def is_expired(self):
"""
@@ -280,56 +398,80 @@ class Ed25519CertificateV1(Ed25519Certificate):
def validate(self, descriptor):
"""
- Validates our signing key and that the given descriptor content matches its
- Ed25519 signature. Supported descriptor types include...
+ Validate our descriptor content matches its ed25519 signature. Supported
+ descriptor types include...
- * server descriptors
+ * :class:`~stem.descriptor.server_descriptor.RelayDescriptor`
+ * :class:`~stem.descriptor.hidden_service.HiddenServiceDescriptorV3`
:param stem.descriptor.__init__.Descriptor descriptor: descriptor to validate
:raises:
* **ValueError** if signing key or descriptor are invalid
- * **ImportError** if cryptography module is unavailable or ed25519 is
- unsupported
+ * **TypeError** if descriptor type is unsupported
+ * **ImportError** if cryptography module or ed25519 support unavailable
"""
- if not stem.prereq._is_crypto_ed25519_supported():
+ if not stem.prereq.is_crypto_available(ed25519 = True):
raise ImportError('Certificate validation requires the cryptography module and ed25519 support')
- from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
- from cryptography.exceptions import InvalidSignature
-
- if not isinstance(descriptor, stem.descriptor.server_descriptor.RelayDescriptor):
- raise ValueError('Certificate validation only supported for server descriptors, not %s' % type(descriptor).__name__)
+ if isinstance(descriptor, stem.descriptor.server_descriptor.RelayDescriptor):
+ signed_content = hashlib.sha256(Ed25519CertificateV1._signed_content(descriptor)).digest()
+ signature = stem.util.str_tools._decode_b64(descriptor.ed25519_signature)
- if descriptor.ed25519_master_key:
- signing_key = base64.b64decode(stem.util.str_tools._to_bytes(descriptor.ed25519_master_key) + b'=')
+ self._validate_server_desc_signing_key(descriptor)
+ elif isinstance(descriptor, stem.descriptor.hidden_service.HiddenServiceDescriptorV3):
+ signed_content = Ed25519CertificateV1._signed_content(descriptor)
+ signature = stem.util.str_tools._decode_b64(descriptor.signature)
else:
- signing_key = self.signing_key()
+ raise TypeError('Certificate validation only supported for server and hidden service descriptors, not %s' % type(descriptor).__name__)
- if not signing_key:
- raise ValueError('Server descriptor missing an ed25519 signing key')
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
+ from cryptography.exceptions import InvalidSignature
try:
- Ed25519PublicKey.from_public_bytes(signing_key).verify(self.signature, base64.b64decode(stem.util.str_tools._to_bytes(self.encoded))[:-ED25519_SIGNATURE_LENGTH])
+ key = Ed25519PublicKey.from_public_bytes(self.key)
+ key.verify(signature, signed_content)
except InvalidSignature:
- raise ValueError('Ed25519KeyCertificate signing key is invalid (Signature was forged or corrupt)')
+ raise ValueError('Descriptor Ed25519 certificate signature invalid (signature forged or corrupt)')
+
+ @staticmethod
+ def _signed_content(descriptor):
+ """
+ Provides this descriptor's signing constant, appended with the portion of
+ the descriptor that's signed.
+ """
- # ed25519 signature validates descriptor content up until the signature itself
+ if isinstance(descriptor, stem.descriptor.server_descriptor.RelayDescriptor):
+ prefix = SIG_PREFIX_SERVER_DESC
+ regex = b'(.+router-sig-ed25519 )'
+ elif isinstance(descriptor, stem.descriptor.hidden_service.HiddenServiceDescriptorV3):
+ prefix = SIG_PREFIX_HS_V3
+ regex = b'(.+)signature '
+ else:
+ raise ValueError('BUG: %s type unexpected' % type(descriptor).__name__)
+
+ match = re.search(regex, descriptor.get_bytes(), re.DOTALL)
- descriptor_content = descriptor.get_bytes()
+ if not match:
+ raise ValueError('Malformed descriptor missing signature line')
- if b'router-sig-ed25519 ' not in descriptor_content:
- raise ValueError("Descriptor doesn't have a router-sig-ed25519 entry.")
+ return prefix + match.group(1)
- signed_content = descriptor_content[:descriptor_content.index(b'router-sig-ed25519 ') + 19]
- descriptor_sha256_digest = hashlib.sha256(ED25519_ROUTER_SIGNATURE_PREFIX + signed_content).digest()
+ def _validate_server_desc_signing_key(self, descriptor):
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
+ from cryptography.exceptions import InvalidSignature
- missing_padding = len(descriptor.ed25519_signature) % 4
- signature_bytes = base64.b64decode(stem.util.str_tools._to_bytes(descriptor.ed25519_signature) + b'=' * missing_padding)
+ if descriptor.ed25519_master_key:
+ signing_key = base64.b64decode(stem.util.str_tools._to_bytes(descriptor.ed25519_master_key) + b'=')
+ else:
+ signing_key = self.signing_key()
+
+ if not signing_key:
+ raise ValueError('Server descriptor missing an ed25519 signing key')
try:
- verify_key = Ed25519PublicKey.from_public_bytes(self.key)
- verify_key.verify(signature_bytes, descriptor_sha256_digest)
+ key = Ed25519PublicKey.from_public_bytes(signing_key)
+ key.verify(self.signature, base64.b64decode(stem.util.str_tools._to_bytes(self.encoded))[:-ED25519_SIGNATURE_LENGTH])
except InvalidSignature:
- raise ValueError('Descriptor Ed25519 certificate signature invalid (Signature was forged or corrupt)')
+ raise ValueError('Ed25519KeyCertificate signing key is invalid (signature forged or corrupt)')
diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py
index 3277e183..3ad2f031 100644
--- a/stem/descriptor/hidden_service.py
+++ b/stem/descriptor/hidden_service.py
@@ -21,6 +21,8 @@ These are only available through the Controller's
BaseHiddenServiceDescriptor - Common parent for hidden service descriptors
|- HiddenServiceDescriptorV2 - Version 2 hidden service descriptor
+- HiddenServiceDescriptorV3 - Version 3 hidden service descriptor
+ |- address_from_identity_key - convert an identity key to address
+ |- identity_key_from_address - convert an address to identity key
+- decrypt - decrypt and parse encrypted layers
OuterLayer - First encrypted layer of a hidden service v3 descriptor
@@ -32,17 +34,24 @@ These are only available through the Controller's
import base64
import binascii
import collections
+import datetime
import hashlib
import io
+import os
import struct
+import time
import stem.client.datatype
+import stem.descriptor.certificate
import stem.prereq
+import stem.util
import stem.util.connection
import stem.util.str_tools
import stem.util.tor_tools
-from stem.descriptor.certificate import Ed25519Certificate
+from stem.client.datatype import CertType
+from stem.descriptor.certificate import ExtensionType, Ed25519Extension, Ed25519Certificate, Ed25519CertificateV1
+from stem.util import slow_ed25519
from stem.descriptor import (
PGP_BLOCK_END,
@@ -67,6 +76,13 @@ if stem.prereq._is_lru_cache_available():
else:
from stem.util.lru_cache import lru_cache
+try:
+ from cryptography.hazmat.backends.openssl.backend import backend
+ X25519_AVAILABLE = backend.x25519_supported()
+except ImportError:
+ X25519_AVAILABLE = False
+
+
REQUIRED_V2_FIELDS = (
'rendezvous-service-descriptor',
'version',
@@ -138,21 +154,245 @@ 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'])):
+class IntroductionPointV3(collections.namedtuple('IntroductionPointV3', ['link_specifiers', 'onion_key_raw', 'auth_key_cert', 'enc_key_raw', 'enc_key_cert', 'legacy_key_raw', 'legacy_key_cert'])):
"""
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
- :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
+ :var str onion_key_raw: base64 ntor introduction point public key
+ :var stem.descriptor.certificate.Ed25519Certificate auth_key_cert: cross-certifier of the signing key with the auth key
+ :var str enc_key_raw: base64 introduction request encryption key
+ :var stem.descriptor.certificate.Ed25519Certificate enc_key_cert: cross-certifier of the signing key by the encryption key
+ :var str legacy_key_raw: base64 legacy introduction point RSA public key
+ :var str legacy_key_cert: base64 cross-certifier of the signing key by the legacy key
"""
+ @staticmethod
+ def parse(content):
+ """
+ Parses an introduction point from its descriptor content.
+
+ :param str content: descriptor content to parse
+
+ :returns: :class:`~stem.descriptor.hidden_service.IntroductionPointV3` for the descriptor content
+
+ :raises: **ValueError** if descriptor content is malformed
+ """
+
+ entry = _descriptor_components(content, False)
+ link_specifiers = IntroductionPointV3._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_cert = entry['auth-key'][0]
+ auth_key_cert = Ed25519Certificate.from_base64(auth_key_cert)
+
+ 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]
+ enc_key_cert = Ed25519Certificate.from_base64(enc_key_cert)
+
+ 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
+
+ return IntroductionPointV3(link_specifiers, onion_key, auth_key_cert, enc_key, enc_key_cert, legacy_key, legacy_key_cert)
+
+ @staticmethod
+ def create(address, port, expiration = None, onion_key = None, enc_key = None, auth_key = None, signing_key = None):
+ """
+ Simplified constructor. For more sophisticated use cases you can use this
+ as a template for how introduction points are properly created.
+
+ :param str address: IPv4 or IPv6 address where the service is reachable
+ :param int port: port where the service is reachable
+ :param datetime.datetime expiration: when certificates should expire
+ :param str onion_key: encoded, X25519PublicKey, or X25519PrivateKey onion key
+ :param str enc_key: encoded, X25519PublicKey, or X25519PrivateKey encryption key
+ :param str auth_key: encoded, Ed25519PublicKey, or Ed25519PrivateKey authentication key
+ :param cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey signing_key: service signing key
+
+ :returns: :class:`~stem.descriptor.hidden_service.IntroductionPointV3` with these attributes
+
+ :raises: **ValueError** if the address, port, or keys are malformed
+ """
+
+ if not stem.prereq.is_crypto_available(ed25519 = True):
+ raise ImportError('Introduction point creation requires the cryptography module ed25519 support')
+ elif not stem.util.connection.is_valid_port(port):
+ raise ValueError("'%s' is an invalid port" % port)
+
+ if stem.util.connection.is_valid_ipv4_address(address):
+ link_specifiers = [stem.client.datatype.LinkByIPv4(address, port)]
+ elif stem.util.connection.is_valid_ipv6_address(address):
+ link_specifiers = [stem.client.datatype.LinkByIPv6(address, port)]
+ else:
+ raise ValueError("'%s' is not a valid IPv4 or IPv6 address" % address)
+
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
+
+ if expiration is None:
+ expiration = datetime.datetime.utcnow() + datetime.timedelta(hours = stem.descriptor.certificate.DEFAULT_EXPIRATION_HOURS)
+
+ onion_key = base64.b64encode(stem.util._pubkey_bytes(onion_key if onion_key else X25519PrivateKey.generate()))
+ enc_key = base64.b64encode(stem.util._pubkey_bytes(enc_key if enc_key else X25519PrivateKey.generate()))
+ auth_key = stem.util._pubkey_bytes(auth_key if auth_key else Ed25519PrivateKey.generate())
+ signing_key = signing_key if signing_key else Ed25519PrivateKey.generate()
+
+ extensions = [Ed25519Extension(ExtensionType.HAS_SIGNING_KEY, None, stem.util._pubkey_bytes(signing_key))]
+ auth_key_cert = Ed25519CertificateV1(CertType.HS_V3_INTRO_AUTH, expiration, 1, auth_key, extensions, signing_key = signing_key)
+ enc_key_cert = Ed25519CertificateV1(CertType.HS_V3_NTOR_ENC, expiration, 1, auth_key, extensions, signing_key = signing_key)
+
+ return IntroductionPointV3(link_specifiers, onion_key, auth_key_cert, enc_key, enc_key_cert, None, None)
+
+ def encode(self):
+ """
+ Descriptor representation of this introduction point.
+
+ :returns: **str** for our descriptor representation
+ """
+
+ lines = []
+
+ link_count = stem.client.datatype.Size.CHAR.pack(len(self.link_specifiers))
+ link_specifiers = link_count + b''.join([l.pack() for l in self.link_specifiers])
+ lines.append('introduction-point %s' % stem.util.str_tools._to_unicode(base64.b64encode(link_specifiers)))
+
+ if self.onion_key_raw:
+ lines.append('onion-key ntor %s' % self.onion_key_raw)
+
+ lines.append('auth-key\n' + self.auth_key_cert.to_base64(pem = True))
+
+ if self.enc_key_raw:
+ lines.append('enc-key ntor %s' % self.enc_key_raw)
+
+ lines.append('enc-key-cert\n' + self.enc_key_cert.to_base64(pem = True))
+
+ if self.legacy_key_raw:
+ lines.append('legacy-key\n' + self.legacy_key_raw)
+
+ if self.legacy_key_cert:
+ lines.append('legacy-key-cert\n' + self.legacy_key_cert)
+
+ return '\n'.join(lines)
+
+ def onion_key(self):
+ """
+ Provides our ntor introduction point public key.
+
+ :returns: ntor :class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey`
+
+ :raises:
+ * **ImportError** if required the cryptography module is unavailable
+ * **EnvironmentError** if OpenSSL x25519 unsupported
+ """
+
+ return IntroductionPointV3._key_as(self.onion_key_raw, x25519 = True)
+
+ def auth_key(self):
+ """
+ Provides our authentication certificate's public key.
+
+ :returns: :class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey`
+
+ :raises:
+ * **ImportError** if required the cryptography module is unavailable
+ * **EnvironmentError** if OpenSSL x25519 unsupported
+ """
+
+ return IntroductionPointV3._key_as(self.auth_key_cert.key, ed25519 = True)
+
+ def enc_key(self):
+ """
+ Provides our encryption key.
+
+ :returns: encryption :class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey`
+
+ :raises:
+ * **ImportError** if required the cryptography module is unavailable
+ * **EnvironmentError** if OpenSSL x25519 unsupported
+ """
+
+ return IntroductionPointV3._key_as(self.enc_key_raw, x25519 = True)
+
+ def legacy_key(self):
+ """
+ Provides our legacy introduction point public key.
+
+ :returns: legacy :class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey`
+
+ :raises:
+ * **ImportError** if required the cryptography module is unavailable
+ * **EnvironmentError** if OpenSSL x25519 unsupported
+ """
+
+ return IntroductionPointV3._key_as(self.legacy_key_raw, x25519 = True)
+
+ @staticmethod
+ def _key_as(value, x25519 = False, ed25519 = False):
+ if value is None or (not x25519 and not ed25519):
+ return value
+ elif not stem.prereq.is_crypto_available():
+ raise ImportError('cryptography module unavailable')
+
+ if x25519:
+ if not X25519_AVAILABLE:
+ # without this the cryptography raises...
+ # cryptography.exceptions.UnsupportedAlgorithm: X25519 is not supported by this version of OpenSSL.
+
+ raise EnvironmentError('OpenSSL x25519 unsupported')
+
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
+ return X25519PublicKey.from_public_bytes(base64.b64decode(value))
+
+ if ed25519:
+ if not stem.prereq.is_crypto_available(ed25519 = True):
+ raise EnvironmentError('cryptography ed25519 unsupported')
+
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
+ return Ed25519PublicKey.from_public_bytes(value)
+
+ @staticmethod
+ def _parse_link_specifiers(content):
+ try:
+ content = base64.b64decode(content)
+ except Exception as exc:
+ raise ValueError('Unable to base64 decode introduction point (%s): %s' % (exc, content))
+
+ link_specifiers = []
+ count, content = stem.client.datatype.Size.CHAR.pop(content)
+
+ for i in range(count):
+ link_specifier, content = stem.client.datatype.LinkSpecifier.pop(content)
+ link_specifiers.append(link_specifier)
+
+ if content:
+ raise ValueError('Introduction point had excessive data (%s)' % content)
+
+ return link_specifiers
+
+ def __hash__(self):
+ if not hasattr(self, '_hash'):
+ self._hash = hash(self.encode())
+
+ return self._hash
+
+ def __eq__(self, other):
+ return hash(self) == hash(other) if isinstance(other, IntroductionPointV3) else False
+
+ def __ne__(self, other):
+ return not self == other
+
class AuthorizedClient(collections.namedtuple('AuthorizedClient', ['id', 'iv', 'cookie'])):
"""
@@ -207,12 +447,6 @@ def _parse_file(descriptor_file, desc_type = None, validate = False, **kwargs):
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]
@@ -228,22 +462,43 @@ def _decrypt_layer(encrypted_block, constant, revision_counter, subcredential, b
ciphertext = encrypted[SALT_LEN:-MAC_LEN]
expected_mac = encrypted[-MAC_LEN:]
- kdf = hashlib.shake_256(blinded_key + subcredential + pack(revision_counter) + salt + constant)
+ cipher, mac_for = _layer_cipher(constant, revision_counter, subcredential, blinded_key, salt)
+
+ if expected_mac != mac_for(ciphertext):
+ raise ValueError('Malformed mac (expected %s, but was %s)' % (expected_mac, mac_for(ciphertext)))
+
+ decryptor = cipher.decryptor()
+ plaintext = decryptor.update(ciphertext) + decryptor.finalize()
+
+ return stem.util.str_tools._to_unicode(plaintext)
+
+
+def _encrypt_layer(plaintext, constant, revision_counter, subcredential, blinded_key):
+ salt = os.urandom(16)
+ cipher, mac_for = _layer_cipher(constant, revision_counter, subcredential, blinded_key, salt)
+
+ encryptor = cipher.encryptor()
+ ciphertext = encryptor.update(plaintext) + encryptor.finalize()
+ encoded = base64.b64encode(salt + ciphertext + mac_for(ciphertext))
+
+ return b'-----BEGIN MESSAGE-----\n%s\n-----END MESSAGE-----' % b'\n'.join(stem.util.str_tools._split_by_length(encoded, 64))
+
+
+def _layer_cipher(constant, revision_counter, subcredential, blinded_key, salt):
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+ from cryptography.hazmat.backends import default_backend
+
+ kdf = hashlib.shake_256(blinded_key + subcredential + struct.pack('>Q', 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()
+ mac_prefix = struct.pack('>Q', len(mac_key)) + mac_key + struct.pack('>Q', len(salt)) + salt
- return stem.util.str_tools._to_unicode(decryptor.update(ciphertext) + decryptor.finalize())
+ return cipher, lambda ciphertext: hashlib.sha3_256(mac_prefix + ciphertext).digest()
def _parse_protocol_versions_line(descriptor, entries):
@@ -310,72 +565,15 @@ def _parse_v3_introduction_points(descriptor, entries):
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
+ div = remaining.find(b'\nintroduction-point ', 10)
+ content, remaining = (remaining[:div], remaining[div + 1:]) if div != -1 else (remaining, '')
- _, 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,
- )
- )
+ introduction_points.append(IntroductionPointV3.parse(content))
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')
@@ -536,8 +734,7 @@ class HiddenServiceDescriptorV2(BaseHiddenServiceDescriptor):
raise DecryptionFailure('Decrypting introduction-points requires the cryptography module')
try:
- missing_padding = len(authentication_cookie) % 4
- authentication_cookie = base64.b64decode(stem.util.str_tools._to_bytes(authentication_cookie) + b'=' * missing_padding)
+ authentication_cookie = stem.util.str_tools._decode_b64(authentication_cookie)
except TypeError as exc:
raise DecryptionFailure('authentication_cookie must be a base64 encoded string (%s)' % exc)
@@ -674,13 +871,15 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
:var int version: **\\*** hidden service descriptor version
:var int lifetime: **\\*** minutes after publication this descriptor is valid
- :var stem.certificate.Ed25519Certificate signing_cert: **\\*** cross-certifier for the short-term descriptor signing key
+ :var stem.descriptor.certificate.Ed25519Certificate signing_cert: **\\*** cross-certifier for the short-term descriptor signing key
:var int revision_counter: **\\*** descriptor revision number
:var str superencrypted: **\\*** encrypted HS-DESC-ENC payload
:var str signature: **\\*** signature of this descriptor
**\\*** attribute is either required when we're parsed with validation or has
a default value, others are left as **None** if undefined
+
+ .. versionadded:: 1.8.0
"""
# TODO: requested this @type on https://trac.torproject.org/projects/tor/ticket/31481
@@ -706,22 +905,105 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
}
@classmethod
- def content(cls, attr = None, exclude = (), sign = False):
- if sign:
- raise NotImplementedError('Signing of %s not implemented' % cls.__name__)
+ def content(cls, attr = None, exclude = (), sign = False, inner_layer = None, outer_layer = None, identity_key = None, signing_key = None, signing_cert = None, revision_counter = None, blinding_nonce = None):
+ """
+ Hidden service v3 descriptors consist of three parts:
- return _descriptor_content(attr, exclude, (
+ * InnerLayer, which most notably contain introduction points where the
+ service can be reached.
+
+ * OuterLayer, which encrypts the InnerLayer among other paremters.
+
+ * HiddenServiceDescriptorV3, which contains the OuterLayer and plaintext
+ parameters.
+
+ Construction through this method can supply any or none of these, with
+ omitted parameters populated with randomized defaults.
+
+ **Ed25519 key blinding takes several seconds**, and as such is disabled if a
+ **blinding_nonce** is not provided. To blind with a random nonce simply
+ call...
+
+ ::
+
+ HiddenServiceDescriptorV3(blinding_nonce = os.urandom(32))
+
+ :param dict attr: keyword/value mappings to be included in plaintext descriptor
+ :param list exclude: mandatory keywords to exclude from the descriptor, this
+ results in an invalid descriptor
+ :param bool sign: includes cryptographic signatures and digests if True
+ :param stem.descriptor.hidden_service.InnerLayer inner_layer: inner
+ encrypted layer
+ :param stem.descriptor.hidden_service.OuterLayer outer_layer: outer
+ encrypted layer
+ :param cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey
+ identity_key: service identity key
+ :param cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey
+ signing_key: service signing key
+ :param stem.descriptor.Ed25519CertificateV1 signing_cert: certificate
+ signing this descriptor
+ :param int revision_counter: descriptor revision number
+ :param bytes blinding_nonce: 32 byte blinding factor to derive the blinding key
+
+ :returns: **str** with the content of a descriptor
+
+ :raises:
+ * **ValueError** if parameters are malformed
+ * **ImportError** if cryptography is unavailable
+ """
+
+ if not stem.prereq.is_crypto_available(ed25519 = True):
+ raise ImportError('Hidden service descriptor creation requires cryptography version 2.6')
+ elif not stem.prereq._is_sha3_available():
+ raise ImportError('Hidden service descriptor creation requires python 3.6+ or the pysha3 module (https://pypi.org/project/pysha3/)')
+ elif blinding_nonce and len(blinding_nonce) != 32:
+ raise ValueError('Blinding nonce must be 32 bytes, but was %i' % len(blinding_nonce))
+
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+
+ inner_layer = inner_layer if inner_layer else InnerLayer.create(exclude = exclude)
+ identity_key = identity_key if identity_key else Ed25519PrivateKey.generate()
+ signing_key = signing_key if signing_key else Ed25519PrivateKey.generate()
+ revision_counter = revision_counter if revision_counter else int(time.time())
+
+ blinded_key = _blinded_pubkey(identity_key, blinding_nonce) if blinding_nonce else b'a' * 32
+ subcredential = HiddenServiceDescriptorV3._subcredential(identity_key, blinded_key)
+ custom_sig = attr.pop('signature') if (attr and 'signature' in attr) else None
+
+ if not outer_layer:
+ outer_layer = OuterLayer.create(
+ exclude = exclude,
+ inner_layer = inner_layer,
+ revision_counter = revision_counter,
+ subcredential = subcredential,
+ blinded_key = blinded_key,
+ )
+
+ if not signing_cert:
+ extensions = [Ed25519Extension(ExtensionType.HAS_SIGNING_KEY, None, blinded_key)]
+
+ signing_cert = Ed25519CertificateV1(cert_type = CertType.HS_V3_DESC_SIGNING, key = signing_key, extensions = extensions)
+ signing_cert.signature = _blinded_sign(signing_cert.pack(), identity_key, blinded_key, blinding_nonce) if blinding_nonce else b'b' * 64
+
+ desc_content = _descriptor_content(attr, exclude, (
('hs-descriptor', '3'),
('descriptor-lifetime', '180'),
- ('descriptor-signing-key-cert', _random_crypto_blob('ED25519 CERT')),
- ('revision-counter', '15'),
- ('superencrypted', _random_crypto_blob('MESSAGE')),
- ('signature', 'wdc7ffr+dPZJ/mIQ1l4WYqNABcmsm6SHW/NL3M3wG7bjjqOJWoPR5TimUXxH52n5Zk0Gc7hl/hz3YYmAx5MvAg'),
- ), ())
+ ('descriptor-signing-key-cert', '\n' + signing_cert.to_base64(pem = True)),
+ ('revision-counter', str(revision_counter)),
+ ('superencrypted', b'\n' + outer_layer._encrypt(revision_counter, subcredential, blinded_key)),
+ ), ()) + b'\n'
+
+ if custom_sig:
+ desc_content += b'signature %s' % custom_sig
+ elif 'signature' not in exclude:
+ sig_content = stem.descriptor.certificate.SIG_PREFIX_HS_V3 + desc_content
+ desc_content += b'signature %s' % base64.b64encode(signing_key.sign(sig_content)).rstrip(b'=')
+
+ return desc_content
@classmethod
- 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 create(cls, attr = None, exclude = (), validate = True, sign = False, inner_layer = None, outer_layer = None, identity_key = None, signing_key = None, signing_cert = None, revision_counter = None, blinding_nonce = None):
+ return cls(cls.content(attr, exclude, sign, inner_layer, outer_layer, identity_key, signing_key, signing_cert, revision_counter, blinding_nonce), validate = validate)
def __init__(self, raw_contents, validate = False):
super(HiddenServiceDescriptorV3, self).__init__(raw_contents, lazy_load = not validate)
@@ -742,6 +1024,9 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
raise ValueError("Hidden service descriptor must end with a 'signature' entry")
self._parse(entries, validate)
+
+ if self.signing_cert and stem.prereq.is_crypto_available(ed25519 = True):
+ self.signing_cert.validate(self)
else:
self._entries = entries
@@ -767,17 +1052,13 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
raise ImportError('Hidden service descriptor decryption requires python 3.6+ or the pysha3 module (https://pypi.org/project/pysha3/)')
if self._inner_layer is None:
- blinded_key = self.signing_cert.signing_key()
+ blinded_key = self.signing_cert.signing_key() if self.signing_cert else None
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()
+ identity_public_key = HiddenServiceDescriptorV3.identity_key_from_address(onion_address)
+ subcredential = HiddenServiceDescriptorV3._subcredential(identity_public_key, blinded_key)
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)
@@ -785,8 +1066,46 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
return self._inner_layer
@staticmethod
- def _public_key_from_address(onion_address):
- # provides our hidden service ed25519 public key
+ def address_from_identity_key(key, suffix = True):
+ """
+ Converts a hidden service identity key into its address. This accepts all
+ key formats (private, public, or public bytes).
+
+ :param Ed25519PublicKey,Ed25519PrivateKey,bytes key: hidden service identity key
+ :param bool suffix: includes the '.onion' suffix if true, excluded otherwise
+
+ :returns: **unicode** hidden service address
+
+ :raises: **ImportError** if sha3 unsupported
+ """
+
+ if not stem.prereq._is_sha3_available():
+ raise ImportError('Hidden service address conversion requires python 3.6+ or the pysha3 module (https://pypi.org/project/pysha3/)')
+
+ key = stem.util._pubkey_bytes(key) # normalize key into bytes
+
+ version = stem.client.datatype.Size.CHAR.pack(3)
+ checksum = hashlib.sha3_256(CHECKSUM_CONSTANT + key + version).digest()[:2]
+ onion_address = base64.b32encode(key + checksum + version)
+
+ return stem.util.str_tools._to_unicode(onion_address + b'.onion' if suffix else onion_address).lower()
+
+ @staticmethod
+ def identity_key_from_address(onion_address):
+ """
+ Converts a hidden service address into its public identity key.
+
+ :param str onion_address: hidden service address
+
+ :returns: **bytes** for the hidden service's public identity key
+
+ :raises:
+ * **ImportError** if sha3 unsupported
+ * **ValueError** if address malformed or checksum is invalid
+ """
+
+ if not stem.prereq._is_sha3_available():
+ raise ImportError('Hidden service address conversion requires python 3.6+ or the pysha3 module (https://pypi.org/project/pysha3/)')
if onion_address.endswith('.onion'):
onion_address = onion_address[:-6]
@@ -813,6 +1132,14 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
return pubkey
+ @staticmethod
+ def _subcredential(identity_key, blinded_key):
+ # credential = H('credential' | public-identity-key)
+ # subcredential = H('subcredential' | credential | blinded-public-key)
+
+ credential = hashlib.sha3_256(b'credential%s' % stem.util._pubkey_bytes(identity_key)).digest()
+ return hashlib.sha3_256(b'subcredential%s%s' % (credential, blinded_key)).digest()
+
class OuterLayer(Descriptor):
"""
@@ -850,6 +1177,39 @@ class OuterLayer(Descriptor):
plaintext = _decrypt_layer(encrypted, b'hsdir-superencrypted-data', revision_counter, subcredential, blinded_key)
return OuterLayer(plaintext)
+ def _encrypt(self, revision_counter, subcredential, blinded_key):
+ # Spec mandated padding: "Before encryption the plaintext is padded with
+ # NUL bytes to the nearest multiple of 10k bytes."
+
+ content = self.get_bytes() + b'\x00' * (len(self.get_bytes()) % 10000)
+
+ # encrypt back into a hidden service descriptor's 'superencrypted' field
+
+ return _encrypt_layer(content, b'hsdir-superencrypted-data', revision_counter, subcredential, blinded_key)
+
+ @classmethod
+ def content(cls, attr = None, exclude = (), validate = True, sign = False, inner_layer = None, revision_counter = None, subcredential = None, blinded_key = None):
+ if not stem.prereq.is_crypto_available(ed25519 = True):
+ raise ImportError('Hidden service layer creation requires cryptography version 2.6')
+
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+
+ inner_layer = inner_layer if inner_layer else InnerLayer.create()
+ revision_counter = revision_counter if revision_counter else 1
+ blinded_key = blinded_key if blinded_key else stem.util._pubkey_bytes(Ed25519PrivateKey.generate())
+ subcredential = subcredential if subcredential else HiddenServiceDescriptorV3._subcredential(Ed25519PrivateKey.generate(), blinded_key)
+
+ return _descriptor_content(attr, exclude, (
+ ('desc-auth-type', 'x25519'),
+ ('desc-auth-ephemeral-key', base64.b64encode(os.urandom(32))),
+ ), (
+ ('encrypted', b'\n' + inner_layer._encrypt(revision_counter, subcredential, blinded_key)),
+ ))
+
+ @classmethod
+ def create(cls, attr = None, exclude = (), validate = True, sign = False, inner_layer = None, revision_counter = None, subcredential = None, blinded_key = None):
+ return cls(cls.content(attr, exclude, validate, sign, inner_layer, revision_counter, subcredential, blinded_key), validate = validate)
+
def __init__(self, content, validate = False):
content = content.rstrip('\x00') # strip null byte padding
@@ -896,16 +1256,37 @@ class InnerLayer(Descriptor):
@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)
+ return InnerLayer(plaintext, validate = True, outer_layer = outer_layer)
+
+ def _encrypt(self, revision_counter, subcredential, blinded_key):
+ # encrypt back into an outer layer's 'encrypted' field
+
+ return _encrypt_layer(self.get_bytes(), b'hsdir-encrypted-data', revision_counter, subcredential, blinded_key)
+
+ @classmethod
+ def content(cls, attr = None, exclude = (), sign = False, introduction_points = None):
+ if introduction_points:
+ suffix = '\n' + '\n'.join(map(IntroductionPointV3.encode, introduction_points))
+ else:
+ suffix = ''
+
+ return _descriptor_content(attr, exclude, (
+ ('create2-formats', '2'),
+ )) + stem.util.str_tools._to_bytes(suffix)
+
+ @classmethod
+ def create(cls, attr = None, exclude = (), validate = True, sign = False, introduction_points = None):
+ return cls(cls.content(attr, exclude, sign, introduction_points), validate = validate)
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
+ # inner layer begins with a few header fields, followed by any
# number of introduction-points
- div = content.find('\nintroduction-point ')
+ content = stem.util.str_tools._to_bytes(content)
+ div = content.find(b'\nintroduction-point ')
if div != -1:
self._unparsed_introduction_points = content[div + 1:]
@@ -922,6 +1303,47 @@ class InnerLayer(Descriptor):
self._entries = entries
+def _blinded_pubkey(identity_key, blinding_nonce):
+ mult = 2 ** (slow_ed25519.b - 2) + sum(2 ** i * slow_ed25519.bit(blinding_nonce, i) for i in range(3, slow_ed25519.b - 2))
+ P = slow_ed25519.decodepoint(stem.util._pubkey_bytes(identity_key))
+ return slow_ed25519.encodepoint(slow_ed25519.scalarmult(P, mult))
+
+
+def _blinded_sign(msg, identity_key, blinded_key, blinding_nonce):
+ from cryptography.hazmat.primitives import serialization
+
+ identity_key_bytes = identity_key.private_bytes(
+ encoding = serialization.Encoding.Raw,
+ format = serialization.PrivateFormat.Raw,
+ encryption_algorithm = serialization.NoEncryption(),
+ )
+
+ # pad private identity key into an ESK (encrypted secret key)
+
+ h = slow_ed25519.H(identity_key_bytes)
+ a = 2 ** (slow_ed25519.b - 2) + sum(2 ** i * slow_ed25519.bit(h, i) for i in range(3, slow_ed25519.b - 2))
+ k = b''.join([h[i:i + 1] for i in range(slow_ed25519.b // 8, slow_ed25519.b // 4)])
+ esk = slow_ed25519.encodeint(a) + k
+
+ # blind the ESK with this nonce
+
+ mult = 2 ** (slow_ed25519.b - 2) + sum(2 ** i * slow_ed25519.bit(blinding_nonce, i) for i in range(3, slow_ed25519.b - 2))
+ s = slow_ed25519.decodeint(esk[:32])
+ s_prime = (s * mult) % slow_ed25519.l
+ k = esk[32:]
+ k_prime = slow_ed25519.H(b'Derive temporary signing key hash input' + k)[:32]
+ blinded_esk = slow_ed25519.encodeint(s_prime) + k_prime
+
+ # finally, sign the message
+
+ a = slow_ed25519.decodeint(blinded_esk[:32])
+ r = slow_ed25519.Hint(b''.join([blinded_esk[i:i + 1] for i in range(slow_ed25519.b // 8, slow_ed25519.b // 4)]) + msg)
+ R = slow_ed25519.scalarmult(slow_ed25519.B, r)
+ S = (r + slow_ed25519.Hint(slow_ed25519.encodepoint(R) + blinded_key + msg) * a) % slow_ed25519.l
+
+ return slow_ed25519.encodepoint(R) + slow_ed25519.encodeint(S)
+
+
# TODO: drop this alias in stem 2.x
HiddenServiceDescriptor = HiddenServiceDescriptorV2
diff --git a/stem/descriptor/router_status_entry.py b/stem/descriptor/router_status_entry.py
index ce662b40..91f2c146 100644
--- a/stem/descriptor/router_status_entry.py
+++ b/stem/descriptor/router_status_entry.py
@@ -21,7 +21,6 @@ sources...
+- RouterStatusEntryMicroV3 - Entry for a microdescriptor flavored v3 document
"""
-import base64
import binascii
import io
@@ -369,12 +368,8 @@ def _base64_to_hex(identity, check_if_fingerprint = True):
:raises: **ValueError** if the result isn't a valid fingerprint
"""
- # trailing equal signs were stripped from the identity
- missing_padding = len(identity) % 4
- identity += '=' * missing_padding
-
try:
- identity_decoded = base64.b64decode(stem.util.str_tools._to_bytes(identity))
+ identity_decoded = stem.util.str_tools._decode_b64(stem.util.str_tools._to_bytes(identity))
except (TypeError, binascii.Error):
raise ValueError("Unable to decode identity string '%s'" % identity)
diff --git a/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py
index ebadcde2..9c29164b 100644
--- a/stem/descriptor/server_descriptor.py
+++ b/stem/descriptor/server_descriptor.py
@@ -406,7 +406,7 @@ def _parse_identity_ed25519_line(descriptor, entries):
_parse_key_block('identity-ed25519', 'ed25519_certificate', 'ED25519 CERT')(descriptor, entries)
if descriptor.ed25519_certificate:
- descriptor.certificate = stem.descriptor.certificate.Ed25519Certificate.parse(descriptor.ed25519_certificate)
+ descriptor.certificate = stem.descriptor.certificate.Ed25519Certificate.from_base64(descriptor.ed25519_certificate)
_parse_master_key_ed25519_line = _parse_simple_line('master-key-ed25519', 'ed25519_master_key')
@@ -858,7 +858,7 @@ class RelayDescriptor(ServerDescriptor):
if onion_key_crosscert_digest != self._onion_key_crosscert_digest():
raise ValueError('Decrypted onion-key-crosscert digest does not match local digest (calculated: %s, local: %s)' % (onion_key_crosscert_digest, self._onion_key_crosscert_digest()))
- if stem.prereq._is_crypto_ed25519_supported() and self.certificate:
+ if stem.prereq.is_crypto_available(ed25519 = True) and self.certificate:
self.certificate.validate(self)
@classmethod
diff --git a/stem/prereq.py b/stem/prereq.py
index e8218e7f..4af6c093 100644
--- a/stem/prereq.py
+++ b/stem/prereq.py
@@ -26,6 +26,9 @@ import inspect
import platform
import sys
+# TODO: in stem 2.x consider replacing these functions with requirement
+# annotations (like our tests)
+
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.org/project/cryptography/"
ZSTD_UNAVAILABLE = 'ZSTD compression requires the zstandard module (https://pypi.org/project/zstandard/)'
LZMA_UNAVAILABLE = 'LZMA compression requires the lzma module (https://docs.python.org/3/library/lzma.html)'
@@ -127,12 +130,14 @@ def is_crypto_available(ed25519 = False):
:param bool ed25519: check for `ed25519 support
<https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ed25519/>`_,
- which was added in version 2.6
+ which requires both cryptography version 2.6 and OpenSSL support
:returns: **True** if we can use the cryptography module and **False**
otherwise
"""
+ from stem.util import log
+
try:
from cryptography.utils import int_from_bytes, int_to_bytes
from cryptography.hazmat.backends import default_backend
@@ -145,11 +150,17 @@ def is_crypto_available(ed25519 = False):
raise ImportError()
if ed25519:
+ # The following import confirms cryptography support (ie. version 2.6+),
+ # whereas ed25519_supported() checks for OpenSSL bindings.
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
+ if not hasattr(backend, 'ed25519_supported') or not backend.ed25519_supported():
+ log.log_once('stem.prereq._is_crypto_ed25519_supported', log.INFO, ED25519_UNSUPPORTED)
+ return False
+
return True
except ImportError:
- from stem.util import log
log.log_once('stem.prereq.is_crypto_available', log.INFO, CRYPTO_UNAVAILABLE)
return False
@@ -253,28 +264,6 @@ def _is_lru_cache_available():
return hasattr(functools, 'lru_cache')
-def _is_crypto_ed25519_supported():
- """
- Checks if ed25519 is supported by current versions of the cryptography
- package and OpenSSL. This is used for verifying ed25519 certificates in relay
- descriptor signatures.
-
- :returns: **True** if ed25519 is supported and **False** otherwise
- """
-
- if not is_crypto_available():
- return False
-
- from stem.util import log
- from cryptography.hazmat.backends.openssl.backend import backend
-
- if hasattr(backend, 'ed25519_supported') and backend.ed25519_supported():
- return True
- else:
- log.log_once('stem.prereq._is_crypto_ed25519_supported', log.INFO, ED25519_UNSUPPORTED)
- return False
-
-
def _is_sha3_available():
"""
Check if hashlib has sha3 support. This requires Python 3.6+ *or* the `pysha3
diff --git a/stem/util/__init__.py b/stem/util/__init__.py
index bff894dc..eb4e0618 100644
--- a/stem/util/__init__.py
+++ b/stem/util/__init__.py
@@ -127,6 +127,37 @@ def datetime_to_unix(timestamp):
return (timestamp - datetime.datetime(1970, 1, 1)).total_seconds()
+def _pubkey_bytes(key):
+ """
+ Normalizes X25509 and ED25519 keys into their public key bytes.
+ """
+
+ if _is_str(key):
+ return key
+
+ if not stem.prereq.is_crypto_available():
+ raise ImportError('Key normalization requires the cryptography module')
+ elif not stem.prereq.is_crypto_available(ed25519 = True):
+ raise ImportError('Key normalization requires the cryptography ed25519 support')
+
+ from cryptography.hazmat.primitives import serialization
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
+
+ if isinstance(key, (X25519PrivateKey, Ed25519PrivateKey)):
+ return key.public_key().public_bytes(
+ encoding = serialization.Encoding.Raw,
+ format = serialization.PublicFormat.Raw,
+ )
+ elif isinstance(key, (X25519PublicKey, Ed25519PublicKey)):
+ return key.public_bytes(
+ encoding = serialization.Encoding.Raw,
+ format = serialization.PublicFormat.Raw,
+ )
+ else:
+ raise ValueError('Key must be a string or cryptographic public/private key (was %s)' % type(key).__name__)
+
+
def _hash_attr(obj, *attributes, **kwargs):
"""
Provide a hash value for the given set of attributes.
diff --git a/stem/util/slow_ed25519.py b/stem/util/slow_ed25519.py
new file mode 100644
index 00000000..fb617464
--- /dev/null
+++ b/stem/util/slow_ed25519.py
@@ -0,0 +1,158 @@
+# Public domain ed25519 implementation from...
+#
+# http://ed25519.cr.yp.to/python/ed25519.py
+#
+# It isn't constant-time. Don't use it except for testing. Also, see
+# warnings about how very slow it is. Only use this for generating
+# test vectors, I'd suggest.
+
+import hashlib
+
+b = 256
+q = 2 ** 255 - 19
+l = 2 ** 252 + 27742317777372353535851937790883648493
+
+
+def H(m):
+ return hashlib.sha512(m).digest()
+
+
+def expmod(b, e, m):
+ if e == 0:
+ return 1
+
+ t = expmod(b, e // 2, m) ** 2 % m
+
+ if e & 1:
+ t = (t * b) % m
+
+ return t
+
+
+def inv(x):
+ return expmod(x, q - 2, q)
+
+
+d = -121665 * inv(121666)
+I = expmod(2, (q - 1) // 4, q)
+
+
+def xrecover(y):
+ xx = (y * y - 1) * inv(d * y * y + 1)
+ x = expmod(xx, (q + 3) // 8, q)
+
+ if (x * x - xx) % q != 0:
+ x = (x * I) % q
+
+ if x % 2 != 0:
+ x = q - x
+
+ return x
+
+
+By = 4 * inv(5)
+Bx = xrecover(By)
+B = [Bx % q, By % q]
+
+
+def edwards(P, Q):
+ x1 = P[0]
+ y1 = P[1]
+ x2 = Q[0]
+ y2 = Q[1]
+ x3 = (x1 * y2 + x2 * y1) * inv(1 + d * x1 * x2 * y1 * y2)
+ y3 = (y1 * y2 + x1 * x2) * inv(1 - d * x1 * x2 * y1 * y2)
+ return [x3 % q, y3 % q]
+
+
+def scalarmult(P, e):
+ if e == 0:
+ return [0, 1]
+
+ Q = scalarmult(P, e // 2)
+ Q = edwards(Q, Q)
+
+ if e & 1:
+ Q = edwards(Q, P)
+
+ return Q
+
+
+def encodeint(y):
+ bits = [(y >> i) & 1 for i in range(b)]
+ return b''.join([chr(sum([bits[i * 8 + j] << j for j in range(8)])) for i in range(b // 8)])
+
+
+def encodepoint(P):
+ x = P[0]
+ y = P[1]
+ bits = [(y >> i) & 1 for i in range(b - 1)] + [x & 1]
+
+ return b''.join([chr(sum([bits[i * 8 + j] << j for j in range(8)])) for i in range(b // 8)])
+
+
+def bit(h, i):
+ return (ord(h[i // 8:i // 8 + 1]) >> (i % 8)) & 1
+
+
+def publickey(sk):
+ h = H(sk)
+ a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2))
+ A = scalarmult(B, a)
+
+ return encodepoint(A)
+
+
+def Hint(m):
+ h = H(m)
+ return sum(2 ** i * bit(h, i) for i in range(2 * b))
+
+
+def signature(m, sk, pk):
+ h = H(sk)
+ a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2))
+ r = Hint(b''.join([h[i:i + 1] for i in range(b // 8, b // 4)]) + m)
+ R = scalarmult(B, r)
+ S = (r + Hint(encodepoint(R) + pk + m) * a) % l
+ return encodepoint(R) + encodeint(S)
+
+
+def isoncurve(P):
+ x = P[0]
+ y = P[1]
+ return (-x * x + y * y - 1 - d * x * x * y * y) % q == 0
+
+
+def decodeint(s):
+ return sum(2 ** i * bit(s, i) for i in range(0, b))
+
+
+def decodepoint(s):
+ y = sum(2 ** i * bit(s, i) for i in range(0, b - 1))
+ x = xrecover(y)
+
+ if x & 1 != bit(s, b - 1):
+ x = q - x
+
+ P = [x, y]
+
+ if not isoncurve(P):
+ raise Exception('decoding point that is not on curve')
+
+ return P
+
+
+def checkvalid(s, m, pk):
+ if len(s) != b // 4:
+ raise Exception('signature length is wrong')
+
+ if len(pk) != b // 8:
+ raise Exception('public-key length is wrong')
+
+ R = decodepoint(s[0:b // 8])
+ A = decodepoint(pk)
+ S = decodeint(s[b // 8:b // 4])
+ h = Hint(encodepoint(R) + pk + m)
+
+ if scalarmult(B, S) != edwards(R, scalarmult(A, h)):
+ raise Exception('signature does not pass verification')
diff --git a/stem/util/str_tools.py b/stem/util/str_tools.py
index aba619b9..869f46b6 100644
--- a/stem/util/str_tools.py
+++ b/stem/util/str_tools.py
@@ -21,6 +21,7 @@ Toolkit for various string activity.
parse_short_time_label - seconds represented by a short time label
"""
+import base64
import codecs
import datetime
import re
@@ -116,6 +117,17 @@ def _to_unicode(msg):
return _to_unicode_impl(msg)
+def _decode_b64(msg):
+ """
+ Base64 decode, without padding concerns.
+ """
+
+ missing_padding = len(msg) % 4
+ padding_chr = b'=' if isinstance(msg, bytes) else '='
+
+ return base64.b64decode(msg + padding_chr * missing_padding)
+
+
def _to_int(msg):
"""
Serializes a string to a number.
diff --git a/test/require.py b/test/require.py
index 1117ea1e..ca5fe3ea 100644
--- a/test/require.py
+++ b/test/require.py
@@ -12,6 +12,7 @@ run.
|- needs - skips the test unless a requirement is met
|
|- cryptography - skips test unless the cryptography module is present
+ |- ed25519_support - skips test unless cryptography has ed25519 support
|- command - requires a command to be on the path
|- proc - requires the platform to have recognized /proc contents
|
@@ -98,7 +99,7 @@ def version(req_version):
cryptography = needs(stem.prereq.is_crypto_available, 'requires cryptography')
-ed25519_support = needs(stem.prereq._is_crypto_ed25519_supported, 'requires ed25519 support')
+ed25519_support = needs(lambda: stem.prereq.is_crypto_available(ed25519 = True), 'requires ed25519 support')
proc = needs(stem.util.proc.is_available, 'proc unavailable')
controller = needs(_can_access_controller, 'no connection')
ptrace = needs(_can_ptrace, 'DisableDebuggerAttachment is set')
diff --git a/test/settings.cfg b/test/settings.cfg
index 8fe79d2a..5443df41 100644
--- a/test/settings.cfg
+++ b/test/settings.cfg
@@ -177,6 +177,8 @@ pycodestyle.ignore stem/descriptor/__init__.py => E402: import stem.descriptor.m
pycodestyle.ignore stem/descriptor/__init__.py => E402: import stem.descriptor.networkstatus
pycodestyle.ignore stem/descriptor/__init__.py => E402: import stem.descriptor.server_descriptor
pycodestyle.ignore stem/descriptor/__init__.py => E402: import stem.descriptor.tordnsel
+pycodestyle.ignore stem/util/slow_ed25519.py => E741: l = 2 ** 252 + 27742317777372353535851937790883648493
+pycodestyle.ignore stem/util/slow_ed25519.py => E741: I = expmod(2, (q - 1) // 4, q)
pycodestyle.ignore test/unit/util/connection.py => W291: _tor tor 15843 10 pipe 0x0 state:
pycodestyle.ignore test/unit/util/connection.py => W291: _tor tor 15843 11 pipe 0x0 state:
diff --git a/test/unit/client/cell.py b/test/unit/client/cell.py
index 22843799..33df79a7 100644
--- a/test/unit/client/cell.py
+++ b/test/unit/client/cell.py
@@ -148,9 +148,9 @@ class TestCell(unittest.TestCase):
expected_certs = (
(CertType.LINK, 1, b'0\x82\x02F0\x82\x01\xaf'),
(CertType.IDENTITY, 2, b'0\x82\x01\xc90\x82\x012'),
- (CertType.UNKNOWN, 4, b'\x01\x04\x00\x06m\x1f'),
- (CertType.UNKNOWN, 5, b'\x01\x05\x00\x06m\n\x01'),
- (CertType.UNKNOWN, 7, b'\x1a\xa5\xb3\xbd\x88\xb1C'),
+ (CertType.ED25519_SIGNING, 4, b'\x01\x04\x00\x06m\x1f'),
+ (CertType.LINK_CERT, 5, b'\x01\x05\x00\x06m\n\x01'),
+ (CertType.ED25519_IDENTITY, 7, b'\x1a\xa5\xb3\xbd\x88\xb1C'),
)
content = test_data('new_link_cells')
diff --git a/test/unit/client/certificate.py b/test/unit/client/certificate.py
index a15779f5..196dd075 100644
--- a/test/unit/client/certificate.py
+++ b/test/unit/client/certificate.py
@@ -13,7 +13,7 @@ class TestCertificate(unittest.TestCase):
((1, b'\x7f\x00\x00\x01'), (CertType.LINK, 1, b'\x7f\x00\x00\x01')),
((2, b'\x7f\x00\x00\x01'), (CertType.IDENTITY, 2, b'\x7f\x00\x00\x01')),
((3, b'\x7f\x00\x00\x01'), (CertType.AUTHENTICATE, 3, b'\x7f\x00\x00\x01')),
- ((4, b'\x7f\x00\x00\x01'), (CertType.UNKNOWN, 4, b'\x7f\x00\x00\x01')),
+ ((4, b'\x7f\x00\x00\x01'), (CertType.ED25519_SIGNING, 4, b'\x7f\x00\x00\x01')),
((CertType.IDENTITY, b'\x7f\x00\x00\x01'), (CertType.IDENTITY, 2, b'\x7f\x00\x00\x01')),
)
diff --git a/test/unit/client/link_specifier.py b/test/unit/client/link_specifier.py
index 470ee276..181627de 100644
--- a/test/unit/client/link_specifier.py
+++ b/test/unit/client/link_specifier.py
@@ -15,7 +15,7 @@ from stem.client.datatype import (
class TestLinkSpecifier(unittest.TestCase):
def test_link_by_ipv4_address(self):
- destination, _ = LinkSpecifier.pop(b'\x00\x06\x01\x02\x03\x04#)')
+ destination = LinkSpecifier.unpack(b'\x00\x06\x01\x02\x03\x04#)')
self.assertEqual(LinkByIPv4, type(destination))
self.assertEqual(0, destination.type)
@@ -23,6 +23,10 @@ class TestLinkSpecifier(unittest.TestCase):
self.assertEqual('1.2.3.4', destination.address)
self.assertEqual(9001, destination.port)
+ destination = LinkByIPv4('1.2.3.4', 9001)
+ self.assertEqual(b'\x00\x06\x01\x02\x03\x04#)', destination.pack())
+ self.assertEqual(b'\x01\x02\x03\x04#)', destination.value)
+
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#)')
@@ -32,6 +36,10 @@ class TestLinkSpecifier(unittest.TestCase):
self.assertEqual('2600:0000:0000:0000:0000:0000:0000:0001', destination.address)
self.assertEqual(9001, destination.port)
+ destination = LinkByIPv6('2600:0000:0000:0000:0000:0000:0000:0001', 9001)
+ self.assertEqual(b'\x01\x12&\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01#)', destination.pack())
+ self.assertEqual(b'&\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01#)', destination.value)
+
def test_link_by_fingerprint(self):
destination, _ = LinkSpecifier.pop(b'\x02\x14CCCCCCCCCCCCCCCCCCCC')
@@ -57,3 +65,15 @@ class TestLinkSpecifier(unittest.TestCase):
def test_wrong_size(self):
self.assertRaisesWith(ValueError, 'Link specifier should have 32 bytes, but only had 7 remaining', LinkSpecifier.pop, b'\x04\x20CCCCCCC')
+
+ def test_pack(self):
+ test_inputs = (
+ b'\x03\x20CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
+ b'\x04\x20CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
+ b'\x01\x12&\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01#)',
+ b'\x00\x06\x01\x02\x03\x04#)',
+ )
+
+ for val in test_inputs:
+ destination, _ = LinkSpecifier.pop(val)
+ self.assertEqual(val, destination.pack())
diff --git a/test/unit/descriptor/certificate.py b/test/unit/descriptor/certificate.py
index 51960525..74ea0a6e 100644
--- a/test/unit/descriptor/certificate.py
+++ b/test/unit/descriptor/certificate.py
@@ -12,7 +12,8 @@ import stem.util.str_tools
import stem.prereq
import test.require
-from stem.descriptor.certificate import ED25519_SIGNATURE_LENGTH, CertType, ExtensionType, ExtensionFlag, Ed25519Certificate, Ed25519CertificateV1, Ed25519Extension
+from stem.client.datatype import Size, CertType
+from stem.descriptor.certificate import ED25519_SIGNATURE_LENGTH, ExtensionType, Ed25519Certificate, Ed25519CertificateV1, Ed25519Extension
from test.unit.descriptor import get_resource
ED25519_CERT = """
@@ -54,20 +55,20 @@ class TestEd25519Certificate(unittest.TestCase):
signing_key = b'\x11' * 32
cert_bytes = certificate(extension_data = [b'\x00\x20\x04\x07' + signing_key, b'\x00\x00\x05\x04'])
- cert = Ed25519Certificate.parse(cert_bytes)
+ cert = Ed25519Certificate.from_base64(cert_bytes)
self.assertEqual(Ed25519CertificateV1, type(cert))
self.assertEqual(1, cert.version)
self.assertEqual(stem.util.str_tools._to_unicode(cert_bytes), cert.encoded)
- self.assertEqual(CertType.SIGNING, cert.type)
+ self.assertEqual(CertType.ED25519_SIGNING, cert.type)
self.assertEqual(datetime.datetime(1970, 1, 1, 0, 0), cert.expiration)
self.assertEqual(1, cert.key_type)
self.assertEqual(b'\x03' * 32, cert.key)
self.assertEqual(b'\x01' * ED25519_SIGNATURE_LENGTH, cert.signature)
self.assertEqual([
- Ed25519Extension(type = ExtensionType.HAS_SIGNING_KEY, flags = [ExtensionFlag.AFFECTS_VALIDATION, ExtensionFlag.UNKNOWN], flag_int = 7, data = signing_key),
- Ed25519Extension(type = 5, flags = [ExtensionFlag.UNKNOWN], flag_int = 4, data = b''),
+ Ed25519Extension(ExtensionType.HAS_SIGNING_KEY, 7, signing_key),
+ Ed25519Extension(5, 4, b''),
], cert.extensions)
self.assertEqual(ExtensionType.HAS_SIGNING_KEY, cert.extensions[0].type)
@@ -78,25 +79,47 @@ class TestEd25519Certificate(unittest.TestCase):
Parse a certificate from a real server descriptor.
"""
- cert = Ed25519Certificate.parse(ED25519_CERT)
+ cert = Ed25519Certificate.from_base64(ED25519_CERT)
self.assertEqual(Ed25519CertificateV1, type(cert))
self.assertEqual(1, cert.version)
self.assertEqual(ED25519_CERT, cert.encoded)
- self.assertEqual(CertType.SIGNING, cert.type)
+ self.assertEqual(CertType.ED25519_SIGNING, cert.type)
self.assertEqual(datetime.datetime(2015, 8, 28, 17, 0), cert.expiration)
self.assertEqual(1, cert.key_type)
self.assertEqual(EXPECTED_CERT_KEY, cert.key)
- self.assertEqual([Ed25519Extension(type = 4, flags = [], flag_int = 0, data = EXPECTED_EXTENSION_DATA)], cert.extensions)
+ self.assertEqual([Ed25519Extension(4, 0, EXPECTED_EXTENSION_DATA)], cert.extensions)
self.assertEqual(EXPECTED_SIGNATURE, cert.signature)
+ def test_extension_encoding(self):
+ """
+ Pack an extension back into what we read.
+ """
+
+ extension = Ed25519Certificate.from_base64(ED25519_CERT).extensions[0]
+ expected = Size.SHORT.pack(len(EXPECTED_EXTENSION_DATA)) + Size.CHAR.pack(4) + Size.CHAR.pack(0) + EXPECTED_EXTENSION_DATA
+
+ self.assertEqual(4, extension.type)
+ self.assertEqual(0, extension.flag_int)
+ self.assertEqual(EXPECTED_EXTENSION_DATA, extension.data)
+ self.assertEqual(expected, extension.pack())
+
+ def test_certificate_encoding(self):
+ """
+ Pack a certificate back into what we read.
+ """
+
+ cert = Ed25519Certificate.from_base64(ED25519_CERT)
+ self.assertEqual(ED25519_CERT, cert.encoded) # read base64 encoding (getting removed in stem 2.x)
+ self.assertEqual(ED25519_CERT, cert.to_base64()) # computed base64 encoding
+
def test_non_base64(self):
"""
Parse data that isn't base64 encoded.
"""
exc_msg = re.escape("Ed25519 certificate wasn't propoerly base64 encoded (Incorrect padding):")
- self.assertRaisesRegexp(ValueError, exc_msg, Ed25519Certificate.parse, '\x02\x0323\x04')
+ self.assertRaisesRegexp(ValueError, exc_msg, Ed25519Certificate.from_base64, '\x02\x0323\x04')
def test_too_short(self):
"""
@@ -104,10 +127,10 @@ class TestEd25519Certificate(unittest.TestCase):
"""
exc_msg = "Ed25519 certificate wasn't propoerly base64 encoded (empty):"
- self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.parse, '')
+ self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.from_base64, '')
exc_msg = 'Ed25519 certificate was 18 bytes, but should be at least 104'
- self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.parse, 'AQQABhtZAaW2GoBED1IjY3A6')
+ self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.from_base64, 'AQQABhtZAaW2GoBED1IjY3A6')
def test_with_invalid_version(self):
"""
@@ -116,7 +139,7 @@ class TestEd25519Certificate(unittest.TestCase):
"""
exc_msg = 'Ed25519 certificate is version 2. Parser presently only supports version 1.'
- self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.parse, certificate(version = 2))
+ self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.from_base64, certificate(version = 2))
def test_with_invalid_cert_type(self):
"""
@@ -124,22 +147,25 @@ class TestEd25519Certificate(unittest.TestCase):
are reserved.
"""
- exc_msg = 'Ed25519 certificate cannot have a type of 0. This is reserved to avoid conflicts with tor CERTS cells.'
- self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.parse, certificate(cert_type = 0))
+ exc_msg = 'Ed25519 certificate type 0 is unrecognized'
+ self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.from_base64, certificate(cert_type = 0))
+
+ exc_msg = 'Ed25519 certificate cannot have a type of 1. This is reserved for CERTS cells.'
+ self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.from_base64, certificate(cert_type = 1))
exc_msg = 'Ed25519 certificate cannot have a type of 7. This is reserved for RSA identity cross-certification.'
- self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.parse, certificate(cert_type = 7))
+ self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.from_base64, certificate(cert_type = 7))
def test_truncated_extension(self):
"""
Include an extension without as much data as it specifies.
"""
- exc_msg = 'Ed25519 extension is missing header field data'
- self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.parse, certificate(extension_data = [b'']))
+ exc_msg = 'Ed25519 extension is missing header fields'
+ self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.from_base64, certificate(extension_data = [b'']))
exc_msg = "Ed25519 extension is truncated. It should have 20480 bytes of data but there's only 2."
- self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.parse, certificate(extension_data = [b'\x50\x00\x00\x00\x15\x12']))
+ self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.from_base64, certificate(extension_data = [b'\x50\x00\x00\x00\x15\x12']))
def test_extra_extension_data(self):
"""
@@ -147,7 +173,7 @@ class TestEd25519Certificate(unittest.TestCase):
"""
exc_msg = 'Ed25519 certificate had 1 bytes of unused extension data'
- self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.parse, certificate(extension_data = [b'\x00\x01\x00\x00\x15\x12']))
+ self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.from_base64, certificate(extension_data = [b'\x00\x01\x00\x00\x15\x12']))
def test_truncated_signing_key(self):
"""
@@ -155,7 +181,7 @@ class TestEd25519Certificate(unittest.TestCase):
"""
exc_msg = 'Ed25519 HAS_SIGNING_KEY extension must be 32 bytes, but was 2.'
- self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.parse, certificate(extension_data = [b'\x00\x02\x04\x07\11\12']))
+ self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.from_base64, certificate(extension_data = [b'\x00\x02\x04\x07\11\12']))
@test.require.ed25519_support
def test_validation_with_descriptor_key(self):
@@ -191,5 +217,5 @@ class TestEd25519Certificate(unittest.TestCase):
with open(get_resource('server_descriptor_with_ed25519'), 'rb') as descriptor_file:
desc = next(stem.descriptor.parse_file(descriptor_file, validate = False))
- cert = Ed25519Certificate.parse(certificate())
- self.assertRaisesWith(ValueError, 'Ed25519KeyCertificate signing key is invalid (Signature was forged or corrupt)', cert.validate, desc)
+ cert = Ed25519Certificate.from_base64(certificate())
+ self.assertRaisesWith(ValueError, 'Ed25519KeyCertificate signing key is invalid (signature forged or corrupt)', cert.validate, desc)
diff --git a/test/unit/descriptor/data/hidden_service_v3_intro_point b/test/unit/descriptor/data/hidden_service_v3_intro_point
new file mode 100644
index 00000000..618d0e80
--- /dev/null
+++ b/test/unit/descriptor/data/hidden_service_v3_intro_point
@@ -0,0 +1,28 @@
+introduction-point AgIUQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0MABgUGBwgjKQ==
+onion-key ntor AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+auth-key
+-----BEGIN ED25519 CERT-----
+AQkABl60Acq8QW8O7ONgImfilmjrEIeISkZmGuedsdkZucakUBZbAQAgBACS5oD6
+V1UufUhMnSo+20b7wHblTqkLd7uE4+bVZX1Soded2A7SaJOyvI2FBNvljCNgl5T/
+eLNpci4yTizyDv2A0/QB4SyaZ2+SOM/uQn3DKKyhUwwNuaD/sSuUI25gkgY=
+-----END ED25519 CERT-----
+enc-key ntor AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+enc-key-cert
+-----BEGIN ED25519 CERT-----
+AQsABqUQARe6uX12UazJAo5Qt2iP0rJ29hq/GEEi28dAsKqCOHa6AQAgBACS5oD6
+V1UufUhMnSo+20b7wHblTqkLd7uE4+bVZX1SoY1XfpJjLTI3tJwIrFM/JFP3XbVF
+CtwFlIHgSS1/M9Rr+eznM17+5hd+0SHL4/+WV5ukxyPOWIL6X1z+KPg4hA0=
+-----END ED25519 CERT-----
+legacy-key
+-----BEGIN RSA PUBLIC KEY-----
+MIGJAoGBAMO3ZXrcA+PclKppGCh9TOG0H6mubTAgji4fLF87GelggQs5bnPdQeaS
+v4HgP42J/mMinSLpbg5LhL5gd7AqwOxe9cpEhbvwrM63ot7gkj2tJqs2PLlokqSx
+ZBEAssKbE/8F2iVoEWoXd8g8Pn5nG7wRKDGGQRAjintrBSncTvfRAgMBAAE=
+-----END RSA PUBLIC KEY-----
+legacy-key-cert
+-----BEGIN CROSSCERT-----
+kuaA+ldVLn1ITJ0qPttG+8B25U6pC3e7hOPm1WV9UqEABl60gH1LLE5C2kl5BBpb
+E2Ajh6kJuf2fXMW7csYYNtPACZjFoG+kb16fh7y9L2pLuBFNKpkVDMsiQVcdwWWg
+Nu6qpGj1vHDR1XUM7ocoXB3QMVXCIxvA9b8k3q7KFvXgImi9GZ7l1/K+emm58MYM
+CxhNKazjiFgXjbs9kf+S9HxaF/Yw
+-----END CROSSCERT-----
diff --git a/test/unit/descriptor/hidden_service_v3.py b/test/unit/descriptor/hidden_service_v3.py
index 37781b5f..4549db2b 100644
--- a/test/unit/descriptor/hidden_service_v3.py
+++ b/test/unit/descriptor/hidden_service_v3.py
@@ -2,16 +2,21 @@
Unit tests for stem.descriptor.hidden_service for version 3.
"""
+import base64
import functools
import unittest
import stem.client.datatype
import stem.descriptor
+import stem.descriptor.hidden_service
import stem.prereq
+import test.require
+
from stem.descriptor.hidden_service import (
- REQUIRED_V3_FIELDS,
+ IntroductionPointV3,
HiddenServiceDescriptorV3,
+ AuthorizedClient,
OuterLayer,
InnerLayer,
)
@@ -22,10 +27,26 @@ from test.unit.descriptor import (
base_expect_invalid_attr_for_text,
)
+try:
+ # added in python 2.7
+ from collections import OrderedDict
+except ImportError:
+ from stem.util.ordereddict import OrderedDict
+
+try:
+ # added in python 3.3
+ from unittest.mock import patch, Mock
+except ImportError:
+ from mock import patch, Mock
+
+require_sha3 = test.require.needs(stem.prereq._is_sha3_available, 'requires sha3')
+require_x25519 = test.require.needs(lambda: stem.descriptor.hidden_service.X25519_AVAILABLE, 'requires openssl x5509')
+
expect_invalid_attr = functools.partial(base_expect_invalid_attr, HiddenServiceDescriptorV3, 'version', 3)
expect_invalid_attr_for_text = functools.partial(base_expect_invalid_attr_for_text, HiddenServiceDescriptorV3, 'version', 3)
-HS_ADDRESS = 'sltib6sxkuxh2scmtuvd5w2g7pahnzkovefxpo4e4ptnkzl5kkq5h2ad.onion'
+HS_ADDRESS = u'sltib6sxkuxh2scmtuvd5w2g7pahnzkovefxpo4e4ptnkzl5kkq5h2ad.onion'
+HS_PUBKEY = b'\x92\xe6\x80\xfaWU.}HL\x9d*>\xdbF\xfb\xc0v\xe5N\xa9\x0bw\xbb\x84\xe3\xe6\xd5e}R\xa1'
EXPECTED_SIGNING_CERT = """\
-----BEGIN ED25519 CERT-----
@@ -35,6 +56,17 @@ BDwQZ8rhp05oCqhhY3oFHqG9KS7HGzv9g2v1/PrVJMbkfpwu1YK4b3zIZAk=
-----END ED25519 CERT-----\
"""
+EXPECTED_OUTER_LAYER = """\
+desc-auth-type foo
+desc-auth-ephemeral-key bar
+auth-client JNil86N07AA epkaL79NtajmgME/egi8oA qosYH4rXisxda3X7p9b6fw
+auth-client 1D8VBAh9hdM 6K/uO3sRqBp6URrKC7GB6Q ElwRj5+6SN9kb8bRhiiQvA
+encrypted
+-----BEGIN MESSAGE-----
+malformed block
+-----END MESSAGE-----\
+"""
+
with open(get_resource('hidden_service_v3')) as descriptor_file:
HS_DESC_STR = descriptor_file.read()
@@ -44,6 +76,9 @@ with open(get_resource('hidden_service_v3_outer_layer')) as outer_layer_file:
with open(get_resource('hidden_service_v3_inner_layer')) as inner_layer_file:
INNER_LAYER_STR = inner_layer_file.read()
+with open(get_resource('hidden_service_v3_intro_point')) as intro_point_file:
+ INTRO_POINT_STR = intro_point_file.read()
+
class TestHiddenServiceDescriptorV3(unittest.TestCase):
def test_real_descriptor(self):
@@ -63,18 +98,13 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase):
self.assertTrue('eaH8VdaTKS' in desc.superencrypted)
self.assertEqual('aglChCQF+lbzKgyxJJTpYGVShV/GMDRJ4+cRGCp+a2y/yX/tLSh7hzqI7rVZrUoGj74Xr1CLMYO3fXYCS+DPDQ', desc.signature)
+ @require_sha3
+ @test.require.ed25519_support
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)
@@ -124,13 +154,14 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase):
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.assertEqual('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', intro_point.onion_key_raw)
+ self.assertTrue('ID2l9EFNrp' in intro_point.auth_key_cert.to_base64())
+ self.assertEqual('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', intro_point.enc_key_raw)
self.assertTrue('ZvjPt5IfeQ', intro_point.enc_key_cert)
- self.assertEqual(None, intro_point.legacy_key)
+ self.assertEqual(None, intro_point.legacy_key_raw)
self.assertEqual(None, intro_point.legacy_key_cert)
+ @test.require.ed25519_support
def test_required_fields(self):
"""
Check that we require the mandatory fields.
@@ -145,10 +176,11 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase):
'signature': 'signature',
}
- for line in REQUIRED_V3_FIELDS:
+ for line in stem.descriptor.hidden_service.REQUIRED_V3_FIELDS:
desc_text = HiddenServiceDescriptorV3.content(exclude = (line,))
expect_invalid_attr_for_text(self, desc_text, line_to_attr[line], None)
+ @test.require.ed25519_support
def test_invalid_version(self):
"""
Checks that our version field expects a numeric value.
@@ -163,6 +195,7 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase):
for test_value in test_values:
expect_invalid_attr(self, {'hs-descriptor': test_value}, 'version')
+ @test.require.ed25519_support
def test_invalid_lifetime(self):
"""
Checks that our lifetime field expects a numeric value.
@@ -177,6 +210,7 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase):
for test_value in test_values:
expect_invalid_attr(self, {'descriptor-lifetime': test_value}, 'lifetime')
+ @test.require.ed25519_support
def test_invalid_revision_counter(self):
"""
Checks that our revision counter field expects a numeric value.
@@ -191,14 +225,232 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase):
for test_value in test_values:
expect_invalid_attr(self, {'revision-counter': test_value}, 'revision_counter')
- def test_public_key_from_address(self):
- 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
-
- self.assertEqual(b'\x92\xe6\x80\xfaWU.}HL\x9d*>\xdbF\xfb\xc0v\xe5N\xa9\x0bw\xbb\x84\xe3\xe6\xd5e}R\xa1', HiddenServiceDescriptorV3._public_key_from_address(HS_ADDRESS))
- self.assertRaisesWith(ValueError, "'boom.onion' isn't a valid hidden service v3 address", HiddenServiceDescriptorV3._public_key_from_address, 'boom')
- self.assertRaisesWith(ValueError, 'Bad checksum (expected def7 but was 842e)', HiddenServiceDescriptorV3._public_key_from_address, '5' * 56)
+ @require_sha3
+ def test_address_from_identity_key(self):
+ self.assertEqual(HS_ADDRESS, HiddenServiceDescriptorV3.address_from_identity_key(HS_PUBKEY))
+
+ @require_sha3
+ def test_identity_key_from_address(self):
+ self.assertEqual(HS_PUBKEY, HiddenServiceDescriptorV3.identity_key_from_address(HS_ADDRESS))
+ self.assertRaisesWith(ValueError, "'boom.onion' isn't a valid hidden service v3 address", HiddenServiceDescriptorV3.identity_key_from_address, 'boom')
+ self.assertRaisesWith(ValueError, 'Bad checksum (expected def7 but was 842e)', HiddenServiceDescriptorV3.identity_key_from_address, '5' * 56)
+
+ def test_intro_point_parse(self):
+ """
+ Parse a v3 introduction point.
+ """
+
+ intro_point = IntroductionPointV3.parse(INTRO_POINT_STR)
+
+ self.assertEqual('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', intro_point.onion_key_raw)
+ self.assertTrue('0Acq8QW8O7O' in intro_point.auth_key_cert.to_base64())
+ self.assertEqual('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', intro_point.enc_key_raw)
+ self.assertTrue('4807i5', intro_point.enc_key_cert.to_base64())
+ self.assertTrue('JAoGBAMO3' in intro_point.legacy_key_raw)
+ self.assertTrue('Ln1ITJ0qP' in intro_point.legacy_key_cert)
+
+ def test_intro_point_encode(self):
+ """
+ Encode an introduction point back into a string.
+ """
+
+ intro_point = IntroductionPointV3.parse(INTRO_POINT_STR)
+ self.assertEqual(INTRO_POINT_STR.rstrip(), intro_point.encode())
+
+ @require_x25519
+ @test.require.ed25519_support
+ def test_intro_point_crypto(self):
+ """
+ Retrieve IntroductionPointV3 cryptographic materials.
+ """
+
+ from cryptography.hazmat.backends.openssl.x25519 import X25519PublicKey
+
+ intro_point = InnerLayer(INNER_LAYER_STR).introduction_points[0]
+
+ self.assertEqual('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', intro_point.onion_key_raw)
+ self.assertEqual('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', intro_point.enc_key_raw)
+
+ self.assertTrue(isinstance(intro_point.onion_key(), X25519PublicKey))
+ self.assertTrue(isinstance(intro_point.enc_key(), X25519PublicKey))
+
+ self.assertEqual(intro_point.onion_key_raw, base64.b64encode(stem.util._pubkey_bytes(intro_point.onion_key())))
+ self.assertEqual(intro_point.enc_key_raw, base64.b64encode(stem.util._pubkey_bytes(intro_point.enc_key())))
+
+ self.assertEqual(None, intro_point.legacy_key_raw)
+ self.assertEqual(None, intro_point.legacy_key())
+
+ @patch('stem.prereq.is_crypto_available', Mock(return_value = False))
+ def test_intro_point_crypto_without_prereq(self):
+ """
+ Fetch cryptographic materials when the module is unavailable.
+ """
+
+ intro_point = InnerLayer(INNER_LAYER_STR).introduction_points[0]
+ self.assertRaisesWith(ImportError, 'cryptography module unavailable', intro_point.onion_key)
+
+ @test.require.ed25519_support
+ def test_intro_point_creation(self):
+ """
+ Create an introduction point, encode it, then re-parse.
+ """
+
+ intro_point = IntroductionPointV3.create('1.1.1.1', 9001)
+
+ self.assertEqual(1, len(intro_point.link_specifiers))
+ self.assertEqual(stem.client.datatype.LinkByIPv4, type(intro_point.link_specifiers[0]))
+ self.assertEqual('1.1.1.1', intro_point.link_specifiers[0].address)
+ self.assertEqual(9001, intro_point.link_specifiers[0].port)
+
+ reparsed = IntroductionPointV3.parse(intro_point.encode())
+ self.assertEqual(intro_point, reparsed)
+
+ @test.require.ed25519_support
+ def test_inner_layer_creation(self):
+ """
+ Internal layer creation.
+ """
+
+ # minimal layer
+
+ self.assertEqual(b'create2-formats 2', InnerLayer.content())
+ self.assertEqual([2], InnerLayer.create().formats)
+
+ # specify their only mandatory parameter (formats)
+
+ self.assertEqual(b'create2-formats 1 2 3', InnerLayer.content({'create2-formats': '1 2 3'}))
+ self.assertEqual([1, 2, 3], InnerLayer.create({'create2-formats': '1 2 3'}).formats)
+
+ # include optional parameters
+
+ desc = InnerLayer.create(OrderedDict((
+ ('intro-auth-required', 'ed25519'),
+ ('single-onion-service', ''),
+ )))
+
+ self.assertEqual([2], desc.formats)
+ self.assertEqual(['ed25519'], desc.intro_auth)
+ self.assertEqual(True, desc.is_single_service)
+ self.assertEqual([], desc.introduction_points)
+
+ # include introduction points
+
+ desc = InnerLayer.create(introduction_points = [
+ IntroductionPointV3.create('1.1.1.1', 9001),
+ IntroductionPointV3.create('2.2.2.2', 9001),
+ IntroductionPointV3.create('3.3.3.3', 9001),
+ ])
+
+ self.assertEqual(3, len(desc.introduction_points))
+ self.assertEqual('1.1.1.1', desc.introduction_points[0].link_specifiers[0].address)
+
+ self.assertTrue(InnerLayer.content(introduction_points = [
+ IntroductionPointV3.create('1.1.1.1', 9001),
+ ]).startswith('create2-formats 2\nintroduction-point AQAGAQEBASMp'))
+
+ @test.require.ed25519_support
+ def test_outer_layer_creation(self):
+ """
+ Outer layer creation.
+ """
+
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+
+ # minimal layer
+
+ self.assertTrue(OuterLayer.content().startswith('desc-auth-type x25519\ndesc-auth-ephemeral-key '))
+ self.assertEqual('x25519', OuterLayer.create().auth_type)
+
+ # specify the parameters
+
+ desc = OuterLayer.create({
+ 'desc-auth-type': 'foo',
+ 'desc-auth-ephemeral-key': 'bar',
+ 'auth-client': [
+ 'JNil86N07AA epkaL79NtajmgME/egi8oA qosYH4rXisxda3X7p9b6fw',
+ '1D8VBAh9hdM 6K/uO3sRqBp6URrKC7GB6Q ElwRj5+6SN9kb8bRhiiQvA',
+ ],
+ 'encrypted': '\n-----BEGIN MESSAGE-----\nmalformed block\n-----END MESSAGE-----',
+ })
+
+ self.assertEqual('foo', desc.auth_type)
+ self.assertEqual('bar', desc.ephemeral_key)
+ self.assertEqual('-----BEGIN MESSAGE-----\nmalformed block\n-----END MESSAGE-----', desc.encrypted)
+
+ self.assertEqual({
+ '1D8VBAh9hdM': AuthorizedClient(id = '1D8VBAh9hdM', iv = '6K/uO3sRqBp6URrKC7GB6Q', cookie = 'ElwRj5+6SN9kb8bRhiiQvA'),
+ 'JNil86N07AA': AuthorizedClient(id = 'JNil86N07AA', iv = 'epkaL79NtajmgME/egi8oA', cookie = 'qosYH4rXisxda3X7p9b6fw'),
+ }, desc.clients)
+
+ self.assertEqual(EXPECTED_OUTER_LAYER, str(desc))
+
+ # create an inner layer then decrypt it
+
+ revision_counter = 5
+ blinded_key = stem.util._pubkey_bytes(Ed25519PrivateKey.generate())
+ subcredential = HiddenServiceDescriptorV3._subcredential(Ed25519PrivateKey.generate(), blinded_key)
+
+ outer_layer = OuterLayer.create(
+ inner_layer = InnerLayer.create(
+ introduction_points = [
+ IntroductionPointV3.create('1.1.1.1', 9001),
+ ]
+ ),
+ revision_counter = revision_counter,
+ subcredential = subcredential,
+ blinded_key = blinded_key,
+ )
+
+ inner_layer = InnerLayer._decrypt(outer_layer, revision_counter, subcredential, blinded_key)
+
+ self.assertEqual(1, len(inner_layer.introduction_points))
+ self.assertEqual('1.1.1.1', inner_layer.introduction_points[0].link_specifiers[0].address)
+
+ @test.require.ed25519_support
+ def test_descriptor_creation(self):
+ """
+ HiddenServiceDescriptorV3 creation.
+ """
+
+ # minimal descriptor
+
+ self.assertTrue(HiddenServiceDescriptorV3.content().startswith('hs-descriptor 3\ndescriptor-lifetime 180\n'))
+ self.assertEqual(180, HiddenServiceDescriptorV3.create().lifetime)
+
+ # specify the parameters
+
+ desc = HiddenServiceDescriptorV3.create({
+ 'hs-descriptor': '4',
+ 'descriptor-lifetime': '123',
+ 'descriptor-signing-key-cert': '\n-----BEGIN ED25519 CERT-----\nmalformed block\n-----END ED25519 CERT-----',
+ 'revision-counter': '5',
+ 'superencrypted': '\n-----BEGIN MESSAGE-----\nmalformed block\n-----END MESSAGE-----',
+ 'signature': 'abcde',
+ }, validate = False)
+
+ self.assertEqual(4, desc.version)
+ self.assertEqual(123, desc.lifetime)
+ self.assertEqual(None, desc.signing_cert) # malformed cert dropped because validation is disabled
+ self.assertEqual(5, desc.revision_counter)
+ self.assertEqual('-----BEGIN MESSAGE-----\nmalformed block\n-----END MESSAGE-----', desc.superencrypted)
+ self.assertEqual('abcde', desc.signature)
+
+ # include introduction points
+
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+
+ identity_key = Ed25519PrivateKey.generate()
+ onion_address = HiddenServiceDescriptorV3.address_from_identity_key(identity_key)
+
+ desc = HiddenServiceDescriptorV3.create(
+ identity_key = identity_key,
+ inner_layer = InnerLayer.create(introduction_points = [
+ IntroductionPointV3.create('1.1.1.1', 9001),
+ IntroductionPointV3.create('2.2.2.2', 9001),
+ IntroductionPointV3.create('3.3.3.3', 9001),
+ ]),
+ )
+
+ inner_layer = desc.decrypt(onion_address)
+ self.assertEqual(3, len(inner_layer.introduction_points))
+ self.assertEqual('1.1.1.1', inner_layer.introduction_points[0].link_specifiers[0].address)
diff --git a/test/unit/descriptor/server_descriptor.py b/test/unit/descriptor/server_descriptor.py
index 6b91d90f..d87d51e9 100644
--- a/test/unit/descriptor/server_descriptor.py
+++ b/test/unit/descriptor/server_descriptor.py
@@ -20,8 +20,9 @@ import stem.version
import stem.util.str_tools
import test.require
+from stem.client.datatype import CertType
from stem.descriptor import DigestHash, DigestEncoding
-from stem.descriptor.certificate import CertType, ExtensionType
+from stem.descriptor.certificate import ExtensionType
from stem.descriptor.server_descriptor import BridgeDistribution, RelayDescriptor, BridgeDescriptor
from test.unit.descriptor import (
@@ -366,7 +367,7 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4=
])
self.assertEqual(1, desc.certificate.version)
- self.assertEqual(CertType.SIGNING, desc.certificate.type)
+ self.assertEqual(CertType.ED25519_SIGNING, desc.certificate.type)
self.assertEqual(datetime.datetime(2015, 8, 28, 17, 0, 0), desc.certificate.expiration)
self.assertEqual(1, desc.certificate.key_type)
self.assertTrue(desc.certificate.key.startswith(b'\xa5\xb6\x1a\x80D\x0f'))