1# Copyright 2015-2019, Damian Johnson and The Tor Project
2# See LICENSE for licensing information
3
4"""
5Parsing for Tor hidden service descriptors as described in Tor's `version 2
6<https://gitweb.torproject.org/torspec.git/tree/rend-spec-v2.txt>`_ and
7`version 3 <https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt>`_
8rend-spec.
9
10Unlike other descriptor types these describe a hidden service rather than a
11relay. They're created by the service, and can only be fetched via relays with
12the HSDir flag.
13
14These are only available through the Controller's
15:func:`~stem.control.Controller.get_hidden_service_descriptor` method.
16
17**Module Overview:**
18
19::
20
21  BaseHiddenServiceDescriptor - Common parent for hidden service descriptors
22    |- HiddenServiceDescriptorV2 - Version 2 hidden service descriptor
23    +- HiddenServiceDescriptorV3 - Version 3 hidden service descriptor
24         |- address_from_identity_key - convert an identity key to address
25         |- identity_key_from_address - convert an address to identity key
26         +- decrypt - decrypt and parse encrypted layers
27
28  OuterLayer - First encrypted layer of a hidden service v3 descriptor
29  InnerLayer - Second encrypted layer of a hidden service v3 descriptor
30
31.. versionadded:: 1.4.0
32"""
33
34import base64
35import binascii
36import collections
37import datetime
38import hashlib
39import io
40import os
41import struct
42import time
43
44import stem.client.datatype
45import stem.descriptor.certificate
46import stem.prereq
47import stem.util
48import stem.util.connection
49import stem.util.str_tools
50import stem.util.tor_tools
51
52from stem.client.datatype import CertType
53from stem.descriptor.certificate import ExtensionType, Ed25519Extension, Ed25519Certificate, Ed25519CertificateV1
54
55from stem.descriptor import (
56  PGP_BLOCK_END,
57  Descriptor,
58  _descriptor_content,
59  _descriptor_components,
60  _read_until_keywords,
61  _bytes_for_block,
62  _value,
63  _values,
64  _parse_simple_line,
65  _parse_if_present,
66  _parse_int_line,
67  _parse_timestamp_line,
68  _parse_key_block,
69  _random_date,
70  _random_crypto_blob,
71)
72
73if stem.prereq._is_lru_cache_available():
74  from functools import lru_cache
75else:
76  from stem.util.lru_cache import lru_cache
77
78try:
79  from cryptography.hazmat.backends.openssl.backend import backend
80  X25519_AVAILABLE = hasattr(backend, 'x25519_supported') and backend.x25519_supported()
81except ImportError:
82  X25519_AVAILABLE = False
83
84
85REQUIRED_V2_FIELDS = (
86  'rendezvous-service-descriptor',
87  'version',
88  'permanent-key',
89  'secret-id-part',
90  'publication-time',
91  'protocol-versions',
92  'signature',
93)
94
95REQUIRED_V3_FIELDS = (
96  'hs-descriptor',
97  'descriptor-lifetime',
98  'descriptor-signing-key-cert',
99  'revision-counter',
100  'superencrypted',
101  'signature',
102)
103
104INTRODUCTION_POINTS_ATTR = {
105  'identifier': None,
106  'address': None,
107  'port': None,
108  'onion_key': None,
109  'service_key': None,
110  'intro_authentication': [],
111}
112
113# introduction-point fields that can only appear once
114
115SINGLE_INTRODUCTION_POINT_FIELDS = [
116  'introduction-point',
117  'ip-address',
118  'onion-port',
119  'onion-key',
120  'service-key',
121]
122
123BASIC_AUTH = 1
124STEALTH_AUTH = 2
125CHECKSUM_CONSTANT = b'.onion checksum'
126
127SALT_LEN = 16
128MAC_LEN = 32
129
130S_KEY_LEN = 32
131S_IV_LEN = 16
132
133
134class DecryptionFailure(Exception):
135  """
136  Failure to decrypt the hidden service descriptor's introduction-points.
137  """
138
139
140# TODO: rename in stem 2.x (add 'V2' and drop plural)
141
142class IntroductionPoints(collections.namedtuple('IntroductionPoints', INTRODUCTION_POINTS_ATTR.keys())):
143  """
144  Introduction point for a v2 hidden service.
145
146  :var str identifier: hash of this introduction point's identity key
147  :var str address: address of this introduction point
148  :var int port: port where this introduction point is listening
149  :var str onion_key: public key for communicating with this introduction point
150  :var str service_key: public key for communicating with this hidden service
151  :var list intro_authentication: tuples of the form (auth_type, auth_data) for
152    establishing a connection
153  """
154
155
156class IntroductionPointV3(collections.namedtuple('IntroductionPointV3', ['link_specifiers', 'onion_key_raw', 'auth_key_cert', 'enc_key_raw', 'enc_key_cert', 'legacy_key_raw', 'legacy_key_cert'])):
157  """
158  Introduction point for a v3 hidden service.
159
160  .. versionadded:: 1.8.0
161
162  :var list link_specifiers: :class:`~stem.client.datatype.LinkSpecifier` where this service is reachable
163  :var unicode onion_key_raw: base64 ntor introduction point public key
164  :var stem.descriptor.certificate.Ed25519Certificate auth_key_cert: cross-certifier of the signing key with the auth key
165  :var unicode enc_key_raw: base64 introduction request encryption key
166  :var stem.descriptor.certificate.Ed25519Certificate enc_key_cert: cross-certifier of the signing key by the encryption key
167  :var str legacy_key_raw: base64 legacy introduction point RSA public key
168  :var str legacy_key_cert: base64 cross-certifier of the signing key by the legacy key
169  """
170
171  @staticmethod
172  def parse(content):
173    """
174    Parses an introduction point from its descriptor content.
175
176    :param str content: descriptor content to parse
177
178    :returns: :class:`~stem.descriptor.hidden_service.IntroductionPointV3` for the descriptor content
179
180    :raises: **ValueError** if descriptor content is malformed
181    """
182
183    entry = _descriptor_components(content, False)
184    link_specifiers = IntroductionPointV3._parse_link_specifiers(_value('introduction-point', entry))
185
186    onion_key_line = _value('onion-key', entry)
187    onion_key = onion_key_line[5:] if onion_key_line.startswith('ntor ') else None
188
189    _, block_type, auth_key_cert = entry['auth-key'][0]
190    auth_key_cert = Ed25519Certificate.from_base64(auth_key_cert)
191
192    if block_type != 'ED25519 CERT':
193      raise ValueError('Expected auth-key to have an ed25519 certificate, but was %s' % block_type)
194
195    enc_key_line = _value('enc-key', entry)
196    enc_key = enc_key_line[5:] if enc_key_line.startswith('ntor ') else None
197
198    _, block_type, enc_key_cert = entry['enc-key-cert'][0]
199    enc_key_cert = Ed25519Certificate.from_base64(enc_key_cert)
200
201    if block_type != 'ED25519 CERT':
202      raise ValueError('Expected enc-key-cert to have an ed25519 certificate, but was %s' % block_type)
203
204    legacy_key = entry['legacy-key'][0][2] if 'legacy-key' in entry else None
205    legacy_key_cert = entry['legacy-key-cert'][0][2] if 'legacy-key-cert' in entry else None
206
207    return IntroductionPointV3(link_specifiers, onion_key, auth_key_cert, enc_key, enc_key_cert, legacy_key, legacy_key_cert)
208
209  @staticmethod
210  def create_for_address(address, port, expiration = None, onion_key = None, enc_key = None, auth_key = None, signing_key = None):
211    """
212    Simplified constructor for a single address/port link specifier.
213
214    :param str address: IPv4 or IPv6 address where the service is reachable
215    :param int port: port where the service is reachable
216    :param datetime.datetime expiration: when certificates should expire
217    :param str onion_key: encoded, X25519PublicKey, or X25519PrivateKey onion key
218    :param str enc_key: encoded, X25519PublicKey, or X25519PrivateKey encryption key
219    :param str auth_key: encoded, Ed25519PublicKey, or Ed25519PrivateKey authentication key
220    :param cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey signing_key: service signing key
221
222    :returns: :class:`~stem.descriptor.hidden_service.IntroductionPointV3` with these attributes
223
224    :raises: **ValueError** if the address, port, or keys are malformed
225    """
226
227    if not stem.prereq.is_crypto_available(ed25519 = True):
228      raise ImportError('Introduction point creation requires the cryptography module ed25519 support')
229    elif not stem.util.connection.is_valid_port(port):
230      raise ValueError("'%s' is an invalid port" % port)
231
232    if stem.util.connection.is_valid_ipv4_address(address):
233      link_specifiers = [stem.client.datatype.LinkByIPv4(address, port)]
234    elif stem.util.connection.is_valid_ipv6_address(address):
235      link_specifiers = [stem.client.datatype.LinkByIPv6(address, port)]
236    else:
237      raise ValueError("'%s' is not a valid IPv4 or IPv6 address" % address)
238
239    return IntroductionPointV3.create_for_link_specifiers(link_specifiers, expiration = None, onion_key = None, enc_key = None, auth_key = None, signing_key = None)
240
241  @staticmethod
242  def create_for_link_specifiers(link_specifiers, expiration = None, onion_key = None, enc_key = None, auth_key = None, signing_key = None):
243    """
244    Simplified constructor. For more sophisticated use cases you can use this
245    as a template for how introduction points are properly created.
246
247    :param list link_specifiers: series of stem.client.datatype.LinkSpecifier where the service is reachable
248    :param datetime.datetime expiration: when certificates should expire
249    :param str onion_key: encoded, X25519PublicKey, or X25519PrivateKey onion key
250    :param str enc_key: encoded, X25519PublicKey, or X25519PrivateKey encryption key
251    :param str auth_key: encoded, Ed25519PublicKey, or Ed25519PrivateKey authentication key
252    :param cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey signing_key: service signing key
253
254    :returns: :class:`~stem.descriptor.hidden_service.IntroductionPointV3` with these attributes
255
256    :raises: **ValueError** if the address, port, or keys are malformed
257    """
258
259    if not stem.prereq.is_crypto_available(ed25519 = True):
260      raise ImportError('Introduction point creation requires the cryptography module ed25519 support')
261
262    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
263    from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
264
265    if expiration is None:
266      expiration = datetime.datetime.utcnow() + datetime.timedelta(hours = stem.descriptor.certificate.DEFAULT_EXPIRATION_HOURS)
267
268    onion_key = stem.util.str_tools._to_unicode(base64.b64encode(stem.util._pubkey_bytes(onion_key if onion_key else X25519PrivateKey.generate())))
269    enc_key = stem.util.str_tools._to_unicode(base64.b64encode(stem.util._pubkey_bytes(enc_key if enc_key else X25519PrivateKey.generate())))
270    auth_key = stem.util._pubkey_bytes(auth_key if auth_key else Ed25519PrivateKey.generate())
271    signing_key = signing_key if signing_key else Ed25519PrivateKey.generate()
272
273    extensions = [Ed25519Extension(ExtensionType.HAS_SIGNING_KEY, None, stem.util._pubkey_bytes(signing_key))]
274    auth_key_cert = Ed25519CertificateV1(CertType.HS_V3_INTRO_AUTH, expiration, 1, auth_key, extensions, signing_key = signing_key)
275    enc_key_cert = Ed25519CertificateV1(CertType.HS_V3_NTOR_ENC, expiration, 1, auth_key, extensions, signing_key = signing_key)
276
277    return IntroductionPointV3(link_specifiers, onion_key, auth_key_cert, enc_key, enc_key_cert, None, None)
278
279  def encode(self):
280    """
281    Descriptor representation of this introduction point.
282
283    :returns: **str** for our descriptor representation
284    """
285
286    lines = []
287
288    link_count = stem.client.datatype.Size.CHAR.pack(len(self.link_specifiers))
289    link_specifiers = link_count + b''.join([l.pack() for l in self.link_specifiers])
290    lines.append('introduction-point %s' % stem.util.str_tools._to_unicode(base64.b64encode(link_specifiers)))
291    lines.append('onion-key ntor %s' % self.onion_key_raw)
292    lines.append('auth-key\n' + self.auth_key_cert.to_base64(pem = True))
293
294    if self.enc_key_raw:
295      lines.append('enc-key ntor %s' % self.enc_key_raw)
296
297    lines.append('enc-key-cert\n' + self.enc_key_cert.to_base64(pem = True))
298
299    if self.legacy_key_raw:
300      lines.append('legacy-key\n' + self.legacy_key_raw)
301
302    if self.legacy_key_cert:
303      lines.append('legacy-key-cert\n' + self.legacy_key_cert)
304
305    return '\n'.join(lines)
306
307  def onion_key(self):
308    """
309    Provides our ntor introduction point public key.
310
311    :returns: ntor :class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey`
312
313    :raises:
314      * **ImportError** if required the cryptography module is unavailable
315      * **EnvironmentError** if OpenSSL x25519 unsupported
316    """
317
318    return IntroductionPointV3._key_as(self.onion_key_raw, x25519 = True)
319
320  def auth_key(self):
321    """
322    Provides our authentication certificate's public key.
323
324    :returns: :class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey`
325
326    :raises:
327      * **ImportError** if required the cryptography module is unavailable
328      * **EnvironmentError** if OpenSSL x25519 unsupported
329    """
330
331    return IntroductionPointV3._key_as(self.auth_key_cert.key, ed25519 = True)
332
333  def enc_key(self):
334    """
335    Provides our encryption key.
336
337    :returns: encryption :class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey`
338
339    :raises:
340      * **ImportError** if required the cryptography module is unavailable
341      * **EnvironmentError** if OpenSSL x25519 unsupported
342    """
343
344    return IntroductionPointV3._key_as(self.enc_key_raw, x25519 = True)
345
346  def legacy_key(self):
347    """
348    Provides our legacy introduction point public key.
349
350    :returns: legacy :class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey`
351
352    :raises:
353      * **ImportError** if required the cryptography module is unavailable
354      * **EnvironmentError** if OpenSSL x25519 unsupported
355    """
356
357    return IntroductionPointV3._key_as(self.legacy_key_raw, x25519 = True)
358
359  @staticmethod
360  def _key_as(value, x25519 = False, ed25519 = False):
361    if value is None or (not x25519 and not ed25519):
362      return value
363    elif not stem.prereq.is_crypto_available():
364      raise ImportError('cryptography module unavailable')
365
366    if x25519:
367      if not X25519_AVAILABLE:
368        # without this the cryptography raises...
369        # cryptography.exceptions.UnsupportedAlgorithm: X25519 is not supported by this version of OpenSSL.
370
371        raise EnvironmentError('OpenSSL x25519 unsupported')
372
373      from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
374      return X25519PublicKey.from_public_bytes(base64.b64decode(value))
375
376    if ed25519:
377      if not stem.prereq.is_crypto_available(ed25519 = True):
378        raise EnvironmentError('cryptography ed25519 unsupported')
379
380      from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
381      return Ed25519PublicKey.from_public_bytes(value)
382
383  @staticmethod
384  def _parse_link_specifiers(content):
385    try:
386      content = base64.b64decode(content)
387    except Exception as exc:
388      raise ValueError('Unable to base64 decode introduction point (%s): %s' % (exc, content))
389
390    link_specifiers = []
391    count, content = stem.client.datatype.Size.CHAR.pop(content)
392
393    for i in range(count):
394      link_specifier, content = stem.client.datatype.LinkSpecifier.pop(content)
395      link_specifiers.append(link_specifier)
396
397    if content:
398      raise ValueError('Introduction point had excessive data (%s)' % content)
399
400    return link_specifiers
401
402  def __hash__(self):
403    if not hasattr(self, '_hash'):
404      self._hash = hash(self.encode())
405
406    return self._hash
407
408  def __eq__(self, other):
409    return hash(self) == hash(other) if isinstance(other, IntroductionPointV3) else False
410
411  def __ne__(self, other):
412    return not self == other
413
414
415class AuthorizedClient(object):
416  """
417  Client authorized to use a v3 hidden service.
418
419  .. versionadded:: 1.8.0
420
421  :var str id: base64 encoded client id
422  :var str iv: base64 encoded randomized initialization vector
423  :var str cookie: base64 encoded authentication cookie
424  """
425
426  def __init__(self, id = None, iv = None, cookie = None):
427    self.id = stem.util.str_tools._to_unicode(id if id else base64.b64encode(os.urandom(8)).rstrip(b'='))
428    self.iv = stem.util.str_tools._to_unicode(iv if iv else base64.b64encode(os.urandom(16)).rstrip(b'='))
429    self.cookie = stem.util.str_tools._to_unicode(cookie if cookie else base64.b64encode(os.urandom(16)).rstrip(b'='))
430
431  def __hash__(self):
432    return stem.util._hash_attr(self, 'id', 'iv', 'cookie', cache = True)
433
434  def __eq__(self, other):
435    return hash(self) == hash(other) if isinstance(other, AuthorizedClient) else False
436
437  def __ne__(self, other):
438    return not self == other
439
440
441def _parse_file(descriptor_file, desc_type = None, validate = False, **kwargs):
442  """
443  Iterates over the hidden service descriptors in a file.
444
445  :param file descriptor_file: file with descriptor content
446  :param class desc_type: BaseHiddenServiceDescriptor subclass
447  :param bool validate: checks the validity of the descriptor's content if
448    **True**, skips these checks otherwise
449  :param dict kwargs: additional arguments for the descriptor constructor
450
451  :returns: iterator for :class:`~stem.descriptor.hidden_service.HiddenServiceDescriptorV2`
452    instances in the file
453
454  :raises:
455    * **ValueError** if the contents is malformed and validate is **True**
456    * **IOError** if the file can't be read
457  """
458
459  if desc_type is None:
460    desc_type = HiddenServiceDescriptorV2
461
462  # Hidden service v3 ends with a signature line, whereas v2 has a pgp style
463  # block following it.
464
465  while True:
466    descriptor_content = _read_until_keywords('signature', descriptor_file, True)
467
468    if desc_type == HiddenServiceDescriptorV2:
469      block_end_prefix = PGP_BLOCK_END.split(' ', 1)[0]
470      descriptor_content += _read_until_keywords(block_end_prefix, descriptor_file, True)
471
472    if descriptor_content:
473      if descriptor_content[0].startswith(b'@type'):
474        descriptor_content = descriptor_content[1:]
475
476      yield desc_type(bytes.join(b'', descriptor_content), validate, **kwargs)
477    else:
478      break  # done parsing file
479
480
481def _decrypt_layer(encrypted_block, constant, revision_counter, subcredential, blinded_key):
482  if encrypted_block.startswith('-----BEGIN MESSAGE-----\n') and encrypted_block.endswith('\n-----END MESSAGE-----'):
483    encrypted_block = encrypted_block[24:-22]
484
485  try:
486    encrypted = base64.b64decode(encrypted_block)
487  except:
488    raise ValueError('Unable to decode encrypted block as base64')
489
490  if len(encrypted) < SALT_LEN + MAC_LEN:
491    raise ValueError('Encrypted block malformed (only %i bytes)' % len(encrypted))
492
493  salt = encrypted[:SALT_LEN]
494  ciphertext = encrypted[SALT_LEN:-MAC_LEN]
495  expected_mac = encrypted[-MAC_LEN:]
496
497  cipher, mac_for = _layer_cipher(constant, revision_counter, subcredential, blinded_key, salt)
498
499  if expected_mac != mac_for(ciphertext):
500    raise ValueError('Malformed mac (expected %s, but was %s)' % (expected_mac, mac_for(ciphertext)))
501
502  decryptor = cipher.decryptor()
503  plaintext = decryptor.update(ciphertext) + decryptor.finalize()
504
505  return stem.util.str_tools._to_unicode(plaintext)
506
507
508def _encrypt_layer(plaintext, constant, revision_counter, subcredential, blinded_key):
509  salt = os.urandom(16)
510  cipher, mac_for = _layer_cipher(constant, revision_counter, subcredential, blinded_key, salt)
511
512  encryptor = cipher.encryptor()
513  ciphertext = encryptor.update(plaintext) + encryptor.finalize()
514  encoded = base64.b64encode(salt + ciphertext + mac_for(ciphertext))
515
516  return b'-----BEGIN MESSAGE-----\n%s\n-----END MESSAGE-----' % b'\n'.join(stem.util.str_tools._split_by_length(encoded, 64))
517
518
519def _layer_cipher(constant, revision_counter, subcredential, blinded_key, salt):
520  from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
521  from cryptography.hazmat.backends import default_backend
522
523  kdf = hashlib.shake_256(blinded_key + subcredential + struct.pack('>Q', revision_counter) + salt + constant)
524  keys = kdf.digest(S_KEY_LEN + S_IV_LEN + MAC_LEN)
525
526  secret_key = keys[:S_KEY_LEN]
527  secret_iv = keys[S_KEY_LEN:S_KEY_LEN + S_IV_LEN]
528  mac_key = keys[S_KEY_LEN + S_IV_LEN:]
529
530  cipher = Cipher(algorithms.AES(secret_key), modes.CTR(secret_iv), default_backend())
531  mac_prefix = struct.pack('>Q', len(mac_key)) + mac_key + struct.pack('>Q', len(salt)) + salt
532
533  return cipher, lambda ciphertext: hashlib.sha3_256(mac_prefix + ciphertext).digest()
534
535
536def _parse_protocol_versions_line(descriptor, entries):
537  value = _value('protocol-versions', entries)
538
539  try:
540    versions = [int(entry) for entry in value.split(',')]
541  except ValueError:
542    raise ValueError('protocol-versions line has non-numeric versoins: protocol-versions %s' % value)
543
544  for v in versions:
545    if v <= 0:
546      raise ValueError('protocol-versions must be positive integers: %s' % value)
547
548  descriptor.protocol_versions = versions
549
550
551def _parse_introduction_points_line(descriptor, entries):
552  _, block_type, block_contents = entries['introduction-points'][0]
553
554  if not block_contents or block_type != 'MESSAGE':
555    raise ValueError("'introduction-points' should be followed by a MESSAGE block, but was a %s" % block_type)
556
557  descriptor.introduction_points_encoded = block_contents
558  descriptor.introduction_points_auth = []  # field was never implemented in tor (#15190)
559
560  try:
561    descriptor.introduction_points_content = _bytes_for_block(block_contents)
562  except TypeError:
563    raise ValueError("'introduction-points' isn't base64 encoded content:\n%s" % block_contents)
564
565
566def _parse_v3_outer_clients(descriptor, entries):
567  # "auth-client" client-id iv encrypted-cookie
568
569  clients = {}
570
571  for value in _values('auth-client', entries):
572    value_comp = value.split()
573
574    if len(value_comp) < 3:
575      raise ValueError('auth-client should have a client-id, iv, and cookie: auth-client %s' % value)
576
577    clients[value_comp[0]] = AuthorizedClient(value_comp[0], value_comp[1], value_comp[2])
578
579  descriptor.clients = clients
580
581
582def _parse_v3_inner_formats(descriptor, entries):
583  value, formats = _value('create2-formats', entries), []
584
585  for entry in value.split(' '):
586    if not entry.isdigit():
587      raise ValueError("create2-formats should only contain integers, but was '%s'" % value)
588
589    formats.append(int(entry))
590
591  descriptor.formats = formats
592
593
594def _parse_v3_introduction_points(descriptor, entries):
595  if hasattr(descriptor, '_unparsed_introduction_points'):
596    introduction_points = []
597    remaining = descriptor._unparsed_introduction_points
598
599    while remaining:
600      div = remaining.find(b'\nintroduction-point ', 10)
601      content, remaining = (remaining[:div], remaining[div + 1:]) if div != -1 else (remaining, '')
602
603      introduction_points.append(IntroductionPointV3.parse(content))
604
605    descriptor.introduction_points = introduction_points
606    del descriptor._unparsed_introduction_points
607
608
609_parse_v2_version_line = _parse_int_line('version', 'version', allow_negative = False)
610_parse_rendezvous_service_descriptor_line = _parse_simple_line('rendezvous-service-descriptor', 'descriptor_id')
611_parse_permanent_key_line = _parse_key_block('permanent-key', 'permanent_key', 'RSA PUBLIC KEY')
612_parse_secret_id_part_line = _parse_simple_line('secret-id-part', 'secret_id_part')
613_parse_publication_time_line = _parse_timestamp_line('publication-time', 'published')
614_parse_v2_signature_line = _parse_key_block('signature', 'signature', 'SIGNATURE')
615
616_parse_v3_version_line = _parse_int_line('hs-descriptor', 'version', allow_negative = False)
617_parse_lifetime_line = _parse_int_line('descriptor-lifetime', 'lifetime', allow_negative = False)
618_parse_signing_cert = Ed25519Certificate._from_descriptor('descriptor-signing-key-cert', 'signing_cert')
619_parse_revision_counter_line = _parse_int_line('revision-counter', 'revision_counter', allow_negative = False)
620_parse_superencrypted_line = _parse_key_block('superencrypted', 'superencrypted', 'MESSAGE')
621_parse_v3_signature_line = _parse_simple_line('signature', 'signature')
622
623_parse_v3_outer_auth_type = _parse_simple_line('desc-auth-type', 'auth_type')
624_parse_v3_outer_ephemeral_key = _parse_simple_line('desc-auth-ephemeral-key', 'ephemeral_key')
625_parse_v3_outer_encrypted = _parse_key_block('encrypted', 'encrypted', 'MESSAGE')
626
627_parse_v3_inner_intro_auth = _parse_simple_line('intro-auth-required', 'intro_auth', func = lambda v: v.split(' '))
628_parse_v3_inner_single_service = _parse_if_present('single-onion-service', 'is_single_service')
629
630
631class BaseHiddenServiceDescriptor(Descriptor):
632  """
633  Hidden service descriptor.
634
635  .. versionadded:: 1.8.0
636  """
637
638  # TODO: rename this class to HiddenServiceDescriptor in stem 2.x
639
640
641class HiddenServiceDescriptorV2(BaseHiddenServiceDescriptor):
642  """
643  Version 2 hidden service descriptor.
644
645  :var str descriptor_id: **\\*** identifier for this descriptor, this is a base32 hash of several fields
646  :var int version: **\\*** hidden service descriptor version
647  :var str permanent_key: **\\*** long term key of the hidden service
648  :var str secret_id_part: **\\*** hash of the time period, cookie, and replica
649    values so our descriptor_id can be validated
650  :var datetime published: **\\*** time in UTC when this descriptor was made
651  :var list protocol_versions: **\\*** list of **int** versions that are supported when establishing a connection
652  :var str introduction_points_encoded: raw introduction points blob
653  :var list introduction_points_auth: **\\*** tuples of the form
654    (auth_method, auth_data) for our introduction_points_content
655    (**deprecated**, always **[]**)
656  :var bytes introduction_points_content: decoded introduction-points content
657    without authentication data, if using cookie authentication this is
658    encrypted
659  :var str signature: signature of the descriptor content
660
661  **\\*** attribute is either required when we're parsed with validation or has
662  a default value, others are left as **None** if undefined
663
664  .. versionchanged:: 1.6.0
665     Moved from the deprecated `pycrypto
666     <https://www.dlitz.net/software/pycrypto/>`_ module to `cryptography
667     <https://pypi.org/project/cryptography/>`_ for validating signatures.
668
669  .. versionchanged:: 1.6.0
670     Added the **skip_crypto_validation** constructor argument.
671  """
672
673  TYPE_ANNOTATION_NAME = 'hidden-service-descriptor'
674
675  ATTRIBUTES = {
676    'descriptor_id': (None, _parse_rendezvous_service_descriptor_line),
677    'version': (None, _parse_v2_version_line),
678    'permanent_key': (None, _parse_permanent_key_line),
679    'secret_id_part': (None, _parse_secret_id_part_line),
680    'published': (None, _parse_publication_time_line),
681    'protocol_versions': ([], _parse_protocol_versions_line),
682    'introduction_points_encoded': (None, _parse_introduction_points_line),
683    'introduction_points_auth': ([], _parse_introduction_points_line),
684    'introduction_points_content': (None, _parse_introduction_points_line),
685    'signature': (None, _parse_v2_signature_line),
686  }
687
688  PARSER_FOR_LINE = {
689    'rendezvous-service-descriptor': _parse_rendezvous_service_descriptor_line,
690    'version': _parse_v2_version_line,
691    'permanent-key': _parse_permanent_key_line,
692    'secret-id-part': _parse_secret_id_part_line,
693    'publication-time': _parse_publication_time_line,
694    'protocol-versions': _parse_protocol_versions_line,
695    'introduction-points': _parse_introduction_points_line,
696    'signature': _parse_v2_signature_line,
697  }
698
699  @classmethod
700  def content(cls, attr = None, exclude = (), sign = False):
701    if sign:
702      raise NotImplementedError('Signing of %s not implemented' % cls.__name__)
703
704    return _descriptor_content(attr, exclude, (
705      ('rendezvous-service-descriptor', 'y3olqqblqw2gbh6phimfuiroechjjafa'),
706      ('version', '2'),
707      ('permanent-key', _random_crypto_blob('RSA PUBLIC KEY')),
708      ('secret-id-part', 'e24kgecavwsznj7gpbktqsiwgvngsf4e'),
709      ('publication-time', _random_date()),
710      ('protocol-versions', '2,3'),
711      ('introduction-points', '\n-----BEGIN MESSAGE-----\n-----END MESSAGE-----'),
712    ), (
713      ('signature', _random_crypto_blob('SIGNATURE')),
714    ))
715
716  @classmethod
717  def create(cls, attr = None, exclude = (), validate = True, sign = False):
718    return cls(cls.content(attr, exclude, sign), validate = validate, skip_crypto_validation = not sign)
719
720  def __init__(self, raw_contents, validate = False, skip_crypto_validation = False):
721    super(HiddenServiceDescriptorV2, self).__init__(raw_contents, lazy_load = not validate)
722    entries = _descriptor_components(raw_contents, validate, non_ascii_fields = ('introduction-points'))
723
724    if validate:
725      for keyword in REQUIRED_V2_FIELDS:
726        if keyword not in entries:
727          raise ValueError("Hidden service descriptor must have a '%s' entry" % keyword)
728        elif keyword in entries and len(entries[keyword]) > 1:
729          raise ValueError("The '%s' entry can only appear once in a hidden service descriptor" % keyword)
730
731      if 'rendezvous-service-descriptor' != list(entries.keys())[0]:
732        raise ValueError("Hidden service descriptor must start with a 'rendezvous-service-descriptor' entry")
733      elif 'signature' != list(entries.keys())[-1]:
734        raise ValueError("Hidden service descriptor must end with a 'signature' entry")
735
736      self._parse(entries, validate)
737
738      if not skip_crypto_validation and stem.prereq.is_crypto_available():
739        signed_digest = self._digest_for_signature(self.permanent_key, self.signature)
740        digest_content = self._content_range('rendezvous-service-descriptor ', '\nsignature\n')
741        content_digest = hashlib.sha1(digest_content).hexdigest().upper()
742
743        if signed_digest != content_digest:
744          raise ValueError('Decrypted digest does not match local digest (calculated: %s, local: %s)' % (signed_digest, content_digest))
745    else:
746      self._entries = entries
747
748  @lru_cache()
749  def introduction_points(self, authentication_cookie = None):
750    """
751    Provided this service's introduction points.
752
753    :returns: **list** of :class:`~stem.descriptor.hidden_service.IntroductionPoints`
754
755    :raises:
756      * **ValueError** if the our introduction-points is malformed
757      * **DecryptionFailure** if unable to decrypt this field
758    """
759
760    content = self.introduction_points_content
761
762    if not content:
763      return []
764    elif authentication_cookie:
765      if not stem.prereq.is_crypto_available():
766        raise DecryptionFailure('Decrypting introduction-points requires the cryptography module')
767
768      try:
769        authentication_cookie = stem.util.str_tools._decode_b64(authentication_cookie)
770      except TypeError as exc:
771        raise DecryptionFailure('authentication_cookie must be a base64 encoded string (%s)' % exc)
772
773      authentication_type = int(binascii.hexlify(content[0:1]), 16)
774
775      if authentication_type == BASIC_AUTH:
776        content = HiddenServiceDescriptorV2._decrypt_basic_auth(content, authentication_cookie)
777      elif authentication_type == STEALTH_AUTH:
778        content = HiddenServiceDescriptorV2._decrypt_stealth_auth(content, authentication_cookie)
779      else:
780        raise DecryptionFailure("Unrecognized authentication type '%s', currently we only support basic auth (%s) and stealth auth (%s)" % (authentication_type, BASIC_AUTH, STEALTH_AUTH))
781
782      if not content.startswith(b'introduction-point '):
783        raise DecryptionFailure('Unable to decrypt the introduction-points, maybe this is the wrong key?')
784    elif not content.startswith(b'introduction-point '):
785      raise DecryptionFailure('introduction-points content is encrypted, you need to provide its authentication_cookie')
786
787    return HiddenServiceDescriptorV2._parse_introduction_points(content)
788
789  @staticmethod
790  def _decrypt_basic_auth(content, authentication_cookie):
791    from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
792    from cryptography.hazmat.backends import default_backend
793
794    try:
795      client_blocks = int(binascii.hexlify(content[1:2]), 16)
796    except ValueError:
797      raise DecryptionFailure("When using basic auth the content should start with a number of blocks but wasn't a hex digit: %s" % binascii.hexlify(content[1:2]))
798
799    # parse the client id and encrypted session keys
800
801    client_entries_length = client_blocks * 16 * 20
802    client_entries = content[2:2 + client_entries_length]
803    client_keys = [(client_entries[i:i + 4], client_entries[i + 4:i + 20]) for i in range(0, client_entries_length, 4 + 16)]
804
805    iv = content[2 + client_entries_length:2 + client_entries_length + 16]
806    encrypted = content[2 + client_entries_length + 16:]
807
808    client_id = hashlib.sha1(authentication_cookie + iv).digest()[:4]
809
810    for entry_id, encrypted_session_key in client_keys:
811      if entry_id != client_id:
812        continue  # not the session key for this client
813
814      # try decrypting the session key
815
816      cipher = Cipher(algorithms.AES(authentication_cookie), modes.CTR(b'\x00' * len(iv)), default_backend())
817      decryptor = cipher.decryptor()
818      session_key = decryptor.update(encrypted_session_key) + decryptor.finalize()
819
820      # attempt to decrypt the intro points with the session key
821
822      cipher = Cipher(algorithms.AES(session_key), modes.CTR(iv), default_backend())
823      decryptor = cipher.decryptor()
824      decrypted = decryptor.update(encrypted) + decryptor.finalize()
825
826      # check if the decryption looks correct
827
828      if decrypted.startswith(b'introduction-point '):
829        return decrypted
830
831    return content  # nope, unable to decrypt the content
832
833  @staticmethod
834  def _decrypt_stealth_auth(content, authentication_cookie):
835    from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
836    from cryptography.hazmat.backends import default_backend
837
838    # byte 1 = authentication type, 2-17 = input vector, 18 on = encrypted content
839    iv, encrypted = content[1:17], content[17:]
840    cipher = Cipher(algorithms.AES(authentication_cookie), modes.CTR(iv), default_backend())
841    decryptor = cipher.decryptor()
842
843    return decryptor.update(encrypted) + decryptor.finalize()
844
845  @staticmethod
846  def _parse_introduction_points(content):
847    """
848    Provides the parsed list of IntroductionPoints for the unencrypted content.
849    """
850
851    introduction_points = []
852    content_io = io.BytesIO(content)
853
854    while True:
855      content = b''.join(_read_until_keywords('introduction-point', content_io, ignore_first = True))
856
857      if not content:
858        break  # reached the end
859
860      attr = dict(INTRODUCTION_POINTS_ATTR)
861      entries = _descriptor_components(content, False)
862
863      for keyword, values in list(entries.items()):
864        value, block_type, block_contents = values[0]
865
866        if keyword in SINGLE_INTRODUCTION_POINT_FIELDS and len(values) > 1:
867          raise ValueError("'%s' can only appear once in an introduction-point block, but appeared %i times" % (keyword, len(values)))
868
869        if keyword == 'introduction-point':
870          attr['identifier'] = value
871        elif keyword == 'ip-address':
872          if not stem.util.connection.is_valid_ipv4_address(value):
873            raise ValueError("'%s' is an invalid IPv4 address" % value)
874
875          attr['address'] = value
876        elif keyword == 'onion-port':
877          if not stem.util.connection.is_valid_port(value):
878            raise ValueError("'%s' is an invalid port" % value)
879
880          attr['port'] = int(value)
881        elif keyword == 'onion-key':
882          attr['onion_key'] = block_contents
883        elif keyword == 'service-key':
884          attr['service_key'] = block_contents
885        elif keyword == 'intro-authentication':
886          auth_entries = []
887
888          for auth_value, _, _ in values:
889            if ' ' not in auth_value:
890              raise ValueError("We expected 'intro-authentication [auth_type] [auth_data]', but had '%s'" % auth_value)
891
892            auth_type, auth_data = auth_value.split(' ')[:2]
893            auth_entries.append((auth_type, auth_data))
894
895      introduction_points.append(IntroductionPoints(**attr))
896
897    return introduction_points
898
899
900class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
901  """
902  Version 3 hidden service descriptor.
903
904  :var int version: **\\*** hidden service descriptor version
905  :var int lifetime: **\\*** minutes after publication this descriptor is valid
906  :var stem.descriptor.certificate.Ed25519Certificate signing_cert: **\\*** cross-certifier for the short-term descriptor signing key
907  :var int revision_counter: **\\*** descriptor revision number
908  :var str superencrypted: **\\*** encrypted HS-DESC-ENC payload
909  :var str signature: **\\*** signature of this descriptor
910
911  **\\*** attribute is either required when we're parsed with validation or has
912  a default value, others are left as **None** if undefined
913
914  .. versionadded:: 1.8.0
915  """
916
917  # TODO: requested this @type on https://trac.torproject.org/projects/tor/ticket/31481
918
919  TYPE_ANNOTATION_NAME = 'hidden-service-descriptor-3'
920
921  ATTRIBUTES = {
922    'version': (None, _parse_v3_version_line),
923    'lifetime': (None, _parse_lifetime_line),
924    'signing_cert': (None, _parse_signing_cert),
925    'revision_counter': (None, _parse_revision_counter_line),
926    'superencrypted': (None, _parse_superencrypted_line),
927    'signature': (None, _parse_v3_signature_line),
928  }
929
930  PARSER_FOR_LINE = {
931    'hs-descriptor': _parse_v3_version_line,
932    'descriptor-lifetime': _parse_lifetime_line,
933    'descriptor-signing-key-cert': _parse_signing_cert,
934    'revision-counter': _parse_revision_counter_line,
935    'superencrypted': _parse_superencrypted_line,
936    'signature': _parse_v3_signature_line,
937  }
938
939  @classmethod
940  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):
941    """
942    Hidden service v3 descriptors consist of three parts:
943
944      * InnerLayer, which most notably contain introduction points where the
945        service can be reached.
946
947      * OuterLayer, which encrypts the InnerLayer among other paremters.
948
949      * HiddenServiceDescriptorV3, which contains the OuterLayer and plaintext
950        parameters.
951
952    Construction through this method can supply any or none of these, with
953    omitted parameters populated with randomized defaults.
954
955    Ed25519 key blinding adds an additional ~20 ms, and as such is disabled by
956    default. To blind with a random nonce simply call...
957
958    ::
959
960      HiddenServiceDescriptorV3.create(blinding_nonce = os.urandom(32))
961
962    :param dict attr: keyword/value mappings to be included in plaintext descriptor
963    :param list exclude: mandatory keywords to exclude from the descriptor, this
964      results in an invalid descriptor
965    :param bool sign: includes cryptographic signatures and digests if True
966    :param stem.descriptor.hidden_service.InnerLayer inner_layer: inner
967      encrypted layer
968    :param stem.descriptor.hidden_service.OuterLayer outer_layer: outer
969      encrypted layer
970    :param cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey
971      identity_key: service identity key
972    :param cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey
973      signing_key: service signing key
974    :param stem.descriptor.Ed25519CertificateV1 signing_cert: certificate
975      signing this descriptor
976    :param int revision_counter: descriptor revision number
977    :param bytes blinding_nonce: 32 byte blinding factor to derive the blinding key
978
979    :returns: **str** with the content of a descriptor
980
981    :raises:
982      * **ValueError** if parameters are malformed
983      * **ImportError** if cryptography is unavailable
984    """
985
986    if not stem.prereq.is_crypto_available(ed25519 = True):
987      raise ImportError('Hidden service descriptor creation requires cryptography version 2.6')
988    elif not stem.prereq._is_sha3_available():
989      raise ImportError('Hidden service descriptor creation requires python 3.6+ or the pysha3 module (https://pypi.org/project/pysha3/)')
990    elif blinding_nonce and len(blinding_nonce) != 32:
991      raise ValueError('Blinding nonce must be 32 bytes, but was %i' % len(blinding_nonce))
992
993    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
994
995    inner_layer = inner_layer if inner_layer else InnerLayer.create(exclude = exclude)
996    identity_key = identity_key if identity_key else Ed25519PrivateKey.generate()
997    signing_key = signing_key if signing_key else Ed25519PrivateKey.generate()
998    revision_counter = revision_counter if revision_counter else int(time.time())
999
1000    blinded_key = _blinded_pubkey(identity_key, blinding_nonce) if blinding_nonce else b'a' * 32
1001    subcredential = HiddenServiceDescriptorV3._subcredential(identity_key, blinded_key)
1002    custom_sig = attr.pop('signature') if (attr and 'signature' in attr) else None
1003
1004    if not outer_layer:
1005      outer_layer = OuterLayer.create(
1006        exclude = exclude,
1007        inner_layer = inner_layer,
1008        revision_counter = revision_counter,
1009        subcredential = subcredential,
1010        blinded_key = blinded_key,
1011      )
1012
1013    if not signing_cert:
1014      extensions = [Ed25519Extension(ExtensionType.HAS_SIGNING_KEY, None, blinded_key)]
1015
1016      signing_cert = Ed25519CertificateV1(cert_type = CertType.HS_V3_DESC_SIGNING, key = signing_key, extensions = extensions)
1017      signing_cert.signature = _blinded_sign(signing_cert.pack(), identity_key, blinded_key, blinding_nonce) if blinding_nonce else b'b' * 64
1018
1019    desc_content = _descriptor_content(attr, exclude, (
1020      ('hs-descriptor', '3'),
1021      ('descriptor-lifetime', '180'),
1022      ('descriptor-signing-key-cert', '\n' + signing_cert.to_base64(pem = True)),
1023      ('revision-counter', str(revision_counter)),
1024      ('superencrypted', b'\n' + outer_layer._encrypt(revision_counter, subcredential, blinded_key)),
1025    ), ()) + b'\n'
1026
1027    if custom_sig:
1028      desc_content += b'signature %s' % stem.util.str_tools._to_bytes(custom_sig)
1029    elif 'signature' not in exclude:
1030      sig_content = stem.descriptor.certificate.SIG_PREFIX_HS_V3 + desc_content
1031      desc_content += b'signature %s' % base64.b64encode(signing_key.sign(sig_content)).rstrip(b'=')
1032
1033    return desc_content
1034
1035  @classmethod
1036  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):
1037    return cls(cls.content(attr, exclude, sign, inner_layer, outer_layer, identity_key, signing_key, signing_cert, revision_counter, blinding_nonce), validate = validate)
1038
1039  def __init__(self, raw_contents, validate = False):
1040    super(HiddenServiceDescriptorV3, self).__init__(raw_contents, lazy_load = not validate)
1041
1042    self._inner_layer = None
1043    entries = _descriptor_components(raw_contents, validate)
1044
1045    if validate:
1046      for keyword in REQUIRED_V3_FIELDS:
1047        if keyword not in entries:
1048          raise ValueError("Hidden service descriptor must have a '%s' entry" % keyword)
1049        elif keyword in entries and len(entries[keyword]) > 1:
1050          raise ValueError("The '%s' entry can only appear once in a hidden service descriptor" % keyword)
1051
1052      if 'hs-descriptor' != list(entries.keys())[0]:
1053        raise ValueError("Hidden service descriptor must start with a 'hs-descriptor' entry")
1054      elif 'signature' != list(entries.keys())[-1]:
1055        raise ValueError("Hidden service descriptor must end with a 'signature' entry")
1056
1057      self._parse(entries, validate)
1058
1059      if self.signing_cert and stem.prereq.is_crypto_available(ed25519 = True):
1060        self.signing_cert.validate(self)
1061    else:
1062      self._entries = entries
1063
1064  def decrypt(self, onion_address):
1065    """
1066    Decrypt this descriptor. Hidden serice descriptors contain two encryption
1067    layers (:class:`~stem.descriptor.hidden_service.OuterLayer` and
1068    :class:`~stem.descriptor.hidden_service.InnerLayer`).
1069
1070    :param str onion_address: hidden service address this descriptor is from
1071
1072    :returns: :class:`~stem.descriptor.hidden_service.InnerLayer` with our
1073      decrypted content
1074
1075    :raises:
1076      * **ImportError** if required cryptography or sha3 module is unavailable
1077      * **ValueError** if unable to decrypt or validation fails
1078    """
1079
1080    if not stem.prereq.is_crypto_available(ed25519 = True):
1081      raise ImportError('Hidden service descriptor decryption requires cryptography version 2.6')
1082    elif not stem.prereq._is_sha3_available():
1083      raise ImportError('Hidden service descriptor decryption requires python 3.6+ or the pysha3 module (https://pypi.org/project/pysha3/)')
1084
1085    if self._inner_layer is None:
1086      blinded_key = self.signing_cert.signing_key() if self.signing_cert else None
1087
1088      if not blinded_key:
1089        raise ValueError('No signing key is present')
1090
1091      identity_public_key = HiddenServiceDescriptorV3.identity_key_from_address(onion_address)
1092      subcredential = HiddenServiceDescriptorV3._subcredential(identity_public_key, blinded_key)
1093
1094      outer_layer = OuterLayer._decrypt(self.superencrypted, self.revision_counter, subcredential, blinded_key)
1095      self._inner_layer = InnerLayer._decrypt(outer_layer, self.revision_counter, subcredential, blinded_key)
1096
1097    return self._inner_layer
1098
1099  @staticmethod
1100  def address_from_identity_key(key, suffix = True):
1101    """
1102    Converts a hidden service identity key into its address. This accepts all
1103    key formats (private, public, or public bytes).
1104
1105    :param Ed25519PublicKey,Ed25519PrivateKey,bytes key: hidden service identity key
1106    :param bool suffix: includes the '.onion' suffix if true, excluded otherwise
1107
1108    :returns: **unicode** hidden service address
1109
1110    :raises: **ImportError** if sha3 unsupported
1111    """
1112
1113    if not stem.prereq._is_sha3_available():
1114      raise ImportError('Hidden service address conversion requires python 3.6+ or the pysha3 module (https://pypi.org/project/pysha3/)')
1115
1116    key = stem.util._pubkey_bytes(key)  # normalize key into bytes
1117
1118    version = stem.client.datatype.Size.CHAR.pack(3)
1119    checksum = hashlib.sha3_256(CHECKSUM_CONSTANT + key + version).digest()[:2]
1120    onion_address = base64.b32encode(key + checksum + version)
1121
1122    return stem.util.str_tools._to_unicode(onion_address + b'.onion' if suffix else onion_address).lower()
1123
1124  @staticmethod
1125  def identity_key_from_address(onion_address):
1126    """
1127    Converts a hidden service address into its public identity key.
1128
1129    :param str onion_address: hidden service address
1130
1131    :returns: **bytes** for the hidden service's public identity key
1132
1133    :raises:
1134      * **ImportError** if sha3 unsupported
1135      * **ValueError** if address malformed or checksum is invalid
1136    """
1137
1138    if not stem.prereq._is_sha3_available():
1139      raise ImportError('Hidden service address conversion requires python 3.6+ or the pysha3 module (https://pypi.org/project/pysha3/)')
1140
1141    if onion_address.endswith('.onion'):
1142      onion_address = onion_address[:-6]
1143
1144    if not stem.util.tor_tools.is_valid_hidden_service_address(onion_address, version = 3):
1145      raise ValueError("'%s.onion' isn't a valid hidden service v3 address" % onion_address)
1146
1147    # onion_address = base32(PUBKEY | CHECKSUM | VERSION) + '.onion'
1148    # CHECKSUM = H('.onion checksum' | PUBKEY | VERSION)[:2]
1149
1150    decoded_address = base64.b32decode(onion_address.upper())
1151
1152    pubkey = decoded_address[:32]
1153    expected_checksum = decoded_address[32:34]
1154    version = decoded_address[34:35]
1155
1156    checksum = hashlib.sha3_256(CHECKSUM_CONSTANT + pubkey + version).digest()[:2]
1157
1158    if expected_checksum != checksum:
1159      checksum_str = stem.util.str_tools._to_unicode(binascii.hexlify(checksum))
1160      expected_checksum_str = stem.util.str_tools._to_unicode(binascii.hexlify(expected_checksum))
1161
1162      raise ValueError('Bad checksum (expected %s but was %s)' % (expected_checksum_str, checksum_str))
1163
1164    return pubkey
1165
1166  @staticmethod
1167  def _subcredential(identity_key, blinded_key):
1168    # credential = H('credential' | public-identity-key)
1169    # subcredential = H('subcredential' | credential | blinded-public-key)
1170
1171    credential = hashlib.sha3_256(b'credential%s' % stem.util._pubkey_bytes(identity_key)).digest()
1172    return hashlib.sha3_256(b'subcredential%s%s' % (credential, blinded_key)).digest()
1173
1174
1175class OuterLayer(Descriptor):
1176  """
1177  Initial encryped layer of a hidden service v3 descriptor (`spec
1178  <https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt#n1154>`_).
1179
1180  .. versionadded:: 1.8.0
1181
1182  :var str auth_type: **\\*** encryption scheme used for descriptor authorization
1183  :var str ephemeral_key: **\\*** base64 encoded x25519 public key
1184  :var dict clients: **\\*** mapping of authorized client ids to their
1185    :class:`~stem.descriptor.hidden_service.AuthorizedClient`
1186  :var str encrypted: **\\*** encrypted descriptor inner layer
1187
1188  **\\*** attribute is either required when we're parsed with validation or has
1189  a default value, others are left as **None** if undefined
1190  """
1191
1192  ATTRIBUTES = {
1193    'auth_type': (None, _parse_v3_outer_auth_type),
1194    'ephemeral_key': (None, _parse_v3_outer_ephemeral_key),
1195    'clients': ({}, _parse_v3_outer_clients),
1196    'encrypted': (None, _parse_v3_outer_encrypted),
1197  }
1198
1199  PARSER_FOR_LINE = {
1200    'desc-auth-type': _parse_v3_outer_auth_type,
1201    'desc-auth-ephemeral-key': _parse_v3_outer_ephemeral_key,
1202    'auth-client': _parse_v3_outer_clients,
1203    'encrypted': _parse_v3_outer_encrypted,
1204  }
1205
1206  @staticmethod
1207  def _decrypt(encrypted, revision_counter, subcredential, blinded_key):
1208    plaintext = _decrypt_layer(encrypted, b'hsdir-superencrypted-data', revision_counter, subcredential, blinded_key)
1209    return OuterLayer(plaintext)
1210
1211  def _encrypt(self, revision_counter, subcredential, blinded_key):
1212    # Spec mandated padding: "Before encryption the plaintext is padded with
1213    # NUL bytes to the nearest multiple of 10k bytes."
1214
1215    content = self.get_bytes() + b'\x00' * (len(self.get_bytes()) % 10000)
1216
1217    # encrypt back into a hidden service descriptor's 'superencrypted' field
1218
1219    return _encrypt_layer(content, b'hsdir-superencrypted-data', revision_counter, subcredential, blinded_key)
1220
1221  @classmethod
1222  def content(cls, attr = None, exclude = (), validate = True, sign = False, inner_layer = None, revision_counter = None, authorized_clients = None, subcredential = None, blinded_key = None):
1223    if not stem.prereq.is_crypto_available(ed25519 = True):
1224      raise ImportError('Hidden service layer creation requires cryptography version 2.6')
1225    elif not stem.prereq._is_sha3_available():
1226      raise ImportError('Hidden service layer creation requires python 3.6+ or the pysha3 module (https://pypi.org/project/pysha3/)')
1227    elif authorized_clients and 'auth-client' in attr:
1228      raise ValueError('Authorized clients cannot be specified through both attr and authorized_clients')
1229
1230    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
1231    from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
1232
1233    inner_layer = inner_layer if inner_layer else InnerLayer.create()
1234    revision_counter = revision_counter if revision_counter else 1
1235    blinded_key = blinded_key if blinded_key else stem.util._pubkey_bytes(Ed25519PrivateKey.generate())
1236    subcredential = subcredential if subcredential else HiddenServiceDescriptorV3._subcredential(Ed25519PrivateKey.generate(), blinded_key)
1237
1238    if not authorized_clients:
1239      authorized_clients = []
1240
1241      if attr and 'auth-client' in attr:
1242        pass  # caller is providing raw auth-client lines through the attr
1243      else:
1244        for i in range(16):
1245          authorized_clients.append(AuthorizedClient())
1246
1247    return _descriptor_content(attr, exclude, [
1248      ('desc-auth-type', 'x25519'),
1249      ('desc-auth-ephemeral-key', base64.b64encode(stem.util._pubkey_bytes(X25519PrivateKey.generate()))),
1250    ] + [
1251      ('auth-client', '%s %s %s' % (c.id, c.iv, c.cookie)) for c in authorized_clients
1252    ], (
1253      ('encrypted', b'\n' + inner_layer._encrypt(revision_counter, subcredential, blinded_key)),
1254    ))
1255
1256  @classmethod
1257  def create(cls, attr = None, exclude = (), validate = True, sign = False, inner_layer = None, revision_counter = None, authorized_clients = None, subcredential = None, blinded_key = None):
1258    return cls(cls.content(attr, exclude, validate, sign, inner_layer, revision_counter, authorized_clients, subcredential, blinded_key), validate = validate)
1259
1260  def __init__(self, content, validate = False):
1261    content = stem.util.str_tools._to_bytes(content).rstrip(b'\x00')  # strip null byte padding
1262
1263    super(OuterLayer, self).__init__(content, lazy_load = not validate)
1264    entries = _descriptor_components(content, validate)
1265
1266    if validate:
1267      self._parse(entries, validate)
1268    else:
1269      self._entries = entries
1270
1271
1272class InnerLayer(Descriptor):
1273  """
1274  Second encryped layer of a hidden service v3 descriptor (`spec
1275  <https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt#n1308>`_).
1276
1277  .. versionadded:: 1.8.0
1278
1279  :var stem.descriptor.hidden_service.OuterLayer outer: enclosing encryption layer
1280
1281  :var list formats: **\\*** recognized CREATE2 cell formats
1282  :var list intro_auth: **\\*** introduction-layer authentication types
1283  :var bool is_single_service: **\\*** **True** if this is a `single onion service <https://gitweb.torproject.org/torspec.git/tree/proposals/260-rend-single-onion.txt>`_, **False** otherwise
1284  :var list introduction_points: :class:`~stem.descriptor.hidden_service.IntroductionPointV3` where this service is reachable
1285
1286  **\\*** attribute is either required when we're parsed with validation or has
1287  a default value, others are left as **None** if undefined
1288  """
1289
1290  ATTRIBUTES = {
1291    'formats': ([], _parse_v3_inner_formats),
1292    'intro_auth': ([], _parse_v3_inner_intro_auth),
1293    'is_single_service': (False, _parse_v3_inner_single_service),
1294    'introduction_points': ([], _parse_v3_introduction_points),
1295  }
1296
1297  PARSER_FOR_LINE = {
1298    'create2-formats': _parse_v3_inner_formats,
1299    'intro-auth-required': _parse_v3_inner_intro_auth,
1300    'single-onion-service': _parse_v3_inner_single_service,
1301  }
1302
1303  @staticmethod
1304  def _decrypt(outer_layer, revision_counter, subcredential, blinded_key):
1305    plaintext = _decrypt_layer(outer_layer.encrypted, b'hsdir-encrypted-data', revision_counter, subcredential, blinded_key)
1306    return InnerLayer(plaintext, validate = True, outer_layer = outer_layer)
1307
1308  def _encrypt(self, revision_counter, subcredential, blinded_key):
1309    # encrypt back into an outer layer's 'encrypted' field
1310
1311    return _encrypt_layer(self.get_bytes(), b'hsdir-encrypted-data', revision_counter, subcredential, blinded_key)
1312
1313  @classmethod
1314  def content(cls, attr = None, exclude = (), sign = False, introduction_points = None):
1315    if introduction_points:
1316      suffix = '\n' + '\n'.join(map(IntroductionPointV3.encode, introduction_points))
1317    else:
1318      suffix = ''
1319
1320    return _descriptor_content(attr, exclude, (
1321      ('create2-formats', '2'),
1322    )) + stem.util.str_tools._to_bytes(suffix)
1323
1324  @classmethod
1325  def create(cls, attr = None, exclude = (), validate = True, sign = False, introduction_points = None):
1326    return cls(cls.content(attr, exclude, sign, introduction_points), validate = validate)
1327
1328  def __init__(self, content, validate = False, outer_layer = None):
1329    super(InnerLayer, self).__init__(content, lazy_load = not validate)
1330    self.outer = outer_layer
1331
1332    # inner layer begins with a few header fields, followed by any
1333    # number of introduction-points
1334
1335    content = stem.util.str_tools._to_bytes(content)
1336    div = content.find(b'\nintroduction-point ')
1337
1338    if div != -1:
1339      self._unparsed_introduction_points = content[div + 1:]
1340      content = content[:div]
1341    else:
1342      self._unparsed_introduction_points = None
1343
1344    entries = _descriptor_components(content, validate)
1345
1346    if validate:
1347      self._parse(entries, validate)
1348      _parse_v3_introduction_points(self, entries)
1349    else:
1350      self._entries = entries
1351
1352
1353def _blinded_pubkey(identity_key, blinding_nonce):
1354  from stem.util import ed25519
1355
1356  mult = 2 ** (ed25519.b - 2) + sum(2 ** i * ed25519.bit(blinding_nonce, i) for i in range(3, ed25519.b - 2))
1357  P = ed25519.decodepoint(stem.util._pubkey_bytes(identity_key))
1358  return ed25519.encodepoint(ed25519.scalarmult(P, mult))
1359
1360
1361def _blinded_sign(msg, identity_key, blinded_key, blinding_nonce):
1362  from cryptography.hazmat.primitives import serialization
1363  from stem.util import ed25519
1364
1365  identity_key_bytes = identity_key.private_bytes(
1366    encoding = serialization.Encoding.Raw,
1367    format = serialization.PrivateFormat.Raw,
1368    encryption_algorithm = serialization.NoEncryption(),
1369  )
1370
1371  # pad private identity key into an ESK (encrypted secret key)
1372
1373  h = ed25519.H(identity_key_bytes)
1374  a = 2 ** (ed25519.b - 2) + sum(2 ** i * ed25519.bit(h, i) for i in range(3, ed25519.b - 2))
1375  k = b''.join([h[i:i + 1] for i in range(ed25519.b // 8, ed25519.b // 4)])
1376  esk = ed25519.encodeint(a) + k
1377
1378  # blind the ESK with this nonce
1379
1380  mult = 2 ** (ed25519.b - 2) + sum(2 ** i * ed25519.bit(blinding_nonce, i) for i in range(3, ed25519.b - 2))
1381  s = ed25519.decodeint(esk[:32])
1382  s_prime = (s * mult) % ed25519.l
1383  k = esk[32:]
1384  k_prime = ed25519.H(b'Derive temporary signing key hash input' + k)[:32]
1385  blinded_esk = ed25519.encodeint(s_prime) + k_prime
1386
1387  # finally, sign the message
1388
1389  a = ed25519.decodeint(blinded_esk[:32])
1390  r = ed25519.Hint(b''.join([blinded_esk[i:i + 1] for i in range(ed25519.b // 8, ed25519.b // 4)]) + msg)
1391  R = ed25519.scalarmult(ed25519.B, r)
1392  S = (r + ed25519.Hint(ed25519.encodepoint(R) + blinded_key + msg) * a) % ed25519.l
1393
1394  return ed25519.encodepoint(R) + ed25519.encodeint(S)
1395
1396
1397# TODO: drop this alias in stem 2.x
1398
1399HiddenServiceDescriptor = HiddenServiceDescriptorV2
1400