# -*- coding: utf-8 -*-
"""General cryptographic utilities."""
from __future__ import print_function
from __future__ import absolute_import
import base64
import binascii
import codecs
import hashlib
import struct
import sys
from Crypto.PublicKey import RSA
from Crypto.Util import asn1
import OpenSSL.crypto
from leekspin import const
from leekspin import rsa
from leekspin import tls
from leekspin.const import TOR_BEGIN_KEY
from leekspin.const import TOR_END_KEY
from leekspin.const import TOR_BEGIN_SIG
from leekspin.const import TOR_END_SIG
from leekspin.const import TOKEN_ONION_KEY
from leekspin.const import TOKEN_ROUTER_SIGNATURE
from leekspin.const import TOKEN_SIGNING_KEY
[docs]class InvalidFingerprint(ValueError):
"""Raised when a key fingerprint is invalid."""
[docs]def addPKCS1Padding(message):
"""Add PKCS#1 padding to **message**.
.. todo:: What version of PKCS#1? PKCS#1 v1.0? See
https://bugs.torproject.org/13042.
.. The double-backslashes in the following bytestrings are so that Sphinx
renders them properly. Each double-backslash is should actually only
be a single backslash in the code.
Each block is 128 bytes total in size:
* 2 bytes for the type info ('\\x00\\x01')
* 1 byte for the separator ('\\x00')
* variable length padding ('\\xFF')
* variable length for the **message**
:param str message: The message will be encoded as bytes before adding
PKCS#1 padding.
:rtype: bytes
:returns: The PKCS#1 padded message.
"""
#if sys.version_info.major == 3:
# if isinstance(message, unicode):
# message = codecs.latin_1_encode(message, 'replace')[0]
#else:
# if isinstance(message, str):
# message = codecs.latin_1_encode(message, 'replace')[0]
padding = b''
typeinfo = b'\x00\x01'
separator = b'\x00'
for x in range(125 - len(message)):
padding += b'\xFF'
PKCS1paddedMessage = typeinfo + padding + separator + message
assert len(PKCS1paddedMessage) == 128
return PKCS1paddedMessage
[docs]def bytesToLong(bites):
"""Convert a byte string to a long integer.
This function was stolen from BridgeDB, commit 5ed5c42e_.
.. The double-backslashes in the following doctest are so that Sphinx
renders the bytestrings properly. Each double-backslash should only
be a single backslash if you actually wish to run this doctest.
>>> from bridgedb.crypto import bytesToLong
>>> bytesToLong('\\x059')
1337L
>>> bytesToLong('I\\x96\\x02\\xd2')
1234567890L
>>> bytesToLong('\\x00\\x00\\x00\\x00I\\x96\\x02\\xd2')
1234567890L
>>> bytesToLong('\\xabT\\xa9\\x8c\\xeb\\x1f\\n\\xd2')
12345678901234567890L
.. _5ed5c42e:
https://github.com/isislovecruft/bridgedb/commit/5ed5c42ec7ef908991a67cf0cddf714f74e35f7e
:param bytes bites: The byte string to convert.
:rtype: long
"""
length = len(bites)
if length % 4:
extra = (4 - length % 4)
bites = b'\000' * extra + bites
length = length + extra
acc = 0L
for index in range(0, length, 4):
acc = (acc << 32) + struct.unpack(b'>I', bites[index:index+4])[0]
return acc
[docs]def longToBytes(number, blocksize=0):
"""Convert a long integer to a byte string.
This function was stolen from BridgeDB, commit 5ed5c42e_.
.. The double-backslashes in the following doctest are so that Sphinx
renders the byte strings properly. Each double-backslash should only
be a single backslash if you actually wish to run this doctest.
>>> from bridgedb.crypto import longToBytes
>>> longToBytes(1337L)
'\\x059'
>>> longToBytes(1234567890L)
'I\\x96\\x02\\xd2'
>>> longToBytes(1234567890L, blocksize=8)
'\\x00\\x00\\x00\\x00I\\x96\\x02\\xd2'
>>> longToBytes(12345678901234567890L)
'\\xabT\\xa9\\x8c\\xeb\\x1f\\n\\xd2'
.. _5ed5c42e:
https://github.com/isislovecruft/bridgedb/commit/5ed5c42ec7ef908991a67cf0cddf714f74e35f7e
:param int number: The long integer to convert.
:param int blocksize: If **blocksize** is given and greater than zero, pad
the front of the byte string with binary zeros so that the length is a
multiple of **blocksize**.
:rtype: bytes
"""
bites = b''
number = long(number)
# Convert the number to a byte string
while number > 0:
bites = struct.pack(b'>I', number & 0xffffffffL) + bites
number = number >> 32
# Strip off any leading zeros
for index in range(len(bites)):
if bites[index] != b'\000'[0]:
break
else:
# Only happens when number == 0:
bites = b'\000'
index = 0
bites = bites[index:]
# Add back some padding bytes. This could be done more efficiently
# w.r.t. the de-padding being done above, but sigh...
if blocksize > 0 and len(bites) % blocksize:
bites = (blocksize - len(bites) % blocksize) * b'\000' + bites
return bytes(bites)
[docs]def chunkInto64CharsPerLine(data, separator=b'\n'):
"""Chunk **data** into lines with 64 characters each.
:param basestring data: The data to be chunked up.
:keyword basestring separator: The character to use to join the chunked
lines.
:rtype: basestring
:returns: The **data**, as a string, with 64 characters (plus the
**separator** character), per line.
"""
chunked = []
while len(data) > 0:
chunked.append(data[:64])
data = data[64:]
lines = separator.join(chunked)
return lines
[docs]def convertToSmooshedFingerprint(fingerprint):
"""Convert to a space-delimited 40 character fingerprint
Given a 49-character string, such as one returned from
:func:`convertToSpaceyFingerprint`::
72C2 F0AE 1C14 F40E D37E D5F5 434B 6471 1A65 8E46
convert it to the following format::
72C2F0AE1C14F40ED37ED5F5434B64711A658E46
:param str fingerprint: A 49-character spacey fingerprint.
:rtype: bytes
:raises InvalidFingerprint: If the fingerprint isn't 49-bytes in length.
:returns: A 40-character smooshed fingerprint without spaces.
"""
if len(fingerprint) != 49:
raise InvalidFingerprint("Invalid fingerprint (!= 49 bytes): %r"
% fingerprint)
return fingerprint.replace(' ', '')
[docs]def convertToSpaceyFingerprint(fingerprint):
"""Convert to a space-delimited 40-character fingerprint
Given a 40 character string, usually the the SHA-1 hash of the
DER encoding of an ASN.1 RSA public key, such as::
72C2F0AE1C14F40ED37ED5F5434B64711A658E46
convert it to the following format::
72C2 F0AE 1C14 F40E D37E D5F5 434B 6471 1A65 8E46
:param str fingerprint: A 40-character hex fingerprint.
:rtype: bytes
:raises InvalidFingerprint: If the fingerprint isn't 40 bytes in length.
:returns: A 4-character space-delimited fingerprint.
"""
if len(fingerprint) != 40:
raise InvalidFingerprint("Invalid fingerprint (< 40 bytes): %r"
% fingerprint)
return b" ".join([fingerprint[i:i+4] for i in range(0, 40, 4)])
[docs]def digestDescriptorContent(content):
# Create the descriptor digest:
descriptorDigest = hashlib.sha1(content)
descriptorDigestBinary = descriptorDigest.digest()
descriptorDigestHex = descriptorDigest.hexdigest()
descriptorDigestHexUpper = descriptorDigestHex.upper()
descriptorDigestHexLower = descriptorDigestHex.lower()
# Remove the hex encoding:
descriptorDigestBytes = descriptorDigestHexLower.decode('hex_codec')
# And add PKCS#1 padding:
descriptorDigestPKCS1 = addPKCS1Padding(descriptorDigestBytes)
return (descriptorDigestBinary, descriptorDigestHexUpper, descriptorDigestPKCS1)
[docs]def getASN1Sequence(privateKey):
"""Get an ASN.1 DER sequence string representation of the key's public
modulus and exponent.
:type privateKey: ``Crypto.PublicKey.RSA``
:param privateKey: A private RSA key.
:rtype: bytes
:returns: The ASN.1 DER-encoded string representation of the public
portions of the **privateKey**.
"""
seq = asn1.DerSequence()
seq.append(privateKey.n)
seq.append(privateKey.e)
asn1seqString = seq.encode()
return asn1seqString
[docs]def getFingerprint(publicKey):
"""Get a digest of the ASN.1 DER-encoded **publicKey**.
:type publicKey: str
:param publicKey: A public key (as within the return parameters of
:func:`generateOnionKey` or :func:`generateSigningKey`.)
:rtype: str
:returns: A spacey fingerprint.
"""
keyDigest = hashlib.sha1(publicKey)
keyDigestBinary = keyDigest.digest()
keyDigestHex = keyDigest.hexdigest()
keyDigestHexUpper = keyDigestHex.upper()
keyDigestBytes = codecs.latin_1_encode(keyDigestHexUpper, 'replace')[0]
fingerprint = convertToSpaceyFingerprint(keyDigestBytes)
return (fingerprint, keyDigestBinary)
[docs]def _generateRSAKey(bits=1024):
"""Generate an RSA key, suitable for e.g. a router/bridge signing or onion
key, or an Hidden Service service or permanent key.
The encodings for the various key and descriptor digests needed are
described in dir-spec.txt and tor-spec.txt, the latter mostly for the
padding and encoding used in the creation of an OR's keys.
: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 a
descriptor.
"""
secretKey = RSA.generate(bits) # generate an RSA key
publicKey = getASN1Sequence(secretKey) # ASN.1 encode it
publicKeyB64 = base64.b64encode(publicKey) # base64 encode it
# Split the base64-encoded string into lines 64 characters long:
publicKeyNoHeaders = chunkInto64CharsPerLine(publicKeyB64)
return (secretKey, publicKey, publicKeyNoHeaders)
[docs]def generateOnionKey(bits=1024):
"""Generate a router's onion key, which is used to encrypt CERT cells.
The encodings for the various key and descriptor digests needed are
described in dir-spec.txt and tor-spec.txt, the latter mostly for the
padding and encoding used in the creation of an OR's keys.
For the ``router`` line in a networkstatus document, the following
encodings are specified:
.. epigraph::
[…] "Identity" is a hash of its identity key, encoded in base64, with
trailing equals sign(s) removed. "Digest" is a hash of its most
recent descriptor as signed (that is, not including the signature),
encoded in base64.
-- dir-spec.txt_ L1504-1512_
Before the hash digest of an OR's identity key is base64-encoded for
inclusion in a networkstatus document, the hash digest is created in the
following manner:
.. epigraph::
When we refer to "the hash of a public key", we mean the SHA-1 hash of the
DER encoding of an ASN.1 RSA public key (as specified in PKCS.1).
[…]
The "legacy identity" and "identity fingerprint" fields are the SHA1
hash of the PKCS#1 ASN1 encoding of the next onion router's identity
(signing) key.
-- tor-spec.txt_ L109-110_ and L784-786_
.. _dir-spec.txt: https://gitweb.torproject.org/torspec.git/tree/dir-spec.txt
.. _L1504-1512: https://gitweb.torproject.org/torspec.git/tree/dir-spec.txt?id=36761c7d5#n1504
.. _tor-spec.txt: https://gitweb.torproject.org/torspec.git/tree/tor-spec.txt
.. _L109-110: https://gitweb.torproject.org/torspec.git/tree/tor-spec.txt?id=36761c7d5#n109
.. _L784-786: https://gitweb.torproject.org/torspec.git/tree/tor-spec.txt?id=36761c7d5#n784
:param int bits: The length of the RSA key, in bits.
:returns: A tuple of strings,
``(onion-key-private, onion-key-public, onion-key-line)``, where
``onion-key-line`` should directly go into a server-descriptor.
"""
(secretOnionKey,
publicOnionKey,
publicOnionKeyNoHeaders) = _generateRSAKey(bits)
# Add key header and footer:
onionKeyWithHeaders = addTorPKHeaderAndFooter(publicOnionKeyNoHeaders)
onionKeyLine = TOKEN_ONION_KEY + onionKeyWithHeaders
return (secretOnionKey, publicOnionKey, onionKeyLine)
[docs]def generateSigningKey(bits=1024):
"""Generate a router's signing-key, which is used to sign e.g. descriptor
contents.
:param int bits: The length of the RSA key, in bits.
:returns: A tuple of strings, ``(signing-key-private, signing-key-public,
signing-key-line)``, where ``signign-key-line`` should directly go into
a descriptor.
"""
(secretSigningKey,
publicSigningKey,
publicSigningKeyNoHeaders) = _generateRSAKey(bits)
# Add key header and footer:
signingKeyWithHeaders = addTorPKHeaderAndFooter(publicSigningKeyNoHeaders)
# Generate the new `signing-key` line for the descriptor:
signingKeyLine = TOKEN_SIGNING_KEY + signingKeyWithHeaders
return (secretSigningKey, publicSigningKey, signingKeyLine)
[docs]def signDescriptorContent(content, privateKey, digest=None,
token=TOKEN_ROUTER_SIGNATURE):
"""Sign the **content** or the **digest** of the content, and postpend it
to the **content**.
:param str content: The contents of a descriptor.
:type privateKey: ``Crypto.PublicKey.RSA``
:param privateKey: A private RSA key.
:type digest: str or ``None``
:param digest: If given, this should be the PKCS#1-padded binary digest of
the descriptor contents (i.e. the third return value from
:func:`digestDescriptorContent`). If the **digest** is given, then
this **digest** will be signed. Otherwise, if ``None``, then
**contents** will be signed.
:param str token: The token to search for when appending the signature to
the end of the descriptor **content**
"""
if digest is None:
(_, _, digest) = digestDescriptorContent(content)
# Generate a signature by signing the PKCS#1-padded digest with the
# private key:
(signatureLong, ) = privateKey.sign(digest, None)
signatureBytes = longToBytes(signatureLong, 128)
signatureBase64 = base64.b64encode(signatureBytes)
signature = chunkInto64CharsPerLine(signatureBase64)
# Add the header and footer:
signatureWithHeaders = addTorSigHeaderAndFooter(signature)
# Add the signature to the descriptor content:
routerSignatureLine = token + signatureWithHeaders
rsStart = content.find(token)
content = content[:rsStart] + routerSignatureLine + b'\n'
return content