[tor-commits] [bridgedb/master] Rewrite the descriptor generator.

isis at torproject.org isis at torproject.org
Sun Jan 12 06:06:30 UTC 2014


commit a74fb1ae635c9f79fc872469775273c1e9d1b1e7
Author: Isis Lovecruft <isis at torproject.org>
Date:   Sat Oct 26 08:42:38 2013 +0000

    Rewrite the descriptor generator.
    
     * ADD better descriptor generator, it makes descriptors according to
       dir-spec.txt now. They are even signed with OpenSSL generated RSA keys, and
       have all the embedded document hashes and fingerprints of the correct keys
       used for signing and everything.
---
 scripts/gen_bridge_descriptors |  836 ++++++++++++++++++++++++++++++++++++----
 1 file changed, 755 insertions(+), 81 deletions(-)

diff --git a/scripts/gen_bridge_descriptors b/scripts/gen_bridge_descriptors
index ff0f589..e272600 100644
--- a/scripts/gen_bridge_descriptors
+++ b/scripts/gen_bridge_descriptors
@@ -1,107 +1,781 @@
-#!/usr/bin/env python -tt
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# gen_bridge_descriptors
+# ----------------------
+# XXX describeme
+#
+# :authors: Matthew Finkel
+#           Isis Lovecruft <isis at torproject.org> 0xA3ADB67A2CDB8B35
+# :licence: distributed with BridgeDB, see included LICENSE file
+# :copyright: (c) 2013 Matthew Finkel
+#             (c) 2013 Isis Agora Lovecruft
+#             (c) 2013 The Tor Project, Inc.
+#______________________________________________________________________________
 
+from __future__ import print_function
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import argparse
+import binascii
+import hashlib
+import ipaddr
+import math
 import sys
 import random
+import re
 import time
-import ipaddr
+
 from datetime import datetime
-import binascii
+from codecs   import open as open
+
+try:
+    import OpenSSL.crypto
+except (ImportError, NameError) as error:
+    print("This script requires pyOpenSSL>=0.13.0")
+    raise SystemExit(error.message)
+try:
+    from bridgedb.parse import versions
+except (ImportError, NameError) as error:
+    print(error.message)
+    print("WARNING: Cannot import bridgedb package!",
+          "Generated descriptor content won't accurately reflect descriptor",
+          "information created by different Tor versions.", sep='\n\t')
+try:
+    import nacl
+    import nacl.secret
+except (ImportError, NameError, IOError):
+    nacl = secret = None
+
+
+#: The version of this script
+__version__ = '0.2.0'
+
+#: The <major>.<minor>.<micro>.<rev> version numbers for tor, taken from the
+#: 'server-versions' line of a consensus file
+SERVER_VERSIONS = """0.2.2.39,0.2.3.24-rc,0.2.3.25,
+0.2.4.5-alpha,0.2.4.6-alpha,0.2.4.7-alpha,0.2.4.8-alpha,0.2.4.9-alpha,
+0.2.4.10-alpha,0.2.4.11-alpha,0.2.4.12-alpha,0.2.4.14-alpha,0.2.4.15-rc,
+0.2.4.16-rc,0.2.4.17-rc,0.2.5.1-alpha""".replace('\n', '').split(',')
+
+#: Strings found in PEM-encoded objects created by Tor
+TOR_BEGIN_KEY = "-----BEGIN RSA PUBLIC KEY-----"
+TOR_END_KEY   = "-----END RSA PUBLIC KEY-----"
+TOR_BEGIN_SK  = "-----BEGIN RSA PRIVATE KEY-----"
+TOR_END_SK    = "-----END RSA PRIVATE KEY-----"
+TOR_BEGIN_SIG = "-----BEGIN SIGNATURE-----"
+TOR_END_SIG   = "-----END SIGNATURE-----"
+
+#: Strings found in PEM-encoded objects created by OpenSSL
+OPENSSL_BEGIN_KEY  = "-----BEGIN PRIVATE KEY-----"
+OPENSSL_END_KEY    = "-----END PRIVATE KEY-----"
+OPENSSL_BEGIN_CERT = "-----BEGIN CERTIFICATE-----"
+OPENSSL_END_CERT   = "-----END CERTIFICATE-----"
+
+PEM = OpenSSL.crypto.FILETYPE_PEM
+
 
+class OpenSSLKeyGenError(Exception):
+    """Raised when there is a problem generating a new key."""
 
-def usage():
-    print "syntax: generatedescriptors.py <count>\n"\
-          "    count: number of descriptors to generate\n"
+
+def getArgParser():
+    """Get our :class:`~argparse.ArgumentParser`."""
+    parser = argparse.ArgumentParser(add_help=True)
+    parser.version = __version__
+    parser.description  = "Generate a signed set of network-status, "
+    parser.description += "extra-info, and server descriptor documents "
+    parser.description += "for mock Tor relays or bridges."
+    infoargs = parser.add_mutually_exclusive_group()
+    verbargs = parser.add_mutually_exclusive_group()
+    infoargs.add_argument("-v", "--verbose", action="store_true",
+                          help="print information to stdout")
+    infoargs.add_argument("-q", "--quiet", action="store_true",
+                          help="don't print anything")
+    verbargs.add_argument("--version", action="store_true",
+                          help="print the %s version and exit".format(
+                              parser.prog))
+    group = parser.add_argument_group()
+    group.title = "required arguments"
+    group.add_argument("-n", "--descriptors", default=0,
+                       help="generate <n> descriptor sets", type=int)
+    return parser
 
 def randomIP():
-    return randomIP4()
+    """Create a random IPv4 or IPv6 address."""
+    maybe = int(random.getrandbits(1))
+    ip = randomIPv4() if maybe else randomIPv6()
+    return ip
 
-def randomIP4():
-    return ipaddr.IPAddress(random.getrandbits(32))
+def randomIPv4():
+    """Create a random IPv4 address."""
+    return ipaddr.IPv4Address(random.getrandbits(32))
 
-def randomPort():
-    return random.randint(1,65535)
+def randomIPv6():
+    """Create a random IPv6 address."""
+    return ipaddr.IPv6Address(random.getrandbits(128))
 
-def gettimestamp():
-    return time.strftime("%Y-%m-%d %H:%M:%S")
+def randomPort():
+    """Get a random integer in the range [1024, 65535]."""
+    return random.randint(1025, 65535)
 
 def getHexString(size):
     s = ""
     for i in xrange(size):
-        s+= random.choice("ABCDEF0123456789")
+        s += random.choice("ABCDEF0123456789")
     return s
 
-def generateDesc():
-
-    baseDesc = "router Unnamed %s %s 0 %s\n"\
-               "opt fingerprint %s\n"\
-               "opt @purpose bridge\n"\
-               "opt published %s\n"\
-               "router-signature\n"
-    fp = "DEAD BEEF F00F DEAD BEEF F00F " + \
-         getHexString(4) + " " + getHexString(4) + " " + \
-         getHexString(4) + " " + getHexString(4)
-    ip = randomIP()
-    orport = randomPort()
-    dirport = randomPort()
-    ID = binascii.a2b_hex(fp.replace(" ", ""))
-    df =  baseDesc % (ip, orport, dirport, fp, gettimestamp())
-    return (df, (ID, ip, orport, dirport))
-
-def generateStatus(info, ID=None, ip=None, orport=None, dirport=None):
-    baseStatus = "r %s %s %s %s %s %d %d\n"\
-                 "s Running Stable\n"
-
-    if info and len(info) == 4:
-        ID = info[0]
-        ip = info[1]
-        orport = info[2]
-        dirport = info[3]
-    return "".join(baseStatus % ("namedontmattah", binascii.b2a_base64(ID)[:-2],
-           "randomstring", gettimestamp(), ip,
-            orport, dirport))
-
-def generateExtraInfo(fp, ip=None):
-    baseExtraInfo = "extra-info %s %s\n"\
-                    "transport %s %s:%d\n"\
-                    "router-signature\n"
-    if not ip:
-        ip = randomIP()
-    return "".join(baseExtraInfo % ("namedontmattah", fp,
-                                    random.choice(["obfs2", "obfs3", "obfs2"]),
-                                    ip, randomPort()))
-if __name__ == "__main__":
-    if len(sys.argv) != 2:
-        usage()
-        sys.exit(0)
+def makeTimeStamp(now=None, fmt=None, variation=False, period=None):
+    """Get a random timestamp suitable for a bridge server descriptor.
+
+    :param int now: The time, in seconds since the Epoch, to generate the
+        timestamp for (and to consider as the maximum time, if other options
+        are enabled).
+    :param string fmt: A strftime(3) format string for the timestamp. If not
+        given, defaults to ISO-8601 format without the 'T' separator.
+    :param bool variation: If True, enable timestamp variation. Otherwise,
+        make all timestamps be set to the current time.
+    :type period: int or None
+    :param period: If given, vary the generated timestamps to be a random time
+        between ``period`` hours ago and the current time. If None, generate
+        completely random timestamps which are anywhere between the Unix Epoch
+        and the current time. This parameter only has an effect if
+        ``variation`` is enabled.
+    """
+    now = int(now) if now is not None else int(time.time())
+    fmt = fmt if fmt else "%Y-%m-%d %H:%M:%S"
+
+    if variation:
+        then = 1
+        if period is not None:
+            secs = int(period) * 3600
+            then = now - secs
+        # Get a random number between one epochseconds number and another
+        diff = random.randint(then, now)
+        # Then rewind the clock
+        now = diff
+
+    return time.strftime(fmt, time.localtime(now))
+
+def shouldHaveOptPrefix(version):
+    """Returns true if a tor ``version`` should have the 'opt ' prefix.
+
+    In tor, up to and including, version 0.2.3.25, server-descriptors (bridge
+    or regular) prefixed several lines with 'opt '. For the 0.2.3.x series,
+    these lines were:
+        - 'protocols'
+        - 'fingerprint'
+        - 'hidden-service-dir'
+        - 'extra-info-digest'
+
+    :param string version: One of ``SERVER_VERSIONS``.
+    :rtype: bool
+    :returns: True if we should include the 'opt ' prefix.
+    """
+    changed_in  = versions.Version('0.2.4.1-alpha', package='tor')
+    our_version = versions.Version(version, package='tor')
+    if our_version < changed_in:
+        return True
+    return False
+
+def makeProtocolsLine(version=None):
+    """Generate an appropriate [bridge-]server-descriptor 'protocols' line.
+
+    :param string version: One of ``SERVER_VERSIONS``.
+    :rtype: string
+    :returns: An '@type [bridge-]server-descriptor' 'protocols' line.
+    """
+    line = ''
+    if (version is not None) and shouldHaveOptPrefix(version):
+        line += 'opt '
+    line += 'protocols Link 1 2 Circuit 1'
+    return line
+
+def convertToSpaceyFingerprint(fingerprint):
+    """Convert a colon-delimited fingerprint to be space-delimited.
+
+    Given a fingerprint with sets of two hexidecimal characters separated by
+    colons, for example a certificate or key fingerprint output from OpenSSL:
+      |
+      | 51:58:9E:8B:BF:5C:F6:5A:68:CB:6D:F4:C7:6F:98:C6:B2:02:69:45
+      |
+
+    convert it to the following format:
+      |
+      | 5158 9E8B BF5C F65A 68CB 6DF4 C76F 98C6 B202 6945
+      |
+
+    :param string fingerprint: A 2-character colon-delimited hex fingerprint.
+    :rtype: string
+    :returns: A 4-character space-delimited fingerprint.
+    """
+    # The boolean of the non-equality is necessary because -1 is somehow
+    # truthy in Python…
+    check = lambda f: bool(f.find(':') != -1)
+
+    while True:
+        if check(fingerprint):
+            fingerprint = ''.join(fingerprint.split(':', 1))
+            if check(fingerprint):
+                fingerprint = ' '.join(fingerprint.split(':', 1))
+                continue
+            break
+        break
+
+    return fingerprint
+
+def makeFingerprintLine(fingerprint, version=None):
+    """Generate an appropriate [bridge-]server-descriptor 'fingerprint' line.
+
+    For example, for tor-0.2.3.25 and prior versions, this would look like:
+      |
+      | opt fingerprint D4BB C339 2560 1B7F 226E 133B A85F 72AF E734 0B29
+      |
+
+    :param string version: One of ``SERVER_VERSIONS``.
+    :param string timestamp: The timestamp, in seconds since Epoch, to record
+        in the 'published' line.
+    :rtype: string
+    :returns: An '@type [bridge-]server-descriptor' 'published' line.
+    """
+    line = ''
+    if (version is not None) and shouldHaveOptPrefix(version):
+        line += 'opt '
+    line += 'fingerprint %s' % convertToSpaceyFingerprint(fingerprint)
+    return line
+
+def makeBandwidthLine(variance=30):
+    """Create a random 'bandwidth' line with some plausible burst variance.
+
+    From torspec.git/dir-spec.txt, §2.1 "Router descriptors":
+      | "bandwidth" bandwidth-avg bandwidth-burst bandwidth-observed NL
+      |
+      | [Exactly once]
+      |
+      |   Estimated bandwidth for this router, in bytes per second.  The
+      |   "average" bandwidth is the volume per second that the OR is willing
+      |   to sustain over long periods; the "burst" bandwidth is the volume
+      |   that the OR is willing to sustain in very short intervals.  The
+      |   "observed" value is an estimate of the capacity this relay can
+      |   handle.  The relay remembers the max bandwidth sustained output over
+      |   any ten second period in the past day, and another sustained input.
+      |   The "observed" value is the lesser of these two numbers.
+
+    The "observed" bandwidth, in this function, is taken as some random value,
+    bounded between 20KB/s and 2MB/s. For example, say:
+
+    >>> import math
+    >>> variance = 25
+    >>> observed = 180376
+    >>> percentage = float(variance) / 100.
+    >>> percentage
+    0.25
+
+    The ``variance`` in this context is the percentage of the "observed"
+    bandwidth, which will be added to the "observed" bandwidth, and becomes
+    the value for the "burst" bandwidth:
+
+    >>> burst = observed + math.ceil(observed * percentage)
+    >>> assert burst > observed
+
+    This doesn't do much, since the "burst" bandwidth in a real
+    [bridge-]server-descriptor is reported by the OR; this function mostly
+    serves to avoid generating completely-crazy, totally-implausible bandwidth
+    values. The "average" bandwidth value is then just the mean value of the
+    other two.
+
+    :param integer variance: The percent of the fake "observed" bandwidth to
+        increase the "burst" bandwidth by.
+    :rtype: string
+    :returns: A "bandwidth" line for a [bridge-]server-descriptor.
+    """
+    observed = random.randint(20 * 2**10, 2 * 2**30)
+    percentage = float(variance) / 100.
+    burst = int(observed + math.ceil(observed * percentage))
+    bandwidths = [burst, observed]
+    nitems = len(bandwidths) if (len(bandwidths) > 0) else float('nan')
+    avg = int(math.ceil(float(sum(bandwidths)) / nitems))
+    line = "bandwidth %s %s %s" % (avg, burst, observed)
+    return line
+
+def makeExtraInfoDigestLine(hexdigest, version):
+    """Create a line to embed the hex SHA-1 digest of the extrainfo.
+
+    :param string hexdigest: Should be the hex-encoded (uppercase) output of
+        the SHA-1 digest of the generated extrainfo document (this is the
+        extra-info descriptor, just without the signature at the end). This is
+        the same exact digest which gets signed by the OR server identity key,
+        and that signature is appended to the extrainfo document to create the
+        extra-info descriptor.
+    :param string version: One of ``SERVER_VERSIONS``.
+    :rtype: string
+    :returns: An ``@type [bridge-]server-descriptor`` 'extra-info-digest'
+        line.
+    """
+    line = ''
+    if (version is not None) and shouldHaveOptPrefix(version):
+        line += 'opt '
+    line += 'extra-info-digest %s' % hexdigest
+    return line
+
+def makeHSDirLine(version):
+    """This line doesn't do much… all the cool kids are HSDirs these days.
+
+    :param string version: One of ``SERVER_VERSIONS``.
+    :rtype: string
+    :returns: An ``@type [bridge-]server-descriptor`` 'hidden-service-dir'
+        line.
+    """
+    line = ''
+    if (version is not None) and shouldHaveOptPrefix(version):
+        line += 'opt '
+    line += 'hidden-service-dir'
+    return line
+
+def createRSAKey(bits=1024):
+    """Create a new RSA keypair.
+
+    :param integer bits: The bitlength of the keypair to generate.
+    :rtype: :class:`OpenSSL.crypto.PKey`
+    :returns: An RSA keypair of bitlength ``bits``.
+    """
+    key = OpenSSL.crypto.PKey()
+    key.generate_key(OpenSSL.crypto.TYPE_RSA, bits)
+    if not key.check():
+        raise OpenSSLKeyGenError("Couldn't create new RSA 1024-bit key")
+    return key
+
+def createNTORKey():
+    """Create a Curve25519 key."""
+    if nacl is None:
+        raise NotImplemented
+
+def createKey(selfsign=True, digest='sha1'):
+    """Create a set of public and private RSA keypairs and corresponding certs.
+
+    :param boolean selfsign: If True, use the private key to sign the public
+        certificate (otherwise, the private key will only sign the private
+        certificate to which it is attached).
+    :param string digest: The digest to use. (default: 'sha1')
+    :rtype: 4-tuple
+    :returns: (private_key, private_cert, public_key, public_cert)
+    """
+    privateKey  = createRSAKey()
+    privateCert = attachKey(privateKey, createTLSCert())
+    publicKey   = privateCert.get_pubkey()
+    publicCert  = attachKey(publicKey, createTLSCert(), selfsign=False)
+
+    if selfsign:
+        # We already signed the publicCert with the publicKey, now we need to
+        # sign the publicCert with the privateKey
+        publicCert.sign(privateKey, digest)
+
+    return (privateKey, privateCert, publicKey, publicCert)
 
-    df = ''
-    sf = ''
-    ei = ''
-    count = int(sys.argv[1])
-    for i in xrange(count):
-        desc, info = generateDesc()
-        df += desc
+def attachKey(key, cert, selfsign=True, digest='sha1', pem=False):
+    """Attach a key to a cert and optionally self-sign the cert.
 
-        sf += generateStatus(info)
-        ei += generateExtraInfo(binascii.b2a_hex(info[0]))
+    :type key: :class:`OpenSSL.crypto.PKey`
+    :param key: A previously generated key, used to generate the other half of
+        the keypair.
+    :type cert: :class:`OpenSSL.crypto.X509`
+    :param cert: A TLS certificate without a public key attached to it, such
+       as one created with :func:`createTLSCert`.
+    :param boolean selfsign: If True, use the ``key`` to self-sign the
+        ``cert``. Note that this will result in several nasty OpenSSL errors
+        if you attempt to export the public key of a cert in order to create
+        another cert which *only* holds the public key. (Otherwise, if you
+        used the first cert in the following example, it contains both halves
+        of the RSA keypair.) Do this instead:
 
+    >>> secret_key = createRSAKey()
+    >>> secret_cert = attachKey(secret_key, createTLSCert(selfsign=True))
+    >>> public_key = secret_cert.get_pubkey()
+    >>> public_cert = attachKey(public_key, createTLSCert, selfsign=False)
+
+    :param string digest: The digest to use. Check your OpenSSL installation
+        to see which are supported. We pretty much only care about 'sha1' and
+        'sha256' here.
+    :param boolean pem: If True, return a 3-tuple of PEM-encoded strings, one
+        for each of (certificate, private_key, public_key), where
+        'certificate' is the original ``cert`` with the ``key`` attached,
+        'private_key' is the private RSA modulus, primes, and exponents
+        exported from the 'certificate', and 'public_key' is the public RSA
+        modulus exported from the cert. NOTE: Using this when passing in a key
+        with only the public RSA modulus (as described above) will result in
+        nasty OpenSSL errors. Trust me, you do *not* want to try to parse
+        OpenSSL's errors.
+    :raises: An infinite, labyrinthine mire of non-Euclidean OpenSSL errors
+        with non-deterministic messages and self-referential errorcodes,
+        tangled upon itself in contempt of sanity, hope, and decent software
+        engineering practices.
+    :returns: If ``pem`` is True, then the values described there are
+        returned. Otherwise, returns the ``cert`` with the ``key`` attached to
+        it.
+    """
+    # Attach the key to the certificate
+    cert.set_pubkey(key)
+
+    if selfsign:
+        # Self-sign the cert with the key, using the specified hash digest
+        cert.sign(key, digest)
+
+    if pem:
+        certificate = OpenSSL.crypto.dump_certificate(PEM, cert)
+        private_key = OpenSSL.crypto.dump_privatekey(PEM, key)
+        public_key = OpenSSL.crypto.dump_privatekey(PEM, cert.get_pubkey())
+        return certificate, private_key, public_key
+    return cert
+
+def createTLSCert(lifetime=None):
+    """Create a TLS certificate.
+
+    :param integer lifetime: The time, in seconds, that the certificate should
+        remain valid for.
+    :rtype: :class:`OpenSSL.crypto.X509`
+    :returns: A certificate, unsigned, and without a key attached to it.
+    """
+    if not lifetime:
+        # see `router_initialize_tls_context()` in src/or/router.c
+        lifetime = 5 + random.randint(0, 361)
+        lifetime = lifetime * 24 * 3600
+        if int(random.getrandbits(1)):
+            lifetime -= 1
+
+    cert = OpenSSL.crypto.X509()
+    cert.gmtime_adj_notBefore(0)        # Not valid before now
+    cert.gmtime_adj_notAfter(lifetime)
+    return cert
+
+def createTLSLinkCert(lifetime=7200):
+    """Create a certificate for the TLS link layer.
+
+    The TLS certificate used for the link layer between Tor relays, and
+    between clients and their bridges/guards, has a shorter lifetime than the
+    other certificates. Currently, these certs expire after two hours.
+
+    :param integer lifetime: The time, in seconds, that the certificate should
+        remain valid for.
+    :rtype: :class:`OpenSSL.crypto.X509`
+    :returns: A certificate, unsigned, and without a key attached to it.
+    """
+    cert = createTLSCert(lifetime)
+    cert.get_subject().CN = 'www.' + getHexString(16) + '.net'
+    cert.get_issuer().CN = 'www.' + getHexString(10) + '.com'
+    return cert
+
+def getPEMPublicKey(cert):
+    publicKey = OpenSSL.crypto.dump_privatekey(PEM, cert.get_pubkey())
+    # It says "PRIVATE KEY" just because the stupid pyOpenSSL wrapper is
+    # braindamaged. You can check that it doesn't include the RSA private
+    # exponents and primes by substituting ``OpenSSL.crypto.FILETYPE_TEXT``
+    # for the above ``PEM``.
+    publicKey = re.sub(OPENSSL_BEGIN_KEY, TOR_BEGIN_KEY, publicKey)
+    publicKey = re.sub(OPENSSL_END_KEY, TOR_END_KEY, publicKey)
+    return publicKey
+
+def getPEMPrivateKey(key):
+    privateKey = OpenSSL.crypto.dump_privatekey(PEM, key)
+    privateKey = re.sub(OPENSSL_BEGIN_KEY, TOR_BEGIN_SK, privateKey)
+    privateKey = re.sub(OPENSSL_END_KEY, TOR_END_SK, privateKey)
+    return privateKey
+
+def makeOnionKeys(bridge=True, digest='sha1'):
+    """Make all the keys and certificates necessary to fake an OR.
+
+    :param boolean bridge: If False, generate a server OR ID key, a signing
+        key, and a TLS certificate/key pair. If True, generate a client ID key
+        as well.
+    :param string digest: The digest to use. (default: 'sha1')
+    :returns: The server ID key, and a tuple of strings (fingerprint,
+       onion-key, signing-key), where onion-key and secret key are the strings
+       which should directly go into a server-descriptor. There are a *ton* of
+       keys and certs in the this function. If you need more for some reason,
+       this is definitely the thing you want to modify.
+    """
+    serverID = createKey(True)
+    SIDSKey, SIDSCert, SIDPKey, SIDPCert = serverID
+    serverLinkCert = createTLSLinkCert()
+    serverLinkCert.sign(SIDSKey, digest)
+
+    if bridge:
+        # For a bridge, a "client" ID key is used to generate the fingerprint
+        clientID = createKey(True)
+        CIDSKey, CIDSCert, CIDPKey, CIDPCert = clientID
+
+        # XXX I think we're missing some of the signatures
+        #     see torspec.git/tor-spec.txt §4.2 on CERTS cells
+        clientLinkCert = createTLSLinkCert()
+        clientLinkCert.sign(CIDSKey, digest)
+    else:
+        CIDSKey, CIDSCert, CIDPKey, CIDPCert = serverID
+
+    signing = createKey()
+    signSKey, signSCert, signPKey, signPCert = signing
+    onion = createKey()
+    onionSKey, onionSCert, onionPKey, onionPCert = onion
+
+    # This is the fingerprint of the server ID key, if we aren't a bridge. If
+    # we are a bridge, then this is the real fingerprint, which goes into our
+    # descriptor (but not the one that other ORs see when they connect to us)
+    fingerprint = CIDPCert.digest(digest)
+
+    onionKeyString   = 'onion-key\n%s' % getPEMPublicKey(onionPCert)
+    signingKeyString = 'signing-key\n%s' % getPEMPublicKey(signPCert)
+
+    # XXX we don't need anything else… right?
+    return SIDSKey, (fingerprint, onionKeyString, signingKeyString)
+
+def generateExtraInfo(fingerprint, ts, ipv4, port):
+    """Create an OR extra-info document.
+
+    See §2.2 "Extra-info documents" in torspec.git/dir-spec.txt.
+
+    :param string fingerprint: A space-separated, hex-encoded, SHA-1 digest of
+        the OR's private identity key. See :func:`convertToSpaceyFingerprint`.
+    :param string ts: An ISO-8601 timestamp. See :func:`makeTimeStamp`.
+    :param string ipv4: An IPv4 address.
+    :param string port: The OR's ORPort.
+    :rtype: string
+    :returns: An extra-info document (unsigned).
+    """
+    extra = []
+    extra.append("extra-info Unnamed %s" % fingerprint)
+    extra.append("published %s" % ts)
+    extra.append("write-history %s (900 s)\n3188736,2226176,2866176" % ts)
+    extra.append("read-history %s (900 s)\n3891200,2483200,2698240" % ts)
+    extra.append("dirreq-write-history %s (900 s)\n1024,0,2048" % ts)
+    extra.append("dirreq-read-history %s (900 s)\n0,0,0" % ts)
+    extra.append("geoip-db-digest %s\ngeoip6-db-digest %s"
+                 % (getHexString(40), getHexString(40)))
+    extra.append("dirreq-stats-end %s (86400 s)\ndirreq-v3-ips" % ts)
+    extra.append("dirreq-v3-reqs\ndirreq-v3-resp")
+    extra.append(
+        "ok=16,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=0,busy=0")
+    extra.append("dirreq-v3-direct-dl complete=0,timeout=0,running=0")
+    extra.append("dirreq-v3-tunneled-dl complete=12,timeout=0,running=0")
+    extra.append("transport obfs3 %s:%d" % (ipv4, port + 1))
+    extra.append("transport obfs2 %s:%d" % (ipv4, port + 2))
+    extra.append("bridge-stats-end %s (86400 s)\nbridge-ips ca=8" % ts)
+    extra.append("bridge-ip-versions v4=8,v6=0\nbridge-ip-transports <OR>=8")
+    extra.append("router-signature")
+
+    return '\n'.join(extra)
+
+def generateNetstatus(idkey_digest, server_desc_digest, timestamp,
+                      ipv4, orport, ipv6=None, dirport=None,
+                      flags='Fast Guard Running Stable Valid',
+                      bandwidth_line=None):
+
+    idkey_b64  = binascii.b2a_base64(idkey_digest)
+    idb64      = str(idkey_b64).strip().rstrip('=========')
+    server_b64 = binascii.b2a_base64(server_desc_digest)
+    srvb64     = str(server_b64).strip()
+
+    if bandwidth_line is not None:
+        bw = int(bandwidth_line.split()[-1]) / 1024  # The 'observed' value
+    dirport = dirport if dirport else 0
+
+    status = []
+    status.append("r Unnamed %s %s %s %s %s %d" % (idb64, srvb64, timestamp,
+                                                   ipv4, orport, dirport))
+    if ipv6 is not None:
+        status.append("a [%s]:%s" % (ipv6, orport))
+    status.append("s %s\nw Bandwidth=%s\np reject 1-65535\n" % (flags, bw))
+
+    return '\n'.join(status)
+
+def signDescriptorDigest(key, descriptorDigest, digest='sha1'):
+    """Ugh...I hate OpenSSL.
+
+    The extra-info-digest is a SHA-1 hash digest of the extrainfo document,
+    that is, the entire extrainfo descriptor up until the end of the
+    'router-signature' line and including the newline, but not the actual
+    signature.
+
+    The signature at the end of the extra-info descriptor is a signature of
+    the above extra-info-digest. This signature is appended to the end of the
+    extrainfo document, and the extra-info-digest is added to the
+    'extra-info-digest' line of the [bridge-]server-descriptor.
+
+    The first one of these was created with a raw digest, the second with a
+    hexdigest. They both encode the the 'sha1' digest type if you check the
+    `-asnparse` output (instead of `-raw -hexdump`).
+
+    ∃!isisⒶwintermute:(feature/9865 *$<>)~/code/torproject/bridgedb/scripts ∴ openssl rsautl -inkey eiprivkey -verify -in eisig1 -raw -hexdump
+    0000 - 00 01 ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
+    0010 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
+    0020 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
+    0030 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
+    0040 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
+    0050 - ff ff ff ff ff ff ff ff-ff ff ff ff 00 30 21 30   .............0!0
+    0060 - 09 06 05 2b 0e 03 02 1a-05 00 04 14 42 25 41 fb   ...+........B%A.
+    0070 - 82 ef 11 f4 5f 2c 95 53-67 2d bb fe 7f c2 34 7f   ...._,.Sg-....4.
+    ∃!isisⒶwintermute:(feature/9865 *$<>)~/code/torproject/bridgedb/scripts ∴ openssl rsautl -inkey eiprivkey -verify -in eisig2 -raw -hexdump
+    0000 - 00 01 ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
+    0010 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
+    0020 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
+    0030 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
+    0040 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
+    0050 - ff ff ff ff ff ff ff ff-ff ff ff ff 00 30 21 30   .............0!0
+    0060 - 09 06 05 2b 0e 03 02 1a-05 00 04 14 44 30 ab 90   ...+........D0..
+    0070 - 93 d1 08 21 df 87 c2 39-2a 04 1c a5 bb 34 44 cd   ...!...9*....4D.
+
+    see http://www.emc.com/collateral/white-papers/h11300-pkcs-1v2-2-rsa-cryptography-standard-wp.pdf
+    for why this function is totally wrong.
+    """
+    sig = binascii.b2a_base64(OpenSSL.crypto.sign(key, descriptorDigest,
+                                                  digest))
+    sigCopy = sig
+    originalLength = len(sigCopy.replace('\n', ''))
+
+    # Only put 64 bytes of the base64 signature per line:
+    sigSplit = []
+    while len(sig) > 0:
+        sigSplit.append(sig[:64])
+        sig = sig[64:]
+    sigFormatted = '\n'.join(sigSplit)
+
+    sigFormattedCopy = sigFormatted
+    formattedLength = len(sigFormattedCopy.replace('\n', ''))
+
+    if originalLength != formattedLength:
+        print("WARNING: signDescriptorDocument(): %s"
+              % "possible bad reformatting for signature.")
+        print("DEBUG: signDescriptorDocument(): original=%d formatted=%d"
+              % (originalLength, formattedLength))
+        print("DEBUG: original:\n%s\nformatted:\n%s"
+              % (sigCopy, sigFormatted))
+
+    sigWithHeaders = TOR_BEGIN_SIG + '\n' \
+                     + sigFormatted \
+                     + TOR_END_SIG + '\n'
+    return sigWithHeaders
+
+def generateDescriptors():
+    """Create keys, certs, signatures, documents and descriptors for an OR.
+
+    :returns:
+        A 3-tuple of strings:
+          - a ``@type [bridge-]extra-info`` descriptor,
+          - a ``@type [bridge-]server-descriptor``, and
+          - a ``@type network-status`` document
+       for a mock Tor relay/bridge.
+    """
+    ipv4 = randomIPv4()
+    ipv6 = randomIPv6()
+    port = randomPort()
+
+    vers = random.choice(SERVER_VERSIONS)
+    uptime = int(random.randint(1800, 63072000))
+    bandwidth = makeBandwidthLine()
+    timestamp = makeTimeStamp(variation=True, period=36)
+    protocols = makeProtocolsLine(vers)
+
+    idkey, (fingerprint, onionkey, signingkey) = makeOnionKeys()
+    idkey_private = getPEMPrivateKey(idkey)
+    idkey_digest = hashlib.sha1(idkey_private).digest()
+
+    fpr = convertToSpaceyFingerprint(fingerprint)
+
+    extrainfo_document = generateExtraInfo(fpr, timestamp, ipv4, port)
+    extrainfo_digest = hashlib.sha1(extrainfo_document).digest()
+    extrainfo_hexdigest = hashlib.sha1(extrainfo_document).hexdigest().upper()
+    extrainfo_sig = signDescriptorDigest(idkey, extrainfo_digest)
+    extrainfo_desc = extrainfo_document + extrainfo_sig
+
+    server = []
+    server.append("@purpose bridge")
+    server.append("router Unnamed %s %s 0 0" % (ipv4, port))
+    server.append("or-address [%s]:%s" % (ipv6, port))
+    server.append("platform Tor %s on Linux" % vers)
+    server.append("%s\npublished %s" % (protocols, timestamp))
+    server.append("%s" % makeFingerprintLine(fingerprint, vers))
+    server.append("uptime %s\n%s" % (uptime, bandwidth))
+    server.append("%s" % makeExtraInfoDigestLine(extrainfo_hexdigest, vers))
+    server.append("%s%s%s" % (onionkey, signingkey, makeHSDirLine(vers)))
+    server.append("contact Somebody <somebody at example.com>")
+    if nacl is not None:
+        server.append("ntor-onion-key %s"
+                      % binascii.b2a_base64(createNTORKey()))
+    server.append("reject *:*\nrouter-signature\n")
+    server_desc = '\n'.join(server)
+
+    server_desc_digest = hashlib.sha1(server_desc).digest()
+    netstatus_desc = generateNetstatus(idkey_digest, server_desc_digest,
+                                       timestamp, ipv4, port, ipv6=ipv6,
+                                       bandwidth_line=bandwidth)
+    server_desc += signDescriptorDigest(idkey, server_desc_digest)
+    return extrainfo_desc, server_desc, netstatus_desc
+
+def writeDescToFile(filename, descriptors):
+    """Open ``filename`` and write a string containing descriptors into it.
+
+    :param string filename: The name of the file to write to.
+    :param string descriptors: A giant string containing descriptors,
+        newlines, formatting, whatever is necessary to make it look like a
+        file tor would generate.
+    """
+    encoding = sys.getfilesystemencoding()
+    descript = descriptors.encode(encoding, 'replace')
     try:
-        f = open("networkstatus-bridges", 'w')
-        f.write(sf)
-        f.close()
-    except:
-        print "Failed to open or write to status file"
+        with open(filename, 'wb', encoding=encoding, errors='replace') as fh:
+            fh.write(descript)
+            fh.flush()
+    except (IOError, OSError) as err:
+        print("Failure while attempting to write descriptors to file '%s': %s"
+              % (filename, err.message))
+
+def create(count):
+    """Generate all types of descriptors and write them to files.
+
+    :param integer count: How many sets of descriptors to generate, i.e. how
+        many mock bridges/relays to create.
+    """
+    if nacl is None:
+        print("WARNING: Can't import PyNaCl. NTOR key generation is disabled.")
+    print("Generating %d bridge descriptors..." % int(count))
 
+    server_descriptors    = list()
+    netstatus_consensus   = list()
+    extrainfo_descriptors = list()
     try:
-        f = open("bridge-descriptors", 'w')
-        f.write(df)
-        f.close()
-    except:
-        print "Failed to open or write to descriptor file"
+        for i in xrange(int(count)):
+            extrainfo, server, netstatus = generateDescriptors()
+            server_descriptors.append(server)
+            netstatus_consensus.append(netstatus)
+            extrainfo_descriptors.append(extrainfo)
+    except KeyboardInterrupt as keyint:
+        print("Received keyboard interrupt.")
+        print("Stopping descriptor creation and exiting.")
+        code = 1515
+    finally:
+        print("Writing descriptors to files...", end="")
+        descriptor_files = {
+            "networkstatus-bridges": ''.join(netstatus_consensus),
+            "bridge-descriptors": ''.join(server_descriptors),
+            "extra-infos": ''.join(extrainfo_descriptors)}
+        for fn, giantstring in descriptor_files.items():
+            writeDescToFile(fn, giantstring)
+        print("Done.")
+        code = 0
+        sys.exit(code)
 
+if __name__ == "__main__":
     try:
-        f = open("extra-infos", 'w')
-        f.write(ei)
-        f.close()
-    except:
-        print "Failed to open or write to extra-info file"
+        parser = getArgParser()
+        options = parser.parse_args()
+
+        if options.quiet:
+            print = lambda x: True
+        if options.version:
+            print("gen_bridge_descriptors-%s" % __version__)
+            sys.exit(0)
+        if options.descriptors and (options.descriptors > 0):
+            create(options.descriptors)
+        else:
+            sys.exit(parser.format_help())
+
+    except Exception as error:
+        sys.exit(error)





More information about the tor-commits mailing list