From 8f33affc2b9e57058c37f2b48b53a3f3782ea965 Mon Sep 17 00:00:00 2001 From: Reverb Chu Date: Sat, 29 Jul 2017 13:53:57 -0700 Subject: [PATCH 01/38] add version var --- httpsig/requests_auth.py | 6 +++--- httpsig/sign.py | 21 +++++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/httpsig/requests_auth.py b/httpsig/requests_auth.py index 6a02896..42f7dc3 100644 --- a/httpsig/requests_auth.py +++ b/httpsig/requests_auth.py @@ -6,7 +6,7 @@ # Python 2 from urlparse import urlparse -from .sign import HeaderSigner +from .sign import HeaderSigner, DEFAULT_VERSION class HTTPSignatureAuth(AuthBase): @@ -19,10 +19,10 @@ class HTTPSignatureAuth(AuthBase): algorithm is one of the six specified algorithms headers is a list of http headers to be included in the signing string, defaulting to "Date" alone. ''' - def __init__(self, key_id='', secret='', algorithm=None, headers=None): + def __init__(self, key_id='', secret='', algorithm=None, headers=None, version=DEFAULT_VERSION): headers = headers or [] self.header_signer = HeaderSigner(key_id=key_id, secret=secret, - algorithm=algorithm, headers=headers) + algorithm=algorithm, headers=headers, version=version) self.uses_host = 'host' in [h.lower() for h in headers] def __call__(self, r): diff --git a/httpsig/sign.py b/httpsig/sign.py index 7125035..2ea3594 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -9,26 +9,27 @@ DEFAULT_SIGN_ALGORITHM = "hmac-sha256" +DEFAULT_VERSION = 'draft-07' class Signer(object): """ When using an RSA algo, the secret is a PEM-encoded private key. When using an HMAC algo, the secret is the HMAC signing secret. - + Password-protected keyfiles are not supported. """ def __init__(self, secret, algorithm=None): if algorithm is None: algorithm = DEFAULT_SIGN_ALGORITHM - + assert algorithm in ALGORITHMS, "Unknown algorithm" if isinstance(secret, six.string_types): secret = secret.encode("ascii") - + self._rsa = None self._hash = None self.sign_algorithm, self.hash_algorithm = algorithm.split('-') - + if self.sign_algorithm == 'rsa': try: rsa_key = RSA.importKey(secret) @@ -36,7 +37,7 @@ def __init__(self, secret, algorithm=None): self._hash = HASHES[self.hash_algorithm] except ValueError: raise HttpSigException("Invalid key.") - + elif self.sign_algorithm == 'hmac': self._hash = HMAC.new(secret, digestmod=HASHES[self.hash_algorithm]) @@ -78,13 +79,14 @@ class HeaderSigner(Signer): :arg algorithm: one of the six specified algorithms :arg headers: a list of http headers to be included in the signing string, defaulting to ['date']. ''' - def __init__(self, key_id, secret, algorithm=None, headers=None): + def __init__(self, key_id, secret, algorithm=None, headers=None, version=DEFAULT_VERSION): if algorithm is None: algorithm = DEFAULT_SIGN_ALGORITHM - + super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm) self.headers = headers or ['date'] self.signature_template = build_signature_template(key_id, algorithm, headers) + self.version = version def sign(self, headers, host=None, method=None, path=None): """ @@ -98,9 +100,8 @@ def sign(self, headers, host=None, method=None, path=None): headers = CaseInsensitiveDict(headers) required_headers = self.headers or ['date'] signable = generate_message(required_headers, headers, host, method, path) - + signature = self._sign(signable) headers['authorization'] = self.signature_template % signature - - return headers + return headers From a0e45fffab5f2d5f00454c09d1659869faad4576 Mon Sep 17 00:00:00 2001 From: "Jarron Shih (RD-US)" Date: Sat, 29 Jul 2017 13:54:53 -0700 Subject: [PATCH 02/38] pylinter --- httpsig/_version.py | 1 - httpsig/tests/__init__.py | 2 +- httpsig/tests/test_signature.py | 2 +- httpsig/tests/test_verify.py | 16 ++++++++-------- httpsig/utils.py | 21 ++++++++++----------- httpsig/verify.py | 24 ++++++++++++------------ 6 files changed, 32 insertions(+), 34 deletions(-) diff --git a/httpsig/_version.py b/httpsig/_version.py index b1a0acd..f511b75 100644 --- a/httpsig/_version.py +++ b/httpsig/_version.py @@ -185,4 +185,3 @@ def get_versions(default={"version": "unknown", "full": ""}, verbose=False): return (versions_from_vcs(tag_prefix, root, verbose) or versions_from_parentdir(parentdir_prefix, root, verbose) or default) - diff --git a/httpsig/tests/__init__.py b/httpsig/tests/__init__.py index 72d4383..d9018eb 100644 --- a/httpsig/tests/__init__.py +++ b/httpsig/tests/__init__.py @@ -1,3 +1,3 @@ from .test_signature import * from .test_utils import * -from .test_verify import * \ No newline at end of file +from .test_verify import * diff --git a/httpsig/tests/test_signature.py b/httpsig/tests/test_signature.py index 00ed29d..4b65b41 100755 --- a/httpsig/tests/test_signature.py +++ b/httpsig/tests/test_signature.py @@ -55,7 +55,7 @@ def test_all(self): 'Content-Length': '18', } signed = hs.sign(unsigned, method='POST', path='/foo?param=value&pet=dog') - + self.assertIn('Date', signed) self.assertEqual(unsigned['Date'], signed['Date']) self.assertIn('Authorization', signed) diff --git a/httpsig/tests/test_verify.py b/httpsig/tests/test_verify.py index f49eeb3..27a5daa 100755 --- a/httpsig/tests/test_verify.py +++ b/httpsig/tests/test_verify.py @@ -23,23 +23,23 @@ def _parse_auth(self, auth): param_dict = {k: v.strip('"') for k, v in param_pairs} return param_dict - + class TestVerifyHMACSHA1(BaseTestCase): def setUp(self): secret = b"something special goes here" - + self.keyId = "Test" self.algorithm = "hmac-sha1" self.sign_secret = secret self.verify_secret = secret - + def test_basic_sign(self): signer = Signer(secret=self.sign_secret, algorithm=self.algorithm) verifier = Verifier(secret=self.verify_secret, algorithm=self.algorithm) GOOD = b"this is a test" BAD = b"this is not the signature you were looking for..." - + # generate signed string signature = signer._sign(GOOD) self.assertTrue(verifier._verify(data=GOOD, signature=signature)) @@ -49,7 +49,7 @@ def test_default(self): unsigned = { 'Date': 'Thu, 05 Jan 2012 21:31:40 GMT' } - + hs = HeaderSigner(key_id="Test", secret=self.sign_secret, algorithm=self.algorithm) signed = hs.sign(unsigned) hv = HeaderVerifier(headers=signed, secret=self.verify_secret) @@ -75,7 +75,7 @@ def test_signed_headers(self): 'Content-Length': '18', } signed = hs.sign(unsigned, method=METHOD, path=PATH) - + hv = HeaderVerifier(headers=signed, secret=self.verify_secret, host=HOST, method=METHOD, path=PATH) self.assertTrue(hv.verify()) @@ -146,11 +146,11 @@ def setUp(self): private_key_path = os.path.join(os.path.dirname(__file__), 'rsa_private.pem') with open(private_key_path, 'rb') as f: private_key = f.read() - + public_key_path = os.path.join(os.path.dirname(__file__), 'rsa_public.pem') with open(public_key_path, 'rb') as f: public_key = f.read() - + self.keyId = "Test" self.algorithm = "rsa-sha1" self.sign_secret = private_key diff --git a/httpsig/utils.py b/httpsig/utils.py index b34e3fa..c5324dc 100644 --- a/httpsig/utils.py +++ b/httpsig/utils.py @@ -42,15 +42,15 @@ def ct_bytes_compare(a, b): result |= ord(x) ^ ord(y) else: result |= x ^ y - + return (result == 0) def generate_message(required_headers, headers, host=None, method=None, path=None): headers = CaseInsensitiveDict(headers) - + if not required_headers: required_headers = ['date'] - + signable_list = [] for h in required_headers: h = h.lower() @@ -58,7 +58,7 @@ def generate_message(required_headers, headers, host=None, method=None, path=Non if not method or not path: raise Exception('method and path arguments required when using "(request-target)"') signable_list.append('%s: %s %s' % (h, method.lower(), path)) - + elif h == 'host': # 'host' special case due to requests lib restrictions # 'host' is not available when adding auth so must use a param @@ -82,11 +82,11 @@ def generate_message(required_headers, headers, host=None, method=None, path=Non def parse_authorization_header(header): if not isinstance(header, six.string_types): header = header.decode("ascii") #HTTP headers cannot be Unicode. - + auth = header.split(" ", 1) if len(auth) > 2: raise ValueError('Invalid authorization header. (eg. Method key1=value1,key2="value, \"2\"")') - + # Split up any args into a dictionary. values = {} if len(auth) == 2: @@ -94,7 +94,7 @@ def parse_authorization_header(header): if auth_value and len(auth_value): # This is tricky string magic. Let urllib do it. fields = parse_http_list(auth_value) - + for item in fields: # Only include keypairs. if '=' in item: @@ -102,13 +102,13 @@ def parse_authorization_header(header): key, value = item.split('=', 1) if not (len(key) and len(value)): continue - + # Unquote values, if quoted. if value[0] == '"': value = value[1:-1] - + values[key] = value - + # ("Signature", {"headers": "date", "algorithm": "hmac-sha256", ... }) return (auth[0], CaseInsensitiveDict(values)) @@ -183,4 +183,3 @@ def get_fingerprint(key): key = base64.b64decode(key) fp_plain = hashlib.md5(key).hexdigest() return ':'.join(a+b for a,b in zip(fp_plain[::2], fp_plain[1::2])) - diff --git a/httpsig/verify.py b/httpsig/verify.py index a6e1ba3..7929a7f 100644 --- a/httpsig/verify.py +++ b/httpsig/verify.py @@ -24,20 +24,20 @@ def _verify(self, data, signature): `data` is the message to verify `signature` is a base64-encoded signature to verify against `data` """ - + if isinstance(data, six.string_types): data = data.encode("ascii") if isinstance(signature, six.string_types): signature = signature.encode("ascii") - + if self.sign_algorithm == 'rsa': h = self._hash.new() h.update(data) return self._rsa.verify(h, b64decode(signature)) - + elif self.sign_algorithm == 'hmac': h = self._sign_hmac(data) s = b64decode(signature) return ct_bytes_compare(h, s) - + else: raise HttpSigException("Unsupported algorithm.") @@ -49,7 +49,7 @@ class HeaderVerifier(Verifier): def __init__(self, headers, secret, required_headers=None, method=None, path=None, host=None): """ Instantiate a HeaderVerifier object. - + :param headers: A dictionary of headers from the HTTP request. :param secret: The HMAC secret or RSA *public* key. :param required_headers: Optional. A list of headers required to be present to validate, even if the signature is otherwise valid. Defaults to ['date']. @@ -58,33 +58,33 @@ def __init__(self, headers, secret, required_headers=None, method=None, path=Non :param host: Optional. The value to use for the Host header, if not supplied in :param:headers. """ required_headers = required_headers or ['date'] - + auth = parse_authorization_header(headers['authorization']) if len(auth) == 2: self.auth_dict = auth[1] else: raise HttpSigException("Invalid authorization header.") - + self.headers = CaseInsensitiveDict(headers) self.required_headers = [s.lower() for s in required_headers] self.method = method self.path = path self.host = host - + super(HeaderVerifier, self).__init__(secret, algorithm=self.auth_dict['algorithm']) def verify(self): """ Verify the headers based on the arguments passed at creation and current properties. - + Raises an Exception if a required header (:param:required_headers) is not found in the signature. Returns True or False. """ auth_headers = self.auth_dict.get('headers', 'date').split(' ') - + if len(set(self.required_headers) - set(auth_headers)) > 0: raise Exception('{} is a required header(s)'.format(', '.join(set(self.required_headers)-set(auth_headers)))) - + signing_str = generate_message(auth_headers, self.headers, self.host, self.method, self.path) - + return self._verify(signing_str, self.auth_dict['signature']) From 2a6f140f85108615d9cb0a523f443b1ff8657cac Mon Sep 17 00:00:00 2001 From: Reverb Chu Date: Sat, 29 Jul 2017 14:01:27 -0700 Subject: [PATCH 03/38] add py35 and py36 support --- .travis.yml | 4 +++- README.rst | 28 ++++++++++++++-------------- setup.py | 2 ++ tox.ini | 2 +- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index 893e0b0..1ea31a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,9 @@ python: - "3.2" - "3.3" - "3.4" + - "3.5" + - "3.6" install: - pip install . - pip install nose -script: nosetests +script: nosetests diff --git a/README.rst b/README.rst index ca0674a..2927557 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ httpsig .. image:: https://travis-ci.org/ahknight/httpsig.svg?branch=master :target: https://travis-ci.org/ahknight/httpsig - + .. image:: https://travis-ci.org/ahknight/httpsig.svg?branch=develop :target: https://travis-ci.org/ahknight/httpsig @@ -20,7 +20,7 @@ See the original project_, original Python module_, original spec_, and `current Requirements ------------ -* Python 2.7, 3.2, 3.3, 3.4 +* Python 2.7, 3.2, 3.3, 3.4, 3.5, 3.6 * PyCrypto_ Optional: @@ -40,21 +40,21 @@ For simple raw signing: .. code:: python import httpsig - + secret = open('rsa_private.pem', 'rb').read() - + sig_maker = httpsig.Signer(secret=secret, algorithm='rsa-sha256') sig_maker.sign('hello world!') For general use with web frameworks: - + .. code:: python import httpsig - + key_id = "Some Key ID" secret = b'some big secret' - + hs = httpsig.HeaderSigner(key_id, secret, algorithm="hmac-sha256", headers=['(request-target)', 'host', 'date']) signed_headers_dict = hs.sign({"Date": "Tue, 01 Jan 2014 01:01:01 GMT", "Host": "example.com"}, method="GET", path="/api/1/object/1") @@ -65,11 +65,11 @@ For use with requests: import json import requests from httpsig.requests_auth import HTTPSignatureAuth - + secret = open('rsa_private.pem', 'rb').read() - + auth = HTTPSignatureAuth(key_id='Test', secret=secret) - z = requests.get('https://api.example.com/path/to/endpoint', + z = requests.get('https://api.example.com/path/to/endpoint', auth=auth, headers={'X-Api-Version': '~6.5'}) Class initialization parameters @@ -81,8 +81,8 @@ Note that keys and secrets should be bytes objects. At attempt will be made to httpsig.Signer(secret, algorithm='rsa-sha256') -``secret``, in the case of an RSA signature, is a string containing private RSA pem. In the case of HMAC, it is a secret password. -``algorithm`` is one of the six allowed signatures: ``rsa-sha1``, ``rsa-sha256``, ``rsa-sha512``, ``hmac-sha1``, ``hmac-sha256``, +``secret``, in the case of an RSA signature, is a string containing private RSA pem. In the case of HMAC, it is a secret password. +``algorithm`` is one of the six allowed signatures: ``rsa-sha1``, ``rsa-sha256``, ``rsa-sha512``, ``hmac-sha1``, ``hmac-sha256``, ``hmac-sha512``. @@ -90,8 +90,8 @@ Note that keys and secrets should be bytes objects. At attempt will be made to httpsig.requests_auth.HTTPSignatureAuth(key_id, secret, algorithm='rsa-sha256', headers=None) -``key_id`` is the label by which the server system knows your RSA signature or password. -``headers`` is the list of HTTP headers that are concatenated and used as signing objects. By default it is the specification's minimum, the ``Date`` HTTP header. +``key_id`` is the label by which the server system knows your RSA signature or password. +``headers`` is the list of HTTP headers that are concatenated and used as signing objects. By default it is the specification's minimum, the ``Date`` HTTP header. ``secret`` and ``algorithm`` are as above. Tests diff --git a/setup.py b/setup.py index 82cf121..eb541fa 100755 --- a/setup.py +++ b/setup.py @@ -31,6 +31,8 @@ "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development :: Libraries :: Python Modules", ], diff --git a/tox.ini b/tox.ini index 5add957..b170c60 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, py32, py33, py34 +envlist = py27, py32, py33, py34, py35, py36 [testenv] commands = python setup.py test From b676906c2edf126d4f087735fe16c8fb144dc673 Mon Sep 17 00:00:00 2001 From: Reverb Chu Date: Sat, 29 Jul 2017 14:15:09 -0700 Subject: [PATCH 04/38] fix pep8 and pylint for sign.py --- httpsig/sign.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index 2ea3594..904a1f9 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -5,10 +5,10 @@ from Crypto.PublicKey import RSA from Crypto.Signature import PKCS1_v1_5 -from .utils import * +from .utils import ALGORITHMS, HASHES, HttpSigException, build_signature_template, CaseInsensitiveDict, generate_message -DEFAULT_SIGN_ALGORITHM = "hmac-sha256" +DEFAULT_SIGN_ALGORITHM = 'hmac-sha256' DEFAULT_VERSION = 'draft-07' @@ -23,8 +23,9 @@ def __init__(self, secret, algorithm=None): if algorithm is None: algorithm = DEFAULT_SIGN_ALGORITHM - assert algorithm in ALGORITHMS, "Unknown algorithm" - if isinstance(secret, six.string_types): secret = secret.encode("ascii") + assert algorithm in ALGORITHMS, 'Unknown algorithm' + if isinstance(secret, six.string_types): + secret = secret.encode('ascii') self._rsa = None self._hash = None @@ -36,7 +37,7 @@ def __init__(self, secret, algorithm=None): self._rsa = PKCS1_v1_5.new(rsa_key) self._hash = HASHES[self.hash_algorithm] except ValueError: - raise HttpSigException("Invalid key.") + raise HttpSigException('Invalid key.') elif self.sign_algorithm == 'hmac': self._hash = HMAC.new(secret, digestmod=HASHES[self.hash_algorithm]) @@ -46,19 +47,25 @@ def algorithm(self): return '%s-%s' % (self.sign_algorithm, self.hash_algorithm) def _sign_rsa(self, data): - if isinstance(data, six.string_types): data = data.encode("ascii") - h = self._hash.new() - h.update(data) - return self._rsa.sign(h) + if isinstance(data, six.string_types): + data = data.encode('ascii') + + _hash = self._hash.new() + _hash.update(data) + return self._rsa.sign(_hash) def _sign_hmac(self, data): - if isinstance(data, six.string_types): data = data.encode("ascii") + if isinstance(data, six.string_types): + data = data.encode('ascii') + hmac = self._hash.copy() hmac.update(data) return hmac.digest() def _sign(self, data): - if isinstance(data, six.string_types): data = data.encode("ascii") + if isinstance(data, six.string_types): + data = data.encode('ascii') + signed = None if self._rsa: signed = self._sign_rsa(data) @@ -66,7 +73,7 @@ def _sign(self, data): signed = self._sign_hmac(data) if not signed: raise SystemError('No valid encryptor found.') - return base64.b64encode(signed).decode("ascii") + return base64.b64encode(signed).decode('ascii') class HeaderSigner(Signer): From bde97663b4dca17542883d2d1170452a864e8087 Mon Sep 17 00:00:00 2001 From: Reverb Chu Date: Sat, 29 Jul 2017 14:21:05 -0700 Subject: [PATCH 05/38] fix pep8 and pylint for verify.py --- httpsig/verify.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/httpsig/verify.py b/httpsig/verify.py index 7929a7f..15af28f 100644 --- a/httpsig/verify.py +++ b/httpsig/verify.py @@ -1,15 +1,11 @@ """ Module to assist in verifying a signed header. """ -import six - -from Crypto.Hash import HMAC -from Crypto.PublicKey import RSA -from Crypto.Signature import PKCS1_v1_5 from base64 import b64decode +import six from .sign import Signer -from .utils import * +from .utils import HttpSigException, ct_bytes_compare, parse_authorization_header, CaseInsensitiveDict, generate_message class Verifier(Signer): @@ -25,18 +21,21 @@ def _verify(self, data, signature): `signature` is a base64-encoded signature to verify against `data` """ - if isinstance(data, six.string_types): data = data.encode("ascii") - if isinstance(signature, six.string_types): signature = signature.encode("ascii") + if isinstance(data, six.string_types): + data = data.encode('ascii') + + if isinstance(signature, six.string_types): + signature = signature.encode('ascii') if self.sign_algorithm == 'rsa': - h = self._hash.new() - h.update(data) - return self._rsa.verify(h, b64decode(signature)) + hash_ = self._hash.new() + hash_.update(data) + return self._rsa.verify(hash_, b64decode(signature)) elif self.sign_algorithm == 'hmac': - h = self._sign_hmac(data) - s = b64decode(signature) - return ct_bytes_compare(h, s) + signed_hmac = self._sign_hmac(data) + decoded_sig = b64decode(signature) + return ct_bytes_compare(signed_hmac, decoded_sig) else: raise HttpSigException("Unsupported algorithm.") @@ -82,8 +81,8 @@ def verify(self): """ auth_headers = self.auth_dict.get('headers', 'date').split(' ') - if len(set(self.required_headers) - set(auth_headers)) > 0: - raise Exception('{} is a required header(s)'.format(', '.join(set(self.required_headers)-set(auth_headers)))) + if set(self.required_headers) - set(auth_headers): + raise Exception('{} is a required header(s)'.format(', '.join(set(self.required_headers) - set(auth_headers)))) signing_str = generate_message(auth_headers, self.headers, self.host, self.method, self.path) From 6b35ec1d4a6669344b2c754cfd45c5e90b607742 Mon Sep 17 00:00:00 2001 From: Reverb Chu Date: Sat, 29 Jul 2017 14:22:34 -0700 Subject: [PATCH 06/38] fix pep8 and pylint for requests_auth.py --- httpsig/requests_auth.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/httpsig/requests_auth.py b/httpsig/requests_auth.py index 42f7dc3..3ed3cf7 100644 --- a/httpsig/requests_auth.py +++ b/httpsig/requests_auth.py @@ -21,17 +21,16 @@ class HTTPSignatureAuth(AuthBase): ''' def __init__(self, key_id='', secret='', algorithm=None, headers=None, version=DEFAULT_VERSION): headers = headers or [] - self.header_signer = HeaderSigner(key_id=key_id, secret=secret, - algorithm=algorithm, headers=headers, version=version) + self.header_signer = HeaderSigner(key_id=key_id, secret=secret, algorithm=algorithm, headers=headers, version=version) self.uses_host = 'host' in [h.lower() for h in headers] - def __call__(self, r): + def __call__(self, request): headers = self.header_signer.sign( - r.headers, - # 'Host' header unavailable in request object at this point - # if 'host' header is needed, extract it from the url - host=urlparse(r.url).netloc if self.uses_host else None, - method=r.method, - path=r.path_url) - r.headers.update(headers) - return r + request.headers, + # 'Host' header unavailable in request object at this point + # if 'host' header is needed, extract it from the url + host=urlparse(request.url).netloc if self.uses_host else None, + method=request.method, + path=request.path_url) + request.headers.update(headers) + return request From 7e8ed4147dc915dea90a84216ea3a86abf8cd126 Mon Sep 17 00:00:00 2001 From: Reverb Chu Date: Sat, 29 Jul 2017 14:26:04 -0700 Subject: [PATCH 07/38] update travis-ci badge --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 2927557..cb885fd 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,11 @@ httpsig ======= -.. image:: https://travis-ci.org/ahknight/httpsig.svg?branch=master - :target: https://travis-ci.org/ahknight/httpsig +.. image:: https://travis-ci.org/GeekMap/httpsig.svg?branch=master + :target: https://travis-ci.org/GeekMap/httpsig -.. image:: https://travis-ci.org/ahknight/httpsig.svg?branch=develop - :target: https://travis-ci.org/ahknight/httpsig +.. image:: https://travis-ci.org/GeekMap/httpsig.svg?branch=dev + :target: https://travis-ci.org/GeekMap/httpsig Sign HTTP requests with secure signatures according to the IETF HTTP Signatures specification (`Draft 3`_). This is a fork of the original module_ to fully support both RSA and HMAC schemes as well as unit test both schemes to prove they work. It's being used in production and is actively-developed. From 26a34dd35abf87bb8d2bf9e0c448e3fc20260267 Mon Sep 17 00:00:00 2001 From: "Jarron Shih (RD-US)" Date: Sat, 29 Jul 2017 14:42:13 -0700 Subject: [PATCH 08/38] support draft-01-07, generate correct message for sign --- httpsig/tests/test_utils.py | 35 +++++++++++++++++++++++++++++++++-- httpsig/utils.py | 14 ++++++++++++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/httpsig/tests/test_utils.py b/httpsig/tests/test_utils.py index 6d79f69..e4bc783 100755 --- a/httpsig/tests/test_utils.py +++ b/httpsig/tests/test_utils.py @@ -6,12 +6,43 @@ import unittest -from httpsig.utils import get_fingerprint +import httpsig.utils + class TestUtils(unittest.TestCase): def test_get_fingerprint(self): with open(os.path.join(os.path.dirname(__file__), 'rsa_public.pem'), 'r') as k: key = k.read() - fingerprint = get_fingerprint(key) + fingerprint = httpsig.utils.get_fingerprint(key) self.assertEqual(fingerprint, "73:61:a2:21:67:e0:df:be:7e:4b:93:1e:15:98:a5:b7") + + def test_generate_message(self): + HOST = "example.org" + METHOD = "POST" + PATH = '/foo' + HTTP_VERSION='HTTP/1.1' + HEADERS = { + 'Host': 'example.org', + 'Date': 'Tue, 07 Jun 2014 20:51:35 GMT', + 'Content-Type': 'application/json', + 'Digest': 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=', + 'Content-Length': 18 + } + cases = { + # from draft-01: 3.1.2. RSA Example + 'request-line host date digest content-length': b'POST /foo HTTP/1.1\nhost: example.org\ndate: Tue, 07 Jun 2014 20:51:35 GMT\ndigest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=\ncontent-length: 18', + # draft-02 + '(request-line) host date digest content-length': b'(request-line): post /foo\nhost: example.org\ndate: Tue, 07 Jun 2014 20:51:35 GMT\ndigest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=\ncontent-length: 18', + # draft-03 + '(request-target) host date digest content-length': b'(request-target): post /foo\nhost: example.org\ndate: Tue, 07 Jun 2014 20:51:35 GMT\ndigest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=\ncontent-length: 18' + } + + for required_headers, result in cases.items(): + assert httpsig.utils.generate_message( + required_headers=required_headers.split(), + headers=HEADERS, + host=HOST, + method=METHOD, + path=PATH, + http_version=HTTP_VERSION) == result diff --git a/httpsig/utils.py b/httpsig/utils.py index c5324dc..40cf3eb 100644 --- a/httpsig/utils.py +++ b/httpsig/utils.py @@ -45,7 +45,8 @@ def ct_bytes_compare(a, b): return (result == 0) -def generate_message(required_headers, headers, host=None, method=None, path=None): + +def generate_message(required_headers, headers, host=None, method=None, path=None, http_version=None): headers = CaseInsensitiveDict(headers) if not required_headers: @@ -54,10 +55,18 @@ def generate_message(required_headers, headers, host=None, method=None, path=Non signable_list = [] for h in required_headers: h = h.lower() - if h == '(request-target)': + if h == '(request-target)': # draft-03 to draft-07 if not method or not path: raise Exception('method and path arguments required when using "(request-target)"') signable_list.append('%s: %s %s' % (h, method.lower(), path)) + elif h == '(request-line)': # draft-02 + if not method or not path: + raise Exception('method and path arguments required when using "(request-line)"') + signable_list.append('%s: %s %s' % (h, method.lower(), path)) + elif h == 'request-line': # draft-00, draft-01 + if not method or not path or not http_version: + raise Exception('method and path arguments required when using "request-line"') + signable_list.append('%s %s %s' % (method, path, http_version)) elif h == 'host': # 'host' special case due to requests lib restrictions @@ -112,6 +121,7 @@ def parse_authorization_header(header): # ("Signature", {"headers": "date", "algorithm": "hmac-sha256", ... }) return (auth[0], CaseInsensitiveDict(values)) + def build_signature_template(key_id, algorithm, headers): """ Build the Signature template for use with the Authorization header. From 0725c182e05e75a858045d79cf3922d14303b6df Mon Sep 17 00:00:00 2001 From: "Jarron Shih (RD-US)" Date: Sat, 29 Jul 2017 14:47:35 -0700 Subject: [PATCH 09/38] Add test msg --- httpsig/tests/test_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httpsig/tests/test_utils.py b/httpsig/tests/test_utils.py index e4bc783..256bed3 100755 --- a/httpsig/tests/test_utils.py +++ b/httpsig/tests/test_utils.py @@ -38,11 +38,11 @@ def test_generate_message(self): '(request-target) host date digest content-length': b'(request-target): post /foo\nhost: example.org\ndate: Tue, 07 Jun 2014 20:51:35 GMT\ndigest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=\ncontent-length: 18' } - for required_headers, result in cases.items(): + for required_headers, result in cases.items(): assert httpsig.utils.generate_message( required_headers=required_headers.split(), headers=HEADERS, host=HOST, method=METHOD, path=PATH, - http_version=HTTP_VERSION) == result + http_version=HTTP_VERSION) == result, 'header: %s\nexpect: %s\n' % (required_headers, result) From 9c79e8bced1941160757ec7199b7cb176fa7d8a0 Mon Sep 17 00:00:00 2001 From: "Jarron Shih (RD-US)" Date: Sat, 29 Jul 2017 14:49:02 -0700 Subject: [PATCH 10/38] linter --- httpsig/tests/test_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/httpsig/tests/test_utils.py b/httpsig/tests/test_utils.py index 256bed3..5040a9d 100755 --- a/httpsig/tests/test_utils.py +++ b/httpsig/tests/test_utils.py @@ -5,7 +5,6 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) import unittest - import httpsig.utils @@ -21,7 +20,7 @@ def test_generate_message(self): HOST = "example.org" METHOD = "POST" PATH = '/foo' - HTTP_VERSION='HTTP/1.1' + HTTP_VERSION = 'HTTP/1.1' HEADERS = { 'Host': 'example.org', 'Date': 'Tue, 07 Jun 2014 20:51:35 GMT', From 324e1dc0be25a3f2700f534a840054828ff712e2 Mon Sep 17 00:00:00 2001 From: Reverb Chu Date: Sat, 29 Jul 2017 15:40:22 -0700 Subject: [PATCH 11/38] remove unused methods --- httpsig/utils.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/httpsig/utils.py b/httpsig/utils.py index 40cf3eb..a708f68 100644 --- a/httpsig/utils.py +++ b/httpsig/utils.py @@ -144,21 +144,6 @@ def build_signature_template(key_id, algorithm, headers): return sig_string -def lkv(d): - parts = [] - while d: - len = struct.unpack('>I', d[:4])[0] - bits = d[4:len+4] - parts.append(bits) - d = d[len+4:] - return parts - -def sig(d): - return lkv(d)[1] - -def is_rsa(keyobj): - return lkv(keyobj.blob)[0] == "ssh-rsa" - # based on http://stackoverflow.com/a/2082169/151401 class CaseInsensitiveDict(dict): def __init__(self, d=None, **kwargs): From f3365f056447f9dd6c46c3c8de532d2c1b24f379 Mon Sep 17 00:00:00 2001 From: Reverb Chu Date: Sat, 29 Jul 2017 15:40:25 -0700 Subject: [PATCH 12/38] fix pep8 and pylint for utils.py --- httpsig/utils.py | 86 +++++++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/httpsig/utils.py b/httpsig/utils.py index a708f68..ccbfcc9 100644 --- a/httpsig/utils.py +++ b/httpsig/utils.py @@ -1,7 +1,7 @@ import re -import struct import hashlib import base64 +from functools import reduce import six try: @@ -11,11 +11,10 @@ # Python 2 from urllib2 import parse_http_list -from Crypto.PublicKey import RSA from Crypto.Hash import SHA, SHA256, SHA512 ALGORITHMS = frozenset(['rsa-sha1', 'rsa-sha256', 'rsa-sha512', 'hmac-sha1', 'hmac-sha256', 'hmac-sha512']) -HASHES = {'sha1': SHA, +HASHES = {'sha1': SHA, 'sha256': SHA256, 'sha512': SHA512} @@ -23,27 +22,23 @@ class HttpSigException(Exception): pass -""" -Constant-time string compare. -http://codahale.com/a-lesson-in-timing-attacks/ -""" -def ct_bytes_compare(a, b): - if not isinstance(a, six.binary_type): - a = a.decode('utf8') - if not isinstance(b, six.binary_type): - b = b.decode('utf8') - - if len(a) != len(b): + +def ct_bytes_compare(byte_l, byte_r): + """ + Constant-time string compare. + http://codahale.com/a-lesson-in-timing-attacks/ + """ + if not isinstance(byte_l, six.binary_type): + byte_l = byte_l.decode('utf8') + if not isinstance(byte_r, six.binary_type): + byte_r = byte_r.decode('utf8') + + if len(byte_l) != len(byte_r): return False - result = 0 - for x, y in zip(a, b): - if six.PY2: - result |= ord(x) ^ ord(y) - else: - result |= x ^ y + result = reduce(lambda r, b: r | (b[0] ^ b[1]), map(lambda b: (ord(b[0]), ord(b[1])) if six.PY2 else b, zip(byte_l, byte_r)), 0) - return (result == 0) + return result == 0 def generate_message(required_headers, headers, host=None, method=None, path=None, http_version=None): @@ -53,46 +48,46 @@ def generate_message(required_headers, headers, host=None, method=None, path=Non required_headers = ['date'] signable_list = [] - for h in required_headers: - h = h.lower() - if h == '(request-target)': # draft-03 to draft-07 + for header in required_headers: + header = header.lower() + if header == '(request-target)': # draft-03 to draft-07 if not method or not path: - raise Exception('method and path arguments required when using "(request-target)"') - signable_list.append('%s: %s %s' % (h, method.lower(), path)) - elif h == '(request-line)': # draft-02 + raise KeyError('method and path arguments required when using "(request-target)"') + signable_list.append('%s: %s %s' % (header, method.lower(), path)) + elif header == '(request-line)': # draft-02 if not method or not path: - raise Exception('method and path arguments required when using "(request-line)"') - signable_list.append('%s: %s %s' % (h, method.lower(), path)) - elif h == 'request-line': # draft-00, draft-01 + raise KeyError('method and path arguments required when using "(request-line)"') + signable_list.append('%s: %s %s' % (header, method.lower(), path)) + elif header == 'request-line': # draft-00, draft-01 if not method or not path or not http_version: - raise Exception('method and path arguments required when using "request-line"') + raise KeyError('method and path arguments required when using "request-line"') signable_list.append('%s %s %s' % (method, path, http_version)) - elif h == 'host': + elif header == 'host': # 'host' special case due to requests lib restrictions # 'host' is not available when adding auth so must use a param # if no param used, defaults back to the 'host' header if not host: if 'host' in headers: - host = headers[h] + host = headers[header] else: - raise Exception('missing required header "%s"' % (h)) - signable_list.append('%s: %s' % (h, host)) + raise KeyError('missing required header "%s"' % (header)) + signable_list.append('%s: %s' % (header, host)) else: - if h not in headers: - raise Exception('missing required header "%s"' % (h)) + if header not in headers: + raise KeyError('missing required header "%s"' % (header)) - signable_list.append('%s: %s' % (h, headers[h])) + signable_list.append('%s: %s' % (header, headers[header])) - signable = '\n'.join(signable_list).encode("ascii") + signable = '\n'.join(signable_list).encode('ascii') return signable def parse_authorization_header(header): if not isinstance(header, six.string_types): - header = header.decode("ascii") #HTTP headers cannot be Unicode. + header = header.decode('ascii') # HTTP headers cannot be Unicode. - auth = header.split(" ", 1) + auth = header.split(' ', 1) if len(auth) > 2: raise ValueError('Invalid authorization header. (eg. Method key1=value1,key2="value, \"2\"")') @@ -100,7 +95,7 @@ def parse_authorization_header(header): values = {} if len(auth) == 2: auth_value = auth[1] - if auth_value and len(auth_value): + if auth_value: # This is tricky string magic. Let urllib do it. fields = parse_http_list(auth_value) @@ -138,8 +133,8 @@ def build_signature_template(key_id, algorithm, headers): if headers: headers = [h.lower() for h in headers] param_map['headers'] = ' '.join(headers) - kv = map('{0[0]}="{0[1]}"'.format, param_map.items()) - kv_string = ','.join(kv) + kv_pairs = map('{0[0]}="{0[1]}"'.format, param_map.items()) + kv_string = ','.join(kv_pairs) sig_string = 'Signature {0}'.format(kv_string) return sig_string @@ -160,6 +155,7 @@ def __getitem__(self, key): def __contains__(self, key): return super(CaseInsensitiveDict, self).__contains__(key.lower()) + # currently busted... def get_fingerprint(key): """ @@ -177,4 +173,4 @@ def get_fingerprint(key): key = key.strip().encode('ascii') key = base64.b64decode(key) fp_plain = hashlib.md5(key).hexdigest() - return ':'.join(a+b for a,b in zip(fp_plain[::2], fp_plain[1::2])) + return ':'.join(a + b for a, b in zip(fp_plain[::2], fp_plain[1::2])) From 60833801ac2319d79dfcda11b29bc6efe3cbc1ed Mon Sep 17 00:00:00 2001 From: "Jarron Shih (RD-US)" Date: Sat, 29 Jul 2017 15:49:44 -0700 Subject: [PATCH 13/38] draft version check. --- httpsig/sign.py | 26 +++++++++++++++---- httpsig/tests/test_signature.py | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index 904a1f9..fc8aa86 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -9,7 +9,8 @@ DEFAULT_SIGN_ALGORITHM = 'hmac-sha256' -DEFAULT_VERSION = 'draft-07' +DEFAULT_DRAFT_VERSION = 'draft-07' +SUPPORTED_DRAFT_VERSION = ['draft-00', 'draft-01', 'draft-02', 'draft-03', 'draft-04', 'draft-05', 'draft-06', 'draft-07'] class Signer(object): @@ -86,14 +87,18 @@ class HeaderSigner(Signer): :arg algorithm: one of the six specified algorithms :arg headers: a list of http headers to be included in the signing string, defaulting to ['date']. ''' - def __init__(self, key_id, secret, algorithm=None, headers=None, version=DEFAULT_VERSION): + def __init__(self, key_id, secret, algorithm=None, headers=None, version=DEFAULT_DRAFT_VERSION): if algorithm is None: algorithm = DEFAULT_SIGN_ALGORITHM super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm) self.headers = headers or ['date'] self.signature_template = build_signature_template(key_id, algorithm, headers) - self.version = version + if version in SUPPORTED_DRAFT_VERSION: + self.version = version + else: + self.version = DEFAULT_DRAFT_VERSION + self._verify_headers_by_draft_version() def sign(self, headers, host=None, method=None, path=None): """ @@ -105,10 +110,21 @@ def sign(self, headers, host=None, method=None, path=None): path is the HTTP path (required when using '(request-target)'). """ headers = CaseInsensitiveDict(headers) - required_headers = self.headers or ['date'] - signable = generate_message(required_headers, headers, host, method, path) + signable = generate_message(self.headers, headers, host, method, path) signature = self._sign(signable) headers['authorization'] = self.signature_template % signature return headers + + def _verify_headers_by_draft_version(self): + HEADER_MAPING = { + 'request-line': ['draft-00', 'draft-01'], + '(request-line)': ['draft-02'], + '(request-target)': ['draft-03', 'draft-04', 'draft-05', 'draft-06', 'draft-07'] + } + + for header, drafts in HEADER_MAPING.items(): + if header in self.headers: + if self.version not in drafts: + raise KeyError('%s is not supported by %s' % (header, self.version)) diff --git a/httpsig/tests/test_signature.py b/httpsig/tests/test_signature.py index 4b65b41..41ba02e 100755 --- a/httpsig/tests/test_signature.py +++ b/httpsig/tests/test_signature.py @@ -68,3 +68,49 @@ def test_all(self): self.assertEqual(params['algorithm'], 'rsa-sha256') self.assertEqual(params['headers'], '(request-target) host date content-type content-md5 content-length') self.assertEqual(params['signature'], 'G8/Uh6BBDaqldRi3VfFfklHSFoq8CMt5NUZiepq0q66e+fS3Up3BmXn0NbUnr3L1WgAAZGplifRAJqp2LgeZ5gXNk6UX9zV3hw5BERLWscWXlwX/dvHQES27lGRCvyFv3djHP6Plfd5mhPWRkmjnvqeOOSS0lZJYFYHJz994s6w=') + + def test_PASS_verify_headers_by_draft_version(self): + testcases = { + 'draft-00': 'request-line', + 'draft-01': 'request-line', + 'draft-02': '(request-line)', + 'draft-03': '(request-target)', + 'draft-04': '(request-target)', + 'draft-05': '(request-target)', + 'draft-06': '(request-target)', + 'draft-07': '(request-target)' + } + for draft, header_req in testcases.items(): + hs = sign.HeaderSigner(key_id='Test', secret=self.key, version=draft, headers=[ + header_req, + 'host', + 'date', + 'content-type', + 'content-md5', + 'content-length' + ]) + + def test_FAIL_verify_headers_by_draft_version(self): + testcases = [ + ('draft-00', '(request-line)'), + ('draft-00', '(request-target)'), + ('draft-01', '(request-line)'), + ('draft-01', '(request-target)'), + ('draft-02', 'request-line'), + ('draft-02', '(request-target)'), + ('draft-03', 'request-line'), + ('draft-03', '(request-line)'), + ] + for draft, header_req in testcases: + try: + sign.HeaderSigner(key_id='Test', secret=self.key, version=draft, headers=[ + header_req, + 'host', + 'date', + 'content-type', + 'content-md5', + 'content-length' + ]) + self.fail('Should raise KeyError in (%s, %s)' % (draft, header_req)) + except KeyError: + pass From d305d150607c36cd59514d30812b47f7ec3701cf Mon Sep 17 00:00:00 2001 From: "Jarron Shih (RD-US)" Date: Sat, 29 Jul 2017 15:56:03 -0700 Subject: [PATCH 14/38] Rename and add more case --- httpsig/sign.py | 10 +++++----- httpsig/tests/test_signature.py | 13 ++++++++++++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index fc8aa86..c6282c6 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -9,8 +9,8 @@ DEFAULT_SIGN_ALGORITHM = 'hmac-sha256' -DEFAULT_DRAFT_VERSION = 'draft-07' -SUPPORTED_DRAFT_VERSION = ['draft-00', 'draft-01', 'draft-02', 'draft-03', 'draft-04', 'draft-05', 'draft-06', 'draft-07'] +DEFAULT_HTTPSIG_VERSION = 'draft-07' +SUPPORTED_HTTPSIG_VERSION = ['draft-00', 'draft-01', 'draft-02', 'draft-03', 'draft-04', 'draft-05', 'draft-06', 'draft-07'] class Signer(object): @@ -87,17 +87,17 @@ class HeaderSigner(Signer): :arg algorithm: one of the six specified algorithms :arg headers: a list of http headers to be included in the signing string, defaulting to ['date']. ''' - def __init__(self, key_id, secret, algorithm=None, headers=None, version=DEFAULT_DRAFT_VERSION): + def __init__(self, key_id, secret, algorithm=None, headers=None, version=DEFAULT_HTTPSIG_VERSION): if algorithm is None: algorithm = DEFAULT_SIGN_ALGORITHM super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm) self.headers = headers or ['date'] self.signature_template = build_signature_template(key_id, algorithm, headers) - if version in SUPPORTED_DRAFT_VERSION: + if version in SUPPORTED_HTTPSIG_VERSION: self.version = version else: - self.version = DEFAULT_DRAFT_VERSION + self.version = DEFAULT_HTTPSIG_VERSION self._verify_headers_by_draft_version() def sign(self, headers, host=None, method=None, path=None): diff --git a/httpsig/tests/test_signature.py b/httpsig/tests/test_signature.py index 41ba02e..92a62b0 100755 --- a/httpsig/tests/test_signature.py +++ b/httpsig/tests/test_signature.py @@ -78,7 +78,8 @@ def test_PASS_verify_headers_by_draft_version(self): 'draft-04': '(request-target)', 'draft-05': '(request-target)', 'draft-06': '(request-target)', - 'draft-07': '(request-target)' + 'draft-07': '(request-target)', + None: '(request-target)' } for draft, header_req in testcases.items(): hs = sign.HeaderSigner(key_id='Test', secret=self.key, version=draft, headers=[ @@ -100,6 +101,16 @@ def test_FAIL_verify_headers_by_draft_version(self): ('draft-02', '(request-target)'), ('draft-03', 'request-line'), ('draft-03', '(request-line)'), + ('draft-04', 'request-line'), + ('draft-04', '(request-line)'), + ('draft-05', 'request-line'), + ('draft-05', '(request-line)'), + ('draft-06', 'request-line'), + ('draft-06', '(request-line)'), + ('draft-07', 'request-line'), + ('draft-07', '(request-line)'), + (None, 'request-line'), + (None, '(request-line)'), ] for draft, header_req in testcases: try: From 0babf4670720bb63ca74a174a3734784d516af99 Mon Sep 17 00:00:00 2001 From: "Jarron Shih (RD-US)" Date: Sat, 29 Jul 2017 15:57:07 -0700 Subject: [PATCH 15/38] rename --- MANIFEST | 2 +- httpsig/tests/{test_signature.py => test_sign.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename httpsig/tests/{test_signature.py => test_sign.py} (100%) diff --git a/MANIFEST b/MANIFEST index d969ca8..daa09f5 100644 --- a/MANIFEST +++ b/MANIFEST @@ -15,6 +15,6 @@ httpsig/verify.py httpsig/tests/__init__.py httpsig/tests/rsa_private.pem httpsig/tests/rsa_public.pem -httpsig/tests/test_signature.py +httpsig/tests/test_sign.py httpsig/tests/test_utils.py httpsig/tests/test_verify.py diff --git a/httpsig/tests/test_signature.py b/httpsig/tests/test_sign.py similarity index 100% rename from httpsig/tests/test_signature.py rename to httpsig/tests/test_sign.py From bb19a9dcb35a6c428de31a50f79523e9e9adeafa Mon Sep 17 00:00:00 2001 From: "Jarron Shih (RD-US)" Date: Sat, 29 Jul 2017 15:59:52 -0700 Subject: [PATCH 16/38] rename --- httpsig/tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpsig/tests/__init__.py b/httpsig/tests/__init__.py index d9018eb..6252bd1 100644 --- a/httpsig/tests/__init__.py +++ b/httpsig/tests/__init__.py @@ -1,3 +1,3 @@ -from .test_signature import * +from .test_sign import * from .test_utils import * from .test_verify import * From 6e2556d0fa4d354409e729f0d90d32d06003d5e6 Mon Sep 17 00:00:00 2001 From: Reverb Chu Date: Sat, 29 Jul 2017 16:14:44 -0700 Subject: [PATCH 17/38] do not run tests twice with tox --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index eb541fa..51f50d7 100755 --- a/setup.py +++ b/setup.py @@ -45,5 +45,6 @@ include_package_data=True, zip_safe=True, install_requires=['pycrypto', 'six'], - test_suite="httpsig.tests", + test_suite='nose.collector', + tests_require=['nose'] ) From ec3929dcf59ae4ae78f381dc937acc936b62929b Mon Sep 17 00:00:00 2001 From: "Jarron Shih (RD-US)" Date: Sat, 29 Jul 2017 16:20:03 -0700 Subject: [PATCH 18/38] rename --- .eggs/README.txt | 6 ++++++ httpsig/requests_auth.py | 11 ++++++++--- httpsig/sign.py | 12 ++++++------ httpsig/tests/test_sign.py | 10 +++++----- 4 files changed, 25 insertions(+), 14 deletions(-) create mode 100644 .eggs/README.txt diff --git a/.eggs/README.txt b/.eggs/README.txt new file mode 100644 index 0000000..5d01668 --- /dev/null +++ b/.eggs/README.txt @@ -0,0 +1,6 @@ +This directory contains eggs that were downloaded by setuptools to build, test, and run plug-ins. + +This directory caches those eggs to prevent repeated downloads. + +However, it is safe to delete this directory. + diff --git a/httpsig/requests_auth.py b/httpsig/requests_auth.py index 3ed3cf7..98bbb21 100644 --- a/httpsig/requests_auth.py +++ b/httpsig/requests_auth.py @@ -6,7 +6,7 @@ # Python 2 from urlparse import urlparse -from .sign import HeaderSigner, DEFAULT_VERSION +from .sign import HeaderSigner class HTTPSignatureAuth(AuthBase): @@ -19,9 +19,14 @@ class HTTPSignatureAuth(AuthBase): algorithm is one of the six specified algorithms headers is a list of http headers to be included in the signing string, defaulting to "Date" alone. ''' - def __init__(self, key_id='', secret='', algorithm=None, headers=None, version=DEFAULT_VERSION): + def __init__(self, key_id='', secret='', algorithm=None, headers=None, httpsig_version=None): headers = headers or [] - self.header_signer = HeaderSigner(key_id=key_id, secret=secret, algorithm=algorithm, headers=headers, version=version) + self.header_signer = HeaderSigner( + key_id=key_id, + secret=secret, + algorithm=algorithm, + headers=headers, + httpsig_version=httpsig_version) self.uses_host = 'host' in [h.lower() for h in headers] def __call__(self, request): diff --git a/httpsig/sign.py b/httpsig/sign.py index c6282c6..44917c8 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -87,17 +87,17 @@ class HeaderSigner(Signer): :arg algorithm: one of the six specified algorithms :arg headers: a list of http headers to be included in the signing string, defaulting to ['date']. ''' - def __init__(self, key_id, secret, algorithm=None, headers=None, version=DEFAULT_HTTPSIG_VERSION): + def __init__(self, key_id, secret, algorithm=None, headers=None, httpsig_version=DEFAULT_HTTPSIG_VERSION): if algorithm is None: algorithm = DEFAULT_SIGN_ALGORITHM super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm) self.headers = headers or ['date'] self.signature_template = build_signature_template(key_id, algorithm, headers) - if version in SUPPORTED_HTTPSIG_VERSION: - self.version = version + if httpsig_version in SUPPORTED_HTTPSIG_VERSION: + self.httpsig_version = httpsig_version else: - self.version = DEFAULT_HTTPSIG_VERSION + self.httpsig_version = DEFAULT_HTTPSIG_VERSION self._verify_headers_by_draft_version() def sign(self, headers, host=None, method=None, path=None): @@ -126,5 +126,5 @@ def _verify_headers_by_draft_version(self): for header, drafts in HEADER_MAPING.items(): if header in self.headers: - if self.version not in drafts: - raise KeyError('%s is not supported by %s' % (header, self.version)) + if self.httpsig_version not in drafts: + raise KeyError('%s is not supported by %s' % (header, self.httpsig_version)) diff --git a/httpsig/tests/test_sign.py b/httpsig/tests/test_sign.py index 92a62b0..c303412 100755 --- a/httpsig/tests/test_sign.py +++ b/httpsig/tests/test_sign.py @@ -81,8 +81,8 @@ def test_PASS_verify_headers_by_draft_version(self): 'draft-07': '(request-target)', None: '(request-target)' } - for draft, header_req in testcases.items(): - hs = sign.HeaderSigner(key_id='Test', secret=self.key, version=draft, headers=[ + for httpsig_version, header_req in testcases.items(): + hs = sign.HeaderSigner(key_id='Test', secret=self.key, httpsig_version=httpsig_version, headers=[ header_req, 'host', 'date', @@ -112,9 +112,9 @@ def test_FAIL_verify_headers_by_draft_version(self): (None, 'request-line'), (None, '(request-line)'), ] - for draft, header_req in testcases: + for httpsig_version, header_req in testcases: try: - sign.HeaderSigner(key_id='Test', secret=self.key, version=draft, headers=[ + sign.HeaderSigner(key_id='Test', secret=self.key, httpsig_version=httpsig_version, headers=[ header_req, 'host', 'date', @@ -122,6 +122,6 @@ def test_FAIL_verify_headers_by_draft_version(self): 'content-md5', 'content-length' ]) - self.fail('Should raise KeyError in (%s, %s)' % (draft, header_req)) + self.fail('Should raise KeyError in (%s, %s)' % (httpsig_version, header_req)) except KeyError: pass From 50faa4e0ed848a713d0ec059ee02354cf28e04ff Mon Sep 17 00:00:00 2001 From: "Jarron Shih (RD-US)" Date: Sat, 29 Jul 2017 16:22:00 -0700 Subject: [PATCH 19/38] gitignore .eggs --- .eggs/README.txt | 6 ------ .gitignore | 1 + 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 .eggs/README.txt diff --git a/.eggs/README.txt b/.eggs/README.txt deleted file mode 100644 index 5d01668..0000000 --- a/.eggs/README.txt +++ /dev/null @@ -1,6 +0,0 @@ -This directory contains eggs that were downloaded by setuptools to build, test, and run plug-ins. - -This directory caches those eggs to prevent repeated downloads. - -However, it is safe to delete this directory. - diff --git a/.gitignore b/.gitignore index 633bd66..edd24ad 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *~ .noseids .tox +.eggs build/ dist/ doc/__build/* From b5f686a0fdf756fda5465268e2ad2c9f214297dd Mon Sep 17 00:00:00 2001 From: Reverb Chu Date: Sat, 29 Jul 2017 16:31:54 -0700 Subject: [PATCH 20/38] integrate coveralls.io --- .travis.yml | 6 +++++- README.rst | 6 ++++++ tox.ini | 7 ++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1ea31a4..e9a7813 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,4 +9,8 @@ python: install: - pip install . - pip install nose -script: nosetests + - pip install coveralls +script: + coverage run --source=httpsig setup.py test +after_success: + coveralls diff --git a/README.rst b/README.rst index cb885fd..3522153 100644 --- a/README.rst +++ b/README.rst @@ -4,9 +4,15 @@ httpsig .. image:: https://travis-ci.org/GeekMap/httpsig.svg?branch=master :target: https://travis-ci.org/GeekMap/httpsig +.. image:: https://coveralls.io/repos/github/GeekMap/httpsig/badge.svg?branch=master +:target: https://coveralls.io/github/GeekMap/httpsig?branch=master + .. image:: https://travis-ci.org/GeekMap/httpsig.svg?branch=dev :target: https://travis-ci.org/GeekMap/httpsig +.. image:: https://coveralls.io/repos/github/GeekMap/httpsig/badge.svg?branch=dev +:target: https://coveralls.io/github/GeekMap/httpsig?branch=dev + Sign HTTP requests with secure signatures according to the IETF HTTP Signatures specification (`Draft 3`_). This is a fork of the original module_ to fully support both RSA and HMAC schemes as well as unit test both schemes to prove they work. It's being used in production and is actively-developed. See the original project_, original Python module_, original spec_, and `current IETF draft`_ for more details on the signing scheme. diff --git a/tox.ini b/tox.ini index b170c60..b080fc5 100644 --- a/tox.ini +++ b/tox.ini @@ -2,4 +2,9 @@ envlist = py27, py32, py33, py34, py35, py36 [testenv] -commands = python setup.py test +passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH +deps = + coveralls +commands = + coverage run --source=httpsig setup.py test + coveralls From 9cb9b8c757314051674ea1d1c2af84b2f0a7d05a Mon Sep 17 00:00:00 2001 From: Reverb Chu Date: Sat, 29 Jul 2017 16:48:39 -0700 Subject: [PATCH 21/38] drop py32 support --- .travis.yml | 1 - README.rst | 6 +++--- setup.py | 1 - 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index e9a7813..53af4b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - "2.7" - - "3.2" - "3.3" - "3.4" - "3.5" diff --git a/README.rst b/README.rst index 3522153..8f8ef99 100644 --- a/README.rst +++ b/README.rst @@ -5,13 +5,13 @@ httpsig :target: https://travis-ci.org/GeekMap/httpsig .. image:: https://coveralls.io/repos/github/GeekMap/httpsig/badge.svg?branch=master -:target: https://coveralls.io/github/GeekMap/httpsig?branch=master + :target: https://coveralls.io/github/GeekMap/httpsig?branch=master .. image:: https://travis-ci.org/GeekMap/httpsig.svg?branch=dev :target: https://travis-ci.org/GeekMap/httpsig .. image:: https://coveralls.io/repos/github/GeekMap/httpsig/badge.svg?branch=dev -:target: https://coveralls.io/github/GeekMap/httpsig?branch=dev + :target: https://coveralls.io/github/GeekMap/httpsig?branch=dev Sign HTTP requests with secure signatures according to the IETF HTTP Signatures specification (`Draft 3`_). This is a fork of the original module_ to fully support both RSA and HMAC schemes as well as unit test both schemes to prove they work. It's being used in production and is actively-developed. @@ -26,7 +26,7 @@ See the original project_, original Python module_, original spec_, and `current Requirements ------------ -* Python 2.7, 3.2, 3.3, 3.4, 3.5, 3.6 +* Python 2.7, 3.3, 3.4, 3.5, 3.6 * PyCrypto_ Optional: diff --git a/setup.py b/setup.py index 51f50d7..5c90a9a 100755 --- a/setup.py +++ b/setup.py @@ -28,7 +28,6 @@ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", From 40e799fccf066408aeadb34625ba35174cc0672f Mon Sep 17 00:00:00 2001 From: "Jarron Shih (RD-US)" Date: Sat, 29 Jul 2017 17:10:10 -0700 Subject: [PATCH 22/38] mv unittest --- .gitignore | 2 ++ MANIFEST | 6 ------ MANIFEST.in | 5 ----- {httpsig => tests}/tests/__init__.py | 0 {httpsig => tests}/tests/rsa_private.pem | 0 {httpsig => tests}/tests/rsa_public.pem | 0 {httpsig => tests}/tests/test_sign.py | 0 {httpsig => tests}/tests/test_utils.py | 0 {httpsig => tests}/tests/test_verify.py | 0 9 files changed, 2 insertions(+), 11 deletions(-) delete mode 100644 MANIFEST.in rename {httpsig => tests}/tests/__init__.py (100%) rename {httpsig => tests}/tests/rsa_private.pem (100%) rename {httpsig => tests}/tests/rsa_public.pem (100%) rename {httpsig => tests}/tests/test_sign.py (100%) rename {httpsig => tests}/tests/test_utils.py (100%) rename {httpsig => tests}/tests/test_verify.py (100%) diff --git a/.gitignore b/.gitignore index edd24ad..1854034 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .noseids .tox .eggs +.coverage build/ dist/ doc/__build/* @@ -12,3 +13,4 @@ doc/__build/* *_rsa.pub locale/ pip-log.txt +MANIFEST diff --git a/MANIFEST b/MANIFEST index daa09f5..a78b1e3 100644 --- a/MANIFEST +++ b/MANIFEST @@ -12,9 +12,3 @@ httpsig/requests_auth.py httpsig/sign.py httpsig/utils.py httpsig/verify.py -httpsig/tests/__init__.py -httpsig/tests/rsa_private.pem -httpsig/tests/rsa_public.pem -httpsig/tests/test_sign.py -httpsig/tests/test_utils.py -httpsig/tests/test_verify.py diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 20b80ef..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -include *.rst -include *.txt -include versioneer.py -include httpsig/_version.py -include httpsig/tests/*.pem diff --git a/httpsig/tests/__init__.py b/tests/tests/__init__.py similarity index 100% rename from httpsig/tests/__init__.py rename to tests/tests/__init__.py diff --git a/httpsig/tests/rsa_private.pem b/tests/tests/rsa_private.pem similarity index 100% rename from httpsig/tests/rsa_private.pem rename to tests/tests/rsa_private.pem diff --git a/httpsig/tests/rsa_public.pem b/tests/tests/rsa_public.pem similarity index 100% rename from httpsig/tests/rsa_public.pem rename to tests/tests/rsa_public.pem diff --git a/httpsig/tests/test_sign.py b/tests/tests/test_sign.py similarity index 100% rename from httpsig/tests/test_sign.py rename to tests/tests/test_sign.py diff --git a/httpsig/tests/test_utils.py b/tests/tests/test_utils.py similarity index 100% rename from httpsig/tests/test_utils.py rename to tests/tests/test_utils.py diff --git a/httpsig/tests/test_verify.py b/tests/tests/test_verify.py similarity index 100% rename from httpsig/tests/test_verify.py rename to tests/tests/test_verify.py From 8f5d449165e5906dd5e0b5f8c4e0e1604b9282fb Mon Sep 17 00:00:00 2001 From: "Jarron Shih (RD-US)" Date: Sat, 29 Jul 2017 17:27:46 -0700 Subject: [PATCH 23/38] config change --- MANIFEST | 14 -------------- MANIFEST.in | 4 ++++ tox.ini | 4 ++-- 3 files changed, 6 insertions(+), 16 deletions(-) delete mode 100644 MANIFEST create mode 100644 MANIFEST.in diff --git a/MANIFEST b/MANIFEST deleted file mode 100644 index a78b1e3..0000000 --- a/MANIFEST +++ /dev/null @@ -1,14 +0,0 @@ -# file GENERATED by distutils, do NOT edit -CHANGELOG.rst -LICENSE.txt -README.rst -requirements.txt -setup.cfg -setup.py -versioneer.py -httpsig/__init__.py -httpsig/_version.py -httpsig/requests_auth.py -httpsig/sign.py -httpsig/utils.py -httpsig/verify.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..6226efd --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include *.rst +include *.txt +include versioneer.py +include httpsig/_version.py diff --git a/tox.ini b/tox.ini index b080fc5..86932b2 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ envlist = py27, py32, py33, py34, py35, py36 [testenv] passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH deps = - coveralls + coverage commands = coverage run --source=httpsig setup.py test - coveralls + coverage report -m From 7f9e22d24d0a6604e8f8ac69fd95873ab398d473 Mon Sep 17 00:00:00 2001 From: "Jarron Shih (RD-US)" Date: Sat, 29 Jul 2017 17:32:09 -0700 Subject: [PATCH 24/38] Add coveragerc. Omit unrelated python file. --- .coveragerc | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..bf620e1 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,4 @@ +[run] +omit = + */_version.py + */__init__.py From 768ad4e4ee49991895bc0b9617e0f5698dd1ef3d Mon Sep 17 00:00:00 2001 From: "Jarron Shih (RD-US)" Date: Sat, 29 Jul 2017 17:44:54 -0700 Subject: [PATCH 25/38] mv tests --- tests/{tests => }/__init__.py | 0 tests/{tests => }/rsa_private.pem | 0 tests/{tests => }/rsa_public.pem | 0 tests/{tests => }/test_sign.py | 0 tests/{tests => }/test_utils.py | 0 tests/{tests => }/test_verify.py | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename tests/{tests => }/__init__.py (100%) rename tests/{tests => }/rsa_private.pem (100%) rename tests/{tests => }/rsa_public.pem (100%) rename tests/{tests => }/test_sign.py (100%) rename tests/{tests => }/test_utils.py (100%) rename tests/{tests => }/test_verify.py (100%) diff --git a/tests/tests/__init__.py b/tests/__init__.py similarity index 100% rename from tests/tests/__init__.py rename to tests/__init__.py diff --git a/tests/tests/rsa_private.pem b/tests/rsa_private.pem similarity index 100% rename from tests/tests/rsa_private.pem rename to tests/rsa_private.pem diff --git a/tests/tests/rsa_public.pem b/tests/rsa_public.pem similarity index 100% rename from tests/tests/rsa_public.pem rename to tests/rsa_public.pem diff --git a/tests/tests/test_sign.py b/tests/test_sign.py similarity index 100% rename from tests/tests/test_sign.py rename to tests/test_sign.py diff --git a/tests/tests/test_utils.py b/tests/test_utils.py similarity index 100% rename from tests/tests/test_utils.py rename to tests/test_utils.py diff --git a/tests/tests/test_verify.py b/tests/test_verify.py similarity index 100% rename from tests/tests/test_verify.py rename to tests/test_verify.py From e6a9e63f70b2f312653715446708a49f5c10e4cf Mon Sep 17 00:00:00 2001 From: Reverb Chu Date: Sat, 29 Jul 2017 17:57:00 -0700 Subject: [PATCH 26/38] set default algo --- httpsig/sign.py | 10 ++-------- tests/test_sign.py | 7 ++----- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index 44917c8..dc916d8 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -20,10 +20,7 @@ class Signer(object): Password-protected keyfiles are not supported. """ - def __init__(self, secret, algorithm=None): - if algorithm is None: - algorithm = DEFAULT_SIGN_ALGORITHM - + def __init__(self, secret, algorithm=DEFAULT_SIGN_ALGORITHM): assert algorithm in ALGORITHMS, 'Unknown algorithm' if isinstance(secret, six.string_types): secret = secret.encode('ascii') @@ -87,10 +84,7 @@ class HeaderSigner(Signer): :arg algorithm: one of the six specified algorithms :arg headers: a list of http headers to be included in the signing string, defaulting to ['date']. ''' - def __init__(self, key_id, secret, algorithm=None, headers=None, httpsig_version=DEFAULT_HTTPSIG_VERSION): - if algorithm is None: - algorithm = DEFAULT_SIGN_ALGORITHM - + def __init__(self, key_id, secret, algorithm=DEFAULT_SIGN_ALGORITHM, headers=None, httpsig_version=DEFAULT_HTTPSIG_VERSION): super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm) self.headers = headers or ['date'] self.signature_template = build_signature_template(key_id, algorithm, headers) diff --git a/tests/test_sign.py b/tests/test_sign.py index c303412..a4d9dee 100755 --- a/tests/test_sign.py +++ b/tests/test_sign.py @@ -10,9 +10,6 @@ from httpsig.utils import parse_authorization_header -sign.DEFAULT_SIGN_ALGORITHM = "rsa-sha256" - - class TestSign(unittest.TestCase): def setUp(self): @@ -21,7 +18,7 @@ def setUp(self): self.key = f.read() def test_default(self): - hs = sign.HeaderSigner(key_id='Test', secret=self.key) + hs = sign.HeaderSigner(key_id='Test', secret=self.key, algorithm='rsa-sha256') unsigned = { 'Date': 'Thu, 05 Jan 2012 21:31:40 GMT' } @@ -39,7 +36,7 @@ def test_default(self): self.assertEqual(params['signature'], 'ATp0r26dbMIxOopqw0OfABDT7CKMIoENumuruOtarj8n/97Q3htHFYpH8yOSQk3Z5zh8UxUym6FYTb5+A0Nz3NRsXJibnYi7brE/4tx5But9kkFGzG+xpUmimN4c3TMN7OFH//+r8hBf7BT9/GmHDUVZT2JzWGLZES2xDOUuMtA=') def test_all(self): - hs = sign.HeaderSigner(key_id='Test', secret=self.key, headers=[ + hs = sign.HeaderSigner(key_id='Test', secret=self.key, algorithm='rsa-sha256', headers=[ '(request-target)', 'host', 'date', From d03f6997b71b0b28502127e5daa0e7995018bf98 Mon Sep 17 00:00:00 2001 From: Reverb Chu Date: Sat, 29 Jul 2017 19:47:06 -0700 Subject: [PATCH 27/38] add unit test for utils.py --- httpsig/utils.py | 46 +++++++------ setup.cfg | 5 +- tests/test_utils.py | 155 ++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 170 insertions(+), 36 deletions(-) diff --git a/httpsig/utils.py b/httpsig/utils.py index ccbfcc9..d004e8e 100644 --- a/httpsig/utils.py +++ b/httpsig/utils.py @@ -7,7 +7,7 @@ try: # Python 3 from urllib.request import parse_http_list -except ImportError: +except ImportError: # pragma: no cover # Python 2 from urllib2 import parse_http_list @@ -29,9 +29,9 @@ def ct_bytes_compare(byte_l, byte_r): http://codahale.com/a-lesson-in-timing-attacks/ """ if not isinstance(byte_l, six.binary_type): - byte_l = byte_l.decode('utf8') + byte_l = byte_l.encode('utf8') if not isinstance(byte_r, six.binary_type): - byte_r = byte_r.decode('utf8') + byte_r = byte_r.encode('utf8') if len(byte_l) != len(byte_r): return False @@ -60,7 +60,7 @@ def generate_message(required_headers, headers, host=None, method=None, path=Non signable_list.append('%s: %s %s' % (header, method.lower(), path)) elif header == 'request-line': # draft-00, draft-01 if not method or not path or not http_version: - raise KeyError('method and path arguments required when using "request-line"') + raise KeyError('method, path and http_version arguments required when using "request-line"') signable_list.append('%s %s %s' % (method, path, http_version)) elif header == 'host': @@ -88,30 +88,28 @@ def parse_authorization_header(header): header = header.decode('ascii') # HTTP headers cannot be Unicode. auth = header.split(' ', 1) - if len(auth) > 2: + if len(auth) < 2: raise ValueError('Invalid authorization header. (eg. Method key1=value1,key2="value, \"2\"")') # Split up any args into a dictionary. values = {} - if len(auth) == 2: - auth_value = auth[1] - if auth_value: - # This is tricky string magic. Let urllib do it. - fields = parse_http_list(auth_value) - - for item in fields: - # Only include keypairs. - if '=' in item: - # Split on the first '=' only. - key, value = item.split('=', 1) - if not (len(key) and len(value)): - continue - - # Unquote values, if quoted. - if value[0] == '"': - value = value[1:-1] - - values[key] = value + auth_value = auth[1] + + # This is tricky string magic. Let urllib do it. + fields = parse_http_list(auth_value) + for item in fields: + # Only include keypairs. + if '=' in item: + # Split on the first '=' only. + key, value = item.split('=', 1) + if not (len(key) and len(value)): + continue + + # Unquote values, if quoted. + if value[0] == '"': + value = value[1:-1] + + values[key] = value # ("Signature", {"headers": "date", "algorithm": "hmac-sha256", ... }) return (auth[0], CaseInsensitiveDict(values)) diff --git a/setup.cfg b/setup.cfg index 3cbd55d..c1b9862 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,5 @@ [bdist_wheel] -universal = True \ No newline at end of file +universal = True + +[nosetests] +nocapture = 1 diff --git a/tests/test_utils.py b/tests/test_utils.py index 5040a9d..e2e50cd 100755 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,20 +1,35 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- import os -import re -import sys -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - import unittest +import six import httpsig.utils class TestUtils(unittest.TestCase): - def test_get_fingerprint(self): - with open(os.path.join(os.path.dirname(__file__), 'rsa_public.pem'), 'r') as k: - key = k.read() - fingerprint = httpsig.utils.get_fingerprint(key) - self.assertEqual(fingerprint, "73:61:a2:21:67:e0:df:be:7e:4b:93:1e:15:98:a5:b7") + def test_ct_bytes_compare(self): + if six.PY3: + test_cases = [ + (b"\xa5Q\x7f\x92q\x8b+\xd5\x83x'^", b"\xa5Q\x7f\x92q\x8b+\xd5\x83x'^", True), + ('123', '123', True), + (b'123', '123', True), + (b'123', b'123', True), + (b'\xe5\xb0\x8d', '對', True), + (b'123456', b'123', False) + ] + elif six.PY2: + test_cases = [ + ("\xa5Q\x7f\x92q\x8b+\xd5\x83x'^", "\xa5Q\x7f\x92q\x8b+\xd5\x83x'^", True), + ('123', '123', True), + ('123', u'123', True), + (u'123', u'123', True), + ('\xe5\xb0\x8d', u'對', True), + ('123456', '123', False) + ] + + for case in test_cases: + self.assertIs(httpsig.utils.ct_bytes_compare(case[0], case[1]), case[2]) def test_generate_message(self): HOST = "example.org" @@ -28,7 +43,7 @@ def test_generate_message(self): 'Digest': 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=', 'Content-Length': 18 } - cases = { + good_test_cases = { # from draft-01: 3.1.2. RSA Example 'request-line host date digest content-length': b'POST /foo HTTP/1.1\nhost: example.org\ndate: Tue, 07 Jun 2014 20:51:35 GMT\ndigest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=\ncontent-length: 18', # draft-02 @@ -37,7 +52,7 @@ def test_generate_message(self): '(request-target) host date digest content-length': b'(request-target): post /foo\nhost: example.org\ndate: Tue, 07 Jun 2014 20:51:35 GMT\ndigest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=\ncontent-length: 18' } - for required_headers, result in cases.items(): + for required_headers, result in good_test_cases.items(): assert httpsig.utils.generate_message( required_headers=required_headers.split(), headers=HEADERS, @@ -45,3 +60,121 @@ def test_generate_message(self): method=METHOD, path=PATH, http_version=HTTP_VERSION) == result, 'header: %s\nexpect: %s\n' % (required_headers, result) + + exception_test_cases = [ + {'required_headers': ['(request-target)'], 'host': HOST, 'headers': HEADERS, 'exception': KeyError, 'method': '', 'path': '', 'http_version': ''}, + {'required_headers': ['(request-target)'], 'host': HOST, 'headers': HEADERS, 'exception': KeyError, 'method': 'GET', 'path': '', 'http_version': ''}, + {'required_headers': ['(request-target)'], 'host': HOST, 'headers': HEADERS, 'exception': KeyError, 'method': '', 'path': '/', 'http_version': ''}, + {'required_headers': ['(request-line)'], 'host': HOST, 'headers': HEADERS, 'exception': KeyError, 'method': '', 'path': ''}, + {'required_headers': ['(request-line)'], 'host': HOST, 'headers': HEADERS, 'exception': KeyError, 'method': 'GET', 'path': '', 'http_version': ''}, + {'required_headers': ['(request-line)'], 'host': HOST, 'headers': HEADERS, 'exception': KeyError, 'method': '', 'path': '/', 'http_version': ''}, + {'required_headers': ['request-line'], 'host': HOST, 'headers': HEADERS, 'exception': KeyError, 'method': '', 'path': '', 'http_version': ''}, + {'required_headers': ['request-line'], 'host': HOST, 'headers': HEADERS, 'exception': KeyError, 'method': 'GET', 'path': '', 'http_version': ''}, + {'required_headers': ['request-line'], 'host': HOST, 'headers': HEADERS, 'exception': KeyError, 'method': '', 'path': '/', 'http_version': ''}, + {'required_headers': ['request-line'], 'host': HOST, 'headers': HEADERS, 'exception': KeyError, 'method': '', 'path': '', 'http_version': 'HTTP/1.1'}, + {'required_headers': ['request-line'], 'host': HOST, 'headers': HEADERS, 'exception': KeyError, 'method': 'GET', 'path': '', 'http_version': 'HTTP/1.1'}, + {'required_headers': ['request-line'], 'host': HOST, 'headers': HEADERS, 'exception': KeyError, 'method': '', 'path': '/', 'http_version': 'HTTP/1.1'}, + {'required_headers': [], 'host': HOST, 'headers': [], 'exception': KeyError, 'method': '', 'path': '', 'http_version': ''}, + {'required_headers': ['haha'], 'host': HOST, 'headers': [], 'exception': KeyError, 'method': '', 'path': '', 'http_version': ''}, + {'required_headers': ['host'], 'host': '', 'headers': [], 'exception': KeyError, 'method': '', 'path': '', 'http_version': ''} + ] + for case in exception_test_cases: + try: + httpsig.utils.generate_message( + required_headers=case['required_headers'], + headers=case['headers'], + host=case['host'], + method=case['method'], + path=case['path'], + http_version=case['http_version']) + self.fail('No exception was raised') + except Exception as ex: + self.assertIsInstance(ex, case['exception'], str(case)) + + def test_parse_authorization_header(self): + if six.PY3: + good_test_cases = [ + ( + 'Signature keyId="Test",algorithm="rsa-sha512",headers="(request-target) host date content-type content-md5 content-length"', + ('Signature', httpsig.utils.CaseInsensitiveDict({ + 'keyId': 'Test', + 'algorithm': 'rsa-sha512', + 'headers': '(request-target) host date content-type content-md5 content-length'})) + ), + ( + b'Signature keyId="Test",algorithm="rsa-sha512",headers="(request-target) host date content-type content-md5 content-length"', + ('Signature', httpsig.utils.CaseInsensitiveDict({ + 'keyId': 'Test', + 'algorithm': 'rsa-sha512', + 'headers': '(request-target) host date content-type content-md5 content-length'})) + ), + ( + 'Signature keyId="Test",algorithm=,headers="(request-target) host date content-type content-md5 content-length",=123', + ('Signature', httpsig.utils.CaseInsensitiveDict({ + 'keyId': 'Test', + 'headers': '(request-target) host date content-type content-md5 content-length'})) + ), + ( + 'Signature keyId="Test", algorithm="rsa-sha512" ,headers="(request-target) host date content-type content-md5 content-length"', + ('Signature', httpsig.utils.CaseInsensitiveDict({ + 'keyId': 'Test', + 'algorithm': 'rsa-sha512', + 'headers': '(request-target) host date content-type content-md5 content-length'})) + ) + ] + elif six.PY2: + good_test_cases = [ + ( + 'Signature keyId="Test",algorithm="rsa-sha512",headers="(request-target) host date content-type content-md5 content-length"', + ('Signature', httpsig.utils.CaseInsensitiveDict({ + 'keyId': 'Test', + 'algorithm': 'rsa-sha512', + 'headers': '(request-target) host date content-type content-md5 content-length'})) + ), + ( + u'Signature keyId="Test",algorithm="rsa-sha512",headers="(request-target) host date content-type content-md5 content-length"', + ('Signature', httpsig.utils.CaseInsensitiveDict({ + 'keyId': 'Test', + 'algorithm': 'rsa-sha512', + 'headers': '(request-target) host date content-type content-md5 content-length'})) + ), + ( + 'Signature keyId="Test",algorithm=,headers="(request-target) host date content-type content-md5 content-length",=123', + ('Signature', httpsig.utils.CaseInsensitiveDict({ + 'keyId': 'Test', + 'headers': '(request-target) host date content-type content-md5 content-length'})) + ), + ( + 'Signature keyId="Test", algorithm="rsa-sha512" ,headers="(request-target) host date content-type content-md5 content-length"', + ('Signature', httpsig.utils.CaseInsensitiveDict({ + 'keyId': 'Test', + 'algorithm': 'rsa-sha512', + 'headers': '(request-target) host date content-type content-md5 content-length'})) + ) + ] + + for case in good_test_cases: + self.assertEqual(httpsig.utils.parse_authorization_header(case[0]), case[1]) + + exception_test_cases = [ + ('Signature-keyId="Test",algorithm="rsa-sha512"', ValueError), + ('', ValueError), + (123, AttributeError) + ] + for case in exception_test_cases: + try: + httpsig.utils.parse_authorization_header(case[0]) + self.fail('No exception was raised.') + except Exception as ex: + self.assertIsInstance(ex, case[1]) + + def test_get_fingerprint(self): + with open(os.path.join(os.path.dirname(__file__), 'rsa_public.pem'), 'r') as k: + key = k.read() + fingerprint = httpsig.utils.get_fingerprint(key) + self.assertEqual(fingerprint, "73:61:a2:21:67:e0:df:be:7e:4b:93:1e:15:98:a5:b7") + + test_case = [('ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFuqGVUQjw8KtwY+JIZx4uGhy2ap4QLjtUOaH/o8vxUeAk7P5Olhxzr2FBnCUjS6iZmuzZzviXI3NhyR2ic661hFlXxkJaEa6DruRakZ6P+uMFPmvE+RsOp0ppcW2uGO5Y8C0OqEMI4NT2E4/LIzM7kmspF7cvJajUa9UQ6ZpKG/YfZpOs6xug8uT+1GCiPC+w/GX2UWtj2kmTUUJZWddSev9kHUDbPl6GwLMmnJ3UB9C7rdNlhupArJAsL+7eAXR9DcV5Fo7kDtFYiZllwRAWVghVjeGyEkSOEEbVCtI+l+V2cFYlta1mPXJQsshKvzYQ25IjnDBnXGg/HpwCppWT httpsig', '99:18:ae:30:be:c4:12:ce:b5:17:2b:56:ee:a9:ab:23')] + + for case in test_case: + self.assertEqual(httpsig.utils.get_fingerprint(case[0]), case[1]) From b4b4dbd5e10db644770d16f6c6c8b072028c83ec Mon Sep 17 00:00:00 2001 From: "Jarron Shih (RD-US)" Date: Sat, 29 Jul 2017 20:11:10 -0700 Subject: [PATCH 28/38] test case for request_auth --- .travis.yml | 1 - setup.py | 2 +- tests/test_request_auth.py | 47 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 tests/test_request_auth.py diff --git a/.travis.yml b/.travis.yml index 53af4b4..d4aba40 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,6 @@ python: - "3.6" install: - pip install . - - pip install nose - pip install coveralls script: coverage run --source=httpsig setup.py test diff --git a/setup.py b/setup.py index 5c90a9a..e895820 100755 --- a/setup.py +++ b/setup.py @@ -45,5 +45,5 @@ zip_safe=True, install_requires=['pycrypto', 'six'], test_suite='nose.collector', - tests_require=['nose'] + tests_require=['nose', 'requests'] ) diff --git a/tests/test_request_auth.py b/tests/test_request_auth.py new file mode 100644 index 0000000..1f82a92 --- /dev/null +++ b/tests/test_request_auth.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +import sys +import os +import unittest +import requests + +from httpsig.requests_auth import HTTPSignatureAuth + + +class TestHTTPSignatureAuth(unittest.TestCase): + def setUp(self): + private_key_path = os.path.join(os.path.dirname(__file__), 'rsa_private.pem') + with open(private_key_path, 'rb') as f: + private_key = f.read() + + public_key_path = os.path.join(os.path.dirname(__file__), 'rsa_public.pem') + with open(public_key_path, 'rb') as f: + public_key = f.read() + + self.keyId = "Test" + self.algorithm = "rsa-sha256" + self.sign_secret = private_key + self.verify_secret = public_key + + def test__call_(self): + auth = HTTPSignatureAuth( + key_id=self.keyId, + secret=self.sign_secret, + algorithm=self.algorithm, + httpsig_version='draft-07', + headers=[ + 'date' + ]) + + request = requests.Request( + method='post', + url='http://example.com/foo?param=value&pet=dog', + auth=auth, + headers={ + 'host': 'example.com', + 'date': 'Thu, 05 Jan 2012 21:31:40 GMT', + 'content-type': 'application/json', + 'digest': 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=', + 'content-length': '18' + }).prepare() + + assert 'ATp0r26dbMIxOopqw0OfABDT7CKMIoENumuruOtarj8n/97Q3htHFYpH8yOSQk3Z5zh8UxUym6FYTb5+A0Nz3NRsXJibnYi7brE/4tx5But9kkFGzG+xpUmimN4c3TMN7OFH//+r8hBf7BT9/GmHDUVZT2JzWGLZES2xDOUuMtA=' in request.headers['authorization'] From e6d99fc6e4d32e88c14910774e773c439b99a7df Mon Sep 17 00:00:00 2001 From: "Jarron Shih (RD-US)" Date: Sat, 29 Jul 2017 20:24:16 -0700 Subject: [PATCH 29/38] import cover ignore --- httpsig/requests_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpsig/requests_auth.py b/httpsig/requests_auth.py index 98bbb21..1ddee49 100644 --- a/httpsig/requests_auth.py +++ b/httpsig/requests_auth.py @@ -2,7 +2,7 @@ try: # Python 3 from urllib.parse import urlparse -except ImportError: +except ImportError: # pragma: no cover # Python 2 from urlparse import urlparse From f30bf7ddd983ae7e5ba15bdf170c8162456cc3bf Mon Sep 17 00:00:00 2001 From: "Jarron Shih (RD-US)" Date: Sat, 29 Jul 2017 20:42:04 -0700 Subject: [PATCH 30/38] README update --- README.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 8f8ef99..df59540 100644 --- a/README.rst +++ b/README.rst @@ -88,17 +88,21 @@ Note that keys and secrets should be bytes objects. At attempt will be made to httpsig.Signer(secret, algorithm='rsa-sha256') ``secret``, in the case of an RSA signature, is a string containing private RSA pem. In the case of HMAC, it is a secret password. -``algorithm`` is one of the six allowed signatures: ``rsa-sha1``, ``rsa-sha256``, ``rsa-sha512``, ``hmac-sha1``, ``hmac-sha256``, -``hmac-sha512``. + +``algorithm`` is one of the six allowed signatures: ``rsa-sha1``, ``rsa-sha256``, ``rsa-sha512``, ``hmac-sha1``, ``hmac-sha256``, ``hmac-sha512``. .. code:: python - httpsig.requests_auth.HTTPSignatureAuth(key_id, secret, algorithm='rsa-sha256', headers=None) + httpsig.requests_auth.HTTPSignatureAuth(key_id, secret, algorithm='rsa-sha256', headers=None, httpsig_version=None) ``key_id`` is the label by which the server system knows your RSA signature or password. + ``headers`` is the list of HTTP headers that are concatenated and used as signing objects. By default it is the specification's minimum, the ``Date`` HTTP header. -``secret`` and ``algorithm`` are as above. + +``httpsig_version`` is the IEFT version. By default it is ``draft-07`` and allowed: ``draft-00`` to ``draft-07``. + +``secret`` and ``algorithm`` are as above. . Tests ----- From 7ea5f11c68a0dd44b312826cc4416dc0708d2958 Mon Sep 17 00:00:00 2001 From: Jarron Shih Date: Sat, 29 Jul 2017 20:47:35 -0700 Subject: [PATCH 31/38] Update README.rst --- README.rst | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index df59540..ef9e07e 100644 --- a/README.rst +++ b/README.rst @@ -87,22 +87,19 @@ Note that keys and secrets should be bytes objects. At attempt will be made to httpsig.Signer(secret, algorithm='rsa-sha256') -``secret``, in the case of an RSA signature, is a string containing private RSA pem. In the case of HMAC, it is a secret password. - -``algorithm`` is one of the six allowed signatures: ``rsa-sha1``, ``rsa-sha256``, ``rsa-sha512``, ``hmac-sha1``, ``hmac-sha256``, ``hmac-sha512``. +:secret: in the case of an RSA signature, is a string containing private RSA pem. In the case of HMAC, it is a secret password. +:algorithm: is one of the six allowed signatures: ``rsa-sha1``, ``rsa-sha256``, ``rsa-sha512``, ``hmac-sha1``, ``hmac-sha256``, ``hmac-sha512``. .. code:: python httpsig.requests_auth.HTTPSignatureAuth(key_id, secret, algorithm='rsa-sha256', headers=None, httpsig_version=None) -``key_id`` is the label by which the server system knows your RSA signature or password. - -``headers`` is the list of HTTP headers that are concatenated and used as signing objects. By default it is the specification's minimum, the ``Date`` HTTP header. - -``httpsig_version`` is the IEFT version. By default it is ``draft-07`` and allowed: ``draft-00`` to ``draft-07``. - -``secret`` and ``algorithm`` are as above. . +:key_id: is the label by which the server system knows your RSA signature or password. +:headers: is the list of HTTP headers that are concatenated and used as signing objects. By default it is the specification's minimum, the ``Date`` HTTP header. +:httpsig_version: is the IEFT version. By default it is ``draft-07`` and allowed: ``draft-00`` to ``draft-07``. +:secret: as above. +:algorithm: as above. Tests ----- From b4c13f315e2af50c614c38d2f9009dc03b76602e Mon Sep 17 00:00:00 2001 From: Jarron Shih Date: Sat, 29 Jul 2017 21:00:30 -0700 Subject: [PATCH 32/38] Update CHANGELOG.rst --- CHANGELOG.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f78e213..20639b5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,16 @@ httpsig Changes --------------- +1.1.3 (2017-July) +------------------- + +* Support IEFT ``draft-00`` to ``draft-07`` +* Python version update: support ``py3.5``, ``py3.6`` and remove ``py3.2`` (due to coverage not supported) +* Python re-format by using linter. +* Complete unittest and enhance coverage to over 90%. +* Bugfix for python3 string encode and decode. + + 1.1.2 (2015-Feb-11) ------------------- From 88837af199aa550631372ce8ee16aec402b505fb Mon Sep 17 00:00:00 2001 From: "Jarron Shih (RD-US)" Date: Sat, 29 Jul 2017 21:43:18 -0700 Subject: [PATCH 33/38] fix test env --- tests/__init__.py | 3 --- tests/test_request_auth.py | 4 ++-- tests/test_sign.py | 4 ---- tests/test_utils.py | 0 tests/test_verify.py | 5 ++--- tox.ini | 2 +- 6 files changed, 5 insertions(+), 13 deletions(-) mode change 100755 => 100644 tests/test_sign.py mode change 100755 => 100644 tests/test_utils.py mode change 100755 => 100644 tests/test_verify.py diff --git a/tests/__init__.py b/tests/__init__.py index 6252bd1..e69de29 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +0,0 @@ -from .test_sign import * -from .test_utils import * -from .test_verify import * diff --git a/tests/test_request_auth.py b/tests/test_request_auth.py index 1f82a92..6b42bea 100644 --- a/tests/test_request_auth.py +++ b/tests/test_request_auth.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -import sys import os import unittest import requests @@ -7,7 +6,8 @@ from httpsig.requests_auth import HTTPSignatureAuth -class TestHTTPSignatureAuth(unittest.TestCase): +class TestHttpSignatureAuth(unittest.TestCase): + def setUp(self): private_key_path = os.path.join(os.path.dirname(__file__), 'rsa_private.pem') with open(private_key_path, 'rb') as f: diff --git a/tests/test_sign.py b/tests/test_sign.py old mode 100755 new mode 100644 index a4d9dee..98a7a46 --- a/tests/test_sign.py +++ b/tests/test_sign.py @@ -1,9 +1,5 @@ #!/usr/bin/env python -import sys import os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - -import json import unittest import httpsig.sign as sign diff --git a/tests/test_utils.py b/tests/test_utils.py old mode 100755 new mode 100644 diff --git a/tests/test_verify.py b/tests/test_verify.py old mode 100755 new mode 100644 index 27a5daa..acb07ed --- a/tests/test_verify.py +++ b/tests/test_verify.py @@ -1,14 +1,13 @@ #!/usr/bin/env python -import sys -import os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import os import json import unittest from httpsig.sign import HeaderSigner, Signer from httpsig.verify import HeaderVerifier, Verifier + class BaseTestCase(unittest.TestCase): def _parse_auth(self, auth): """Basic Authorization header parsing.""" diff --git a/tox.ini b/tox.ini index 86932b2..2d34ea2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, py32, py33, py34, py35, py36 +envlist = py27, py33, py34, py35, py36 [testenv] passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH From e973641c6f7c5f98e82df6c45ae72fa6d1ca31c8 Mon Sep 17 00:00:00 2001 From: Reverb Chu Date: Sun, 30 Jul 2017 10:31:10 -0700 Subject: [PATCH 34/38] add test cases for verify.py --- httpsig/verify.py | 7 +------ tests/test_verify.py | 39 +++++++++++++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/httpsig/verify.py b/httpsig/verify.py index 15af28f..d8674e0 100644 --- a/httpsig/verify.py +++ b/httpsig/verify.py @@ -58,12 +58,7 @@ def __init__(self, headers, secret, required_headers=None, method=None, path=Non """ required_headers = required_headers or ['date'] - auth = parse_authorization_header(headers['authorization']) - if len(auth) == 2: - self.auth_dict = auth[1] - else: - raise HttpSigException("Invalid authorization header.") - + self.auth_dict = parse_authorization_header(headers['authorization'])[1] self.headers = CaseInsensitiveDict(headers) self.required_headers = [s.lower() for s in required_headers] self.method = method diff --git a/tests/test_verify.py b/tests/test_verify.py index acb07ed..9a0789c 100644 --- a/tests/test_verify.py +++ b/tests/test_verify.py @@ -1,11 +1,13 @@ #!/usr/bin/env python import os -import json +import six import unittest from httpsig.sign import HeaderSigner, Signer from httpsig.verify import HeaderVerifier, Verifier +from httpsig.utils import HttpSigException +import httpsig.utils class BaseTestCase(unittest.TestCase): @@ -36,13 +38,19 @@ def test_basic_sign(self): signer = Signer(secret=self.sign_secret, algorithm=self.algorithm) verifier = Verifier(secret=self.verify_secret, algorithm=self.algorithm) - GOOD = b"this is a test" - BAD = b"this is not the signature you were looking for..." + good_case = b"this is a test" + # BAD = b"this is not the signature you were looking for..." + + if six.PY3: + bad_test_cases = ['this is not the signature you were looking for...', b'this is not the signature you were looking for...'] + if six.PY2: + bad_test_cases = [u'this is not the signature you were looking for...', 'this is not the signature you were looking for...'] # generate signed string - signature = signer._sign(GOOD) - self.assertTrue(verifier._verify(data=GOOD, signature=signature)) - self.assertFalse(verifier._verify(data=BAD, signature=signature)) + signature = signer._sign(good_case) + self.assertTrue(verifier._verify(data=good_case, signature=signature)) + for case in bad_test_cases: + self.assertFalse(verifier._verify(data=case, signature=signature)) def test_default(self): unsigned = { @@ -128,12 +136,29 @@ def test_extra_auth_headers(self): hv = HeaderVerifier(headers=signed, secret=self.verify_secret, method=METHOD, path=PATH, required_headers=['date', '(request-target)']) self.assertTrue(hv.verify()) + def test_unsupported_algorithm(self): + unsigned = { + 'Date': 'Thu, 05 Jan 2012 21:31:40 GMT' + } + weird_algorithm = 'xxx-sha257' + + try: + original_algorithms = httpsig.sign.ALGORITHMS + httpsig.sign.ALGORITHMS = frozenset([weird_algorithm]) + Verifier(secret=self.verify_secret, algorithm=weird_algorithm)._verify(data='123', signature='456') + self.fail('No exception was raised for false algorithm') + except Exception as ex: + self.assertIsInstance(ex, HttpSigException) + finally: + httpsig.sign.ALGORITHMS = original_algorithms + class TestVerifyHMACSHA256(TestVerifyHMACSHA1): def setUp(self): super(TestVerifyHMACSHA256, self).setUp() self.algorithm = "hmac-sha256" + class TestVerifyHMACSHA512(TestVerifyHMACSHA1): def setUp(self): super(TestVerifyHMACSHA512, self).setUp() @@ -155,11 +180,13 @@ def setUp(self): self.sign_secret = private_key self.verify_secret = public_key + class TestVerifyRSASHA256(TestVerifyRSASHA1): def setUp(self): super(TestVerifyRSASHA256, self).setUp() self.algorithm = "rsa-sha256" + class TestVerifyRSASHA512(TestVerifyRSASHA1): def setUp(self): super(TestVerifyRSASHA512, self).setUp() From af22bf30c4f13d50c81e4ef7ec2ffb172df406d3 Mon Sep 17 00:00:00 2001 From: Reverb Chu Date: Sun, 30 Jul 2017 11:07:18 -0700 Subject: [PATCH 35/38] hide sign_hmac and sign_rsa methods --- httpsig/sign.py | 27 ++++++++++++--------------- httpsig/verify.py | 2 +- tests/test_verify.py | 2 +- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index dc916d8..990e263 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -44,35 +44,32 @@ def __init__(self, secret, algorithm=DEFAULT_SIGN_ALGORITHM): def algorithm(self): return '%s-%s' % (self.sign_algorithm, self.hash_algorithm) - def _sign_rsa(self, data): - if isinstance(data, six.string_types): - data = data.encode('ascii') - + def __sign_rsa(self, data): _hash = self._hash.new() _hash.update(data) return self._rsa.sign(_hash) - def _sign_hmac(self, data): - if isinstance(data, six.string_types): - data = data.encode('ascii') - + def __sign_hmac(self, data): hmac = self._hash.copy() hmac.update(data) return hmac.digest() def _sign(self, data): - if isinstance(data, six.string_types): - data = data.encode('ascii') - + '''return raw signed string''' signed = None if self._rsa: - signed = self._sign_rsa(data) + signed = self.__sign_rsa(data) elif self._hash: - signed = self._sign_hmac(data) + signed = self.__sign_hmac(data) if not signed: raise SystemError('No valid encryptor found.') - return base64.b64encode(signed).decode('ascii') + return signed + + def sign(self, data): + if isinstance(data, six.string_types): + data = data.encode('ascii') + return base64.b64encode(self._sign(data)).decode('ascii') class HeaderSigner(Signer): ''' @@ -106,7 +103,7 @@ def sign(self, headers, host=None, method=None, path=None): headers = CaseInsensitiveDict(headers) signable = generate_message(self.headers, headers, host, method, path) - signature = self._sign(signable) + signature = super(HeaderSigner, self).sign(signable) headers['authorization'] = self.signature_template % signature return headers diff --git a/httpsig/verify.py b/httpsig/verify.py index d8674e0..a1931d6 100644 --- a/httpsig/verify.py +++ b/httpsig/verify.py @@ -33,7 +33,7 @@ def _verify(self, data, signature): return self._rsa.verify(hash_, b64decode(signature)) elif self.sign_algorithm == 'hmac': - signed_hmac = self._sign_hmac(data) + signed_hmac = self._sign(data) decoded_sig = b64decode(signature) return ct_bytes_compare(signed_hmac, decoded_sig) diff --git a/tests/test_verify.py b/tests/test_verify.py index 9a0789c..ffab48d 100644 --- a/tests/test_verify.py +++ b/tests/test_verify.py @@ -47,7 +47,7 @@ def test_basic_sign(self): bad_test_cases = [u'this is not the signature you were looking for...', 'this is not the signature you were looking for...'] # generate signed string - signature = signer._sign(good_case) + signature = signer.sign(good_case) self.assertTrue(verifier._verify(data=good_case, signature=signature)) for case in bad_test_cases: self.assertFalse(verifier._verify(data=case, signature=signature)) From 8594dd48fb70e5830bfff85a39940d18f92f235a Mon Sep 17 00:00:00 2001 From: Reverb Chu Date: Sun, 30 Jul 2017 11:07:29 -0700 Subject: [PATCH 36/38] add test cases for sign.py --- tests/test_sign.py | 48 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/test_sign.py b/tests/test_sign.py index 98a7a46..99b7094 100644 --- a/tests/test_sign.py +++ b/tests/test_sign.py @@ -1,9 +1,10 @@ #!/usr/bin/env python import os import unittest +import six import httpsig.sign as sign -from httpsig.utils import parse_authorization_header +from httpsig.utils import parse_authorization_header, HttpSigException class TestSign(unittest.TestCase): @@ -118,3 +119,48 @@ def test_FAIL_verify_headers_by_draft_version(self): self.fail('Should raise KeyError in (%s, %s)' % (httpsig_version, header_req)) except KeyError: pass + + def test_signer_init(self): + + signature = 'ksTqAWZHgRktH3uKbXydPNEMqpjqALFiJVJXeCKj8zA=' + secret = 'i am a secret' + data = 'i am some data' + algorithm = 'hmac-sha256' + + # test good case: default string type + signer = sign.Signer(secret, algorithm=algorithm) + self.assertEqual(signer.sign(data), signature) + self.assertEqual(signer.algorithm, algorithm) + + # test good case: unicode/byte string type for secret + signer = sign.Signer(secret.encode() if six.PY3 else secret.decode(), algorithm=algorithm) + self.assertEqual(signer.sign(data), signature) + + # test good case: unicode/byte string type for data + signer = sign.Signer(secret, algorithm=algorithm) + self.assertEqual(signer.sign(data.encode() if six.PY3 else data.decode()), signature) + + # test invalid hash algorithm + weird_algorithm = 'rsa-sha257' + original_algorithms = sign.ALGORITHMS + sign.ALGORITHMS = frozenset([weird_algorithm]) + try: + signer = sign.Signer(secret, algorithm=weird_algorithm) + self.fail('No exception was raised') + except Exception as ex: + self.assertIsInstance(ex, HttpSigException) + finally: + sign.ALGORITHMS = original_algorithms + + # test invalid sign algorithm + weird_algorithm = 'xxx-sha256' + original_algorithms = sign.ALGORITHMS + sign.ALGORITHMS = frozenset([weird_algorithm]) + try: + signer = sign.Signer(secret, algorithm=weird_algorithm) + signer.sign('nothing') + self.fail('No exception was raised') + except Exception as ex: + self.assertIsInstance(ex, SystemError) + finally: + sign.ALGORITHMS = original_algorithms From d28d0926107efe0ea1b394924c517e789418b073 Mon Sep 17 00:00:00 2001 From: Reverb C Date: Sun, 30 Jul 2017 11:35:55 -0700 Subject: [PATCH 37/38] Update README.rst --- README.rst | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index ef9e07e..afb6092 100644 --- a/README.rst +++ b/README.rst @@ -1,19 +1,21 @@ httpsig ======= -.. image:: https://travis-ci.org/GeekMap/httpsig.svg?branch=master - :target: https://travis-ci.org/GeekMap/httpsig +:master: + .. image:: https://travis-ci.org/GeekMap/httpsig.svg?branch=master + :target: https://travis-ci.org/GeekMap/httpsig -.. image:: https://coveralls.io/repos/github/GeekMap/httpsig/badge.svg?branch=master - :target: https://coveralls.io/github/GeekMap/httpsig?branch=master + .. image:: https://coveralls.io/repos/github/GeekMap/httpsig/badge.svg?branch=master + :target: https://coveralls.io/github/GeekMap/httpsig?branch=master -.. image:: https://travis-ci.org/GeekMap/httpsig.svg?branch=dev - :target: https://travis-ci.org/GeekMap/httpsig +:dev: + .. image:: https://travis-ci.org/GeekMap/httpsig.svg?branch=dev + :target: https://travis-ci.org/GeekMap/httpsig -.. image:: https://coveralls.io/repos/github/GeekMap/httpsig/badge.svg?branch=dev - :target: https://coveralls.io/github/GeekMap/httpsig?branch=dev + .. image:: https://coveralls.io/repos/github/GeekMap/httpsig/badge.svg?branch=dev + :target: https://coveralls.io/github/GeekMap/httpsig?branch=dev -Sign HTTP requests with secure signatures according to the IETF HTTP Signatures specification (`Draft 3`_). This is a fork of the original module_ to fully support both RSA and HMAC schemes as well as unit test both schemes to prove they work. It's being used in production and is actively-developed. +Sign HTTP requests with secure signatures according to the IETF HTTP Signatures specification from draft 00 to 07. This is a fork of the original module_ to fully support both RSA and HMAC schemes as well as unit test both schemes to prove they work. It's being used in production and is actively-developed. See the original project_, original Python module_, original spec_, and `current IETF draft`_ for more details on the signing scheme. @@ -21,7 +23,6 @@ See the original project_, original Python module_, original spec_, and `current .. _module: https://github.com/zzsnzmn/py-http-signature .. _spec: https://github.com/joyent/node-http-signature/blob/master/http_signing.md .. _`current IETF draft`: https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ -.. _`Draft 3`: http://tools.ietf.org/html/draft-cavage-http-signatures-03 Requirements ------------ @@ -58,11 +59,11 @@ For general use with web frameworks: import httpsig - key_id = "Some Key ID" - secret = b'some big secret' + key_id = 'some key ID' + secret = 'some big secret' - hs = httpsig.HeaderSigner(key_id, secret, algorithm="hmac-sha256", headers=['(request-target)', 'host', 'date']) - signed_headers_dict = hs.sign({"Date": "Tue, 01 Jan 2014 01:01:01 GMT", "Host": "example.com"}, method="GET", path="/api/1/object/1") + hs = httpsig.HeaderSigner(key_id, secret, algorithm='hmac-sha256', headers=['(request-target)', 'host', 'date']) + signed_headers_dict = hs.sign({'Date': 'Tue, 01 Jan 2014 01:01:01 GMT', 'Host': 'example.com'}, method='GET', path='/api/1/object/1') For use with requests: @@ -88,7 +89,7 @@ Note that keys and secrets should be bytes objects. At attempt will be made to httpsig.Signer(secret, algorithm='rsa-sha256') :secret: in the case of an RSA signature, is a string containing private RSA pem. In the case of HMAC, it is a secret password. -:algorithm: is one of the six allowed signatures: ``rsa-sha1``, ``rsa-sha256``, ``rsa-sha512``, ``hmac-sha1``, ``hmac-sha256``, ``hmac-sha512``. +:algorithm: is one of the six allowed hash-sign algorithm combinations: ``rsa-sha1``, ``rsa-sha256``, ``rsa-sha512``, ``hmac-sha1``, ``hmac-sha256``, ``hmac-sha512``. .. code:: python From f6c75d9a7d51ba210aa2316935b59e41cf5a2626 Mon Sep 17 00:00:00 2001 From: Reverb Chu Date: Sun, 30 Jul 2017 21:58:03 -0700 Subject: [PATCH 38/38] revert readme for pr --- README.rst | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index afb6092..69905f1 100644 --- a/README.rst +++ b/README.rst @@ -1,19 +1,11 @@ httpsig ======= -:master: - .. image:: https://travis-ci.org/GeekMap/httpsig.svg?branch=master - :target: https://travis-ci.org/GeekMap/httpsig +.. image:: https://travis-ci.org/ahknight/httpsig.svg?branch=master + :target: https://travis-ci.org/ahknight/httpsig - .. image:: https://coveralls.io/repos/github/GeekMap/httpsig/badge.svg?branch=master - :target: https://coveralls.io/github/GeekMap/httpsig?branch=master - -:dev: - .. image:: https://travis-ci.org/GeekMap/httpsig.svg?branch=dev - :target: https://travis-ci.org/GeekMap/httpsig - - .. image:: https://coveralls.io/repos/github/GeekMap/httpsig/badge.svg?branch=dev - :target: https://coveralls.io/github/GeekMap/httpsig?branch=dev +.. image:: https://travis-ci.org/ahknight/httpsig.svg?branch=develop + :target: https://travis-ci.org/ahknight/httpsig Sign HTTP requests with secure signatures according to the IETF HTTP Signatures specification from draft 00 to 07. This is a fork of the original module_ to fully support both RSA and HMAC schemes as well as unit test both schemes to prove they work. It's being used in production and is actively-developed.