commit 6b4c3f08545009a68cb6b2a24c2357603e484609 Author: Isis Lovecruft isis@torproject.org Date: Fri Dec 5 18:43:49 2014 -0800
Add method for verifying a Bridge's extrainfo router-signature.
* ADD new method bridgedb.bridges.Bridge._verifyExtraInfoSignature(). * ADD new function bridgedb.crypto.removePKCS1Padding(). --- lib/bridgedb/bridges.py | 109 ++++++++++++++++++++++++++++++++++++++++++++++- lib/bridgedb/crypto.py | 41 ++++++++++++++++++ 2 files changed, 149 insertions(+), 1 deletion(-)
diff --git a/lib/bridgedb/bridges.py b/lib/bridgedb/bridges.py index 17486e2..d2e528d 100644 --- a/lib/bridgedb/bridges.py +++ b/lib/bridgedb/bridges.py @@ -11,13 +11,20 @@
from __future__ import print_function
+import base64 +import codecs import hashlib import ipaddr import logging import os
+from Crypto.Util import asn1 +from Crypto.Util.number import bytes_to_long +from Crypto.Util.number import long_to_bytes + from bridgedb import safelog from bridgedb import bridgerequest +from bridgedb.crypto import removePKCS1Padding from bridgedb.parse.addr import isIPAddress from bridgedb.parse.addr import isIPv6 from bridgedb.parse.addr import isValidIP @@ -55,6 +62,10 @@ class ServerDescriptorWithoutNetworkstatus(MalformedBridgeInfo): mentioned in the latest ``@type bridge-networkstatus`` document. """
+class InvalidExtraInfoSignature(MalformedBridgeInfo): + """Raised if the signature on an ``@type bridge-extrainfo`` is invalid.""" + + class Flags(object): """All the flags which a :class:`Bridge` may have."""
@@ -1174,7 +1185,91 @@ class Bridge(object):
self.extrainfoDigest = descriptor.extrainfoDigest
- def updateFromExtraInfoDescriptor(self, descriptor): + def _verifyExtraInfoSignature(self, descriptor): + """Verify the signature on the contents of this :class:`Bridge`'s + ``@type bridge-extrainfo`` descriptor. + + :type descriptor: + :api:`stem.descriptor.extrainfo_descriptor.RelayExtraInfoDescriptor` + :param descriptor: An ``@type bridge-extrainfo`` descriptor for this + :class:`Bridge`, parsed with Stem. + :raises InvalidExtraInfoSignature: if the signature was invalid, + missing, malformed, or couldn't be verified successfully. + :returns: ``None`` if the signature was valid and verifiable. + """ + # The blocksize is always 128 bits for a 1024-bit key + BLOCKSIZE = 128 + + TOR_SIGNING_KEY_HEADER = u'-----BEGIN RSA PUBLIC KEY-----\n' + TOR_SIGNING_KEY_FOOTER = u'-----END RSA PUBLIC KEY-----' + TOR_BEGIN_SIGNATURE = u'-----BEGIN SIGNATURE-----\n' + TOR_END_SIGNATURE = u'-----END SIGNATURE-----\n' + + logging.info("Verifying extrainfo signature for %s..." % self) + + # Get the bytes of the descriptor signature without the headers: + document, signature = descriptor.get_bytes().split(TOR_BEGIN_SIGNATURE) + signature = signature.replace(TOR_END_SIGNATURE, '') + signature = signature.replace('\n', '') + signature = signature.strip() + + try: + # Get the ASN.1 sequence: + sequence = asn1.DerSequence() + + key = self.signingKey + key = key.strip(TOR_SIGNING_KEY_HEADER) + key = key.strip(TOR_SIGNING_KEY_FOOTER) + key = key.replace('\n', '') + key = base64.b64decode(key) + + sequence.decode(key) + + modulus = sequence[0] + publicExponent = sequence[1] + + # The public exponent of RSA signing-keys should always be 65537, + # but we're not going to turn them down if they want to use a + # potentially dangerous exponent. + if publicExponent != 65537: # pragma: no cover + logging.warn("Odd RSA exponent in signing-key for %s: %s" % + (self, publicExponent)) + + # Base64 decode the signature: + signatureDecoded = base64.b64decode(signature) + + # Convert the signature to a long: + signatureLong = bytes_to_long(signatureDecoded) + + # Decrypt the long signature with the modulus and public exponent: + decryptedInt = pow(signatureLong, publicExponent, modulus) + + # Then convert it back to a byte array: + decryptedBytes = long_to_bytes(decryptedInt, BLOCKSIZE) + + # Remove the PKCS#1 padding from the signature: + unpadded = removePKCS1Padding(decryptedBytes) + + # This is the hexadecimal SHA-1 hash digest of the descriptor document + # as it was signed: + signedDigest = codecs.encode(unpadded, 'hex_codec') + actualDigest = hashlib.sha1(document).hexdigest() + + except Exception as error: + logging.debug("Error verifying extrainfo signature: %s" % error) + raise InvalidExtraInfoSignature( + "Extrainfo signature for %s couldn't be decoded: %s" % + (self, signature)) + else: + if signedDigest != actualDigest: + raise InvalidExtraInfoSignature( + ("The extrainfo digest signed by bridge %s didn't match the " + "actual digest.\nSigned digest: %s\nActual digest: %s") % + (self, signedDigest, actualDigest)) + else: + logging.info("Extrainfo signature was verified successfully!") + + def updateFromExtraInfoDescriptor(self, descriptor, verify=True): """Update this bridge's information from an extrainfo descriptor.
.. todo:: The ``transport`` attribute of Stem's @@ -1186,5 +1281,17 @@ class Bridge(object): :type descriptor: :api:`stem.descriptor.extrainfo_descriptor.BridgeExtraInfoDescriptor` :param descriptor: DOCDOC + :param bool verify: If ``True``, check that the ``router-signature`` + on the extrainfo **descriptor** is a valid signature from + :data:`signingkey`. """ + if verify: + try: + self._verifyExtraInfoSignature(descriptor) + except InvalidExtraInfoSignature as error: + logging.warn(error) + logging.info(("Tossing extrainfo descriptor due to an invalid " + "signature.")) + return + self.descriptors['extrainfo'] = descriptor diff --git a/lib/bridgedb/crypto.py b/lib/bridgedb/crypto.py index e8cf8fd..63e532b 100644 --- a/lib/bridgedb/crypto.py +++ b/lib/bridgedb/crypto.py @@ -87,6 +87,9 @@ GPGME_CONTEXT_HOMEDIR = '.gnupg' GPGME_CONTEXT_BINARY = which('gpg2') or which('gpg') # These will be lists
+class PKCS1PaddingError(Exception): + """Raised when there is a problem adding or removing PKCS#1 padding.""" + class RSAKeyGenerationError(Exception): """Raised when there was an error creating an RSA keypair."""
@@ -273,6 +276,44 @@ def getHMACFunc(key, hex=True): return h_tmp.digest() return hmac_fn
+def removePKCS1Padding(message): + """Remove PKCS#1 padding from a **message**. + + (PKCS#1 v1.0? see https://bugs.torproject.org/13042) + + 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** + + For more information on the structure of PKCS#1 padding, see :rfc:`2313`, + particularly the notes in §8.1. + + :param str message: A message which is PKCS#1 padded. + :raises PKCS1PaddingError: if there is an issue parsing the **message**. + :rtype: bytes + :returns: The message without the PKCS#1 padding. + """ + padding = b'\xFF' + typeinfo = b'\x00\x01' + separator = b'\x00' + + unpadded = None + + try: + if message.index(typeinfo) != 0: + raise PKCS1PaddingError("Couldn't find PKCS#1 identifier bytes!") + start = message.index(separator, 2) + 1 # 2 bytes for the typeinfo, + # and 1 byte for the separator. + except ValueError: + raise PKCS1PaddingError("Couldn't find PKCS#1 separator byte!") + else: + unpadded = message[start:] + + return unpadded + def _createGPGMEErrorInterpreters(): """Create a mapping of GPGME ERRNOs ←→ human-readable error names/causes.