From 6958fe35a6ae57ed18a9d4dd870398703b4e4779 Mon Sep 17 00:00:00 2001 From: fulder Date: Mon, 24 Aug 2020 11:06:51 +0200 Subject: [PATCH 01/32] Add hs2019 to algorithm list and set to default sign algorithm --- httpsig/sign.py | 3 ++- httpsig/utils.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index 94e2180..cd44553 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -4,11 +4,12 @@ from Crypto.Hash import HMAC from Crypto.PublicKey import RSA from Crypto.Signature import PKCS1_v1_5 +from Crypto.Signature import PKCS1_PSS from .utils import * -DEFAULT_SIGN_ALGORITHM = "hmac-sha256" +DEFAULT_SIGN_ALGORITHM = "hs2019" class Signer(object): diff --git a/httpsig/utils.py b/httpsig/utils.py index 5f80ef0..9cef5e5 100644 --- a/httpsig/utils.py +++ b/httpsig/utils.py @@ -19,7 +19,8 @@ 'rsa-sha512', 'hmac-sha1', 'hmac-sha256', - 'hmac-sha512']) + 'hmac-sha512', + 'hs2019']) HASHES = {'sha1': SHA, 'sha256': SHA256, 'sha512': SHA512} From 0e18601fd2e72ae1a62ae367c2546397a3528427 Mon Sep 17 00:00:00 2001 From: fulder Date: Mon, 24 Aug 2020 11:07:14 +0200 Subject: [PATCH 02/32] Print deprecation message on old algorithms --- httpsig/sign.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/httpsig/sign.py b/httpsig/sign.py index cd44553..d6fe822 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -24,6 +24,10 @@ def __init__(self, secret, algorithm=None): algorithm = DEFAULT_SIGN_ALGORITHM assert algorithm in ALGORITHMS, "Unknown algorithm" + + if algorithm != DEFAULT_SIGN_ALGORITHM: + print("Algorithm: {} is deprecated please update to {}".format(algorithm, DEFAULT_SIGN_ALGORITHM)) + if isinstance(secret, six.string_types): secret = secret.encode("ascii") From 39298a0b1ba97c599f98615b720ef68cefb074bc Mon Sep 17 00:00:00 2001 From: fulder Date: Mon, 24 Aug 2020 11:22:04 +0200 Subject: [PATCH 03/32] Requre digital signature algorithm while using 'hs2019' algorithm --- httpsig/sign.py | 20 ++++++++++++++++++-- httpsig/utils.py | 4 ++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index d6fe822..258fdda 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -19,11 +19,12 @@ class Signer(object): Password-protected keyfiles are not supported. """ - def __init__(self, secret, algorithm=None): + def __init__(self, secret, algorithm=None, digital_signature_algorithm=None): if algorithm is None: algorithm = DEFAULT_SIGN_ALGORITHM assert algorithm in ALGORITHMS, "Unknown algorithm" + assert digital_signature_algorithm in DIGITAL_SIGNATURE_ALGORITHMS, "Unsupported digital signature algrotihm" if algorithm != DEFAULT_SIGN_ALGORITHM: print("Algorithm: {} is deprecated please update to {}".format(algorithm, DEFAULT_SIGN_ALGORITHM)) @@ -33,7 +34,13 @@ def __init__(self, secret, algorithm=None): self._rsa = None self._hash = None - self.sign_algorithm, self.hash_algorithm = algorithm.split('-') + + if "-" in algorithm: + self.sign_algorithm, self.hash_algorithm = algorithm.split('-') + elif algorithm == "hs2019": + assert digital_signature_algorithm is not None, "Required digital signature algorithm not specified" + self.sign_algorithm = digital_signature_algorithm + self.hash_algorithm = "sha512" if self.sign_algorithm == 'rsa': try: @@ -47,6 +54,15 @@ def __init__(self, secret, algorithm=None): self._hash = HMAC.new(secret, digestmod=HASHES[self.hash_algorithm]) + elif self.sign_algorithm == "PSS": + try: + rsa_key = RSA.importKey(secret) + self._rsa = PKCS1_PSS.new(rsa_key) + self._hash = HASHES[self.hash_algorithm] + except ValueError: + raise HttpSigException("Invalid key.") + + @property def algorithm(self): return '%s-%s' % (self.sign_algorithm, self.hash_algorithm) diff --git a/httpsig/utils.py b/httpsig/utils.py index 9cef5e5..d6b21e4 100644 --- a/httpsig/utils.py +++ b/httpsig/utils.py @@ -25,6 +25,10 @@ 'sha256': SHA256, 'sha512': SHA512} +DIGITAL_SIGNATURE_ALGORITHMS = frozenset([ + "PSS" +]) + class HttpSigException(Exception): pass From 3e74497094ec9b843af090884b9cd60ad7b6d0cd Mon Sep 17 00:00:00 2001 From: fulder Date: Mon, 24 Aug 2020 11:27:24 +0200 Subject: [PATCH 04/32] Set algorithm class variable instead of property --- httpsig/sign.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index 258fdda..44770d6 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -34,6 +34,7 @@ def __init__(self, secret, algorithm=None, digital_signature_algorithm=None): self._rsa = None self._hash = None + self.algorithm = algorithm if "-" in algorithm: self.sign_algorithm, self.hash_algorithm = algorithm.split('-') @@ -62,11 +63,6 @@ def __init__(self, secret, algorithm=None, digital_signature_algorithm=None): except ValueError: raise HttpSigException("Invalid key.") - - @property - 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") From 813d2a19e9f7484d360c3b22b0dc3c9c780dee19 Mon Sep 17 00:00:00 2001 From: fulder Date: Mon, 24 Aug 2020 11:28:51 +0200 Subject: [PATCH 05/32] Remove duplicated default sign algorithm --- httpsig/sign.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index 44770d6..0aac4a5 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -107,9 +107,6 @@ class HeaderSigner(Signer): 'authorization'. """ def __init__(self, key_id, secret, algorithm=None, headers=None, sign_header='authorization'): - 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( From c2654c704616de560a80a9e43712c2058168bf55 Mon Sep 17 00:00:00 2001 From: fulder Date: Mon, 24 Aug 2020 11:37:54 +0200 Subject: [PATCH 06/32] Rename long digital signature algorithm to just sign_algorithm --- httpsig/sign.py | 12 ++++++------ httpsig/utils.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index 0aac4a5..8e05227 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -19,12 +19,12 @@ class Signer(object): Password-protected keyfiles are not supported. """ - def __init__(self, secret, algorithm=None, digital_signature_algorithm=None): + def __init__(self, secret, algorithm=None, sign_algorithm=None): if algorithm is None: algorithm = DEFAULT_SIGN_ALGORITHM assert algorithm in ALGORITHMS, "Unknown algorithm" - assert digital_signature_algorithm in DIGITAL_SIGNATURE_ALGORITHMS, "Unsupported digital signature algrotihm" + assert sign_algorithm in SIGN_ALGORITHMS, "Unsupported digital signature algrotihm" if algorithm != DEFAULT_SIGN_ALGORITHM: print("Algorithm: {} is deprecated please update to {}".format(algorithm, DEFAULT_SIGN_ALGORITHM)) @@ -39,8 +39,8 @@ def __init__(self, secret, algorithm=None, digital_signature_algorithm=None): if "-" in algorithm: self.sign_algorithm, self.hash_algorithm = algorithm.split('-') elif algorithm == "hs2019": - assert digital_signature_algorithm is not None, "Required digital signature algorithm not specified" - self.sign_algorithm = digital_signature_algorithm + assert sign_algorithm is not None, "Required digital signature algorithm not specified" + self.sign_algorithm = sign_algorithm self.hash_algorithm = "sha512" if self.sign_algorithm == 'rsa': @@ -106,8 +106,8 @@ class HeaderSigner(Signer): :arg sign_header: header used to include signature, defaulting to 'authorization'. """ - def __init__(self, key_id, secret, algorithm=None, headers=None, sign_header='authorization'): - super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm) + def __init__(self, key_id, secret, algorithm=None, sign_algorithm=None, headers=None, sign_header='authorization'): + super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm, sign_algorithm=sign_algorithm) self.headers = headers or ['date'] self.signature_template = build_signature_template( key_id, algorithm, headers, sign_header) diff --git a/httpsig/utils.py b/httpsig/utils.py index d6b21e4..135926f 100644 --- a/httpsig/utils.py +++ b/httpsig/utils.py @@ -25,7 +25,7 @@ 'sha256': SHA256, 'sha512': SHA512} -DIGITAL_SIGNATURE_ALGORITHMS = frozenset([ +SIGN_ALGORITHMS = frozenset([ "PSS" ]) From bd001d8ca1aa6bb6215e5d4521204eecdeee6426 Mon Sep 17 00:00:00 2001 From: fulder Date: Mon, 24 Aug 2020 11:40:17 +0200 Subject: [PATCH 07/32] Add sign_algorithm to HeaderSigner docstring --- httpsig/sign.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index 8e05227..9b80efa 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -100,7 +100,8 @@ class HeaderSigner(Signer): to use :arg secret: a PEM-encoded RSA private key or an HMAC secret (must match the algorithm) - :arg algorithm: one of the six specified algorithms + :arg algorithm: one of the seven specified algorithms + :arg sign_algorithm: required for 'hs2019' algorithm. Sign algorithm for the secret :arg headers: a list of http headers to be included in the signing string, defaulting to ['date']. :arg sign_header: header used to include signature, defaulting to From 79ca3a2c36d360005b4cf3fb7d7494a269fe154b Mon Sep 17 00:00:00 2001 From: fulder Date: Mon, 24 Aug 2020 11:51:07 +0200 Subject: [PATCH 08/32] Require sign_algorithm for 'hs2019' algorith in Verifier --- httpsig/verify.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/httpsig/verify.py b/httpsig/verify.py index 17e313d..4db0da5 100644 --- a/httpsig/verify.py +++ b/httpsig/verify.py @@ -4,7 +4,7 @@ import base64 import six -from .sign import Signer +from .sign import Signer, DEFAULT_SIGN_ALGORITHM from .utils import * @@ -37,6 +37,11 @@ def _verify(self, data, signature): s = base64.b64decode(signature) return ct_bytes_compare(h, s) + elif self.sign_algorithm == 'PSS': + h = self._hash.new() + h.update(data) + return self._rsa.verify(h, base64.b64decode(signature)) + else: raise HttpSigException("Unsupported algorithm.") @@ -47,7 +52,7 @@ class HeaderVerifier(Verifier): """ def __init__(self, headers, secret, required_headers=None, method=None, - path=None, host=None, sign_header='authorization'): + path=None, host=None, sign_header='authorization', sign_algorithm=None): """ Instantiate a HeaderVerifier object. @@ -66,6 +71,8 @@ def __init__(self, headers, secret, required_headers=None, method=None, header, if not supplied in :param:headers. :param sign_header: Optional. The header where the signature is. Default is 'authorization'. + :param sign_algorithm: Required for 'hs2019' algorithm, specifies the + digital signature algorithm (derived from keyId) to use. """ required_headers = required_headers or ['date'] self.headers = CaseInsensitiveDict(headers) @@ -84,8 +91,13 @@ def __init__(self, headers, secret, required_headers=None, method=None, self.path = path self.host = host + if self.auth_dict['algorithm'] != DEFAULT_SIGN_ALGORITHM: + print("Algorithm: {} is deprecated please update to {}".format(self.auth_dict['algorithm'], DEFAULT_SIGN_ALGORITHM)) + elif self.auth_dict['algorithm'] == DEFAULT_SIGN_ALGORITHM and self.sign_algorithm is None: + raise HttpSigException("Required sign algorithm for {} algorithm not set".format(DEFAULT_SIGN_ALGORITHM)) + super(HeaderVerifier, self).__init__( - secret, algorithm=self.auth_dict['algorithm']) + secret, algorithm=self.auth_dict['algorithm'], sign_algorithm=sign_algorithm) def verify(self): """ From 86ef10996a8deb64e3fce01f156641e3da7f2633 Mon Sep 17 00:00:00 2001 From: fulder Date: Mon, 24 Aug 2020 11:53:47 +0200 Subject: [PATCH 09/32] Accept None sign_algorithm for backward compability --- httpsig/sign.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index 9b80efa..e34ac85 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -24,7 +24,7 @@ def __init__(self, secret, algorithm=None, sign_algorithm=None): algorithm = DEFAULT_SIGN_ALGORITHM assert algorithm in ALGORITHMS, "Unknown algorithm" - assert sign_algorithm in SIGN_ALGORITHMS, "Unsupported digital signature algrotihm" + assert sign_algorithm is None or sign_algorithm in SIGN_ALGORITHMS, "Unsupported digital signature algorithm" if algorithm != DEFAULT_SIGN_ALGORITHM: print("Algorithm: {} is deprecated please update to {}".format(algorithm, DEFAULT_SIGN_ALGORITHM)) From 40529548126586ac77560cfff105a4114d9b1716 Mon Sep 17 00:00:00 2001 From: fulder Date: Mon, 24 Aug 2020 12:00:15 +0200 Subject: [PATCH 10/32] Take back setting algorithm to default twice --- httpsig/sign.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/httpsig/sign.py b/httpsig/sign.py index e34ac85..c4d3d48 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -108,6 +108,9 @@ class HeaderSigner(Signer): 'authorization'. """ def __init__(self, key_id, secret, algorithm=None, sign_algorithm=None, headers=None, sign_header='authorization'): + if algorithm is None: + algorithm = DEFAULT_SIGN_ALGORITHM + super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm, sign_algorithm=sign_algorithm) self.headers = headers or ['date'] self.signature_template = build_signature_template( From 1e01f5c1152a47c1630a91308e9c184a7c4af68b Mon Sep 17 00:00:00 2001 From: fulder Date: Mon, 24 Aug 2020 15:55:21 +0200 Subject: [PATCH 11/32] Update tests to hs2019 and add one deprecated test using old keys --- .../{rsa_private.pem => rsa_private_1024.pem} | 0 httpsig/tests/rsa_private_2048.pem | 27 ++++++++++ .../{rsa_public.pem => rsa_public_1024.pem} | 2 +- httpsig/tests/rsa_public_2048.pem | 9 ++++ httpsig/tests/test_signature.py | 53 +++++++++++++------ httpsig/tests/test_utils.py | 2 +- httpsig/tests/test_verify.py | 53 ++++++++++++++----- 7 files changed, 117 insertions(+), 29 deletions(-) rename httpsig/tests/{rsa_private.pem => rsa_private_1024.pem} (100%) create mode 100644 httpsig/tests/rsa_private_2048.pem rename httpsig/tests/{rsa_public.pem => rsa_public_1024.pem} (90%) create mode 100644 httpsig/tests/rsa_public_2048.pem diff --git a/httpsig/tests/rsa_private.pem b/httpsig/tests/rsa_private_1024.pem similarity index 100% rename from httpsig/tests/rsa_private.pem rename to httpsig/tests/rsa_private_1024.pem diff --git a/httpsig/tests/rsa_private_2048.pem b/httpsig/tests/rsa_private_2048.pem new file mode 100644 index 0000000..153a8ec --- /dev/null +++ b/httpsig/tests/rsa_private_2048.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQB7eXXK+gSpDXsvZkcXd19X85iemJd0KywRH+/W+1J1j8pd+O1l +H2He8GLaDFCwFijTvTmptfMYB2XyvG8/tPpaSzaIbSBlKXWxSo1fdUMf2e7SbqVr +Fi5DolPrIfRpVqw4iqnTZZ46Y2vfa57Ee3NRF5zoagMS9BM7nfuCKvzZcUK81V75 +hup5kpMHW1ofBZAPwQMm8CoXD1bpM+acN1N+63vgTY2QyUq2yJOI3HJvyFZTw+Sj +/ialYtDvDTluBH98i4504OIA6z0SCijF11irvAOSPc0GVXB8HjtUlqbD0BD6Hyqg +MeXgi9nGJhJDnJDiCVlPwg6Ni+h3nW/sXXopAgMBAAECggEANkOg8v2CAtG7647l +e3io3DxgPIMPPKykhzoj67Uz/hqdc0MtAZ4TIyk+KFn1NA3pD3U/3EfseAj4Uv9h +XPwqcnhPlRFwhUT9RldfXi5ou5zJio26ASAUYQD8JIAdrBW9RnQaQp+MNFjxVZU0 +h2FBwse/25yLkU7XDQJXQFOoH988Dpozz1y8q11NxurakR67+xtqO5KG7FZdwCsN +W2Z7gTm7T59NYdHevFi2b91hdBdLWCn9RPduEvRViQY5KzzkT6cg493G3vCPXxCy +9C9aCNF7PXghy/im7dLz+H28xYls3KPOJve2dmvox2+aPH66TgXkfj/kfULJmHZq +el3dIQKBgQDAxiqPcEF1Fq4UOoipCvcpiyz0gdFFw1x58km9GOpDdDK1bqcFc2z/ +GEoauWVl/PZZJdmht1zzkg4R3Izpbsg1IFxd3m7KbcfOK2bA9h2QPmjW8OwSu4/h +/l8mDsNF5crOdBnUHacgHhL1SJx323Yu3z9PmiN9wLW1gyYkh82SzQKBgQCj+LWP +1DZdsHOs224CjGjfj02PsaV5RNgD7Qqk5VcQFHzmJTAqoroPzJNjUD1sUnXXJHI0 +JL533giIsxQxnyca1qtxaO6KA4baykQtKKQqKTWhE2oowS1howHRbLShq1Hxvw9S +QSS0ZAo5DyjZLMkVnlB+v7sXJR8X0Ru8qHKczQKBgQCBMEy1c/VqEpj21YNgRgj9 +vleSRK2KozIGR2lDYL8eFXEmRdGIxaH2EsEWx8g8YRp3A/aleczBLtBfB/8nMSba +86TzA24cGxYcBNoH1uhZEnoQEcUjiK8UNPRu/NXAsg8H7KaikHy/+WebGd5CNMEv +CE3VeubuD4e27P1S3e/WwQKBgDzgGjASvjhcSSXUtWv2yvyszEPb1S5Hk9cpSvlb +N859fL1I8y/xCBjTf6iwYo1zs9Iy8r9PIPOJmCuAKLAfgToilrXdGipdEtTpoRQO +8ZvBfuqVNaV5yqpkBUnGDO20mBCjOUH1c3YRagYzDZxLV0BSbVoRPpliK8AA30ZU +V3DFAoGAfaPc8p6o7tCaPMpRxynIAvgIqg4sIBJdX/G4Q+SZeZR/mFlfpuhY4kzh +CL+RKAhOyOaYsSxlk4vB954y4UZFl6/t2W6gNxouelA77TgV2/rjx/fLk06J+RIF +QQkiAXwUZ2xpmdnUk+UREBwrB3LoU9kZM6fKX/LB4QEZuOmbERQ= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/httpsig/tests/rsa_public.pem b/httpsig/tests/rsa_public_1024.pem similarity index 90% rename from httpsig/tests/rsa_public.pem rename to httpsig/tests/rsa_public_1024.pem index b3bbf6c..80135fd 100644 --- a/httpsig/tests/rsa_public.pem +++ b/httpsig/tests/rsa_public_1024.pem @@ -3,4 +3,4 @@ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3 6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6 Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw oYi+1hqp1fIekaxsyQIDAQAB ------END PUBLIC KEY----- +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/httpsig/tests/rsa_public_2048.pem b/httpsig/tests/rsa_public_2048.pem new file mode 100644 index 0000000..4675c47 --- /dev/null +++ b/httpsig/tests/rsa_public_2048.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBITANBgkqhkiG9w0BAQEFAAOCAQ4AMIIBCQKCAQB7eXXK+gSpDXsvZkcXd19X +85iemJd0KywRH+/W+1J1j8pd+O1lH2He8GLaDFCwFijTvTmptfMYB2XyvG8/tPpa +SzaIbSBlKXWxSo1fdUMf2e7SbqVrFi5DolPrIfRpVqw4iqnTZZ46Y2vfa57Ee3NR +F5zoagMS9BM7nfuCKvzZcUK81V75hup5kpMHW1ofBZAPwQMm8CoXD1bpM+acN1N+ +63vgTY2QyUq2yJOI3HJvyFZTw+Sj/ialYtDvDTluBH98i4504OIA6z0SCijF11ir +vAOSPc0GVXB8HjtUlqbD0BD6HyqgMeXgi9nGJhJDnJDiCVlPwg6Ni+h3nW/sXXop +AgMBAAE= +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/httpsig/tests/test_signature.py b/httpsig/tests/test_signature.py index b8b4c90..e03cb31 100755 --- a/httpsig/tests/test_signature.py +++ b/httpsig/tests/test_signature.py @@ -10,7 +10,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) -sign.DEFAULT_SIGN_ALGORITHM = "rsa-sha256" +sign.DEFAULT_SIGN_ALGORITHM = "hs2019" class TestSign(unittest.TestCase): @@ -19,17 +19,22 @@ class TestSign(unittest.TestCase): header_host = 'example.com' header_date = 'Thu, 05 Jan 2014 21:31:40 GMT' header_content_type = 'application/json' - header_digest = 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=' + header_digest = 'SHA-512=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=' header_content_length = '18' def setUp(self): - self.key_path = os.path.join( - os.path.dirname(__file__), 'rsa_private.pem') - with open(self.key_path, 'rb') as f: - self.key = f.read() + self.key_path_2048 = os.path.join( + os.path.dirname(__file__), 'rsa_private_2048.pem') + with open(self.key_path_2048, 'rb') as f: + self.key_2048 = f.read() + + self.key_path_1024 = os.path.join( + os.path.dirname(__file__), 'rsa_private_1024.pem') + with open(self.key_path_1024, 'rb') as f: + self.key_1024 = f.read() def test_default(self): - hs = sign.HeaderSigner(key_id='Test', secret=self.key) + hs = sign.HeaderSigner(key_id='Test', secret=self.key_2048, sign_algorithm="PSS", salt_length=0) unsigned = { 'Date': self.header_date } @@ -43,11 +48,11 @@ def test_default(self): self.assertIn('algorithm', params) self.assertIn('signature', params) self.assertEqual(params['keyId'], 'Test') - self.assertEqual(params['algorithm'], 'rsa-sha256') - self.assertEqual(params['signature'], 'jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w=') # noqa: E501 + self.assertEqual(params['algorithm'], 'hs2019') + self.assertEqual(params['signature'], 'T8+Cj3Zp2cBDm2r8/loPgfHUSSFXXyZJNxxbNx1NvKVz/r5T4z6pVxhl9rqk8WfYHMdlh2aT5hCrYKvhs88Jy0DDmeUP4nELWRsO1BF0oAqHfcrbEikZQL7jA6z0guVaLr0S5QRGmd1K5HUEkP/vYEOns+FRL+JrFG4dNJNESvG5iyKUoaXfoZCFdqtzLlIteEAL7dW/kaX/dE116wfpbem1eCABuGopRhuFtjqLKVjuUVwyP/zSYTqd9j+gDhinkAifTJPxbGMh0b5LZdNCqw5irT9NkTcTFRXDp8ioX8r805Z9QhjT7H+rSo350U2LsAFoQ9ttryPBOoMPCiQTlw==') # noqa: E501 def test_basic(self): - hs = sign.HeaderSigner(key_id='Test', secret=self.key, headers=[ + hs = sign.HeaderSigner(key_id='Test', secret=self.key_2048, sign_algorithm="PSS", salt_length=0, headers=[ '(request-target)', 'host', 'date', @@ -68,13 +73,13 @@ def test_basic(self): self.assertIn('algorithm', params) self.assertIn('signature', params) self.assertEqual(params['keyId'], 'Test') - self.assertEqual(params['algorithm'], 'rsa-sha256') + self.assertEqual(params['algorithm'], 'hs2019') self.assertEqual( params['headers'], '(request-target) host date') - self.assertEqual(params['signature'], 'HUxc9BS3P/kPhSmJo+0pQ4IsCo007vkv6bUm4Qehrx+B1Eo4Mq5/6KylET72ZpMUS80XvjlOPjKzxfeTQj4DiKbAzwJAb4HX3qX6obQTa00/qPDXlMepD2JtTw33yNnm/0xV7fQuvILN/ys+378Ysi082+4xBQFwvhNvSoVsGv4=') # noqa: E501 + self.assertEqual(params['signature'], 'KkF4oeOJJH9TaYjQdaU634G7AVmM5Bf3fnfJCBZ7G0H5puW5XlQTpduA+TgouKOJhbv4aRRpunPzCHUxUjEvrR3TSALqW1EOsBwCVIusE9CnrhL7vUOvciIDai/jI15RsfR9+XyTmOSFbsI07E8mmywr3nLeWX6AAFDMO2vWc21zZxrSc13vFfAkVvFhXLxO4g0bBm6Z4m5/9ytWtdE0Gf3St2kY8aZTedllRCS8cMx8GVAIw/qYGeIlGKUCZKxrFxnviN7gfxixwova6lcxpppIo+WXxEiwMJfSQBlx0WGn3A3twCv6TsIxPOVUEW4jcogDh+jGFf1aGdVyHquTRQ==') # noqa: E501 def test_all(self): - hs = sign.HeaderSigner(key_id='Test', secret=self.key, headers=[ + hs = sign.HeaderSigner(key_id='Test', secret=self.key_2048, sign_algorithm="PSS", salt_length=0, headers=[ '(request-target)', 'host', 'date', @@ -101,8 +106,26 @@ def test_all(self): self.assertIn('algorithm', params) self.assertIn('signature', params) self.assertEqual(params['keyId'], 'Test') - self.assertEqual(params['algorithm'], 'rsa-sha256') + self.assertEqual(params['algorithm'], 'hs2019') self.assertEqual( params['headers'], '(request-target) host date content-type digest content-length') - self.assertEqual(params['signature'], 'Ef7MlxLXoBovhil3AlyjtBwAL9g4TN3tibLj7uuNB3CROat/9KaeQ4hW2NiJ+pZ6HQEOx9vYZAyi+7cmIkmJszJCut5kQLAwuX+Ms/mUFvpKlSo9StS2bMXDBNjOh4Auj774GFj4gwjS+3NhFeoqyr/MuN6HsEnkvn6zdgfE2i0=') # noqa: E501 + self.assertEqual(params['signature'], 'Ur8ehf0YlxBIRyXJG+iBBubrMlxWxDqpYgEaABq5ukcant30Gygkrs4ujFWxlR8pbBS/kDewYdlNhJOsVva2Y/ZSmardYHWYuSw3QjW0KON7nfVT/hijDFCAAzDDOqS6uSJimWmyko23bt2XDydMS2ekGoRFXxQcCtd2piWDpwaHneZiUu4njoiyRVZo9dLWMe9i9QR/14tjWO+PinfSlo1Bs1uMKGjx3EDRSw76cMHXb0VURzVf08ShBxsnts8o/l8TPNyMgcqeEuNaMFTr3rMMpfkeLtBcBljqnvPjusAPmzJxi6aElophSmuPpwSgC/QCHOxT99mEObrf0VDRNw==') # noqa: E501 + + def test_default_deprecated_256(self): + hs = sign.HeaderSigner(key_id='Test', secret=self.key_1024, algorithm="rsa-sha256") + unsigned = { + 'Date': self.header_date + } + signed = hs.sign(unsigned) + self.assertIn('Date', signed) + self.assertEqual(unsigned['Date'], signed['Date']) + self.assertIn('Authorization', signed) + auth = parse_authorization_header(signed['authorization']) + params = auth[1] + self.assertIn('keyId', params) + self.assertIn('algorithm', params) + self.assertIn('signature', params) + self.assertEqual(params['keyId'], 'Test') + self.assertEqual(params['algorithm'], 'rsa-sha256') + self.assertEqual(params['signature'], 'jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w=') # noqa: E501 diff --git a/httpsig/tests/test_utils.py b/httpsig/tests/test_utils.py index aa53acd..9263659 100755 --- a/httpsig/tests/test_utils.py +++ b/httpsig/tests/test_utils.py @@ -11,7 +11,7 @@ class TestUtils(unittest.TestCase): def test_get_fingerprint(self): with open(os.path.join( - os.path.dirname(__file__), 'rsa_public.pem'), 'r') as k: + os.path.dirname(__file__), 'rsa_public_1024.pem'), 'r') as k: key = k.read() fingerprint = get_fingerprint(key) self.assertEqual( diff --git a/httpsig/tests/test_verify.py b/httpsig/tests/test_verify.py index 6e6d9eb..fe8853b 100755 --- a/httpsig/tests/test_verify.py +++ b/httpsig/tests/test_verify.py @@ -43,11 +43,12 @@ def setUp(self): self.algorithm = "hmac-sha1" self.sign_secret = secret self.verify_secret = secret + self.sign_algorithm = None def test_basic_sign(self): - signer = Signer(secret=self.sign_secret, algorithm=self.algorithm) + signer = Signer(secret=self.sign_secret, algorithm=self.algorithm, sign_algorithm=self.sign_algorithm) verifier = Verifier( - secret=self.verify_secret, algorithm=self.algorithm) + secret=self.verify_secret, algorithm=self.algorithm, sign_algorithm=self.sign_algorithm) GOOD = b"this is a test" BAD = b"this is not the signature you were looking for..." @@ -64,10 +65,10 @@ def test_default(self): hs = HeaderSigner( key_id="Test", secret=self.sign_secret, algorithm=self.algorithm, - sign_header=self.sign_header) + sign_header=self.sign_header, sign_algorithm=self.sign_algorithm) signed = hs.sign(unsigned) hv = HeaderVerifier( - headers=signed, secret=self.verify_secret, sign_header=self.sign_header) + headers=signed, secret=self.verify_secret, sign_header=self.sign_header, sign_algorithm=self.sign_algorithm) self.assertTrue(hv.verify()) def test_signed_headers(self): @@ -86,7 +87,8 @@ def test_signed_headers(self): 'content-type', 'digest', 'content-length' - ]) + ], + sign_algorithm=self.sign_algorithm) unsigned = { 'Host': HOST, 'Date': self.header_date, @@ -99,7 +101,7 @@ def test_signed_headers(self): hv = HeaderVerifier( headers=signed, secret=self.verify_secret, host=HOST, method=METHOD, path=PATH, - sign_header=self.sign_header) + sign_header=self.sign_header, sign_algorithm=self.sign_algorithm) self.assertTrue(hv.verify()) def test_incorrect_headers(self): @@ -116,7 +118,8 @@ def test_incorrect_headers(self): 'date', 'content-type', 'digest', - 'content-length']) + 'content-length'], + sign_algorithm=self.sign_algorithm) unsigned = { 'Host': HOST, 'Date': self.header_date, @@ -129,7 +132,7 @@ def test_incorrect_headers(self): hv = HeaderVerifier(headers=signed, secret=self.verify_secret, required_headers=["some-other-header"], host=HOST, method=METHOD, path=PATH, - sign_header=self.sign_header) + sign_header=self.sign_header, sign_algorithm=self.sign_algorithm) with self.assertRaises(Exception): hv.verify() @@ -148,7 +151,8 @@ def test_extra_auth_headers(self): 'content-type', 'digest', 'content-length' - ]) + ], + sign_algorithm=self.sign_algorithm) unsigned = { 'Host': HOST, 'Date': self.header_date, @@ -163,7 +167,8 @@ def test_extra_auth_headers(self): method=METHOD, path=PATH, sign_header=self.sign_header, - required_headers=['date', '(request-target)']) + required_headers=['date', '(request-target)'], + sign_algorithm=self.sign_algorithm) self.assertTrue(hv.verify()) @@ -186,13 +191,13 @@ class TestVerifyRSASHA1(TestVerifyHMACSHA1): def setUp(self): private_key_path = os.path.join( os.path.dirname(__file__), - 'rsa_private.pem') + 'rsa_private_1024.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') + 'rsa_public_1024.pem') with open(public_key_path, 'rb') as f: public_key = f.read() @@ -200,6 +205,7 @@ def setUp(self): self.algorithm = "rsa-sha1" self.sign_secret = private_key self.verify_secret = public_key + self.sign_algorithm = None class TestVerifyRSASHA256(TestVerifyRSASHA1): @@ -218,3 +224,26 @@ def setUp(self): class TestVerifyRSASHA512ChangeHeader(TestVerifyRSASHA1): sign_header = 'Signature' + + +class TestVerifyHS2019PSS(TestVerifyHMACSHA1): + + def setUp(self): + private_key_path = os.path.join( + os.path.dirname(__file__), + 'rsa_private_2048.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_2048.pem') + with open(public_key_path, 'rb') as f: + public_key = f.read() + + self.keyId = "Test" + self.algorithm = "hs2019" + self.sign_secret = private_key + self.verify_secret = public_key + self.sign_algorithm = "PSS" + From 619a13c728174da76b7e30a6b70649d85203cf7b Mon Sep 17 00:00:00 2001 From: fulder Date: Mon, 24 Aug 2020 15:55:38 +0200 Subject: [PATCH 12/32] Make PSS salt configurable In order to be able to decode the PSS message, the salt length need to be known. --- httpsig/sign.py | 20 ++++++++++++-------- httpsig/verify.py | 7 ++++--- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index c4d3d48..070c66a 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -10,6 +10,7 @@ DEFAULT_SIGN_ALGORITHM = "hs2019" +DEFAULT_SALT_LENGTH = 20 class Signer(object): @@ -19,9 +20,11 @@ class Signer(object): Password-protected keyfiles are not supported. """ - def __init__(self, secret, algorithm=None, sign_algorithm=None): + def __init__(self, secret, algorithm=None, sign_algorithm=None, salt_length=None): if algorithm is None: algorithm = DEFAULT_SIGN_ALGORITHM + if salt_length is None: + salt_length = DEFAULT_SALT_LENGTH assert algorithm in ALGORITHMS, "Unknown algorithm" assert sign_algorithm is None or sign_algorithm in SIGN_ALGORITHMS, "Unsupported digital signature algorithm" @@ -58,7 +61,7 @@ def __init__(self, secret, algorithm=None, sign_algorithm=None): elif self.sign_algorithm == "PSS": try: rsa_key = RSA.importKey(secret) - self._rsa = PKCS1_PSS.new(rsa_key) + self._rsa = PKCS1_PSS.new(rsa_key, saltLen=salt_length) self._hash = HASHES[self.hash_algorithm] except ValueError: raise HttpSigException("Invalid key.") @@ -100,18 +103,19 @@ class HeaderSigner(Signer): to use :arg secret: a PEM-encoded RSA private key or an HMAC secret (must match the algorithm) - :arg algorithm: one of the seven specified algorithms - :arg sign_algorithm: required for 'hs2019' algorithm. Sign algorithm for the secret - :arg headers: a list of http headers to be included in the signing + :param algorithm: one of the seven specified algorithms + :param sign_algorithm: required for 'hs2019' algorithm. Sign algorithm for the secret + :param sign_algorithm: Custom salt length for 'hs2019' and 'PSS' sign algorithm. + :param headers: a list of http headers to be included in the signing string, defaulting to ['date']. - :arg sign_header: header used to include signature, defaulting to + :param sign_header: header used to include signature, defaulting to 'authorization'. """ - def __init__(self, key_id, secret, algorithm=None, sign_algorithm=None, headers=None, sign_header='authorization'): + def __init__(self, key_id, secret, algorithm=None, sign_algorithm=None, salt_length=None, headers=None, sign_header='authorization'): if algorithm is None: algorithm = DEFAULT_SIGN_ALGORITHM - super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm, sign_algorithm=sign_algorithm) + super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm, sign_algorithm=sign_algorithm, salt_length=salt_length) self.headers = headers or ['date'] self.signature_template = build_signature_template( key_id, algorithm, headers, sign_header) diff --git a/httpsig/verify.py b/httpsig/verify.py index 4db0da5..7501daf 100644 --- a/httpsig/verify.py +++ b/httpsig/verify.py @@ -52,7 +52,7 @@ class HeaderVerifier(Verifier): """ def __init__(self, headers, secret, required_headers=None, method=None, - path=None, host=None, sign_header='authorization', sign_algorithm=None): + path=None, host=None, sign_header='authorization', sign_algorithm=None, salt_length=None): """ Instantiate a HeaderVerifier object. @@ -73,6 +73,7 @@ def __init__(self, headers, secret, required_headers=None, method=None, Default is 'authorization'. :param sign_algorithm: Required for 'hs2019' algorithm, specifies the digital signature algorithm (derived from keyId) to use. + :param sign_algorithm: Custom salt length for 'hs2019' and 'PSS' sign algorithm. """ required_headers = required_headers or ['date'] self.headers = CaseInsensitiveDict(headers) @@ -93,11 +94,11 @@ def __init__(self, headers, secret, required_headers=None, method=None, if self.auth_dict['algorithm'] != DEFAULT_SIGN_ALGORITHM: print("Algorithm: {} is deprecated please update to {}".format(self.auth_dict['algorithm'], DEFAULT_SIGN_ALGORITHM)) - elif self.auth_dict['algorithm'] == DEFAULT_SIGN_ALGORITHM and self.sign_algorithm is None: + elif self.auth_dict['algorithm'] == DEFAULT_SIGN_ALGORITHM and sign_algorithm is None: raise HttpSigException("Required sign algorithm for {} algorithm not set".format(DEFAULT_SIGN_ALGORITHM)) super(HeaderVerifier, self).__init__( - secret, algorithm=self.auth_dict['algorithm'], sign_algorithm=sign_algorithm) + secret, algorithm=self.auth_dict['algorithm'], sign_algorithm=sign_algorithm, salt_length=salt_length) def verify(self): """ From 0a57e36cfc44af94f1d861b046979e8c2d9f7c39 Mon Sep 17 00:00:00 2001 From: fulder Date: Mon, 24 Aug 2020 16:58:16 +0200 Subject: [PATCH 13/32] Set default salt to None, i.e. digest size --- httpsig/sign.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index 070c66a..eb62e60 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -10,7 +10,7 @@ DEFAULT_SIGN_ALGORITHM = "hs2019" -DEFAULT_SALT_LENGTH = 20 +DEFAULT_SALT_LENGTH = None class Signer(object): From c4e36fb4ec612630c540676c249a02f7886f6d2d Mon Sep 17 00:00:00 2001 From: fulder Date: Tue, 25 Aug 2020 08:58:51 +0200 Subject: [PATCH 14/32] Create sign_algorithms with first PSS class --- httpsig/sign.py | 36 ++++++------- httpsig/sign_algorithms.py | 53 ++++++++++++++++++++ httpsig/tests/test_signature.py | 22 ++++++-- httpsig/tests/test_verify.py | 89 ++++++++++++++++----------------- httpsig/utils.py | 4 -- httpsig/verify.py | 9 ++-- 6 files changed, 131 insertions(+), 82 deletions(-) create mode 100644 httpsig/sign_algorithms.py diff --git a/httpsig/sign.py b/httpsig/sign.py index eb62e60..1a0a3fe 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -1,16 +1,14 @@ +from __future__ import print_function import base64 import six from Crypto.Hash import HMAC from Crypto.PublicKey import RSA from Crypto.Signature import PKCS1_v1_5 -from Crypto.Signature import PKCS1_PSS - +from .sign_algorithms import SIGN_ALGORITHMS from .utils import * - DEFAULT_SIGN_ALGORITHM = "hs2019" -DEFAULT_SALT_LENGTH = None class Signer(object): @@ -20,14 +18,15 @@ class Signer(object): Password-protected keyfiles are not supported. """ - def __init__(self, secret, algorithm=None, sign_algorithm=None, salt_length=None): + + def __init__(self, secret, algorithm=None, sign_algorithm=None): if algorithm is None: algorithm = DEFAULT_SIGN_ALGORITHM - if salt_length is None: - salt_length = DEFAULT_SALT_LENGTH assert algorithm in ALGORITHMS, "Unknown algorithm" - assert sign_algorithm is None or sign_algorithm in SIGN_ALGORITHMS, "Unsupported digital signature algorithm" + + if sign_algorithm is not None and sign_algorithm.__class__.__name__ not in SIGN_ALGORITHMS: + raise HttpSigException("Unsupported digital signature algorithm") if algorithm != DEFAULT_SIGN_ALGORITHM: print("Algorithm: {} is deprecated please update to {}".format(algorithm, DEFAULT_SIGN_ALGORITHM)) @@ -38,13 +37,13 @@ def __init__(self, secret, algorithm=None, sign_algorithm=None, salt_length=None self._rsa = None self._hash = None self.algorithm = algorithm + self.secret = secret if "-" in algorithm: self.sign_algorithm, self.hash_algorithm = algorithm.split('-') elif algorithm == "hs2019": assert sign_algorithm is not None, "Required digital signature algorithm not specified" self.sign_algorithm = sign_algorithm - self.hash_algorithm = "sha512" if self.sign_algorithm == 'rsa': try: @@ -58,14 +57,6 @@ def __init__(self, secret, algorithm=None, sign_algorithm=None, salt_length=None self._hash = HMAC.new(secret, digestmod=HASHES[self.hash_algorithm]) - elif self.sign_algorithm == "PSS": - try: - rsa_key = RSA.importKey(secret) - self._rsa = PKCS1_PSS.new(rsa_key, saltLen=salt_length) - self._hash = HASHES[self.hash_algorithm] - except ValueError: - raise HttpSigException("Invalid key.") - def _sign_rsa(self, data): if isinstance(data, six.string_types): data = data.encode("ascii") @@ -88,6 +79,8 @@ def sign(self, data): signed = self._sign_rsa(data) elif self._hash: signed = self._sign_hmac(data) + elif self.sign_algorithm.__class__.__name__ in SIGN_ALGORITHMS: + signed = self.sign_algorithm.sign(self.secret, data) if not signed: raise SystemError('No valid encryptor found.') return base64.b64encode(signed).decode("ascii") @@ -111,14 +104,15 @@ class HeaderSigner(Signer): :param sign_header: header used to include signature, defaulting to 'authorization'. """ - def __init__(self, key_id, secret, algorithm=None, sign_algorithm=None, salt_length=None, headers=None, sign_header='authorization'): + + def __init__(self, key_id, secret, algorithm=None, sign_algorithm=None, headers=None, sign_header='authorization'): if algorithm is None: algorithm = DEFAULT_SIGN_ALGORITHM - super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm, sign_algorithm=sign_algorithm, salt_length=salt_length) + super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm, sign_algorithm=sign_algorithm) self.headers = headers or ['date'] self.signature_template = build_signature_template( - key_id, algorithm, headers, sign_header) + key_id, algorithm, headers, sign_header) self.sign_header = sign_header def sign(self, headers, host=None, method=None, path=None): @@ -134,7 +128,7 @@ 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) + required_headers, headers, host, method, path) signature = super(HeaderSigner, self).sign(signable) headers[self.sign_header] = self.signature_template % signature diff --git a/httpsig/sign_algorithms.py b/httpsig/sign_algorithms.py new file mode 100644 index 0000000..8018175 --- /dev/null +++ b/httpsig/sign_algorithms.py @@ -0,0 +1,53 @@ +import base64 + +import six +from Crypto.PublicKey import RSA +from Crypto.Signature import PKCS1_PSS +from httpsig.utils import HttpSigException, HASHES + +DEFAULT_HASH_ALGORITHM = "sha512" + + +class PSS(object): + + def __init__(self, hash_algorithm=DEFAULT_HASH_ALGORITHM, salt_length=None, mgfunc=None): + if hash_algorithm not in HASHES: + raise HttpSigException("Unsupported hash algorithm") + + if hash_algorithm != DEFAULT_HASH_ALGORITHM: + raise HttpSigException( + "Hash algorithm: {} is deprecated. Please use: {}".format(hash_algorithm, DEFAULT_HASH_ALGORITHM)) + + self.hash_algorithm = HASHES[hash_algorithm] + self.salt_length = salt_length + self.mgfunc = mgfunc + + def _create_pss(self, key): + try: + rsa_key = RSA.importKey(key) + pss = PKCS1_PSS.new(rsa_key, saltLen=self.salt_length, mgfunc=self.mgfunc) + except ValueError: + raise HttpSigException("Invalid key.") + return pss + + def sign(self, private_key, data): + pss = self._create_pss(private_key) + + if isinstance(data, six.string_types): + data = data.encode("ascii") + + h = self.hash_algorithm.new() + h.update(data) + return pss.sign(h) + + def verify(self, public_key, data, signature): + pss = self._create_pss(public_key) + + h = self.hash_algorithm.new() + h.update(data) + return pss.verify(h, base64.b64decode(signature)) + + +SIGN_ALGORITHMS = frozenset([ + "PSS" +]) diff --git a/httpsig/tests/test_signature.py b/httpsig/tests/test_signature.py index e03cb31..00a7fed 100755 --- a/httpsig/tests/test_signature.py +++ b/httpsig/tests/test_signature.py @@ -4,9 +4,11 @@ import unittest -import httpsig.sign as sign -from httpsig.utils import parse_authorization_header +import pytest +import httpsig.sign as sign +from httpsig.sign_algorithms import PSS +from httpsig.utils import parse_authorization_header, HttpSigException sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) @@ -34,7 +36,7 @@ def setUp(self): self.key_1024 = f.read() def test_default(self): - hs = sign.HeaderSigner(key_id='Test', secret=self.key_2048, sign_algorithm="PSS", salt_length=0) + hs = sign.HeaderSigner(key_id='Test', secret=self.key_2048, sign_algorithm=PSS(hash_algorithm="sha512", salt_length=0)) unsigned = { 'Date': self.header_date } @@ -52,7 +54,7 @@ def test_default(self): self.assertEqual(params['signature'], 'T8+Cj3Zp2cBDm2r8/loPgfHUSSFXXyZJNxxbNx1NvKVz/r5T4z6pVxhl9rqk8WfYHMdlh2aT5hCrYKvhs88Jy0DDmeUP4nELWRsO1BF0oAqHfcrbEikZQL7jA6z0guVaLr0S5QRGmd1K5HUEkP/vYEOns+FRL+JrFG4dNJNESvG5iyKUoaXfoZCFdqtzLlIteEAL7dW/kaX/dE116wfpbem1eCABuGopRhuFtjqLKVjuUVwyP/zSYTqd9j+gDhinkAifTJPxbGMh0b5LZdNCqw5irT9NkTcTFRXDp8ioX8r805Z9QhjT7H+rSo350U2LsAFoQ9ttryPBOoMPCiQTlw==') # noqa: E501 def test_basic(self): - hs = sign.HeaderSigner(key_id='Test', secret=self.key_2048, sign_algorithm="PSS", salt_length=0, headers=[ + hs = sign.HeaderSigner(key_id='Test', secret=self.key_2048, sign_algorithm=PSS(salt_length=0), headers=[ '(request-target)', 'host', 'date', @@ -79,7 +81,7 @@ def test_basic(self): self.assertEqual(params['signature'], 'KkF4oeOJJH9TaYjQdaU634G7AVmM5Bf3fnfJCBZ7G0H5puW5XlQTpduA+TgouKOJhbv4aRRpunPzCHUxUjEvrR3TSALqW1EOsBwCVIusE9CnrhL7vUOvciIDai/jI15RsfR9+XyTmOSFbsI07E8mmywr3nLeWX6AAFDMO2vWc21zZxrSc13vFfAkVvFhXLxO4g0bBm6Z4m5/9ytWtdE0Gf3St2kY8aZTedllRCS8cMx8GVAIw/qYGeIlGKUCZKxrFxnviN7gfxixwova6lcxpppIo+WXxEiwMJfSQBlx0WGn3A3twCv6TsIxPOVUEW4jcogDh+jGFf1aGdVyHquTRQ==') # noqa: E501 def test_all(self): - hs = sign.HeaderSigner(key_id='Test', secret=self.key_2048, sign_algorithm="PSS", salt_length=0, headers=[ + hs = sign.HeaderSigner(key_id='Test', secret=self.key_2048, sign_algorithm=PSS("sha512", salt_length=0), headers=[ '(request-target)', 'host', 'date', @@ -129,3 +131,13 @@ def test_default_deprecated_256(self): self.assertEqual(params['keyId'], 'Test') self.assertEqual(params['algorithm'], 'rsa-sha256') self.assertEqual(params['signature'], 'jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w=') # noqa: E501 + + def test_unsupported_hash_algorithm(self): + with pytest.raises(HttpSigException) as e: + sign.HeaderSigner(key_id='Test', secret=self.key_2048, sign_algorithm=PSS("sha123", salt_length=0)) + self.assertEqual(str(e.value), "Unsupported hash algorithm") + + def test_deprecated_hash_algorithm(self): + with pytest.raises(HttpSigException) as e: + sign.HeaderSigner(key_id='Test', secret=self.key_2048, sign_algorithm=PSS("sha256", salt_length=0)) + self.assertEqual(str(e.value), "Hash algorithm: sha256 is deprecated. Please use: sha512") \ No newline at end of file diff --git a/httpsig/tests/test_verify.py b/httpsig/tests/test_verify.py index fe8853b..d5f9785 100755 --- a/httpsig/tests/test_verify.py +++ b/httpsig/tests/test_verify.py @@ -4,9 +4,9 @@ import unittest from httpsig.sign import HeaderSigner, Signer +from httpsig.sign_algorithms import PSS from httpsig.verify import HeaderVerifier, Verifier - sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) @@ -48,7 +48,7 @@ def setUp(self): def test_basic_sign(self): signer = Signer(secret=self.sign_secret, algorithm=self.algorithm, sign_algorithm=self.sign_algorithm) verifier = Verifier( - secret=self.verify_secret, algorithm=self.algorithm, sign_algorithm=self.sign_algorithm) + secret=self.verify_secret, algorithm=self.algorithm, sign_algorithm=self.sign_algorithm) GOOD = b"this is a test" BAD = b"this is not the signature you were looking for..." @@ -76,19 +76,19 @@ def test_signed_headers(self): METHOD = self.test_method PATH = self.test_path hs = HeaderSigner( - key_id="Test", - secret=self.sign_secret, - algorithm=self.algorithm, - sign_header=self.sign_header, - headers=[ - '(request-target)', - 'host', - 'date', - 'content-type', - 'digest', - 'content-length' - ], - sign_algorithm=self.sign_algorithm) + key_id="Test", + secret=self.sign_secret, + algorithm=self.algorithm, + sign_header=self.sign_header, + headers=[ + '(request-target)', + 'host', + 'date', + 'content-type', + 'digest', + 'content-length' + ], + sign_algorithm=self.sign_algorithm) unsigned = { 'Host': HOST, 'Date': self.header_date, @@ -99,9 +99,9 @@ def test_signed_headers(self): signed = hs.sign(unsigned, method=METHOD, path=PATH) hv = HeaderVerifier( - headers=signed, secret=self.verify_secret, - host=HOST, method=METHOD, path=PATH, - sign_header=self.sign_header, sign_algorithm=self.sign_algorithm) + headers=signed, secret=self.verify_secret, + host=HOST, method=METHOD, path=PATH, + sign_header=self.sign_header, sign_algorithm=self.sign_algorithm) self.assertTrue(hv.verify()) def test_incorrect_headers(self): @@ -141,18 +141,18 @@ def test_extra_auth_headers(self): METHOD = "POST" PATH = '/foo?param=value&pet=dog' hs = HeaderSigner( - key_id="Test", - secret=self.sign_secret, - sign_header=self.sign_header, - algorithm=self.algorithm, headers=[ - '(request-target)', - 'host', - 'date', - 'content-type', - 'digest', - 'content-length' - ], - sign_algorithm=self.sign_algorithm) + key_id="Test", + secret=self.sign_secret, + sign_header=self.sign_header, + algorithm=self.algorithm, headers=[ + '(request-target)', + 'host', + 'date', + 'content-type', + 'digest', + 'content-length' + ], + sign_algorithm=self.sign_algorithm) unsigned = { 'Host': HOST, 'Date': self.header_date, @@ -162,13 +162,13 @@ def test_extra_auth_headers(self): } signed = hs.sign(unsigned, method=METHOD, path=PATH) hv = HeaderVerifier( - headers=signed, - secret=self.verify_secret, - method=METHOD, - path=PATH, - sign_header=self.sign_header, - required_headers=['date', '(request-target)'], - sign_algorithm=self.sign_algorithm) + headers=signed, + secret=self.verify_secret, + method=METHOD, + path=PATH, + sign_header=self.sign_header, + required_headers=['date', '(request-target)'], + sign_algorithm=self.sign_algorithm) self.assertTrue(hv.verify()) @@ -190,13 +190,13 @@ class TestVerifyRSASHA1(TestVerifyHMACSHA1): def setUp(self): private_key_path = os.path.join( - os.path.dirname(__file__), + os.path.dirname(__file__), 'rsa_private_1024.pem') with open(private_key_path, 'rb') as f: private_key = f.read() public_key_path = os.path.join( - os.path.dirname(__file__), + os.path.dirname(__file__), 'rsa_public_1024.pem') with open(public_key_path, 'rb') as f: public_key = f.read() @@ -229,15 +229,11 @@ class TestVerifyRSASHA512ChangeHeader(TestVerifyRSASHA1): class TestVerifyHS2019PSS(TestVerifyHMACSHA1): def setUp(self): - private_key_path = os.path.join( - os.path.dirname(__file__), - 'rsa_private_2048.pem') + private_key_path = os.path.join(os.path.dirname(__file__), 'rsa_private_2048.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_2048.pem') + public_key_path = os.path.join(os.path.dirname(__file__), 'rsa_public_2048.pem') with open(public_key_path, 'rb') as f: public_key = f.read() @@ -245,5 +241,4 @@ def setUp(self): self.algorithm = "hs2019" self.sign_secret = private_key self.verify_secret = public_key - self.sign_algorithm = "PSS" - + self.sign_algorithm = PSS(salt_length=0) diff --git a/httpsig/utils.py b/httpsig/utils.py index 135926f..9cef5e5 100644 --- a/httpsig/utils.py +++ b/httpsig/utils.py @@ -25,10 +25,6 @@ 'sha256': SHA256, 'sha512': SHA512} -SIGN_ALGORITHMS = frozenset([ - "PSS" -]) - class HttpSigException(Exception): pass diff --git a/httpsig/verify.py b/httpsig/verify.py index 7501daf..b6bc26e 100644 --- a/httpsig/verify.py +++ b/httpsig/verify.py @@ -5,6 +5,7 @@ import six from .sign import Signer, DEFAULT_SIGN_ALGORITHM +from .sign_algorithms import SIGN_ALGORITHMS from .utils import * @@ -37,10 +38,8 @@ def _verify(self, data, signature): s = base64.b64decode(signature) return ct_bytes_compare(h, s) - elif self.sign_algorithm == 'PSS': - h = self._hash.new() - h.update(data) - return self._rsa.verify(h, base64.b64decode(signature)) + elif self.sign_algorithm.__class__.__name__ in SIGN_ALGORITHMS: + return self.sign_algorithm.verify(self.secret, data, signature) else: raise HttpSigException("Unsupported algorithm.") @@ -98,7 +97,7 @@ def __init__(self, headers, secret, required_headers=None, method=None, raise HttpSigException("Required sign algorithm for {} algorithm not set".format(DEFAULT_SIGN_ALGORITHM)) super(HeaderVerifier, self).__init__( - secret, algorithm=self.auth_dict['algorithm'], sign_algorithm=sign_algorithm, salt_length=salt_length) + secret, algorithm=self.auth_dict['algorithm'], sign_algorithm=sign_algorithm) def verify(self): """ From 68b736955050c44e205fd4bfbcfb6044965d3e10 Mon Sep 17 00:00:00 2001 From: fulder Date: Tue, 25 Aug 2020 09:53:01 +0200 Subject: [PATCH 15/32] Test creating superclass for sign algorithm --- httpsig/sign.py | 10 +++------- httpsig/sign_algorithms.py | 20 ++++++++++++++------ httpsig/verify.py | 5 ++--- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index 1a0a3fe..af63f3e 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -5,7 +5,7 @@ from Crypto.Hash import HMAC from Crypto.PublicKey import RSA from Crypto.Signature import PKCS1_v1_5 -from .sign_algorithms import SIGN_ALGORITHMS +from .sign_algorithms import SignAlgorithm from .utils import * DEFAULT_SIGN_ALGORITHM = "hs2019" @@ -19,15 +19,12 @@ class Signer(object): Password-protected keyfiles are not supported. """ - def __init__(self, secret, algorithm=None, sign_algorithm=None): + def __init__(self, secret, algorithm=None, sign_algorithm: SignAlgorithm=None): if algorithm is None: algorithm = DEFAULT_SIGN_ALGORITHM assert algorithm in ALGORITHMS, "Unknown algorithm" - if sign_algorithm is not None and sign_algorithm.__class__.__name__ not in SIGN_ALGORITHMS: - raise HttpSigException("Unsupported digital signature algorithm") - if algorithm != DEFAULT_SIGN_ALGORITHM: print("Algorithm: {} is deprecated please update to {}".format(algorithm, DEFAULT_SIGN_ALGORITHM)) @@ -79,7 +76,7 @@ def sign(self, data): signed = self._sign_rsa(data) elif self._hash: signed = self._sign_hmac(data) - elif self.sign_algorithm.__class__.__name__ in SIGN_ALGORITHMS: + elif isinstance(self.sign_algorithm, SignAlgorithm): signed = self.sign_algorithm.sign(self.secret, data) if not signed: raise SystemError('No valid encryptor found.') @@ -98,7 +95,6 @@ class HeaderSigner(Signer): match the algorithm) :param algorithm: one of the seven specified algorithms :param sign_algorithm: required for 'hs2019' algorithm. Sign algorithm for the secret - :param sign_algorithm: Custom salt length for 'hs2019' and 'PSS' sign algorithm. :param headers: a list of http headers to be included in the signing string, defaulting to ['date']. :param sign_header: header used to include signature, defaulting to diff --git a/httpsig/sign_algorithms.py b/httpsig/sign_algorithms.py index 8018175..a10fb4c 100644 --- a/httpsig/sign_algorithms.py +++ b/httpsig/sign_algorithms.py @@ -4,11 +4,24 @@ from Crypto.PublicKey import RSA from Crypto.Signature import PKCS1_PSS from httpsig.utils import HttpSigException, HASHES +from abc import ABCMeta, abstractmethod DEFAULT_HASH_ALGORITHM = "sha512" -class PSS(object): +class SignAlgorithm(object): + __metaclass__ = ABCMeta + + @abstractmethod + def sign(self, *args): + raise NotImplementedError() + + @abstractmethod + def verify(self, *args): + raise NotImplementedError() + + +class PSS(SignAlgorithm): def __init__(self, hash_algorithm=DEFAULT_HASH_ALGORITHM, salt_length=None, mgfunc=None): if hash_algorithm not in HASHES: @@ -46,8 +59,3 @@ def verify(self, public_key, data, signature): h = self.hash_algorithm.new() h.update(data) return pss.verify(h, base64.b64decode(signature)) - - -SIGN_ALGORITHMS = frozenset([ - "PSS" -]) diff --git a/httpsig/verify.py b/httpsig/verify.py index b6bc26e..c529391 100644 --- a/httpsig/verify.py +++ b/httpsig/verify.py @@ -5,7 +5,7 @@ import six from .sign import Signer, DEFAULT_SIGN_ALGORITHM -from .sign_algorithms import SIGN_ALGORITHMS +from .sign_algorithms import SignAlgorithm from .utils import * @@ -38,7 +38,7 @@ def _verify(self, data, signature): s = base64.b64decode(signature) return ct_bytes_compare(h, s) - elif self.sign_algorithm.__class__.__name__ in SIGN_ALGORITHMS: + elif isinstance(self.sign_algorithm, SignAlgorithm): return self.sign_algorithm.verify(self.secret, data, signature) else: @@ -72,7 +72,6 @@ def __init__(self, headers, secret, required_headers=None, method=None, Default is 'authorization'. :param sign_algorithm: Required for 'hs2019' algorithm, specifies the digital signature algorithm (derived from keyId) to use. - :param sign_algorithm: Custom salt length for 'hs2019' and 'PSS' sign algorithm. """ required_headers = required_headers or ['date'] self.headers = CaseInsensitiveDict(headers) From 2060e8353a2445183642ac8a30ef352af986f08a Mon Sep 17 00:00:00 2001 From: fulder Date: Tue, 25 Aug 2020 10:00:35 +0200 Subject: [PATCH 16/32] Check for subclasses of SignAlgorithms instead of hardcoded list --- httpsig/sign.py | 5 ++++- httpsig/verify.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index af63f3e..a2bbd58 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -19,12 +19,15 @@ class Signer(object): Password-protected keyfiles are not supported. """ - def __init__(self, secret, algorithm=None, sign_algorithm: SignAlgorithm=None): + def __init__(self, secret, algorithm=None, sign_algorithm=None): if algorithm is None: algorithm = DEFAULT_SIGN_ALGORITHM assert algorithm in ALGORITHMS, "Unknown algorithm" + if sign_algorithm is not None and not issubclass(type(sign_algorithm), SignAlgorithm): + raise HttpSigException("Unsupported digital signature algorithm") + if algorithm != DEFAULT_SIGN_ALGORITHM: print("Algorithm: {} is deprecated please update to {}".format(algorithm, DEFAULT_SIGN_ALGORITHM)) diff --git a/httpsig/verify.py b/httpsig/verify.py index c529391..7911c98 100644 --- a/httpsig/verify.py +++ b/httpsig/verify.py @@ -38,7 +38,7 @@ def _verify(self, data, signature): s = base64.b64decode(signature) return ct_bytes_compare(h, s) - elif isinstance(self.sign_algorithm, SignAlgorithm): + elif issubclass(type(self.sign_algorithm), SignAlgorithm): return self.sign_algorithm.verify(self.secret, data, signature) else: From dc2b90b3b4c666c9e49fd73af990877fe3b77b37 Mon Sep 17 00:00:00 2001 From: fulder Date: Tue, 25 Aug 2020 10:01:06 +0200 Subject: [PATCH 17/32] Rename default sign algorithm to default algorithm --- httpsig/sign.py | 10 +++++----- httpsig/tests/test_signature.py | 2 +- httpsig/verify.py | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index a2bbd58..0f984fd 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -8,7 +8,7 @@ from .sign_algorithms import SignAlgorithm from .utils import * -DEFAULT_SIGN_ALGORITHM = "hs2019" +DEFAULT_ALGORITHM = "hs2019" class Signer(object): @@ -21,15 +21,15 @@ class Signer(object): def __init__(self, secret, algorithm=None, sign_algorithm=None): if algorithm is None: - algorithm = DEFAULT_SIGN_ALGORITHM + algorithm = DEFAULT_ALGORITHM assert algorithm in ALGORITHMS, "Unknown algorithm" if sign_algorithm is not None and not issubclass(type(sign_algorithm), SignAlgorithm): raise HttpSigException("Unsupported digital signature algorithm") - if algorithm != DEFAULT_SIGN_ALGORITHM: - print("Algorithm: {} is deprecated please update to {}".format(algorithm, DEFAULT_SIGN_ALGORITHM)) + if algorithm != DEFAULT_ALGORITHM: + print("Algorithm: {} is deprecated please update to {}".format(algorithm, DEFAULT_ALGORITHM)) if isinstance(secret, six.string_types): secret = secret.encode("ascii") @@ -106,7 +106,7 @@ class HeaderSigner(Signer): def __init__(self, key_id, secret, algorithm=None, sign_algorithm=None, headers=None, sign_header='authorization'): if algorithm is None: - algorithm = DEFAULT_SIGN_ALGORITHM + algorithm = DEFAULT_ALGORITHM super(HeaderSigner, self).__init__(secret=secret, algorithm=algorithm, sign_algorithm=sign_algorithm) self.headers = headers or ['date'] diff --git a/httpsig/tests/test_signature.py b/httpsig/tests/test_signature.py index 00a7fed..5e3d4a6 100755 --- a/httpsig/tests/test_signature.py +++ b/httpsig/tests/test_signature.py @@ -12,7 +12,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) -sign.DEFAULT_SIGN_ALGORITHM = "hs2019" +sign.DEFAULT_ALGORITHM = "hs2019" class TestSign(unittest.TestCase): diff --git a/httpsig/verify.py b/httpsig/verify.py index 7911c98..ace7975 100644 --- a/httpsig/verify.py +++ b/httpsig/verify.py @@ -4,7 +4,7 @@ import base64 import six -from .sign import Signer, DEFAULT_SIGN_ALGORITHM +from .sign import Signer, DEFAULT_ALGORITHM from .sign_algorithms import SignAlgorithm from .utils import * @@ -90,10 +90,10 @@ def __init__(self, headers, secret, required_headers=None, method=None, self.path = path self.host = host - if self.auth_dict['algorithm'] != DEFAULT_SIGN_ALGORITHM: - print("Algorithm: {} is deprecated please update to {}".format(self.auth_dict['algorithm'], DEFAULT_SIGN_ALGORITHM)) - elif self.auth_dict['algorithm'] == DEFAULT_SIGN_ALGORITHM and sign_algorithm is None: - raise HttpSigException("Required sign algorithm for {} algorithm not set".format(DEFAULT_SIGN_ALGORITHM)) + if self.auth_dict['algorithm'] != DEFAULT_ALGORITHM: + print("Algorithm: {} is deprecated please update to {}".format(self.auth_dict['algorithm'], DEFAULT_ALGORITHM)) + elif self.auth_dict['algorithm'] == DEFAULT_ALGORITHM and sign_algorithm is None: + raise HttpSigException("Required sign algorithm for {} algorithm not set".format(DEFAULT_ALGORITHM)) super(HeaderVerifier, self).__init__( secret, algorithm=self.auth_dict['algorithm'], sign_algorithm=sign_algorithm) From 76c26c73640c0aa8bef69d9fbeb902b3170c7076 Mon Sep 17 00:00:00 2001 From: fulder Date: Tue, 25 Aug 2020 10:07:05 +0200 Subject: [PATCH 18/32] Fix failing test due to invalid signature --- httpsig/tests/test_signature.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httpsig/tests/test_signature.py b/httpsig/tests/test_signature.py index 5e3d4a6..ffc4369 100755 --- a/httpsig/tests/test_signature.py +++ b/httpsig/tests/test_signature.py @@ -21,7 +21,7 @@ class TestSign(unittest.TestCase): header_host = 'example.com' header_date = 'Thu, 05 Jan 2014 21:31:40 GMT' header_content_type = 'application/json' - header_digest = 'SHA-512=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=' + header_digest = 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=' header_content_length = '18' def setUp(self): @@ -112,7 +112,7 @@ def test_all(self): self.assertEqual( params['headers'], '(request-target) host date content-type digest content-length') - self.assertEqual(params['signature'], 'Ur8ehf0YlxBIRyXJG+iBBubrMlxWxDqpYgEaABq5ukcant30Gygkrs4ujFWxlR8pbBS/kDewYdlNhJOsVva2Y/ZSmardYHWYuSw3QjW0KON7nfVT/hijDFCAAzDDOqS6uSJimWmyko23bt2XDydMS2ekGoRFXxQcCtd2piWDpwaHneZiUu4njoiyRVZo9dLWMe9i9QR/14tjWO+PinfSlo1Bs1uMKGjx3EDRSw76cMHXb0VURzVf08ShBxsnts8o/l8TPNyMgcqeEuNaMFTr3rMMpfkeLtBcBljqnvPjusAPmzJxi6aElophSmuPpwSgC/QCHOxT99mEObrf0VDRNw==') # noqa: E501 + self.assertEqual(params['signature'], 'bxWyLDB/Tuhzxd/tWG2g60l3Goyk9XJZzj2ouNKizZuZoe1Ngj+19N11bhK7FABHJ7lSzH5g6fp5LkN894ivIv6N29L2sPssuAkqgzNXyvYkp4KWOr5j7sVpApmRH7gf7THljcXosmrYk5gdBTspixpJJJ5LGkkPKCRAFurmi/LqopSH6cJbLJNIccTu2dTMGEeDOqqNterVmfonpZyPeBsEEwoeOo6d8zgHzB/1Xxk7dfELFbA1c0LE5kZbwEIEFPmS01YFz6EJW7Aj8kzvzwQRyvgDobi25niGOy/D7JVHvtDjBIaJedFuFJSb8rZ2DGryBQ6NwchMp3f2MUoTGg==') # noqa: E501 def test_default_deprecated_256(self): hs = sign.HeaderSigner(key_id='Test', secret=self.key_1024, algorithm="rsa-sha256") From 179ab946a4083f33d93e3cf3fff2cd775a91310e Mon Sep 17 00:00:00 2001 From: fulder Date: Tue, 25 Aug 2020 10:21:45 +0200 Subject: [PATCH 19/32] Import sign_algorithms in httpsig init --- httpsig/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/httpsig/__init__.py b/httpsig/__init__.py index 01cb860..b723018 100644 --- a/httpsig/__init__.py +++ b/httpsig/__init__.py @@ -2,6 +2,7 @@ from .sign import Signer, HeaderSigner from .verify import Verifier, HeaderVerifier +from .sign_algorithms import * try: __version__ = get_distribution(__name__).version From 4613083df4efed348417de2913e0cc19ea9e8369 Mon Sep 17 00:00:00 2001 From: fulder Date: Tue, 25 Aug 2020 10:41:13 +0200 Subject: [PATCH 20/32] Update README with newest algorithm --- README.rst | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index bcc9c96..13d72d9 100644 --- a/README.rst +++ b/README.rst @@ -49,7 +49,7 @@ For simple raw signing: secret = open('rsa_private.pem', 'rb').read() - sig_maker = httpsig.Signer(secret=secret, algorithm='rsa-sha256') + sig_maker = httpsig.Signer(secret=secret, algorithm='hs2019', sign_algorithm=httpsig.PSS()) sig_maker.sign('hello world!') For general use with web frameworks: @@ -59,9 +59,9 @@ For general use with web frameworks: import httpsig key_id = "Some Key ID" - secret = b'some big secret' + secret = open('rsa_private.pem', 'rb').read() - hs = httpsig.HeaderSigner(key_id, secret, algorithm="hmac-sha256", headers=['(request-target)', 'host', 'date']) + hs = httpsig.HeaderSigner(key_id, secret, algorithm="hs2019", sign_algorithm=httpsig.PSS(), 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: @@ -74,9 +74,9 @@ For use with requests: secret = open('rsa_private.pem', 'rb').read() - auth = HTTPSignatureAuth(key_id='Test', secret=secret) + auth = HTTPSignatureAuth(key_id='Test', secret=secret, sign_algorithm=httpsig.PSS()) z = requests.get('https://api.example.com/path/to/endpoint', - auth=auth, headers={'X-Api-Version': '~6.5'}) + auth=auth, headers={'X-Api-Version': '~6.5', 'Date': 'Tue, 01 Jan 2014 01:01:01 GMT') Class initialization parameters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -85,20 +85,22 @@ Note that keys and secrets should be bytes objects. At attempt will be made to .. code:: python - httpsig.Signer(secret, algorithm='rsa-sha256') + httpsig.Signer(secret, algorithm='hs2019', sign_algorithm=httpsig.PSS()) ``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``, +``algorithm`` should be set to 'hs2019' the other six signatures are now deprecated: ``rsa-sha1``, ``rsa-sha256``, ``rsa-sha512``, ``hmac-sha1``, ``hmac-sha256``, ``hmac-sha512``. +``sign_algorithm`` The digital signature algorithm derived from ``keyId``. Currently supported algorithms: ``httpsig.PSS`` .. code:: python - httpsig.requests_auth.HTTPSignatureAuth(key_id, secret, algorithm='rsa-sha256', headers=None) + httpsig.requests_auth.HTTPSignatureAuth(key_id, secret, algorithm='hs2019', sign_algorithm=httpsig.PSS(), headers=None) -``key_id`` is the label by which the server system knows your RSA signature or password. +``key_id`` is the label by which the server system knows your secret. ``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. +``sign_algorithm`` The digital signature algorithm derived from ``keyId``. Currently supported algorithms: ``httpsig.PSS`` Tests ----- From 1b061f9c2565a4301f4e33de06723fe62070952b Mon Sep 17 00:00:00 2001 From: fulder Date: Tue, 25 Aug 2020 10:41:32 +0200 Subject: [PATCH 21/32] Add sign_algorithm parameter to HTTPSignatureAuth --- httpsig/requests_auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httpsig/requests_auth.py b/httpsig/requests_auth.py index 8a00310..b504e9c 100644 --- a/httpsig/requests_auth.py +++ b/httpsig/requests_auth.py @@ -21,11 +21,11 @@ class HTTPSignatureAuth(requests.auth.AuthBase): 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, sign_algorithm=None, headers=None): headers = headers or [] self.header_signer = HeaderSigner( key_id=key_id, secret=secret, - algorithm=algorithm, headers=headers) + algorithm=algorithm, sign_algorithm=sign_algorithm, headers=headers) self.uses_host = 'host' in [h.lower() for h in headers] def __call__(self, r): From 77c42ad9ed71efb9f3dca38b2ae7086019cbf6ef Mon Sep 17 00:00:00 2001 From: fulder Date: Tue, 25 Aug 2020 10:59:40 +0200 Subject: [PATCH 22/32] Use issubclass instead of isinstance in Singer construct --- httpsig/sign.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index 0f984fd..0b1ec07 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -79,7 +79,7 @@ def sign(self, data): signed = self._sign_rsa(data) elif self._hash: signed = self._sign_hmac(data) - elif isinstance(self.sign_algorithm, SignAlgorithm): + elif issubclass(type(self.sign_algorithm), SignAlgorithm): signed = self.sign_algorithm.sign(self.secret, data) if not signed: raise SystemError('No valid encryptor found.') From 08ca125118be38ec98e528afc40a393cbc86cf1e Mon Sep 17 00:00:00 2001 From: fulder Date: Tue, 25 Aug 2020 11:04:09 +0200 Subject: [PATCH 23/32] Remove old unused HeaderVerfier salt_length parameter --- httpsig/verify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpsig/verify.py b/httpsig/verify.py index ace7975..055a74a 100644 --- a/httpsig/verify.py +++ b/httpsig/verify.py @@ -51,7 +51,7 @@ class HeaderVerifier(Verifier): """ def __init__(self, headers, secret, required_headers=None, method=None, - path=None, host=None, sign_header='authorization', sign_algorithm=None, salt_length=None): + path=None, host=None, sign_header='authorization', sign_algorithm=None): """ Instantiate a HeaderVerifier object. From fb263d19256e08e5592d380bef92c0b6b1fb2b0a Mon Sep 17 00:00:00 2001 From: fulder Date: Tue, 25 Aug 2020 11:11:07 +0200 Subject: [PATCH 24/32] Enforce parameters for superclass sign/verify funcs --- httpsig/sign_algorithms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httpsig/sign_algorithms.py b/httpsig/sign_algorithms.py index a10fb4c..e4d2656 100644 --- a/httpsig/sign_algorithms.py +++ b/httpsig/sign_algorithms.py @@ -13,11 +13,11 @@ class SignAlgorithm(object): __metaclass__ = ABCMeta @abstractmethod - def sign(self, *args): + def sign(self, private, data): raise NotImplementedError() @abstractmethod - def verify(self, *args): + def verify(self, public, data, signature): raise NotImplementedError() From f97f6c0b31dd435909165bbe5a5c896f39fb960b Mon Sep 17 00:00:00 2001 From: fulder Date: Tue, 25 Aug 2020 11:16:14 +0200 Subject: [PATCH 25/32] Bump README draft to version 12 --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 13d72d9..466507f 100644 --- a/README.rst +++ b/README.rst @@ -7,7 +7,7 @@ httpsig .. 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 (`Draft 8`_). 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 (`Draft 12`_). 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. @@ -15,7 +15,7 @@ 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 8`: http://tools.ietf.org/html/draft-cavage-http-signatures-08 +.. _`Draft 12`: http://tools.ietf.org/html/draft-cavage-http-signatures-12 Requirements ------------ From c5163f89529b794c69b636e23faad5fdd20f5087 Mon Sep 17 00:00:00 2001 From: fulder Date: Wed, 26 Aug 2020 16:45:29 +0200 Subject: [PATCH 26/32] Throw exception on algorithm signature parameter and dervice algorithm mismatch --- httpsig/verify.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/httpsig/verify.py b/httpsig/verify.py index 055a74a..7158c7b 100644 --- a/httpsig/verify.py +++ b/httpsig/verify.py @@ -90,6 +90,11 @@ def __init__(self, headers, secret, required_headers=None, method=None, self.path = path self.host = host + if 'algorithm' in self.auth_dict and self.auth_dict['algorithm'] != self.algorithm: + raise HttpSigException( + "Algorithm mismath, signature parameter algorithm was: {}, but algorithm dervice from key is: {}".format( + self.auth_dict['algorithm'], self.algorithm)) + if self.auth_dict['algorithm'] != DEFAULT_ALGORITHM: print("Algorithm: {} is deprecated please update to {}".format(self.auth_dict['algorithm'], DEFAULT_ALGORITHM)) elif self.auth_dict['algorithm'] == DEFAULT_ALGORITHM and sign_algorithm is None: From 912ff8594b6504cf223bd112f99be88b2c7132b7 Mon Sep 17 00:00:00 2001 From: fulder Date: Wed, 26 Aug 2020 17:02:28 +0200 Subject: [PATCH 27/32] Return false in verify on algorith mismatch --- httpsig/tests/test_verify.py | 14 ++++++++++++++ httpsig/verify.py | 14 ++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/httpsig/tests/test_verify.py b/httpsig/tests/test_verify.py index d5f9785..0e1ada6 100755 --- a/httpsig/tests/test_verify.py +++ b/httpsig/tests/test_verify.py @@ -242,3 +242,17 @@ def setUp(self): self.sign_secret = private_key self.verify_secret = public_key self.sign_algorithm = PSS(salt_length=0) + + def test_algorithm_mismatch(self): + unsigned = { + 'Date': self.header_date + } + + hs = HeaderSigner( + key_id="Test", secret=self.sign_secret, algorithm=self.algorithm, + sign_header=self.sign_header, sign_algorithm=self.sign_algorithm) + signed = hs.sign(unsigned) + + hv = HeaderVerifier( + headers=signed, secret=self.verify_secret, sign_header=self.sign_header, algorithm="rsa-sha256", sign_algorithm=self.sign_algorithm) + self.assertFalse(hv.verify()) \ No newline at end of file diff --git a/httpsig/verify.py b/httpsig/verify.py index 7158c7b..33cb2b2 100644 --- a/httpsig/verify.py +++ b/httpsig/verify.py @@ -51,7 +51,7 @@ class HeaderVerifier(Verifier): """ def __init__(self, headers, secret, required_headers=None, method=None, - path=None, host=None, sign_header='authorization', sign_algorithm=None): + path=None, host=None, sign_header='authorization', algorithm=None, sign_algorithm=None): """ Instantiate a HeaderVerifier object. @@ -70,6 +70,7 @@ def __init__(self, headers, secret, required_headers=None, method=None, header, if not supplied in :param:headers. :param sign_header: Optional. The header where the signature is. Default is 'authorization'. + :param algorithm: Algorithm derived from keyId (required for draft version >= 12) :param sign_algorithm: Required for 'hs2019' algorithm, specifies the digital signature algorithm (derived from keyId) to use. """ @@ -89,11 +90,7 @@ def __init__(self, headers, secret, required_headers=None, method=None, self.method = method self.path = path self.host = host - - if 'algorithm' in self.auth_dict and self.auth_dict['algorithm'] != self.algorithm: - raise HttpSigException( - "Algorithm mismath, signature parameter algorithm was: {}, but algorithm dervice from key is: {}".format( - self.auth_dict['algorithm'], self.algorithm)) + self.derived_algorithm = algorithm if self.auth_dict['algorithm'] != DEFAULT_ALGORITHM: print("Algorithm: {} is deprecated please update to {}".format(self.auth_dict['algorithm'], DEFAULT_ALGORITHM)) @@ -112,6 +109,11 @@ def verify(self): not found in the signature. Returns True or False. """ + if 'algorithm' in self.auth_dict and self.derived_algorithm is not None and self.auth_dict['algorithm'] != self.derived_algorithm: + print("Algorithm mismatch, signature parameter algorithm was: {}, but algorithm derived from key is: {}".format( + self.auth_dict['algorithm'], self.derived_algorithm)) + return False + auth_headers = self.auth_dict.get('headers', 'date').split(' ') if len(set(self.required_headers) - set(auth_headers)) > 0: From 822cad0879802effe8c2aee5da1303fcfd3d8a39 Mon Sep 17 00:00:00 2001 From: fulder Date: Wed, 26 Aug 2020 17:03:34 +0200 Subject: [PATCH 28/32] Add test for correct derived algorithm --- httpsig/tests/test_verify.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/httpsig/tests/test_verify.py b/httpsig/tests/test_verify.py index 0e1ada6..57fd562 100755 --- a/httpsig/tests/test_verify.py +++ b/httpsig/tests/test_verify.py @@ -255,4 +255,18 @@ def test_algorithm_mismatch(self): hv = HeaderVerifier( headers=signed, secret=self.verify_secret, sign_header=self.sign_header, algorithm="rsa-sha256", sign_algorithm=self.sign_algorithm) - self.assertFalse(hv.verify()) \ No newline at end of file + self.assertFalse(hv.verify()) + + def test_correct_derviced_algorithm(self): + unsigned = { + 'Date': self.header_date + } + + hs = HeaderSigner( + key_id="Test", secret=self.sign_secret, algorithm=self.algorithm, + sign_header=self.sign_header, sign_algorithm=self.sign_algorithm) + signed = hs.sign(unsigned) + + hv = HeaderVerifier( + headers=signed, secret=self.verify_secret, sign_header=self.sign_header, algorithm="hs2019", sign_algorithm=self.sign_algorithm) + self.assertTrue(hv.verify()) \ No newline at end of file From 251cce9be722662c166e9b2124c6f25e09389ccb Mon Sep 17 00:00:00 2001 From: fulder Date: Thu, 27 Aug 2020 10:16:39 +0200 Subject: [PATCH 29/32] Move algorithm checks to Signer --- httpsig/sign.py | 2 ++ httpsig/verify.py | 7 +------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/httpsig/sign.py b/httpsig/sign.py index 0b1ec07..4a52645 100644 --- a/httpsig/sign.py +++ b/httpsig/sign.py @@ -30,6 +30,8 @@ def __init__(self, secret, algorithm=None, sign_algorithm=None): if algorithm != DEFAULT_ALGORITHM: print("Algorithm: {} is deprecated please update to {}".format(algorithm, DEFAULT_ALGORITHM)) + elif algorithm == DEFAULT_ALGORITHM and sign_algorithm is None: + raise HttpSigException("Required sign algorithm for {} algorithm not set".format(DEFAULT_ALGORITHM)) if isinstance(secret, six.string_types): secret = secret.encode("ascii") diff --git a/httpsig/verify.py b/httpsig/verify.py index 33cb2b2..4c1b4d3 100644 --- a/httpsig/verify.py +++ b/httpsig/verify.py @@ -92,13 +92,8 @@ def __init__(self, headers, secret, required_headers=None, method=None, self.host = host self.derived_algorithm = algorithm - if self.auth_dict['algorithm'] != DEFAULT_ALGORITHM: - print("Algorithm: {} is deprecated please update to {}".format(self.auth_dict['algorithm'], DEFAULT_ALGORITHM)) - elif self.auth_dict['algorithm'] == DEFAULT_ALGORITHM and sign_algorithm is None: - raise HttpSigException("Required sign algorithm for {} algorithm not set".format(DEFAULT_ALGORITHM)) - super(HeaderVerifier, self).__init__( - secret, algorithm=self.auth_dict['algorithm'], sign_algorithm=sign_algorithm) + secret, algorithm=self.auth_dict['algorithm'], sign_algorithm=sign_algorithm) def verify(self): """ From c6951c26b69279024e92662c6b7ba2b35999ac16 Mon Sep 17 00:00:00 2001 From: fulder Date: Thu, 27 Aug 2020 11:00:29 +0200 Subject: [PATCH 30/32] Fix typo in test name --- httpsig/tests/test_verify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpsig/tests/test_verify.py b/httpsig/tests/test_verify.py index 57fd562..c2254a5 100755 --- a/httpsig/tests/test_verify.py +++ b/httpsig/tests/test_verify.py @@ -257,7 +257,7 @@ def test_algorithm_mismatch(self): headers=signed, secret=self.verify_secret, sign_header=self.sign_header, algorithm="rsa-sha256", sign_algorithm=self.sign_algorithm) self.assertFalse(hv.verify()) - def test_correct_derviced_algorithm(self): + def test_correct_derived_algorithm(self): unsigned = { 'Date': self.header_date } From f517f39e93fdc924e04d411c9356f51729fb2bbe Mon Sep 17 00:00:00 2001 From: fulder Date: Fri, 28 Aug 2020 14:13:48 +0200 Subject: [PATCH 31/32] Default to hash lenght for salt in PSS --- httpsig/sign_algorithms.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/httpsig/sign_algorithms.py b/httpsig/sign_algorithms.py index e4d2656..0179f94 100644 --- a/httpsig/sign_algorithms.py +++ b/httpsig/sign_algorithms.py @@ -35,27 +35,37 @@ def __init__(self, hash_algorithm=DEFAULT_HASH_ALGORITHM, salt_length=None, mgfu self.salt_length = salt_length self.mgfunc = mgfunc - def _create_pss(self, key): + def _create_pss(self, key, salt_length): try: rsa_key = RSA.importKey(key) - pss = PKCS1_PSS.new(rsa_key, saltLen=self.salt_length, mgfunc=self.mgfunc) + pss = PKCS1_PSS.new(rsa_key, saltLen=salt_length, mgfunc=self.mgfunc) except ValueError: raise HttpSigException("Invalid key.") return pss def sign(self, private_key, data): - pss = self._create_pss(private_key) - if isinstance(data, six.string_types): data = data.encode("ascii") h = self.hash_algorithm.new() h.update(data) + + salt_length = self.salt_length + if salt_length is None: + salt_length = h.digest_size + + pss = self._create_pss(private_key, salt_length) + return pss.sign(h) def verify(self, public_key, data, signature): - pss = self._create_pss(public_key) - h = self.hash_algorithm.new() h.update(data) + + salt_length = self.salt_length + if salt_length is None: + salt_length = h.digest_size + + pss = self._create_pss(public_key, salt_length) + return pss.verify(h, base64.b64decode(signature)) From f419b3471171b7c147e31d16b9e6fc575a3072b5 Mon Sep 17 00:00:00 2001 From: fulder Date: Fri, 28 Aug 2020 14:22:55 +0200 Subject: [PATCH 32/32] Get hash digest_size in PSS construct --- httpsig/sign_algorithms.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/httpsig/sign_algorithms.py b/httpsig/sign_algorithms.py index 0179f94..c9c7921 100644 --- a/httpsig/sign_algorithms.py +++ b/httpsig/sign_algorithms.py @@ -35,10 +35,13 @@ def __init__(self, hash_algorithm=DEFAULT_HASH_ALGORITHM, salt_length=None, mgfu self.salt_length = salt_length self.mgfunc = mgfunc - def _create_pss(self, key, salt_length): + if self.salt_length is None: + self.salt_length = self.hash_algorithm.digest_size + + def _create_pss(self, key): try: rsa_key = RSA.importKey(key) - pss = PKCS1_PSS.new(rsa_key, saltLen=salt_length, mgfunc=self.mgfunc) + pss = PKCS1_PSS.new(rsa_key, saltLen=self.salt_length, mgfunc=self.mgfunc) except ValueError: raise HttpSigException("Invalid key.") return pss @@ -50,11 +53,7 @@ def sign(self, private_key, data): h = self.hash_algorithm.new() h.update(data) - salt_length = self.salt_length - if salt_length is None: - salt_length = h.digest_size - - pss = self._create_pss(private_key, salt_length) + pss = self._create_pss(private_key) return pss.sign(h) @@ -62,10 +61,6 @@ def verify(self, public_key, data, signature): h = self.hash_algorithm.new() h.update(data) - salt_length = self.salt_length - if salt_length is None: - salt_length = h.digest_size - - pss = self._create_pss(public_key, salt_length) + pss = self._create_pss(public_key) return pss.verify(h, base64.b64decode(signature))