Permalink
Browse files

Provide a pure-python fallback for when M2Crypto is not available.

  • Loading branch information...
1 parent 44260da commit 427924622dfcec425d1443d33d6d0de376eabb63 @rfk rfk committed May 8, 2012
@@ -0,0 +1,18 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+"""
+
+Best-effort crypto primitives for PyBrowserID.
+
+If you have M2Crypto installed, this package will provide a nice fast
+implementation of the RSKey and DSKey classes needed to do the crypto
+work behind BrowserID. If you don't, you'll get a very slow but very
+portable pure-python version.
+
+"""
+
+try:
+ from browserid.crypto.m2 import Key, RSKey, DSKey
+except ImportError:
+ from browserid.crypto.fallback import Key, RSKey, DSKey # NOQA
@@ -1,6 +1,6 @@
#
# This monkey-patches M2Crypto's RSA and DSA support to allow us
-# to create keys directly from a given set of parameters. It's
+# to create keys directly from a given set of parameters.
# It's based on the following patch from the M2Crypt bugtracker:
#
# https://bugzilla.osafoundation.org/show_bug.cgi?id=12981
@@ -0,0 +1,178 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+"""
+
+Crypto primitives implemented in pure python.
+
+This file provides the "slow path" crypto implementation for PyBrowserID.
+It implements everything in pure python, so it's very slow but very portable.
+
+There is also a faster version built on M2Crypto, which should be picked up
+automatically if you have that package installed.
+
+"""
+
+import os
+from binascii import unhexlify
+
+
+class Key(object):
+ """Generic base class for Key objects."""
+
+ def verify(self, signed_data, signature):
+ raise NotImplementedError
+
+ def sign(self, data):
+ raise NotImplementedError
+
+
+# These constants are needed for encoding the name of the hash
+# algorithm into the RSA signature, per PKCS #1.
+RSA_DIGESTINFO_HEADER = {
+ "sha1": "3021300906052b0e03021a05000414",
+ "sha256": "3031300d060960864801650304020105000420",
+}
+
+
+class RSKey(object):
+ """Generic base class for RSA key objects.
+
+ Concrete subclasses should provide the SIZE, HASHNAME and HASHMOD
+ attributes.
+ """
+
+ SIZE = None
+ HASHNAME = None
+ HASHMOD = None
+
+ def __init__(self, data):
+ _check_keys(data, ("e", "n"))
+ self.e = long(data["e"])
+ self.n = long(data["n"])
+ try:
+ self.d = long(data["d"])
+ except KeyError:
+ self.d = None
+
+ def verify(self, signed_data, signature):
+ n, e = self.n, self.e
+ m = long(signature, 16)
+ c = pow(m, e, n)
+ padded_digest = hex(c)[2:].rstrip("L").rjust(self.SIZE * 2, "0")
+ return padded_digest == self._get_digest(signed_data)
+
+ def sign(self, data):
+ n, e, d = self.n, self.e, self.d
+ if not d:
+ raise ValueError("private key not present")
+ c = long(self._get_digest(data), 16)
+ m = pow(c, d, n)
+ return hex(m)[2:].rstrip("L")
+
+ def _get_digest(self, data):
+ digest = self.HASHMOD(data).hexdigest()
+ padded_digest = "00" + RSA_DIGESTINFO_HEADER[self.HASHNAME] + digest
+ padding_len = (self.SIZE * 2) - 4 - len(padded_digest)
+ padded_digest = "0001" + ("f" * padding_len) + padded_digest
+ return padded_digest
+
+
+class DSKey(object):
+ """Generic base class for DSA key objects.
+
+ Concrete subclasses should provide the BITLENGTH and HASHMOD attributes.
+ """
+
+ BITLENGTH = None
+ HASHMOD = None
+
+ def __init__(self, data):
+ _check_keys(data, ("p", "q", "g", "y"))
+ self.p = long(data["p"], 16)
+ self.q = long(data["q"], 16)
+ self.g = long(data["g"], 16)
+ self.y = long(data["y"], 16)
+ if "x" in data:
+ self.x = long(data["x"], 16)
+ else:
+ self.x = None
+
+ def verify(self, signed_data, signature):
+ p, q, g, y = self.p, self.q, self.g, self.y
+ signature = signature.encode("hex")
+ hexlength = self.BITLENGTH / 4
+ signature = signature.rjust(hexlength * 2, "0")
+ if len(signature) != hexlength * 2:
+ return False
+ r = long(signature[:hexlength], 16)
+ s = long(signature[hexlength:], 16)
+ if r <= 0 or r >= q:
+ return False
+ if s <= 0 or s >= q:
+ return False
+ w = modinv(s, q)
+ u1 = (long(self.HASHMOD(signed_data).hexdigest(), 16) * w) % q
+ u2 = (r * w) % q
+ v = ((pow(g, u1, p) * pow(y, u2, p)) % p) % q
+ return (v == r)
+
+ def sign(self, data):
+ p, q, g, y, x = self.p, self.q, self.g, self.y, self.x
+ if not x:
+ raise ValueError("private key not present")
+ # We need to do lots of if-not-this-then-start-over type tests.
+ # A while loop with continue statements is the cleanest way to do so.
+ while True:
+ k = long(os.urandom(self.BITLENGTH / 8).encode("hex"), 16) % q
+ if k == 0:
+ continue
+ r = pow(g, k, p) % q
+ if r == 0:
+ continue
+ h = (long(self.HASHMOD(data).hexdigest(), 16) + (x * r)) % q
+ s = (modinv(k, q) * h) % q
+ if s == 0:
+ continue
+ break
+ assert 0 < r < q
+ assert 0 < s < q
+ bytelength = self.BITLENGTH / 8
+ r_bytes = int2bytes(r).rjust(bytelength, "\x00")
+ s_bytes = int2bytes(s).rjust(bytelength, "\x00")
+ return r_bytes + s_bytes
+
+
+def modinv(a, m):
+ """Find the modular inverse of a, with modulus m."""
+ # This is a transliteration of the algorithm as it was described
+ # to me by Wikipedia, using the Extended Euclidean Algorithm.
+ x = 0
+ lastx = 1
+ y = 1
+ lasty = 0
+ b = m
+ while b != 0:
+ a, (q, b) = b, divmod(a, b)
+ x, lastx = lastx - (q * x), x
+ y, lasty = lasty - (q * y), y
+ return lastx % m
+
+
+def int2bytes(x):
+ """Convert a Python long integer to bigendian bytestring."""
+ # It's faster to go via hex encoding in C code than it is to try
+ # encoding directly into binary with a python-level loop.
+ # (and hex-slice-strip seems consistently faster than using "%x" format)
+ hexbytes = hex(x)[2:].rstrip("L")
+ if len(hexbytes) % 2:
+ hexbytes = "0" + hexbytes
+ return unhexlify(hexbytes)
+
+
+def _check_keys(data, keys):
+ """Verify that the given data dict contains the specified keys."""
+ for key in keys:
+ if not key in data:
+ msg = 'missing %s in data - %s' % (key, str(data.keys()))
+ raise ValueError(msg)
View
@@ -0,0 +1,159 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+"""
+
+Crypto primitives built on top of M2Crypto.
+
+This file provides the "fast path" crypto implementation for PyBrowserID.
+It uses the public-key-crypto routines from M2Crypto for nice fast operation.
+
+There is also a pure-python fallback module that's slower, but avoid
+having to install M2Crypto.
+
+"""
+
+import struct
+from binascii import unhexlify
+
+from browserid.crypto._m2_monkeypatch import DSA as _DSA
+from browserid.crypto._m2_monkeypatch import RSA as _RSA
+
+
+class Key(object):
+ """Generic base class for Key objects."""
+
+ def verify(self, signed_data, signature):
+ raise NotImplementedError # pragma: nocover
+
+ def sign(self, data):
+ raise NotImplementedError # pragma: nocover
+
+
+#
+# RSA keys, implemented using the RSA support in M2Crypto.
+#
+
+class RSKey(object):
+
+ SIZE = None
+ HASHMOD = None
+
+ def __init__(self, data=None, obj=None):
+ if data is None and obj is None:
+ raise ValueError('You should specify either data or obj')
+ if obj is not None:
+ self.rsa = obj
+ else:
+ _check_keys(data, ('e', 'n'))
+ e = int2mpint(int(data["e"]))
+ n = int2mpint(int(data["n"]))
+ try:
+ d = int2mpint(int(data["d"]))
+ except KeyError:
+ self.rsa = _RSA.new_pub_key((e, n))
+ else:
+ self.rsa = _RSA.new_key((e, n, d))
+
+ def verify(self, signed_data, signature):
+ digest = self.HASHMOD(signed_data).digest()
+ try:
+ return self.rsa.verify(digest, signature, self.HASHNAME)
+ except _RSA.RSAError:
+ return False
+
+ def sign(self, data):
+ digest = self.HASHMOD(data).digest()
+ return self.rsa.sign(digest, self.HASHNAME)
+
+
+#
+# DSA keys, implemented using the DSA support in M2Crypto, along with
+# some formatting tweaks to match what the browserid node-js server does.
+#
+
+class DSKey(object):
+
+ BITLENGTH = None
+ HASHMOD = None
+
+ def __init__(self, data=None, obj=None):
+ if data is None and obj is None:
+ raise ValueError('You should specify either data or obj')
+ if obj:
+ self.dsa = obj
+ else:
+ _check_keys(data, ('p', 'q', 'g', 'y'))
+
+ self.p = p = long(data["p"], 16)
+ self.q = q = long(data["q"], 16)
+ self.g = g = long(data["g"], 16)
+ self.y = y = long(data["y"], 16)
+ if "x" not in data:
+ self.x = None
+ self.dsa = _DSA.load_pub_key_params(int2mpint(p), int2mpint(q),
+ int2mpint(g), int2mpint(y))
+ else:
+ self.x = x = long(data["x"], 16)
+ self.dsa = _DSA.load_key_params(int2mpint(p), int2mpint(q),
+ int2mpint(g), int2mpint(y),
+ int2mpint(x))
+
+ def verify(self, signed_data, signature):
+ # Restore any leading zero bytes that might have been stripped.
+ signature = signature.encode("hex")
+ hexlength = self.BITLENGTH / 4
+ signature = signature.rjust(hexlength * 2, "0")
+ if len(signature) != hexlength * 2:
+ return False
+ # Split the signature into "r" and "s" components.
+ r = long(signature[:hexlength], 16)
+ s = long(signature[hexlength:], 16)
+ if r <= 0 or r >= self.q:
+ return False
+ if s <= 0 or s >= self.q:
+ return False
+ # Now we can check the digest.
+ digest = self.HASHMOD(signed_data).digest()
+ return self.dsa.verify(digest, int2mpint(r), int2mpint(s))
+
+ def sign(self, data):
+ if not self.x:
+ raise ValueError("private key not present")
+ digest = self.HASHMOD(data).digest()
+ r, s = self.dsa.sign(digest)
+ # We need precisely "bytelength" bytes from each integer.
+ # M2Crypto might give us more or less, so snip and pad appropriately.
+ bytelength = self.BITLENGTH / 8
+ r_bytes = r[4:].rjust(bytelength, "\x00")[-bytelength:]
+ s_bytes = s[4:].rjust(bytelength, "\x00")[-bytelength:]
+ return r_bytes + s_bytes
+
+
+#
+# Other helper functions.
+#
+
+
+def int2mpint(x):
+ """Convert a Python long integer to a string in OpenSSL's MPINT format."""
+ # MPINT is big-endian bytes with a size prefix.
+ # It's faster to go via hex encoding in C code than it is to try
+ # encoding directly into binary with a python-level loop.
+ # (and hex-slice-strip seems consistently faster than using "%x" format)
+ hexbytes = hex(x)[2:].rstrip("L")
+ if len(hexbytes) % 2:
+ hexbytes = "0" + hexbytes
+ bytes = unhexlify(hexbytes)
+ # Add an extra significant byte that's just zero. I think this is only
+ # necessary if the number has its MSB set, to prevent it being mistaken
+ # for a sign bit. I do it uniformly since it's valid and simpler.
+ return struct.pack(">I", len(bytes) + 1) + "\x00" + bytes
+
+
+def _check_keys(data, keys):
+ """Verify that the given data dict contains the specified keys."""
+ for key in keys:
+ if not key in data:
+ msg = 'missing %s in data - %s' % (key, str(data.keys()))
+ raise ValueError(msg)
Oops, something went wrong.

0 comments on commit 4279246

Please sign in to comment.