Open Table Of Contents

Source code for leekspin.rendezvous

# -*- coding: utf-8 -*-

"""Functions for creating mock Hidden Service
``rendezvous-service-descriptors``.


An example Hidden Service descriptor
====================================
..
   This HS descriptor was taken from dgoulet's tor branch bug14847_027_05, from
   the file src/test/test_hs.c.

This is `DuckDuckGo`_'s actual Hidden Service
``rendezvous-service-descriptor`` for their search service at
http://3g2upl4pq6kufc4m.onion/::

    rendezvous-service-descriptor g5ojobzupf275beh5ra72uyhb3dkpxwg\r\n\
    version 2\r\n\
    permanent-key\r\n\
    -----BEGIN RSA PUBLIC KEY-----\r\n\
    MIGJAoGBAJ/SzzgrXPxTlFrKVhXh3buCWv2QfcNgncUpDpKouLn3AtPH5Ocys0jE\r\n\
    aZSKdvaiQ62md2gOwj4x61cFNdi05tdQjS+2thHKEm/KsB9BGLSLBNJYY356bupg\r\n\
    I5gQozM65ENelfxYlysBjJ52xSDBd8C4f/p9umdzaaaCmzXG/nhzAgMBAAE=\r\n\
    -----END RSA PUBLIC KEY-----\r\n\
    secret-id-part anmjoxxwiupreyajjt5yasimfmwcnxlf\r\n\
    publication-time 2015-03-11 19:00:00\r\n\
    protocol-versions 2,3\r\n\
    introduction-points\r\n\
    -----BEGIN MESSAGE-----\r\n\
    aW50cm9kdWN0aW9uLXBvaW50IDd1bnd4cmg2dG5kNGh6eWt1Z3EzaGZzdHduc2ll\r\n\
    cmhyCmlwLWFkZHJlc3MgMTg4LjEzOC4xMjEuMTE4Cm9uaW9uLXBvcnQgOTAwMQpv\r\n\
    bmlvbi1rZXkKLS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JR0pBb0dC\r\n\
    QUxGRVVyeVpDbk9ROEhURmV5cDVjMTRObWVqL1BhekFLTTBxRENTNElKUWh0Y3g1\r\n\
    NXpRSFdOVWIKQ2hHZ0JqR1RjV3ZGRnA0N3FkdGF6WUZhVXE2c0lQKzVqeWZ5b0Q4\r\n\
    UmJ1bzBwQmFWclJjMmNhYUptWWM0RDh6Vgpuby9sZnhzOVVaQnZ1cWY4eHIrMDB2\r\n\
    S0JJNmFSMlA2OE1WeDhrMExqcUpUU2RKOE9idm9yQWdNQkFBRT0KLS0tLS1FTkQg\r\n\
    UlNBIFBVQkxJQyBLRVktLS0tLQpzZXJ2aWNlLWtleQotLS0tLUJFR0lOIFJTQSBQ\r\n\
    VUJMSUMgS0VZLS0tLS0KTUlHSkFvR0JBTnJHb0ozeTlHNXQzN2F2ekI1cTlwN1hG\r\n\
    VUplRUVYMUNOaExnWmJXWGJhVk5OcXpoZFhyL0xTUQppM1Z6dW5OaUs3cndUVnE2\r\n\
    K2QyZ1lRckhMMmIvMXBBY3ZKWjJiNSs0bTRRc0NibFpjRENXTktRbHJnRWN5WXRJ\r\n\
    CkdscXJTbFFEaXA0ZnNrUFMvNDVkWTI0QmJsQ3NGU1k3RzVLVkxJck4zZFpGbmJr\r\n\
    NEZIS1hBZ01CQUFFPQotLS0tLUVORCBSU0EgUFVCTElDIEtFWS0tLS0tCmludHJv\r\n\
    ZHVjdGlvbi1wb2ludCBiNGM3enlxNXNheGZzN2prNXFibG1wN3I1b3pwdHRvagpp\r\n\
    cC1hZGRyZXNzIDEwOS4xNjkuNDUuMjI2Cm9uaW9uLXBvcnQgOTAwMQpvbmlvbi1r\r\n\
    ZXkKLS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JR0pBb0dCQU8xSXpw\r\n\
    WFFUTUY3RXZUb1NEUXpzVnZiRVFRQUQrcGZ6NzczMVRXZzVaUEJZY1EyUkRaeVp4\r\n\
    OEQKNUVQSU1FeUE1RE83cGd0ak5LaXJvYXJGMC8yempjMkRXTUlSaXZyU29YUWVZ\r\n\
    ZXlMM1pzKzFIajJhMDlCdkYxZAp6MEswblRFdVhoNVR5V3lyMHdsbGI1SFBnTlI0\r\n\
    MS9oYkprZzkwZitPVCtIeGhKL1duUml2QWdNQkFBRT0KLS0tLS1FTkQgUlNBIFBV\r\n\
    QkxJQyBLRVktLS0tLQpzZXJ2aWNlLWtleQotLS0tLUJFR0lOIFJTQSBQVUJMSUMg\r\n\
    S0VZLS0tLS0KTUlHSkFvR0JBSzNWZEJ2ajFtQllLL3JrcHNwcm9Ub0llNUtHVmth\r\n\
    QkxvMW1tK1I2YUVJek1VZFE1SjkwNGtyRwpCd3k5NC8rV0lGNFpGYXh5Z2phejl1\r\n\
    N2pKY1k3ZGJhd1pFeG1hYXFCRlRwL2h2ZG9rcHQ4a1ByRVk4OTJPRHJ1CmJORUox\r\n\
    N1FPSmVMTVZZZk5Kcjl4TWZCQ3JQai8zOGh2RUdrbWVRNmRVWElvbVFNaUJGOVRB\r\n\
    Z01CQUFFPQotLS0tLUVORCBSU0EgUFVCTElDIEtFWS0tLS0tCmludHJvZHVjdGlv\r\n\
    bi1wb2ludCBhdjVtcWl0Y2Q3cjJkandsYmN0c2Jlc2R3eGt0ZWtvegppcC1hZGRy\r\n\
    ZXNzIDE0NC43Ni44LjczCm9uaW9uLXBvcnQgNDQzCm9uaW9uLWtleQotLS0tLUJF\r\n\
    R0lOIFJTQSBQVUJMSUMgS0VZLS0tLS0KTUlHSkFvR0JBTzVweVZzQmpZQmNmMXBE\r\n\
    dklHUlpmWXUzQ05nNldka0ZLMGlvdTBXTGZtejZRVDN0NWhzd3cyVwpjejlHMXhx\r\n\
    MmN0Nkd6VWkrNnVkTDlITTRVOUdHTi9BbW8wRG9GV1hKWHpBQkFXd2YyMVdsd1lW\r\n\
    eFJQMHRydi9WCkN6UDkzcHc5OG5vSmdGUGRUZ05iMjdKYmVUZENLVFBrTEtscXFt\r\n\
    b3NveUN2RitRa25vUS9BZ01CQUFFPQotLS0tLUVORCBSU0EgUFVCTElDIEtFWS0t\r\n\
    LS0tCnNlcnZpY2Uta2V5Ci0tLS0tQkVHSU4gUlNBIFBVQkxJQyBLRVktLS0tLQpN\r\n\
    SUdKQW9HQkFMVjNKSmtWN3lTNU9jc1lHMHNFYzFQOTVRclFRR3ZzbGJ6Wi9zRGxl\r\n\
    RlpKYXFSOUYvYjRUVERNClNGcFMxcU1GbldkZDgxVmRGMEdYRmN2WVpLamRJdHU2\r\n\
    SndBaTRJeEhxeXZtdTRKdUxrcXNaTEFLaXRLVkx4eGsKeERlMjlDNzRWMmJrOTRJ\r\n\
    MEgybTNKS2tzTHVwc3VxWWRVUmhOVXN0SElKZmgyZmNIalF0bEFnTUJBQUU9Ci0t\r\n\
    LS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0KCg==\r\n\
    -----END MESSAGE-----\r\n\
    signature\r\n\
    -----BEGIN SIGNATURE-----\r\n\
    d4OuCE5OLAOnRB6cQN6WyMEmg/BHem144Vec+eYgeWoKwx3MxXFplUjFxgnMlmwN\r\n\
    PcftsZf2ztN0sbNCtPgDL3d0PqvxY3iHTQAI8EbaGq/IAJUZ8U4y963dD5+Bn6JQ\r\n\
    myE3ctmh0vy5+QxSiRjmQBkuEpCyks7LvWvHYrhnmcg=\r\n\
    -----END SIGNATURE-----

.. _DuckDuckGo: https://duckduckgo.com
"""

import base64
import datetime
import hashlib
import logging
import random
import time

from Crypto.Cipher import AES
from Crypto.Util import Counter

from leekspin import crypto
from leekspin import util
from leekspin.const import TOKEN_HS_PROTO_VERSIONS
from leekspin.const import TOKEN_HS_PUBLICATION
from leekspin.const import TOKEN_PERMANENT_KEY
from leekspin.const import TOKEN_REND_SERV
from leekspin.const import TOKEN_SECRET_ID_PART
from leekspin.const import TOR_BEGIN_MSG
from leekspin.const import TOR_END_MSG
from leekspin.torversions import shouldSupportHSIntroV0


[docs]def calculateSecretIDPart(permanentID, currentTime, descriptorCookie=None, replica=0): """Calculate the current ``secret-id-part`` for a Hidden Service. .. note: This is used in :func:`generateRendServiceLine` to calculate the Hidden Service's current ``descriptor-id``. From `rend-spec.txt`_ §1.3:: "secret-id-part" SP secret-id-part NL [Exactly once] The result of the following operation as explained above, formatted as 32 base32 chars. Using this secret id part, everyone can verify that the signed descriptor belongs to "descriptor-id". secret-id-part = H(time-period | descriptor-cookie | replica) .. _rend-spec.txt: https://gitweb.torproject.org/torspec.git/tree/rend-spec.txt """ H = hashlib.sha1 permanentIDbyte = crypto.bytesToLong(permanentID[0]) timePeriod = str((currentTime + permanentIDbyte * 86400 / 256) / 86400) secretID = H(str(timePeriod)) if descriptorCookie: secretID.update(str(descriptorCookie)) secretID.update(str(replica)) secretIDPart = secretID.digest() secretIDB32 = base64.b32encode(secretIDPart).lower() secretIDPartLine = TOKEN_SECRET_ID_PART + secretIDB32 return (secretIDPart, secretIDPartLine)
[docs]def createDescriptorCookie(length=128): """Create an Hidden Service descriptor-cookie. :param int length: The length of the cookie, in bits. :rtype: bytes :returns: A random bytestring of the specified **length**. """ cookie = crypto.longToBytes(random.getrandbits(128)) return cookie
[docs]def generateIntroPoints(descriptorCookie=None, introductionPoints=2): """Generate the ``introduction-points`` message for an HS descriptor. From `rend-spec.txt`_ §1.3:: "introduction-points" NL encrypted-string [At most once] A list of introduction points. If the optional "descriptor-cookie" is used, this list is encrypted with AES in CTR mode with a random initialization vector of 128 bits that is written to the beginning of the encrypted string, and the "descriptor-cookie" as secret key of 128 bits length. The string containing the introduction point data (either encrypted or not) is encoded in base64, and surrounded with "-----BEGIN MESSAGE-----" and "-----END MESSAGE-----". The unencrypted string may begin with: "service-authentication" auth-type auth-data NL [Any number] The service-specific authentication data can be used to perform client authentication. This data is independent of the selected introduction point as opposed to "intro-authentication" below. The format of auth-data (base64-encoded or PEM format) depends on auth-type. See section 2 of this document for details on auth mechanisms. Subsequently, an arbitrary number of introduction point entries may follow, each containing the following data: "introduction-point" SP identifier NL [At start, exactly once] The identifier of this introduction point: the base32 encoded hash of this introduction point's identity key. "ip-address" SP ip4 NL [Exactly once] The IP address of this introduction point. "onion-port" SP port NL [Exactly once] The TCP port on which the introduction point is listening for incoming onion requests. "onion-key" NL a public key in PEM format [Exactly once] The public key that can be used to encrypt messages to this introduction point. "service-key" NL a public key in PEM format [Exactly once] The public key that can be used to encrypt messages to the hidden service. "intro-authentication" auth-type auth-data NL [Any number] The introduction-point-specific authentication data can be used to perform client authentication. This data depends on the selected introduction point as opposed to "service-authentication" above. The format of auth-data (base64-encoded or PEM format) depends on auth-type. See section 2 of this document for details on auth mechanisms. (This ends the fields in the encrypted portion of the descriptor.) [It's ok for Bob to advertise 0 introduction points. He might want to do that if he previously advertised some introduction points, and now he doesn't have any. -RD] .. _rend-spec.txt: https://gitweb.torproject.org/torspec.git/tree/rend-spec.txt """ # Tor stores all generated authorization data for the authorization # protocols described in Sections 2.1 and 2.2 in a new file using the # following file format: # # "client-name" human-readable client identifier NL # "descriptor-cookie" 128-bit key ^= 22 base64 chars NL # # If the authorization protocol of Section 2.2 is used, Tor also generates # and stores the following data: # # "client-key" NL a public key in PEM format # "service-authentication" auth-type auth-data NL # XXX We only currently support authTupe 0 (REND_NO_AUTH). #authType = random.randint(0, 2) authType = 0 if authType == 0: # XXX Do we need authData for REND_NO_AUTH? It seems from tor's source # that the "session-key" for the AES-CTR rounds is ``"(none)"``? authData = 'XXX' elif authType == 1: authData = 'XXX' elif authType == 2: # XXX Need client-key? authData = 'XXX' unencrypted = [] # TODO: This part is *super* wastful. In the future, we might want to # make it save the necessary descriptor keys from the generation of some # relay router descriptors, then reuse those pre-generated routers to pick # intro points and generate their unencrypted data strings. for i in range(introductionPoints): # "introduction-point" SP identifier NL (_, publicSigningKey, _) = crypto.generateSigningKey() (_, fingerprintBinary) = crypto.getFingerprint(publicSigningKey) identifier = base64.b32encode(fingerprintBinary).lower() # "onion-key" NL a public key in PEM format (secretOnionKey, publicOnionKey, publicOnionKeyLine) = crypto.generateOnionKey() # "service-key" NL a public key in PEM format (secretServiceKey, publicServiceKey, publicServiceKeyLine) = crypto.generateOnionKey() introPoint = [] introPoint.append(b"introduction-point %s" % identifier) introPoint.append(b"ip-address %s" % util.randomIPv4()) introPoint.append(b"onion-port %s" % util.randomPort()) introPoint.append(publicOnionKeyLine) introPoint.append(publicServiceKeyLine.replace("onion-key", "service-key")) # XXX Need to implement intro-authentication auth-data: # "intro-authentication" auth-type auth-data NL if authType == 2: introPoint.append(b"intro-authentication %s" % (authType, authData)) IP = "\n".join(introPoint) unencrypted.append(IP) unencrypted = "\n".join(unencrypted) # See §2.1 of rend-spec.txt if authType == 1 or authType == 2 or authType == 0: # When generating a hidden service descriptor, the service encrypts # the introduction-point part with a single randomly generated # symmetric 128-bit session key using AES-CTR as described for v2 # hidden service descriptors in rend-spec. Afterwards, the service # encrypts the session key to all descriptor cookies using # AES. Authorized client should be able to efficiently find the # session key that is encrypted for him/her, so that 4 octet long # client ID are generated consisting of descriptor cookie and # initialization vector. Descriptors always contain a number of # encrypted session keys that is a multiple of 16 by adding fake # entries. Encrypted session keys are ordered by client IDs in order # to conceal addition or removal of authorized clients by the service # provider. sessionKey = createDescriptorCookie() # XXX last two bytes counter = Counter.new(128) ciphre = AES.new(sessionKey, mode=AES.MODE_CTR, IV=b"XXX", counter=counter) encrypted = ciphre.encrypt(unencrypted) encryptedB64 = base64.b64encode(encrypted) encryptedNoHeaders = crypto.chunkInto64CharsPerLine(encryptedB64, separator=b"\r\n") # See §2.2 of rend-spec.txt #elif authType == 2: intros = [] if authType != 0: intros.append(b"service-authentication %s %s" % (authType, authData)) intros.append(b"introduction-points") intros.append(TOR_BEGIN_MSG) intros.append(encryptedNoHeaders) intros.append(TOR_END_MSG) introPoints = "\r\n".join(intros) return introPoints
[docs]def generatePublicationTimeLine(currentTime): """Generate a ``publication-time`` line for a Hidden Service descriptor. From `rend-spec.txt`_ §1.3:: "publication-time" SP YYYY-MM-DD HH:MM:SS NL [Exactly once] A timestamp when this descriptor has been created. It should be rounded down to the nearest hour. .. _rend-spec.txt: https://gitweb.torproject.org/torspec.git/tree/rend-spec.txt :param int currentTime: The current time, in seconds since Epoch, as an integer. :rtype: str :returns: An Hidden Service ``publication-time`` line. """ remainder = currentTime % 3600 # Round down to the nearest hour timestamp = datetime.datetime.fromtimestamp(currentTime - remainder) return TOKEN_HS_PUBLICATION + str(timestamp)
[docs]def generateRendServiceLine(permanentID, secretIDPart, replica=0): """Create the ``rendezvous-service-descriptor`` line for an HS descriptor. From `rend-spec.txt`_ §1.3:: "rendezvous-service-descriptor" SP descriptor-id NL [At start, exactly once] Indicates the beginning of the descriptor. "descriptor-id" is a periodically changing identifier of 160 bits formatted as 32 base32 chars that is calculated by the hidden service and its clients. The "descriptor-id" is calculated by performing the following operation: descriptor-id = H(permanent-id | H(time-period | descriptor-cookie | replica)) "permanent-id" is the permanent identifier of the hidden service, consisting of 80 bits. It can be calculated by computing the hash value of the public hidden service key and truncating after the first 80 bits: permanent-id = H(public-key)[:10] Note: If Bob's OP has "stealth" authorization enabled (see Section 2.2), it uses the client key in place of the public hidden service key. "H(time-period | descriptor-cookie | replica)" is the (possibly secret) id part that is necessary to verify that the hidden service is the true originator of this descriptor and that is therefore contained in the descriptor, too. The descriptor ID can only be created by the hidden service and its clients, but the "signature" below can only be created by the service. "time-period" changes periodically as a function of time and "permanent-id". The current value for "time-period" can be calculated using the following formula: time-period = (current-time + permanent-id-byte * 86400 / 256) / 86400 "current-time" contains the current system time in seconds since 1970-01-01 00:00, e.g. 1188241957. "permanent-id-byte" is the first (unsigned) byte of the permanent identifier (which is in network order), e.g. 143. Adding the product of "permanent-id-byte" and 86400 (seconds per day), divided by 256, prevents "time-period" from changing for all descriptors at the same time of the day. The result of the overall operation is a (network-ordered) 32-bit integer, e.g. 13753 or 0x000035B9 with the example values given above. "descriptor-cookie" is an optional secret password of 128 bits that is shared between the hidden service provider and its clients. If the descriptor-cookie is left out, the input to the hash function is 128 bits shorter. "replica" denotes the number of the replica. A service publishes multiple descriptors with different descriptor IDs in order to distribute them to different places on the ring. .. _rend-spec.txt: https://gitweb.torproject.org/torspec.git/tree/rend-spec.txt :param str permanentID: The permanent identifier of this Hidden Service, i.e. the HS's .onion address (without the ``.onion`` part at the end), as is returned from :func:`generatePermanentID`. :param str secretIDPart: The current ``secret-id-part`` for this **replica** number. See :func:`calculateSecretIDPart`. :rtype: str :returns: An HS ``rendezvous-service-descriptor`` line. """ H = hashlib.sha1 descriptorID = H(permanentID + secretIDPart).digest() encoded = base64.b32encode(descriptorID).lower() line = TOKEN_REND_SERV + (b"%s" % encoded) return line
[docs]def generatePermanentKey(bits=1024): """Generate a Hidden Service's ``permanent-key``. From `rend-spec.txt`_ §1.3:: "permanent-key" NL a public key in PEM format [Exactly once] The public key of the hidden service which is required to verify the "descriptor-id" and the "signature". .. _rend-spec.txt: https://gitweb.torproject.org/torspec.git/tree/rend-spec.txt :param int bits: The length of the RSA key, in bits. :returns: A tuple of strings, ``(key-private, key-public, key-line)``, where ``key-line`` should be appropriate for placement directly into an hidden service descriptor. """ (secretPermanentKey, publicPermanentKey, publicPermanentKeyNoHeaders) = crypto._generateRSAKey(bits) permanentKeyWithHeaders = crypto.addTorPKHeaderAndFooter(publicPermanentKeyNoHeaders) permanentKeyLine = TOKEN_PERMANENT_KEY + permanentKeyWithHeaders return (secretPermanentKey, publicPermanentKey, permanentKeyLine)
[docs]def generatePermanentID(publicPermanentKey): """Generate a Hidden Service's ``permanent-id``. From `rend-spec.txt`_ §1.3:: "permanent-id" is the permanent identifier of the hidden service, consisting of 80 bits. It can be calculated by computing the hash value of the public hidden service key and truncating after the first 80 bits: permanent-id = H(public-key)[:10] Note: If Bob's OP has "stealth" authorization enabled (see Section 2.2), it uses the client key in place of the public hidden service key. .. _rend-spec.txt: https://gitweb.torproject.org/torspec.git/tree/rend-spec.txt """ permanentID = hashlib.sha1(publicPermanentKey).digest()[:10] return permanentID
[docs]def generateProtocolVersionsLine(version): """Generate a ``protocol-versions`` line for an HS descriptor. From `rend-spec.txt`_ §1.3:: "protocol-versions" SP version-string NL [Exactly once] A comma-separated list of recognized and permitted version numbers for use in INTRODUCE cells; these versions are described in section 1.8 below. Version numbers are positive integers. From `rend-spec.txt`_ §1.8:: Alice builds a separate circuit to one of Bob's chosen introduction points, and sends it a RELAY_COMMAND_INTRODUCE1 cell containing: Cleartext PK_ID Identifier for Bob's PK [20 octets] Encrypted to Bob's PK: (in the v0 intro protocol) RP Rendezvous point's nickname [20 octets] RC Rendezvous cookie [20 octets] g^x Diffie-Hellman data, part 1 [128 octets] OR (in the v1 intro protocol) VER Version byte: set to 1. [1 octet] RP Rendezvous point nick or ID [42 octets] RC Rendezvous cookie [20 octets] g^x Diffie-Hellman data, part 1 [128 octets] OR (in the v2 intro protocol) VER Version byte: set to 2. [1 octet] IP Rendezvous point's address [4 octets] PORT Rendezvous point's OR port [2 octets] ID Rendezvous point identity ID [20 octets] KLEN Length of onion key [2 octets] KEY Rendezvous point onion key [KLEN octets] RC Rendezvous cookie [20 octets] g^x Diffie-Hellman data, part 1 [128 octets] OR (in the v3 intro protocol) VER Version byte: set to 3. [1 octet] […] Through Tor 0.2.0.6-alpha, clients only generated the v0 introduction format, whereas hidden services have understood and accepted v0, v1, and v2 since 0.1.1.x. As of Tor 0.2.0.7-alpha and 0.1.2.18, clients switched to using the v2 intro format. .. _rend-spec.txt: https://gitweb.torproject.org/torspec.git/tree/rend-spec.txt :param str version: One of :data:`leekspin.torversions.SERVER_VERSIONS`. :rtype: str :returns: An Hidden Service ``protocol-versions`` string. """ if shouldSupportHSIntroV0(version): versions = '0,1,2' else: versions = '2,3' return TOKEN_HS_PROTO_VERSIONS + versions
[docs]def generateVersionLine(version): """Determine the appropriate version number of an HS descriptor. From `rend-spec.txt`_ §1.3:: "version" SP version-number NL [Exactly once] The version number of this descriptor's format. Version numbers are a positive integer. .. _rend-spec.txt: https://gitweb.torproject.org/torspec.git/tree/rend-spec.txt .. todo:: Which versions are which? What does the ``version`` string in an Hidden Service ``rendezvous-service-descriptor`` mean? Is it the revision number of the descriptor? The supported handshake protocol version? Currently, we just return the string ``"version 2"``, no matter what ``version`` is passed in. :param str version: One of :data:`leekspin.torversions.SERVER_VERSIONS`. :rtype: str :returns: An Hidden Service ``version`` string. """ #return b"version %b" % version return b"version 2"