diff --git a/.gitignore b/.gitignore index 1b0684b..a49d9c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,99 @@ -*.pyc -**/*.pyc -*.egg-info -build -dist +# Created by https://www.gitignore.io/api/python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +.venv/ +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# End of https://www.gitignore.io/api/python + /apidesign.txt /pycrypto/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..34e77cc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: python +python: + - "2.7" + - "3.5" + - "3.6" +install: pip install tox-travis +script: tox +cache: pip diff --git a/MANIFEST.in b/MANIFEST.in index 4bf4483..9561fb1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -include README.md \ No newline at end of file +include README.rst diff --git a/README.md b/README.rst similarity index 71% rename from README.md rename to README.rst index 8e7e911..af90d27 100644 --- a/README.md +++ b/README.rst @@ -1,47 +1,56 @@ python-jws -===== -A Python implementation of [JSON Web Signatures draft 02](http://self-issued.info/docs/draft-jones-json-web-signature.html) +========== -Also now works on Python 3.3+ as well as Python 2.7+. However, it's a naive conversion to support both Python 2 and Python 3 so there may well be hidden bugs. +A Python implementation of `JSON Web Signatures draft +02 `__ + +Also now works on Python 3.3+ as well as Python 2.7+. However, it's a +naive conversion to support both Python 2 and Python 3 so there may well +be hidden bugs. Installing ---------- - $ pip install jws +:: + $ pip install jws Algorithms ---------- -The JWS spec reserves several algorithms for cryptographic signing. Out of the 9, this library currently supports 7: +The JWS spec reserves several algorithms for cryptographic signing. Out +of the 9, this library currently supports 7: -**HMAC** – native +**HMAC** -- native -* HS256 – HMAC using SHA-256 hash algorithm -* HS384 – HMAC using SHA-384 hash algorithm -* HS512 – HMAC using SHA-512 hash algorithm +- HS256 -- HMAC using SHA-256 hash algorithm +- HS384 -- HMAC using SHA-384 hash algorithm +- HS512 -- HMAC using SHA-512 hash algorithm +**RSA** -- requires pycrypto >= 2.5: ``pip install pycrypto`` -**RSA** – requires pycrypto >= 2.5: ``pip install pycrypto`` +- RS256 -- RSA using SHA-256 hash algorithm -* RS256 – RSA using SHA-256 hash algorithm +**ECDSA** -- requires ecdsa lib: ``pip install ecdsa`` -**ECDSA** – requires ecdsa lib: ``pip install ecdsa`` - -* ES256 – ECDSA using P-256 curve and SHA-256 hash algorithm -* ES384 – ECDSA using P-384 curve and SHA-384 hash algorithm -* ES512 – ECDSA using P-521 curve and SHA-512 hash algorithm +- ES256 -- ECDSA using P-256 curve and SHA-256 hash algorithm +- ES384 -- ECDSA using P-384 curve and SHA-384 hash algorithm +- ES512 -- ECDSA using P-521 curve and SHA-512 hash algorithm There is also a mechanism for extending functionality by adding your own -algorithms without cracking open the whole codebase. See the advanced usage -section for an example. +algorithms without cracking open the whole codebase. See the advanced +usage section for an example. -For RSA and ECDSA, all crypto libraries are lazily loaded so you won't need the dependencies unless you try to use the functionality. +For RSA and ECDSA, all crypto libraries are lazily loaded so you won't +need the dependencies unless you try to use the functionality. Usage ----- + Let's check out some examples. +:: + >>> import jws >>> header = { 'alg': 'HS256' } >>> payload = { 'claim': 'JSON is the raddest.', 'iss': 'brianb' } @@ -55,6 +64,8 @@ Let's check out some examples. Now with a real key! +:: + >>> import ecdsa >>> sk256 = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p) >>> vk = sk256.get_verifying_key() @@ -65,8 +76,11 @@ Now with a real key! Advanced Usage -------------- + Make this file +:: + # file: sillycrypto.py import jws from jws.algos import AlgorithmBase, SignatureError @@ -90,6 +104,8 @@ Make this file And in an interpreter: +:: + >>> import jws >>> header = { 'alg': 'F7U12' } >>> payload = { 'claim': 'wutt' } @@ -107,9 +123,8 @@ And in an interpreter: .... jws.exceptions.SignatureError: nope - Other Stuff ---------- +----------- Check out https://github.com/brianloveswords/python-jws/blob/master/examples/minijwt.py @@ -120,10 +135,11 @@ https://github.com/brianloveswords/python-jws/blob/master/examples/ragecrypto.py for a rage-comic inspired cryptography extension. TODO -------- -* Write about all the rad stuff that can be done around headers (as extensible as crypto algos) -* Pull in JWK support +---- +- Write about all the rad stuff that can be done around headers (as + extensible as crypto algos) +- Pull in JWK support Tests ----- diff --git a/README.txt b/README.txt deleted file mode 100644 index 8e7e911..0000000 --- a/README.txt +++ /dev/null @@ -1,136 +0,0 @@ -python-jws -===== -A Python implementation of [JSON Web Signatures draft 02](http://self-issued.info/docs/draft-jones-json-web-signature.html) - -Also now works on Python 3.3+ as well as Python 2.7+. However, it's a naive conversion to support both Python 2 and Python 3 so there may well be hidden bugs. - -Installing ----------- - $ pip install jws - - - -Algorithms ----------- -The JWS spec reserves several algorithms for cryptographic signing. Out of the 9, this library currently supports 7: - - -**HMAC** – native - -* HS256 – HMAC using SHA-256 hash algorithm -* HS384 – HMAC using SHA-384 hash algorithm -* HS512 – HMAC using SHA-512 hash algorithm - - -**RSA** – requires pycrypto >= 2.5: ``pip install pycrypto`` - -* RS256 – RSA using SHA-256 hash algorithm - -**ECDSA** – requires ecdsa lib: ``pip install ecdsa`` - -* ES256 – ECDSA using P-256 curve and SHA-256 hash algorithm -* ES384 – ECDSA using P-384 curve and SHA-384 hash algorithm -* ES512 – ECDSA using P-521 curve and SHA-512 hash algorithm - -There is also a mechanism for extending functionality by adding your own -algorithms without cracking open the whole codebase. See the advanced usage -section for an example. - -For RSA and ECDSA, all crypto libraries are lazily loaded so you won't need the dependencies unless you try to use the functionality. - -Usage ------ -Let's check out some examples. - - >>> import jws - >>> header = { 'alg': 'HS256' } - >>> payload = { 'claim': 'JSON is the raddest.', 'iss': 'brianb' } - >>> signature = jws.sign(header, payload, 'secret') - >>> jws.verify(header, payload, signature, 'secret') - True - >>> jws.verify(header, payload, signature, 'badbadbad') - Traceback (most recent call last): - ... - jws.exceptions.SignatureError: Could not validate signature - -Now with a real key! - - >>> import ecdsa - >>> sk256 = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p) - >>> vk = sk256.get_verifying_key() - >>> header = { 'alg': 'ES256' } - >>> sig = jws.sign(header, payload, sk256) - >>> jws.verify(header, payload, sig, vk) - True - -Advanced Usage --------------- -Make this file - - # file: sillycrypto.py - import jws - from jws.algos import AlgorithmBase, SignatureError - class FXUY(AlgorithmBase): - def __init__(self, x, y): - self.x = int(x) - self.y = int(y) - def sign(self, msg, key): - return 'verysecure' * self.x + key * self.y - - def verify(self, msg, sig, key): - if sig != self.sign(msg, key): - raise SignatureError('nope') - return True - - jws.algos.CUSTOM += [ - # a regular expression with two named matching groups. (x and y) - # named groups will be sent to the class constructor - (r'^F(?P\d)U(?P\d{2})$', FXUY), - ] - -And in an interpreter: - - >>> import jws - >>> header = { 'alg': 'F7U12' } - >>> payload = { 'claim': 'wutt' } - >>> sig = jws.sign(header, payload, '') - Traceback (most recent call last): - .... - jws.exceptions.AlgorithmNotImplemented: "F7U12" not implemented. - >>> - >>> import sillycrypto - >>> sig = jws.sign(header, payload, '') - >>> jws.verify(header, payload, sig, '') - True - >>> jws.verify(header, payload, sig, 'y u no verify?') - Traceback (most recent call last): - .... - jws.exceptions.SignatureError: nope - - -Other Stuff ---------- - -Check out -https://github.com/brianloveswords/python-jws/blob/master/examples/minijwt.py -for a 14-line implemention of JWT. - -See -https://github.com/brianloveswords/python-jws/blob/master/examples/ragecrypto.py -for a rage-comic inspired cryptography extension. - -TODO -------- -* Write about all the rad stuff that can be done around headers (as extensible as crypto algos) -* Pull in JWK support - - -Tests ------ - -use nosetests - -License -------- - -MIT diff --git a/fabfile.py b/fabfile.py index 153bed1..a76a6d6 100644 --- a/fabfile.py +++ b/fabfile.py @@ -2,7 +2,7 @@ # Automate the release def release(): - local("python setup.py sdist register upload") - local("python2.5 setup.py bdist_egg register upload") - local("python2.6 setup.py bdist_egg register upload") - local("python2.7 setup.py bdist_egg register upload") \ No newline at end of file + local("python setup.py sdist bdist_wheel upload") + local("python2.5 setup.py bdist_egg upload") + local("python2.6 setup.py bdist_egg upload") + local("python2.7 setup.py bdist_egg upload") diff --git a/jws/utils.py b/jws/utils.py index 091adf4..3cbf66f 100644 --- a/jws/utils.py +++ b/jws/utils.py @@ -2,6 +2,7 @@ import base64 import json +import hmac import sys if sys.version < '3': @@ -30,21 +31,24 @@ def from_base64(a): return base64url_decode(a) def encode(a): return to_base64(to_json(a)) def decode(a): return from_json(from_base64(a)) -#Taken from Django Source Code -def constant_time_compare(val1, val2): - """ - Returns True if the two strings are equal, False otherwise. - - The time taken is independent of the number of characters that match. - - For the sake of simplicity, this function executes in constant time only - when the two strings have the same length. It short-circuits when they - have different lengths. - """ - if len(val1) != len(val2): - return False - result = 0 - for x, y in zip(val1, val2): - result |= ord(x) ^ ord(y) - return result == 0 +if hmac.compare_digest: + constant_time_compare = hmac.compare_digest +else: + # Taken from Django Source Code + def constant_time_compare(val1, val2): + """ + Returns True if the two strings are equal, False otherwise. + + The time taken is independent of the number of characters that match. + + For the sake of simplicity, this function executes in constant time only + when the two strings have the same length. It short-circuits when they + have different lengths. + """ + if len(val1) != len(val2): + return False + result = 0 + for x, y in zip(val1, val2): + result |= ord(x) ^ ord(y) + return result == 0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..3c6e79c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/setup.py b/setup.py index b49390c..2f6f923 100644 --- a/setup.py +++ b/setup.py @@ -14,11 +14,14 @@ def read(fname): keywords = "jws json web security signing", url = "http://github.com/brianlovesdata/python-jws", packages=['jws'], - long_description=read('README.md'), + extras_require={ + 'rsa': ['pycrypto>=2.6.1, <3.0.0'], + 'ecdsa': ['ecdsa>=0.13.0, <0.14.0'], + }, + long_description=read('README.rst'), classifiers=[ "Development Status :: 3 - Alpha", "Topic :: Utilities", "License :: OSI Approved :: MIT License", ], - test_suite = 'nose.collector', ) diff --git a/jws/tests.py b/tests/test_jws.py similarity index 98% rename from jws/tests.py rename to tests/test_jws.py index 092ae0c..2f1c8af 100644 --- a/jws/tests.py +++ b/tests/test_jws.py @@ -142,7 +142,7 @@ def test_valid_hmac512(self): def test_invalid_hmac(self): header = {'alg': 'HS512'} sig = jws.sign(header, self.payload, 'secret') - self.assertRaises(jws.SignatureError(header, self.payload, sig, 'failwhale')) + self.assertRaises(jws.SignatureError, jws.verify, header, self.payload, sig, 'failwhale') class TestJWS_rsa(unittest.TestCase): private = rsa.generate(2048) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..e827fcd --- /dev/null +++ b/tox.ini @@ -0,0 +1,22 @@ +[tox] +envlist = py{27,35,36}, lint + +[testenv] +commands = py.test {posargs} +deps = + pycrypto==2.6.1 + ecdsa==0.13.0 + pytest==3.0.5 + +[testenv:lint] +deps = + flake8==3.2.1 +commands=flake8 jws tests setup.py + +[travis] +python = + 3.6: py36, lint + +[flake8] +ignore = E131,E201,E202,E228,E231,E241,E251,E261,E262,E266,E301,E302,E305,E306,E701,E704,F401,F403,F405,F821 +max_line_length=118