From 1da2d4a52d55d64f77d7c6d6b52cdba555dc0e0b Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Tue, 6 Jan 2015 08:00:28 -0600 Subject: [PATCH 1/9] Fixes #70. Refactored all HMAC, RSA, and EC code into seperate classes in the algorithms module Added register_algorithm to add new algorithms. --- jwt/__init__.py | 198 ++++++--------------------------------------- jwt/algorithms.py | 200 ++++++++++++++++++++++++++++++++++++++++++++++ tests/test_jwt.py | 72 +++++------------ 3 files changed, 241 insertions(+), 229 deletions(-) create mode 100644 jwt/algorithms.py diff --git a/jwt/__init__.py b/jwt/__init__.py index 3a70913e..c03b2ad5 100644 --- a/jwt/__init__.py +++ b/jwt/__init__.py @@ -22,6 +22,7 @@ # Functions 'encode', 'decode', + 'register_algorithm', # Exceptions 'InvalidTokenError', @@ -33,9 +34,22 @@ # Deprecated aliases 'ExpiredSignature', 'InvalidAudience', - 'InvalidIssuer', + 'InvalidIssuer' ] +_algorithms = {} + +def register_algorithm(alg_id, alg_obj): + if alg_id in _algorithms: + raise ValueError('Algorithm already has a handler.') + + if not isinstance(alg_obj, Algorithm): + raise TypeError('Object is not of type `Algorithm`') + + _algorithms[alg_id] = alg_obj + +from jwt.algorithms import Algorithm, _register_default_algorithms +_register_default_algorithms() class InvalidTokenError(Exception): pass @@ -62,169 +76,6 @@ class InvalidIssuerError(InvalidTokenError): InvalidAudience = InvalidAudienceError InvalidIssuer = InvalidIssuerError -signing_methods = { - 'none': lambda msg, key: b'', - 'HS256': lambda msg, key: hmac.new(key, msg, hashlib.sha256).digest(), - 'HS384': lambda msg, key: hmac.new(key, msg, hashlib.sha384).digest(), - 'HS512': lambda msg, key: hmac.new(key, msg, hashlib.sha512).digest() -} - -verify_methods = { - 'HS256': lambda msg, key: hmac.new(key, msg, hashlib.sha256).digest(), - 'HS384': lambda msg, key: hmac.new(key, msg, hashlib.sha384).digest(), - 'HS512': lambda msg, key: hmac.new(key, msg, hashlib.sha512).digest() -} - - -def prepare_HS_key(key): - if not isinstance(key, string_types) and not isinstance(key, bytes): - raise TypeError('Expecting a string- or bytes-formatted key.') - - if isinstance(key, text_type): - key = key.encode('utf-8') - - return key - -prepare_key_methods = { - 'none': lambda key: None, - 'HS256': prepare_HS_key, - 'HS384': prepare_HS_key, - 'HS512': prepare_HS_key -} - -try: - from cryptography.hazmat.primitives import interfaces, hashes - from cryptography.hazmat.primitives.serialization import ( - load_pem_private_key, load_pem_public_key, load_ssh_public_key - ) - from cryptography.hazmat.primitives.asymmetric import ec, padding - from cryptography.hazmat.backends import default_backend - from cryptography.exceptions import InvalidSignature - - def sign_rsa(msg, key, hashalg): - signer = key.signer( - padding.PKCS1v15(), - hashalg - ) - - signer.update(msg) - return signer.finalize() - - def verify_rsa(msg, key, hashalg, sig): - verifier = key.verifier( - sig, - padding.PKCS1v15(), - hashalg - ) - - verifier.update(msg) - - try: - verifier.verify() - return True - except InvalidSignature: - return False - - signing_methods.update({ - 'RS256': lambda msg, key: sign_rsa(msg, key, hashes.SHA256()), - 'RS384': lambda msg, key: sign_rsa(msg, key, hashes.SHA384()), - 'RS512': lambda msg, key: sign_rsa(msg, key, hashes.SHA512()) - }) - - verify_methods.update({ - 'RS256': lambda msg, key, sig: verify_rsa(msg, key, hashes.SHA256(), sig), - 'RS384': lambda msg, key, sig: verify_rsa(msg, key, hashes.SHA384(), sig), - 'RS512': lambda msg, key, sig: verify_rsa(msg, key, hashes.SHA512(), sig) - }) - - def prepare_RS_key(key): - if isinstance(key, interfaces.RSAPrivateKey) or \ - isinstance(key, interfaces.RSAPublicKey): - return key - - if isinstance(key, string_types): - if isinstance(key, text_type): - key = key.encode('utf-8') - - try: - if key.startswith(b'ssh-rsa'): - key = load_ssh_public_key(key, backend=default_backend()) - else: - key = load_pem_private_key(key, password=None, backend=default_backend()) - except ValueError: - key = load_pem_public_key(key, backend=default_backend()) - else: - raise TypeError('Expecting a PEM-formatted key.') - - return key - - prepare_key_methods.update({ - 'RS256': prepare_RS_key, - 'RS384': prepare_RS_key, - 'RS512': prepare_RS_key - }) - - def sign_ecdsa(msg, key, hashalg): - signer = key.signer(ec.ECDSA(hashalg)) - - signer.update(msg) - return signer.finalize() - - def verify_ecdsa(msg, key, hashalg, sig): - verifier = key.verifier(sig, ec.ECDSA(hashalg)) - - verifier.update(msg) - - try: - verifier.verify() - return True - except InvalidSignature: - return False - - signing_methods.update({ - 'ES256': lambda msg, key: sign_ecdsa(msg, key, hashes.SHA256()), - 'ES384': lambda msg, key: sign_ecdsa(msg, key, hashes.SHA384()), - 'ES512': lambda msg, key: sign_ecdsa(msg, key, hashes.SHA512()), - }) - - verify_methods.update({ - 'ES256': lambda msg, key, sig: verify_ecdsa(msg, key, hashes.SHA256(), sig), - 'ES384': lambda msg, key, sig: verify_ecdsa(msg, key, hashes.SHA384(), sig), - 'ES512': lambda msg, key, sig: verify_ecdsa(msg, key, hashes.SHA512(), sig), - }) - - def prepare_ES_key(key): - if isinstance(key, interfaces.EllipticCurvePrivateKey) or \ - isinstance(key, interfaces.EllipticCurvePublicKey): - return key - - if isinstance(key, string_types): - if isinstance(key, text_type): - key = key.encode('utf-8') - - # Attempt to load key. We don't know if it's - # a Signing Key or a Verifying Key, so we try - # the Verifying Key first. - try: - key = load_pem_public_key(key, backend=default_backend()) - except ValueError: - key = load_pem_private_key(key, password=None, backend=default_backend()) - - else: - raise TypeError('Expecting a PEM-formatted key.') - - return key - - prepare_key_methods.update({ - 'ES256': prepare_ES_key, - 'ES384': prepare_ES_key, - 'ES512': prepare_ES_key - }) - -except ImportError: - pass - - def base64url_decode(input): rem = len(input) % 4 @@ -290,8 +141,10 @@ def encode(payload, key, algorithm='HS256', headers=None, json_encoder=None): # Segments signing_input = b'.'.join(segments) try: - key = prepare_key_methods[algorithm](key) - signature = signing_methods[algorithm](signing_input, key) + alg_obj = _algorithms[algorithm] + key = alg_obj.prepare_key(key) + signature = alg_obj.sign(signing_input, key) + except KeyError: raise NotImplementedError('Algorithm not supported') @@ -360,17 +213,12 @@ def verify_signature(payload, signing_input, header, signature, key='', raise TypeError('audience must be a string or None') try: - algorithm = header['alg'].upper() - key = prepare_key_methods[algorithm](key) + alg_obj = _algorithms[header['alg'].upper()] + key = alg_obj.prepare_key(key) - if algorithm.startswith('HS'): - expected = verify_methods[algorithm](signing_input, key) + if not alg_obj.verify(signing_input, key, signature): + raise DecodeError('Signature verification failed') - if not constant_time_compare(signature, expected): - raise DecodeError('Signature verification failed') - else: - if not verify_methods[algorithm](signing_input, key, signature): - raise DecodeError('Signature verification failed') except KeyError: raise DecodeError('Algorithm not supported') diff --git a/jwt/algorithms.py b/jwt/algorithms.py new file mode 100644 index 00000000..2f6f1138 --- /dev/null +++ b/jwt/algorithms.py @@ -0,0 +1,200 @@ +import hashlib +import hmac +import sys + +from jwt import register_algorithm + +if sys.version_info >= (3, 0, 0): + unicode = str + basestring = str + +try: + from cryptography.hazmat.primitives import interfaces, hashes + from cryptography.hazmat.primitives.serialization import ( + load_pem_private_key, load_pem_public_key, load_ssh_public_key + ) + from cryptography.hazmat.primitives.asymmetric import ec, padding + from cryptography.hazmat.backends import default_backend + from cryptography.exceptions import InvalidSignature + + has_crypto = True +except ImportError: + has_crypto = False + + +def _register_default_algorithms(): + register_algorithm('none', NoneAlgorithm()) + register_algorithm('HS256', HMACAlgorithm(hashlib.sha256)) + register_algorithm('HS384', HMACAlgorithm(hashlib.sha384)) + register_algorithm('HS512', HMACAlgorithm(hashlib.sha512)) + + if has_crypto: + register_algorithm('RS256', RSAAlgorithm(hashes.SHA256())) + register_algorithm('RS384', RSAAlgorithm(hashes.SHA384())) + register_algorithm('RS512', RSAAlgorithm(hashes.SHA512())) + + register_algorithm('ES256', ECAlgorithm(hashes.SHA256())) + register_algorithm('ES384', ECAlgorithm(hashes.SHA384())) + register_algorithm('ES512', ECAlgorithm(hashes.SHA512())) + + +class Algorithm(object): + def prepare_key(self, key): + pass + + def sign(self, msg, key): + pass + + def verify(self, msg, key, sig): + pass + + +class NoneAlgorithm(Algorithm): + def prepare_key(self, key): + return None + + def sign(self, msg, key): + return b'' + + def verify(self, msg, key): + return True + + +class HMACAlgorithm(Algorithm): + def __init__(self, hash_alg): + self.hash_alg = hash_alg + + def prepare_key(self, key): + if not isinstance(key, basestring) and not isinstance(key, bytes): + raise TypeError('Expecting a string- or bytes-formatted key.') + + if isinstance(key, unicode): + key = key.encode('utf-8') + + return key + + def sign(self, msg, key): + return hmac.new(key, msg, self.hash_alg).digest() + + def verify(self, msg, key, sig): + return self._constant_time_compare(sig, self.sign(msg, key)) + + try: + _constant_time_compare = staticmethod(hmac.compare_digest) + except AttributeError: + # Fallback for Python < 2.7.7 and Python < 3.3 + @staticmethod + 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. + """ + if len(val1) != len(val2): + return False + + result = 0 + + if sys.version_info >= (3, 0, 0): + # Bytes are numbers + for x, y in zip(val1, val2): + result |= x ^ y + else: + for x, y in zip(val1, val2): + result |= ord(x) ^ ord(y) + + return result == 0 + +if has_crypto: + + class RSAAlgorithm(Algorithm): + def __init__(self, hash_alg): + self.hash_alg = hash_alg + + def prepare_key(self, key): + if isinstance(key, interfaces.RSAPrivateKey) or \ + isinstance(key, interfaces.RSAPublicKey): + return key + + if isinstance(key, basestring): + if isinstance(key, unicode): + key = key.encode('utf-8') + + try: + if key.startswith(b'ssh-rsa'): + key = load_ssh_public_key(key, backend=default_backend()) + else: + key = load_pem_private_key(key, password=None, backend=default_backend()) + except ValueError: + key = load_pem_public_key(key, backend=default_backend()) + else: + raise TypeError('Expecting a PEM-formatted key.') + + return key + + def sign(self, msg, key): + signer = key.signer( + padding.PKCS1v15(), + self.hash_alg + ) + + signer.update(msg) + return signer.finalize() + + def verify(self, msg, key, sig): + verifier = key.verifier( + sig, + padding.PKCS1v15(), + self.hash_alg + ) + + verifier.update(msg) + + try: + verifier.verify() + return True + except InvalidSignature: + return False + + class ECAlgorithm(Algorithm): + def __init__(self, hash_alg): + self.hash_alg = hash_alg + + def prepare_key(self, key): + if isinstance(key, interfaces.EllipticCurvePrivateKey) or \ + isinstance(key, interfaces.EllipticCurvePublicKey): + return key + + if isinstance(key, basestring): + if isinstance(key, unicode): + key = key.encode('utf-8') + + # Attempt to load key. We don't know if it's + # a Signing Key or a Verifying Key, so we try + # the Verifying Key first. + try: + key = load_pem_public_key(key, backend=default_backend()) + except ValueError: + key = load_pem_private_key(key, password=None, backend=default_backend()) + + else: + raise TypeError('Expecting a PEM-formatted key.') + + return key + + def sign(self, msg, key): + signer = key.signer(ec.ECDSA(self.hash_alg)) + + signer.update(msg) + return signer.finalize() + + def verify(self, msg, key, sig): + verifier = key.verifier(sig, ec.ECDSA(self.hash_alg)) + + verifier.update(msg) + + try: + verifier.verify() + return True + except InvalidSignature: + return False diff --git a/tests/test_jwt.py b/tests/test_jwt.py index a57ab31c..bd9ca065 100644 --- a/tests/test_jwt.py +++ b/tests/test_jwt.py @@ -45,6 +45,10 @@ def setUp(self): self.payload = {'iss': 'jeff', 'exp': utc_timestamp() + 15, 'claim': 'insanity'} + def test_register_algorithm_rejects_non_algorithm_obj(self): + with self.assertRaises(TypeError): + jwt.register_algorithm('AAA123', {}) + def test_encode_decode(self): secret = 'secret' jwt_message = jwt.encode(self.payload, secret) @@ -549,35 +553,15 @@ def test_encode_decode_with_rsa_sha512(self): load_output = jwt.load(jwt_message) jwt.verify_signature(key=pub_rsakey, *load_output) - def test_rsa_related_signing_methods(self): - if has_crypto: - self.assertTrue('RS256' in jwt.signing_methods) - self.assertTrue('RS384' in jwt.signing_methods) - self.assertTrue('RS512' in jwt.signing_methods) - else: - self.assertFalse('RS256' in jwt.signing_methods) - self.assertFalse('RS384' in jwt.signing_methods) - self.assertFalse('RS512' in jwt.signing_methods) - - def test_rsa_related_verify_methods(self): - if has_crypto: - self.assertTrue('RS256' in jwt.verify_methods) - self.assertTrue('RS384' in jwt.verify_methods) - self.assertTrue('RS512' in jwt.verify_methods) - else: - self.assertFalse('RS256' in jwt.verify_methods) - self.assertFalse('RS384' in jwt.verify_methods) - self.assertFalse('RS512' in jwt.verify_methods) - - def test_rsa_related_key_preparation_methods(self): + def test_rsa_related_algorithms(self): if has_crypto: - self.assertTrue('RS256' in jwt.prepare_key_methods) - self.assertTrue('RS384' in jwt.prepare_key_methods) - self.assertTrue('RS512' in jwt.prepare_key_methods) + self.assertTrue('RS256' in jwt._algorithms) + self.assertTrue('RS384' in jwt._algorithms) + self.assertTrue('RS512' in jwt._algorithms) else: - self.assertFalse('RS256' in jwt.prepare_key_methods) - self.assertFalse('RS384' in jwt.prepare_key_methods) - self.assertFalse('RS512' in jwt.prepare_key_methods) + self.assertFalse('RS256' in jwt._algorithms) + self.assertFalse('RS384' in jwt._algorithms) + self.assertFalse('RS512' in jwt._algorithms) @unittest.skipIf(not has_crypto, "Can't run without cryptography library") def test_encode_decode_with_ecdsa_sha256(self): @@ -669,35 +653,15 @@ def test_encode_decode_with_ecdsa_sha512(self): load_output = jwt.load(jwt_message) jwt.verify_signature(key=pub_eckey, *load_output) - def test_ecdsa_related_signing_methods(self): - if has_crypto: - self.assertTrue('ES256' in jwt.signing_methods) - self.assertTrue('ES384' in jwt.signing_methods) - self.assertTrue('ES512' in jwt.signing_methods) - else: - self.assertFalse('ES256' in jwt.signing_methods) - self.assertFalse('ES384' in jwt.signing_methods) - self.assertFalse('ES512' in jwt.signing_methods) - - def test_ecdsa_related_verify_methods(self): - if has_crypto: - self.assertTrue('ES256' in jwt.verify_methods) - self.assertTrue('ES384' in jwt.verify_methods) - self.assertTrue('ES512' in jwt.verify_methods) - else: - self.assertFalse('ES256' in jwt.verify_methods) - self.assertFalse('ES384' in jwt.verify_methods) - self.assertFalse('ES512' in jwt.verify_methods) - - def test_ecdsa_related_key_preparation_methods(self): + def test_ecdsa_related_algorithms(self): if has_crypto: - self.assertTrue('ES256' in jwt.prepare_key_methods) - self.assertTrue('ES384' in jwt.prepare_key_methods) - self.assertTrue('ES512' in jwt.prepare_key_methods) + self.assertTrue('ES256' in jwt._algorithms) + self.assertTrue('ES384' in jwt._algorithms) + self.assertTrue('ES512' in jwt._algorithms) else: - self.assertFalse('ES256' in jwt.prepare_key_methods) - self.assertFalse('ES384' in jwt.prepare_key_methods) - self.assertFalse('ES512' in jwt.prepare_key_methods) + self.assertFalse('ES256' in jwt._algorithms) + self.assertFalse('ES384' in jwt._algorithms) + self.assertFalse('ES512' in jwt._algorithms) def test_check_audience(self): payload = { From 9b0f0f13d9c2f74d36d0c4304a024fa7652392be Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Tue, 6 Jan 2015 08:15:11 -0600 Subject: [PATCH 2/9] Created utils.py to hold functions like constant_time_compare and base64-encoding --- jwt/__init__.py | 15 +-------------- jwt/algorithms.py | 29 ++--------------------------- jwt/utils.py | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 41 deletions(-) create mode 100644 jwt/utils.py diff --git a/jwt/__init__.py b/jwt/__init__.py index c03b2ad5..de3456df 100644 --- a/jwt/__init__.py +++ b/jwt/__init__.py @@ -12,6 +12,7 @@ from datetime import datetime, timedelta from calendar import timegm from collections import Mapping +from utils import base64url_encode, base64url_decode from .compat import (json, string_types, text_type, constant_time_compare, timedelta_total_seconds) @@ -70,25 +71,11 @@ class InvalidAudienceError(InvalidTokenError): class InvalidIssuerError(InvalidTokenError): pass - # Compatibility aliases (deprecated) ExpiredSignature = ExpiredSignatureError InvalidAudience = InvalidAudienceError InvalidIssuer = InvalidIssuerError -def base64url_decode(input): - rem = len(input) % 4 - - if rem > 0: - input += b'=' * (4 - rem) - - return base64.urlsafe_b64decode(input) - - -def base64url_encode(input): - return base64.urlsafe_b64encode(input).replace(b'=', b'') - - def header(jwt): if isinstance(jwt, text_type): jwt = jwt.encode('utf-8') diff --git a/jwt/algorithms.py b/jwt/algorithms.py index 2f6f1138..ab9ae032 100644 --- a/jwt/algorithms.py +++ b/jwt/algorithms.py @@ -3,6 +3,7 @@ import sys from jwt import register_algorithm +from utils import constant_time_compare if sys.version_info >= (3, 0, 0): unicode = str @@ -77,33 +78,7 @@ def sign(self, msg, key): return hmac.new(key, msg, self.hash_alg).digest() def verify(self, msg, key, sig): - return self._constant_time_compare(sig, self.sign(msg, key)) - - try: - _constant_time_compare = staticmethod(hmac.compare_digest) - except AttributeError: - # Fallback for Python < 2.7.7 and Python < 3.3 - @staticmethod - 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. - """ - if len(val1) != len(val2): - return False - - result = 0 - - if sys.version_info >= (3, 0, 0): - # Bytes are numbers - for x, y in zip(val1, val2): - result |= x ^ y - else: - for x, y in zip(val1, val2): - result |= ord(x) ^ ord(y) - - return result == 0 + return constant_time_compare(sig, self.sign(msg, key)) if has_crypto: diff --git a/jwt/utils.py b/jwt/utils.py new file mode 100644 index 00000000..16000b5d --- /dev/null +++ b/jwt/utils.py @@ -0,0 +1,39 @@ +import base64 +import hmac + +def base64url_decode(input): + rem = len(input) % 4 + + if rem > 0: + input += b'=' * (4 - rem) + + return base64.urlsafe_b64decode(input) + + +def base64url_encode(input): + return base64.urlsafe_b64encode(input).replace(b'=', b'') + +try: + constant_time_compare = hmac.compare_digest +except AttributeError: + # Fallback for Python < 2.7.7 and Python < 3.3 + 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. + """ + if len(val1) != len(val2): + return False + + result = 0 + + if sys.version_info >= (3, 0, 0): + # Bytes are numbers + for x, y in zip(val1, val2): + result |= x ^ y + else: + for x, y in zip(val1, val2): + result |= ord(x) ^ ord(y) + + return result == 0 From 3958141e922e29e9c6342aded58e9707226b91a2 Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Tue, 6 Jan 2015 08:20:26 -0600 Subject: [PATCH 3/9] Fixed some style issues (reordered imports, removed unused imports, PEP8, etc.) --- jwt/__init__.py | 14 ++++++++------ jwt/algorithms.py | 2 +- jwt/utils.py | 2 ++ setup.py | 3 ++- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/jwt/__init__.py b/jwt/__init__.py index de3456df..66dc05d7 100644 --- a/jwt/__init__.py +++ b/jwt/__init__.py @@ -5,14 +5,15 @@ http://self-issued.info/docs/draft-jones-json-web-token-01.html """ -import base64 import binascii -import hashlib -import hmac -from datetime import datetime, timedelta +import sys + from calendar import timegm from collections import Mapping -from utils import base64url_encode, base64url_decode +from datetime import datetime, timedelta + +from jwt.algorithms import Algorithm, _register_default_algorithms +from jwt.utils import base64url_decode, base64url_encode from .compat import (json, string_types, text_type, constant_time_compare, timedelta_total_seconds) @@ -40,6 +41,7 @@ _algorithms = {} + def register_algorithm(alg_id, alg_obj): if alg_id in _algorithms: raise ValueError('Algorithm already has a handler.') @@ -49,7 +51,6 @@ def register_algorithm(alg_id, alg_obj): _algorithms[alg_id] = alg_obj -from jwt.algorithms import Algorithm, _register_default_algorithms _register_default_algorithms() class InvalidTokenError(Exception): @@ -76,6 +77,7 @@ class InvalidIssuerError(InvalidTokenError): InvalidAudience = InvalidAudienceError InvalidIssuer = InvalidIssuerError + def header(jwt): if isinstance(jwt, text_type): jwt = jwt.encode('utf-8') diff --git a/jwt/algorithms.py b/jwt/algorithms.py index ab9ae032..427dafd7 100644 --- a/jwt/algorithms.py +++ b/jwt/algorithms.py @@ -3,7 +3,7 @@ import sys from jwt import register_algorithm -from utils import constant_time_compare +from jwt.utils import constant_time_compare if sys.version_info >= (3, 0, 0): unicode = str diff --git a/jwt/utils.py b/jwt/utils.py index 16000b5d..6e3cfb9b 100644 --- a/jwt/utils.py +++ b/jwt/utils.py @@ -1,5 +1,7 @@ import base64 import hmac +import sys + def base64url_decode(input): rem = len(input) % 4 diff --git a/setup.py b/setup.py index 62d5df74..e703db6f 100755 --- a/setup.py +++ b/setup.py @@ -1,8 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- import os -import sys import re +import sys + from setuptools import setup From db1d3563db4aa2eed916c4722d3345bf708b1ec2 Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Tue, 6 Jan 2015 08:25:07 -0600 Subject: [PATCH 4/9] Algorithm base class methods now raise NotImplementedError --- jwt/algorithms.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jwt/algorithms.py b/jwt/algorithms.py index 427dafd7..1fd0ca08 100644 --- a/jwt/algorithms.py +++ b/jwt/algorithms.py @@ -41,13 +41,13 @@ def _register_default_algorithms(): class Algorithm(object): def prepare_key(self, key): - pass + raise NotImplementedError def sign(self, msg, key): - pass + raise NotImplementedError def verify(self, msg, key, sig): - pass + raise NotImplementedError class NoneAlgorithm(Algorithm): From e49582f6d1bad64d3bf5260049aab4eb08a94e70 Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Tue, 6 Jan 2015 08:29:38 -0600 Subject: [PATCH 5/9] Moved jwt.algorithms imports back to their original location --- jwt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jwt/__init__.py b/jwt/__init__.py index 66dc05d7..d48528f1 100644 --- a/jwt/__init__.py +++ b/jwt/__init__.py @@ -12,7 +12,6 @@ from collections import Mapping from datetime import datetime, timedelta -from jwt.algorithms import Algorithm, _register_default_algorithms from jwt.utils import base64url_decode, base64url_encode from .compat import (json, string_types, text_type, constant_time_compare, @@ -51,6 +50,7 @@ def register_algorithm(alg_id, alg_obj): _algorithms[alg_id] = alg_obj +from jwt.algorithms import Algorithm, _register_default_algorithms # NOQA _register_default_algorithms() class InvalidTokenError(Exception): From 8e2adaefbf9e92e128ea034d6c5ab52dc1053047 Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Sun, 18 Jan 2015 10:36:09 -0600 Subject: [PATCH 6/9] Fixed a couple of anomalies after the last rebase. --- jwt/__init__.py | 5 ++--- jwt/algorithms.py | 17 ++++++----------- jwt/utils.py | 27 --------------------------- 3 files changed, 8 insertions(+), 41 deletions(-) diff --git a/jwt/__init__.py b/jwt/__init__.py index d48528f1..a1c11e22 100644 --- a/jwt/__init__.py +++ b/jwt/__init__.py @@ -6,7 +6,6 @@ """ import binascii -import sys from calendar import timegm from collections import Mapping @@ -14,8 +13,7 @@ from jwt.utils import base64url_decode, base64url_encode -from .compat import (json, string_types, text_type, constant_time_compare, - timedelta_total_seconds) +from .compat import (json, string_types, text_type, timedelta_total_seconds) __version__ = '0.4.1' @@ -53,6 +51,7 @@ def register_algorithm(alg_id, alg_obj): from jwt.algorithms import Algorithm, _register_default_algorithms # NOQA _register_default_algorithms() + class InvalidTokenError(Exception): pass diff --git a/jwt/algorithms.py b/jwt/algorithms.py index 1fd0ca08..93906366 100644 --- a/jwt/algorithms.py +++ b/jwt/algorithms.py @@ -1,13 +1,8 @@ import hashlib import hmac -import sys from jwt import register_algorithm -from jwt.utils import constant_time_compare - -if sys.version_info >= (3, 0, 0): - unicode = str - basestring = str +from jwt.compat import constant_time_compare, string_types, text_type try: from cryptography.hazmat.primitives import interfaces, hashes @@ -66,10 +61,10 @@ def __init__(self, hash_alg): self.hash_alg = hash_alg def prepare_key(self, key): - if not isinstance(key, basestring) and not isinstance(key, bytes): + if not isinstance(key, string_types) and not isinstance(key, text_type): raise TypeError('Expecting a string- or bytes-formatted key.') - if isinstance(key, unicode): + if isinstance(key, text_type): key = key.encode('utf-8') return key @@ -92,7 +87,7 @@ def prepare_key(self, key): return key if isinstance(key, basestring): - if isinstance(key, unicode): + if isinstance(key, text_type): key = key.encode('utf-8') try: @@ -140,8 +135,8 @@ def prepare_key(self, key): isinstance(key, interfaces.EllipticCurvePublicKey): return key - if isinstance(key, basestring): - if isinstance(key, unicode): + if isinstance(key, string_types): + if isinstance(key, text_type): key = key.encode('utf-8') # Attempt to load key. We don't know if it's diff --git a/jwt/utils.py b/jwt/utils.py index 6e3cfb9b..e6c1ef3b 100644 --- a/jwt/utils.py +++ b/jwt/utils.py @@ -1,6 +1,4 @@ import base64 -import hmac -import sys def base64url_decode(input): @@ -14,28 +12,3 @@ def base64url_decode(input): def base64url_encode(input): return base64.urlsafe_b64encode(input).replace(b'=', b'') - -try: - constant_time_compare = hmac.compare_digest -except AttributeError: - # Fallback for Python < 2.7.7 and Python < 3.3 - 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. - """ - if len(val1) != len(val2): - return False - - result = 0 - - if sys.version_info >= (3, 0, 0): - # Bytes are numbers - for x, y in zip(val1, val2): - result |= x ^ y - else: - for x, y in zip(val1, val2): - result |= ord(x) ^ ord(y) - - return result == 0 From 81a5932d006877033b5e1579b94cb8c87ca9b5ed Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Sun, 18 Jan 2015 10:47:11 -0600 Subject: [PATCH 7/9] Added comments for algorithms module and register_algorithm. --- jwt/__init__.py | 1 + jwt/algorithms.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/jwt/__init__.py b/jwt/__init__.py index a1c11e22..b9a9986a 100644 --- a/jwt/__init__.py +++ b/jwt/__init__.py @@ -40,6 +40,7 @@ def register_algorithm(alg_id, alg_obj): + """ Registers a new Algorithm for use when creating and verifying JWTs """ if alg_id in _algorithms: raise ValueError('Algorithm already has a handler.') diff --git a/jwt/algorithms.py b/jwt/algorithms.py index 93906366..3e8ea383 100644 --- a/jwt/algorithms.py +++ b/jwt/algorithms.py @@ -19,6 +19,7 @@ def _register_default_algorithms(): + """ Registers the algorithms that are implemented by the library """ register_algorithm('none', NoneAlgorithm()) register_algorithm('HS256', HMACAlgorithm(hashlib.sha256)) register_algorithm('HS384', HMACAlgorithm(hashlib.sha384)) @@ -35,17 +36,33 @@ def _register_default_algorithms(): class Algorithm(object): + """ The interface for an algorithm used to sign and verify JWTs """ def prepare_key(self, key): + """ + Performs necessary validation and conversions on the key and returns + the key value in the proper format for sign() and verify() + """ raise NotImplementedError def sign(self, msg, key): + """ + Returns a digital signature for the specified message using the + specified key value + """ raise NotImplementedError def verify(self, msg, key, sig): + """ + Verifies that the specified digital signature is valid for the specified + message and key values. + """ raise NotImplementedError class NoneAlgorithm(Algorithm): + """ + Placeholder for use when no signing or verification operations are required + """ def prepare_key(self, key): return None @@ -57,6 +74,10 @@ def verify(self, msg, key): class HMACAlgorithm(Algorithm): + """ + Performs signing and verification operations using HMAC and the specified + hash function + """ def __init__(self, hash_alg): self.hash_alg = hash_alg @@ -78,6 +99,11 @@ def verify(self, msg, key, sig): if has_crypto: class RSAAlgorithm(Algorithm): + """ + Performs signing and verification operations using RSASSA-PKCS-v1_5 and + the specified hash function + """ + def __init__(self, hash_alg): self.hash_alg = hash_alg @@ -127,6 +153,10 @@ def verify(self, msg, key, sig): return False class ECAlgorithm(Algorithm): + """ + Performs signing and verification operations using ECDSA and the + specified hash function + """ def __init__(self, hash_alg): self.hash_alg = hash_alg From f39d7eeda7dbd1aadcc44a364a717c2877b7db96 Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Sun, 18 Jan 2015 10:49:45 -0600 Subject: [PATCH 8/9] Replaced reference to basestring with string_types that I missed when rebasing earlier. --- jwt/algorithms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jwt/algorithms.py b/jwt/algorithms.py index 3e8ea383..cf3bd473 100644 --- a/jwt/algorithms.py +++ b/jwt/algorithms.py @@ -112,7 +112,7 @@ def prepare_key(self, key): isinstance(key, interfaces.RSAPublicKey): return key - if isinstance(key, basestring): + if isinstance(key, string_types): if isinstance(key, text_type): key = key.encode('utf-8') From eff1505b5df81540eb6c3c73b1d8c924fd1c2ca5 Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Sun, 18 Jan 2015 10:53:19 -0600 Subject: [PATCH 9/9] Accidentally replaced a reference to bytes with text_type inadvertantly. Reverting... --- jwt/algorithms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jwt/algorithms.py b/jwt/algorithms.py index cf3bd473..89ea75bd 100644 --- a/jwt/algorithms.py +++ b/jwt/algorithms.py @@ -82,7 +82,7 @@ def __init__(self, hash_alg): self.hash_alg = hash_alg def prepare_key(self, key): - if not isinstance(key, string_types) and not isinstance(key, text_type): + if not isinstance(key, string_types) and not isinstance(key, bytes): raise TypeError('Expecting a string- or bytes-formatted key.') if isinstance(key, text_type):