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