From 6da39501640099b55c5d8afcb1d8a277bdb8c7dc Mon Sep 17 00:00:00 2001 From: Gasper Zejn Date: Tue, 30 May 2017 18:32:27 +0200 Subject: [PATCH 01/12] On windows, use pycryptodome, which comes with prebuilt binary packages. Cryptography may still require extra libraries. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a58561cb..5352c3db 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- import os - +import sys import jose import platform @@ -25,7 +25,7 @@ def get_packages(package): def get_install_requires(): - if platform.python_implementation() == 'PyPy': + if sys.platform == "win32" or platform.python_implementation() == 'PyPy': crypto_lib = 'pycryptodome >=3.3.1, <3.4.0' else: crypto_lib = 'pycrypto >=2.6.0, <2.7.0' From a1cb19a763df053ef9fa967fa85b5ac180cb0ab4 Mon Sep 17 00:00:00 2001 From: Gasper Zejn Date: Thu, 1 Jun 2017 08:48:20 +0200 Subject: [PATCH 02/12] Implement pure python rsa signing based on rsa module. --- jose/backends/rsa_backend.py | 83 ++++++++++++++++++++++++++++++++ requirements.txt | 2 + setup.py | 1 + tests/algorithms/test_RSA.py | 93 ++++++++++++++++++++++++++++++++++++ 4 files changed, 179 insertions(+) create mode 100644 jose/backends/rsa_backend.py diff --git a/jose/backends/rsa_backend.py b/jose/backends/rsa_backend.py new file mode 100644 index 00000000..355f316f --- /dev/null +++ b/jose/backends/rsa_backend.py @@ -0,0 +1,83 @@ +import rsa as pyrsa +import six + +from jose.backends.base import Key +from jose.constants import ALGORITHMS +from jose.exceptions import JWKError +from jose.utils import base64_to_long + + +class RSAKey(Key): + SHA256 = 'SHA-256' + SHA384 = 'SHA-384' + SHA512 = 'SHA-512' + + def __init__(self, key, algorithm): + if algorithm not in ALGORITHMS.RSA: + raise JWKError('hash_alg: %s is not a valid hash algorithm' % algorithm) + + self.hash_alg = { + ALGORITHMS.RS256: self.SHA256, + ALGORITHMS.RS384: self.SHA384, + ALGORITHMS.RS512: self.SHA512 + }.get(algorithm) + self._algorithm = algorithm + + if isinstance(key, dict): + self.prepared_key = self._process_jwk(key) + return + + if isinstance(key, (pyrsa.PublicKey, pyrsa.PrivateKey)): + self._prepared_key = key + return + + if isinstance(key, six.string_types): + key = key.encode('utf-8') + + if isinstance(key, six.binary_type): + try: + self._prepared_key = pyrsa.PublicKey.load_pkcs1(key) + except ValueError: + try: + self._prepared_key = pyrsa.PrivateKey.load_pkcs1(key) + except ValueError as e: + raise JWKError(e) + return + raise JWKError('Unable to parse an RSA_JWK from key: %s' % key) + + def _process_jwk(self, jwk_dict): + if not jwk_dict.get('kty') == 'RSA': + raise JWKError("Incorrect key type. Expected: 'RSA', Recieved: %s" % jwk_dict.get('kty')) + + e = base64_to_long(jwk_dict.get('e', 256)) + n = base64_to_long(jwk_dict.get('n')) + + verifying_key = pyrsa.PublicKey(e=e, n=n) + return verifying_key + + def sign(self, msg): + print(self._algorithm) + return pyrsa.sign(msg, self._prepared_key, self.hash_alg) + + def verify(self, msg, sig): + try: + return pyrsa.verify(msg, sig, self._prepared_key) + except pyrsa.pkcs1.VerificationError: + return False + + def public_key(self): + if isinstance(self._prepared_key, pyrsa.PublicKey): + return self + return self.__class__(pyrsa.PublicKey(n=self._prepared_key.n, e=self._prepared_key.e), self._algorithm) + + def to_pem(self): + import rsa.pem + + if isinstance(self._prepared_key, rsa.PrivateKey): + pem = self._prepared_key.save_pkcs1() + else: + # this is a PKCS#8 DER header to identify rsaEncryption + header = b'0\x82\x04\xbd\x02\x01\x000\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00' + der = self._prepared_key.save_pkcs1(format='DER') + pem = rsa.pem.save_pem(header + der, pem_marker='PUBLIC KEY') + return pem diff --git a/requirements.txt b/requirements.txt index fb3ef680..f75de0a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ pycrypto six future +rsa +ecdsa diff --git a/setup.py b/setup.py index a58561cb..1dc32196 100644 --- a/setup.py +++ b/setup.py @@ -34,6 +34,7 @@ def get_install_requires(): 'six <2.0', 'ecdsa <1.0', 'future <1.0', + 'rsa' ] diff --git a/tests/algorithms/test_RSA.py b/tests/algorithms/test_RSA.py index 5c222724..8dca462a 100644 --- a/tests/algorithms/test_RSA.py +++ b/tests/algorithms/test_RSA.py @@ -3,6 +3,7 @@ from jose.backends.pycrypto_backend import RSAKey from jose.backends.cryptography_backend import CryptographyRSAKey +from jose.backends.rsa_backend import RSAKey as PurePythonRSAKey from jose.constants import ALGORITHMS from jose.exceptions import JOSEError, JWKError @@ -185,3 +186,95 @@ def test_pycrypto_invalid_signature(self): assert public_key.verify(msg, signature) == True assert public_key.verify(msg, 1) == False + + +class TestPythonRSA: + def test_RSA_key(self): + PurePythonRSAKey(private_key, ALGORITHMS.RS256) + + def test_RSA_key_instance(self): + import rsa + key = rsa.PublicKey( + e=65537, + n=26057131595212989515105618545799160306093557851986992545257129318694524535510983041068168825614868056510242030438003863929818932202262132630250203397069801217463517914103389095129323580576852108653940669240896817348477800490303630912852266209307160550655497615975529276169196271699168537716821419779900117025818140018436554173242441334827711966499484119233207097432165756707507563413323850255548329534279691658369466534587631102538061857114141268972476680597988266772849780811214198186940677291891818952682545840788356616771009013059992237747149380197028452160324144544057074406611859615973035412993832273216732343819, + ) + + pubkey = PurePythonRSAKey(key, ALGORITHMS.RS256) + pem = pubkey.to_pem() + assert pem.startswith(b'-----BEGIN PUBLIC KEY-----') + + def test_invalid_algorithm(self): + with pytest.raises(JWKError): + PurePythonRSAKey(private_key, ALGORITHMS.ES256) + + with pytest.raises(JWKError): + PurePythonRSAKey({'kty': 'bla'}, ALGORITHMS.RS256) + + def test_RSA_jwk(self): + d = { + "kty": "RSA", + "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", + "e": "AQAB", + } + PurePythonRSAKey(d, ALGORITHMS.RS256) + + def test_string_secret(self): + key = 'secret' + with pytest.raises(JOSEError): + PurePythonRSAKey(key, ALGORITHMS.RS256) + + def test_object(self): + key = object() + with pytest.raises(JOSEError): + PurePythonRSAKey(key, ALGORITHMS.RS256) + + def test_bad_cert(self): + key = '-----BEGIN CERTIFICATE-----' + with pytest.raises(JOSEError): + PurePythonRSAKey(key, ALGORITHMS.RS256) + + def test_get_public_key(self): + key = PurePythonRSAKey(private_key, ALGORITHMS.RS256) + public_key = key.public_key() + public_key2 = public_key.public_key() + assert public_key == public_key2 + + key = RSAKey(private_key, ALGORITHMS.RS256) + public_key = key.public_key() + public_key2 = public_key.public_key() + assert public_key == public_key2 + + def test_to_pem(self): + key = PurePythonRSAKey(private_key, ALGORITHMS.RS256) + assert key.to_pem().strip() == private_key.strip() + + key = RSAKey(private_key, ALGORITHMS.RS256) + assert key.to_pem().strip() == private_key.strip() + + def test_signing_parity(self): + key1 = RSAKey(private_key, ALGORITHMS.RS256) + vkey1 = key1.public_key() + key2 = PurePythonRSAKey(private_key, ALGORITHMS.RS256) + vkey2 = key2.public_key() + + msg = b'test' + sig1 = key1.sign(msg) + sig2 = key2.sign(msg) + + assert vkey1.verify(msg, sig1) + assert vkey1.verify(msg, sig2) + assert vkey2.verify(msg, sig1) + assert vkey2.verify(msg, sig2) + + # invalid signature + assert not vkey2.verify(msg, b'n' * 64) + + def test_pycrypto_invalid_signature(self): + + key = RSAKey(private_key, ALGORITHMS.RS256) + msg = b'test' + signature = key.sign(msg) + public_key = key.public_key() + + assert public_key.verify(msg, signature) == True + assert public_key.verify(msg, 1) == False \ No newline at end of file From 8dbe68cd814d3c0703147945084492cd9efe38e8 Mon Sep 17 00:00:00 2001 From: Gasper Zejn Date: Thu, 1 Jun 2017 08:52:00 +0200 Subject: [PATCH 03/12] Parameter is not optional. --- jose/backends/rsa_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jose/backends/rsa_backend.py b/jose/backends/rsa_backend.py index 355f316f..5223ae58 100644 --- a/jose/backends/rsa_backend.py +++ b/jose/backends/rsa_backend.py @@ -49,7 +49,7 @@ def _process_jwk(self, jwk_dict): if not jwk_dict.get('kty') == 'RSA': raise JWKError("Incorrect key type. Expected: 'RSA', Recieved: %s" % jwk_dict.get('kty')) - e = base64_to_long(jwk_dict.get('e', 256)) + e = base64_to_long(jwk_dict.get('e')) n = base64_to_long(jwk_dict.get('n')) verifying_key = pyrsa.PublicKey(e=e, n=n) From 93b700da00dbd140c8ac36ad034462bcb7462c19 Mon Sep 17 00:00:00 2001 From: Gasper Zejn Date: Thu, 1 Jun 2017 08:52:51 +0200 Subject: [PATCH 04/12] Fixes --- jose/backends/rsa_backend.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jose/backends/rsa_backend.py b/jose/backends/rsa_backend.py index 5223ae58..358aa6eb 100644 --- a/jose/backends/rsa_backend.py +++ b/jose/backends/rsa_backend.py @@ -56,12 +56,12 @@ def _process_jwk(self, jwk_dict): return verifying_key def sign(self, msg): - print(self._algorithm) return pyrsa.sign(msg, self._prepared_key, self.hash_alg) def verify(self, msg, sig): try: - return pyrsa.verify(msg, sig, self._prepared_key) + pyrsa.verify(msg, sig, self._prepared_key) + return True except pyrsa.pkcs1.VerificationError: return False @@ -73,7 +73,7 @@ def public_key(self): def to_pem(self): import rsa.pem - if isinstance(self._prepared_key, rsa.PrivateKey): + if isinstance(self._prepared_key, pyrsa.PrivateKey): pem = self._prepared_key.save_pkcs1() else: # this is a PKCS#8 DER header to identify rsaEncryption From d12fecd2eb012862b8d7654c879dccf5ccce833f Mon Sep 17 00:00:00 2001 From: Gasper Zejn Date: Thu, 1 Jun 2017 09:00:24 +0200 Subject: [PATCH 05/12] Enable Python RSA backend as a fallback. --- jose/backends/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jose/backends/__init__.py b/jose/backends/__init__.py index 5b115270..c0089468 100644 --- a/jose/backends/__init__.py +++ b/jose/backends/__init__.py @@ -2,7 +2,10 @@ try: from jose.backends.pycrypto_backend import RSAKey except ImportError: - from jose.backends.cryptography_backend import CryptographyRSAKey as RSAKey + try: + from jose.backends.cryptography_backend import CryptographyRSAKey as RSAKey + except ImportError: + from jose.backends.rsa_backend import RSAKey try: from jose.backends.cryptography_backend import CryptographyECKey as ECKey From f3aabbea3fc591b7291d44b8e02c6542c69b86cb Mon Sep 17 00:00:00 2001 From: Gasper Zejn Date: Thu, 1 Jun 2017 09:19:10 +0200 Subject: [PATCH 06/12] Update pip to latest version before installing anything --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 5f5a8149..8aa8ae2a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ python: - "3.6" - "pypy-5.3.1" install: + - pip install -U pip - pip install -U tox codecov tox-travis script: - tox From 3495543316269a002f65ef60c146aad52de49d53 Mon Sep 17 00:00:00 2001 From: Gasper Zejn Date: Thu, 1 Jun 2017 09:23:22 +0200 Subject: [PATCH 07/12] Also list six as a dependency. --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 7b7c87bc..4140a5fa 100644 --- a/tox.ini +++ b/tox.ini @@ -3,8 +3,10 @@ envlist = py{26,27,33,34,py} [testenv] commands = + pip --version py.test --cov-report term-missing --cov jose deps = + six future pycrypto ecdsa From dd435659c977dbcce03f2840d5e444c5f906064d Mon Sep 17 00:00:00 2001 From: Gasper Zejn Date: Sat, 13 Jan 2018 19:59:21 +0100 Subject: [PATCH 08/12] Add Python RSAKey to parametrized tests and remote redundant tests. --- tests/algorithms/test_RSA.py | 117 ++++------------------------------- 1 file changed, 13 insertions(+), 104 deletions(-) diff --git a/tests/algorithms/test_RSA.py b/tests/algorithms/test_RSA.py index 87fd6311..5c936157 100644 --- a/tests/algorithms/test_RSA.py +++ b/tests/algorithms/test_RSA.py @@ -71,7 +71,7 @@ class TestRSAAlgorithm: - @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey, PurePythonRSAKey]) def test_RSA_key(self, Backend): assert not Backend(private_key, ALGORITHMS.RS256).is_public() @@ -94,25 +94,25 @@ def test_cryptography_RSA_key_instance(self): pem = pubkey.to_pem() assert pem.startswith(b'-----BEGIN PUBLIC KEY-----') - @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey, PurePythonRSAKey]) def test_string_secret(self, Backend): key = 'secret' with pytest.raises(JOSEError): Backend(key, ALGORITHMS.RS256) - @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey, PurePythonRSAKey]) def test_object(self, Backend): key = object() with pytest.raises(JOSEError): Backend(key, ALGORITHMS.RS256) - @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey, PurePythonRSAKey]) def test_bad_cert(self, Backend): key = '-----BEGIN CERTIFICATE-----' with pytest.raises(JOSEError): Backend(key, ALGORITHMS.RS256) - @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey, PurePythonRSAKey]) def test_invalid_algorithm(self, Backend): with pytest.raises(JWKError): Backend(private_key, ALGORITHMS.ES256) @@ -120,7 +120,7 @@ def test_invalid_algorithm(self, Backend): with pytest.raises(JWKError): Backend({'kty': 'bla'}, ALGORITHMS.RS256) - @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey, PurePythonRSAKey]) def test_RSA_jwk(self, Backend): key = { "kty": "RSA", @@ -158,13 +158,13 @@ def test_RSA_jwk(self, Backend): # None of the extra parameters are present, but 'key' is still private. assert not Backend(key, ALGORITHMS.RS256).is_public() - @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey, PurePythonRSAKey]) def test_string_secret(self, Backend): key = 'secret' with pytest.raises(JOSEError): Backend(key, ALGORITHMS.RS256) - @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey, PurePythonRSAKey]) def test_get_public_key(self, Backend): key = Backend(private_key, ALGORITHMS.RS256) public_key = key.public_key() @@ -173,7 +173,7 @@ def test_get_public_key(self, Backend): assert public_key2.is_public() assert public_key == public_key2 - @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey, PurePythonRSAKey]) def test_to_pem(self, Backend): key = Backend(private_key, ALGORITHMS.RS256) assert key.to_pem().strip() == private_key.strip() @@ -208,7 +208,7 @@ def assert_roundtrip(self, key, Backend): ALGORITHMS.RS256 ).to_dict() == key.to_dict() - @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey, PurePythonRSAKey]) def test_to_dict(self, Backend): key = Backend(private_key, ALGORITHMS.RS256) self.assert_parameters(key.to_dict(), private=True) @@ -216,8 +216,8 @@ def test_to_dict(self, Backend): self.assert_roundtrip(key, Backend) self.assert_roundtrip(key.public_key(), Backend) - @pytest.mark.parametrize("BackendSign", [RSAKey, CryptographyRSAKey]) - @pytest.mark.parametrize("BackendVerify", [RSAKey, CryptographyRSAKey]) + @pytest.mark.parametrize("BackendSign", [RSAKey, CryptographyRSAKey, PurePythonRSAKey]) + @pytest.mark.parametrize("BackendVerify", [RSAKey, CryptographyRSAKey, PurePythonRSAKey]) def test_signing_parity(self, BackendSign, BackendVerify): key_sign = BackendSign(private_key, ALGORITHMS.RS256) key_verify = BackendVerify(private_key, ALGORITHMS.RS256).public_key() @@ -231,7 +231,7 @@ def test_signing_parity(self, BackendSign, BackendVerify): # invalid signature assert not key_verify.verify(msg, b'n' * 64) - @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey]) + @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey, PurePythonRSAKey]) def test_pycrypto_unencoded_cleartext(self, Backend): key = Backend(private_key, ALGORITHMS.RS256) @@ -243,94 +243,3 @@ def test_pycrypto_unencoded_cleartext(self, Backend): assert public_key.verify(msg, signature) == True assert public_key.verify(msg, 1) == False - -class TestPythonRSA: - def test_RSA_key(self): - PurePythonRSAKey(private_key, ALGORITHMS.RS256) - - def test_RSA_key_instance(self): - import rsa - key = rsa.PublicKey( - e=65537, - n=26057131595212989515105618545799160306093557851986992545257129318694524535510983041068168825614868056510242030438003863929818932202262132630250203397069801217463517914103389095129323580576852108653940669240896817348477800490303630912852266209307160550655497615975529276169196271699168537716821419779900117025818140018436554173242441334827711966499484119233207097432165756707507563413323850255548329534279691658369466534587631102538061857114141268972476680597988266772849780811214198186940677291891818952682545840788356616771009013059992237747149380197028452160324144544057074406611859615973035412993832273216732343819, - ) - - pubkey = PurePythonRSAKey(key, ALGORITHMS.RS256) - pem = pubkey.to_pem() - assert pem.startswith(b'-----BEGIN PUBLIC KEY-----') - - def test_invalid_algorithm(self): - with pytest.raises(JWKError): - PurePythonRSAKey(private_key, ALGORITHMS.ES256) - - with pytest.raises(JWKError): - PurePythonRSAKey({'kty': 'bla'}, ALGORITHMS.RS256) - - def test_RSA_jwk(self): - d = { - "kty": "RSA", - "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", - "e": "AQAB", - } - PurePythonRSAKey(d, ALGORITHMS.RS256) - - def test_string_secret(self): - key = 'secret' - with pytest.raises(JOSEError): - PurePythonRSAKey(key, ALGORITHMS.RS256) - - def test_object(self): - key = object() - with pytest.raises(JOSEError): - PurePythonRSAKey(key, ALGORITHMS.RS256) - - def test_bad_cert(self): - key = '-----BEGIN CERTIFICATE-----' - with pytest.raises(JOSEError): - PurePythonRSAKey(key, ALGORITHMS.RS256) - - def test_get_public_key(self): - key = PurePythonRSAKey(private_key, ALGORITHMS.RS256) - public_key = key.public_key() - public_key2 = public_key.public_key() - assert public_key == public_key2 - - key = RSAKey(private_key, ALGORITHMS.RS256) - public_key = key.public_key() - public_key2 = public_key.public_key() - assert public_key == public_key2 - - def test_to_pem(self): - key = PurePythonRSAKey(private_key, ALGORITHMS.RS256) - assert key.to_pem().strip() == private_key.strip() - - key = RSAKey(private_key, ALGORITHMS.RS256) - assert key.to_pem().strip() == private_key.strip() - - def test_signing_parity(self): - key1 = RSAKey(private_key, ALGORITHMS.RS256) - vkey1 = key1.public_key() - key2 = PurePythonRSAKey(private_key, ALGORITHMS.RS256) - vkey2 = key2.public_key() - - msg = b'test' - sig1 = key1.sign(msg) - sig2 = key2.sign(msg) - - assert vkey1.verify(msg, sig1) - assert vkey1.verify(msg, sig2) - assert vkey2.verify(msg, sig1) - assert vkey2.verify(msg, sig2) - - # invalid signature - assert not vkey2.verify(msg, b'n' * 64) - - def test_pycrypto_invalid_signature(self): - - key = RSAKey(private_key, ALGORITHMS.RS256) - msg = b'test' - signature = key.sign(msg) - public_key = key.public_key() - - assert public_key.verify(msg, signature) == True - assert public_key.verify(msg, 1) == False From de10ef4d05d57fe0438aec8df3cb59432db8421c Mon Sep 17 00:00:00 2001 From: Gasper Zejn Date: Sun, 14 Jan 2018 09:24:55 +0100 Subject: [PATCH 09/12] Implement to_dict and improve process_jwk to support private keys and conform to standard and tests. --- jose/backends/rsa_backend.py | 120 +++++++++++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 4 deletions(-) diff --git a/jose/backends/rsa_backend.py b/jose/backends/rsa_backend.py index 358aa6eb..0deb8d9e 100644 --- a/jose/backends/rsa_backend.py +++ b/jose/backends/rsa_backend.py @@ -4,7 +4,71 @@ from jose.backends.base import Key from jose.constants import ALGORITHMS from jose.exceptions import JWKError -from jose.utils import base64_to_long +from jose.utils import base64_to_long, long_to_base64 + + +# Functions gcd and rsa_recover_prime_factors were copied from cryptography 1.9 +# to enable pure python rsa module to be in compliance with section 6.3.1 of RFC7518 +# which requires only private exponent (d) for private key. + +def _gcd(a, b): + """Calculate the Greatest Common Divisor of a and b. + + Unless b==0, the result will have the same sign as b (so that when + b is divided by it, the result comes out positive). + """ + while b: + a, b = b, a%b + return a + + +# Controls the number of iterations rsa_recover_prime_factors will perform +# to obtain the prime factors. Each iteration increments by 2 so the actual +# maximum attempts is half this number. +_MAX_RECOVERY_ATTEMPTS = 1000 + + +def _rsa_recover_prime_factors(n, e, d): + """ + Compute factors p and q from the private exponent d. We assume that n has + no more than two factors. This function is adapted from code in PyCrypto. + """ + # See 8.2.2(i) in Handbook of Applied Cryptography. + ktot = d * e - 1 + # The quantity d*e-1 is a multiple of phi(n), even, + # and can be represented as t*2^s. + t = ktot + while t % 2 == 0: + t = t // 2 + # Cycle through all multiplicative inverses in Zn. + # The algorithm is non-deterministic, but there is a 50% chance + # any candidate a leads to successful factoring. + # See "Digitalized Signatures and Public Key Functions as Intractable + # as Factorization", M. Rabin, 1979 + spotted = False + a = 2 + while not spotted and a < _MAX_RECOVERY_ATTEMPTS: + k = t + # Cycle through all values a^{t*2^i}=a^k + while k < ktot: + cand = pow(a, k, n) + # Check if a^k is a non-trivial root of unity (mod n) + if cand != 1 and cand != (n - 1) and pow(cand, 2, n) == 1: + # We have found a number such that (cand-1)(cand+1)=0 (mod n). + # Either of the terms divides n. + p = _gcd(cand + 1, n) + spotted = True + break + k *= 2 + # This value was not any good... let's try another! + a += 2 + if not spotted: + raise ValueError("Unable to compute factors p and q from exponent d.") + # Found ! + q, r = divmod(n, p) + assert r == 0 + p, q = sorted((p, q), reverse=True) + return (p, q) class RSAKey(Key): @@ -24,7 +88,7 @@ def __init__(self, key, algorithm): self._algorithm = algorithm if isinstance(key, dict): - self.prepared_key = self._process_jwk(key) + self._prepared_key = self._process_jwk(key) return if isinstance(key, (pyrsa.PublicKey, pyrsa.PrivateKey)): @@ -52,8 +116,28 @@ def _process_jwk(self, jwk_dict): e = base64_to_long(jwk_dict.get('e')) n = base64_to_long(jwk_dict.get('n')) - verifying_key = pyrsa.PublicKey(e=e, n=n) - return verifying_key + if not 'd' in jwk_dict: + return pyrsa.PublicKey(e=e, n=n) + else: + d = base64_to_long(jwk_dict.get('d')) + extra_params = ['p', 'q', 'dp', 'dq', 'qi'] + + if any(k in jwk_dict for k in extra_params): + # Precomputed private key parameters are available. + if not all(k in jwk_dict for k in extra_params): + # These values must be present when 'p' is according to + # Section 6.3.2 of RFC7518, so if they are not we raise + # an error. + raise JWKError('Precomputed private key parameters are incomplete.') + + p = base64_to_long(jwk_dict['p']) + q = base64_to_long(jwk_dict['q']) + return pyrsa.PrivateKey(e=e, n=n, d=d, p=p, q=q) + else: + p, q = _rsa_recover_prime_factors(n, e, d) + return pyrsa.PrivateKey(n=n, e=e, d=d, p=p, q=q) + + def sign(self, msg): return pyrsa.sign(msg, self._prepared_key, self.hash_alg) @@ -65,6 +149,9 @@ def verify(self, msg, sig): except pyrsa.pkcs1.VerificationError: return False + def is_public(self): + return isinstance(self._prepared_key, pyrsa.PublicKey) + def public_key(self): if isinstance(self._prepared_key, pyrsa.PublicKey): return self @@ -81,3 +168,28 @@ def to_pem(self): der = self._prepared_key.save_pkcs1(format='DER') pem = rsa.pem.save_pem(header + der, pem_marker='PUBLIC KEY') return pem + + def to_dict(self): + if not self.is_public(): + public_key = self.public_key()._prepared_key + else: + public_key = self._prepared_key + + data = { + 'alg': self._algorithm, + 'kty': 'RSA', + 'n': long_to_base64(public_key.n), + 'e': long_to_base64(public_key.e), + } + + if not self.is_public(): + data.update({ + 'd': long_to_base64(self._prepared_key.d), + 'p': long_to_base64(self._prepared_key.p), + 'q': long_to_base64(self._prepared_key.q), + 'dp': long_to_base64(self._prepared_key.exp1), + 'dq': long_to_base64(self._prepared_key.exp2), + 'qi': long_to_base64(self._prepared_key.coef), + }) + + return data From 90443549f6e17f0bc5d228dd3226233efe67e6b5 Mon Sep 17 00:00:00 2001 From: Gasper Zejn Date: Mon, 15 Jan 2018 14:10:37 +0100 Subject: [PATCH 10/12] Fix to_pem for private keys to support both PKCS#1 and PKCS#8. --- jose/backends/cryptography_backend.py | 11 ++++++++-- jose/backends/pycrypto_backend.py | 19 +++++++++--------- jose/backends/rsa_backend.py | 29 ++++++++++++++++++++++----- tests/algorithms/test_RSA.py | 8 +++++++- 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/jose/backends/cryptography_backend.py b/jose/backends/cryptography_backend.py index 1a1bc308..1794bf3f 100644 --- a/jose/backends/cryptography_backend.py +++ b/jose/backends/cryptography_backend.py @@ -282,16 +282,23 @@ def public_key(self): return self return self.__class__(self.prepared_key.public_key(), self._algorithm) - def to_pem(self): + def to_pem(self, pem_format='PKCS8'): if self.is_public(): return self.prepared_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo ) + if pem_format == 'PKCS8': + fmt = serialization.PrivateFormat.PKCS8 + elif pem_format == 'PKCS1': + fmt = serialization.PrivateFormat.TraditionalOpenSSL + else: + raise ValueError("Invalid format specified: %r" % pem_format) + return self.prepared_key.private_bytes( encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, + format=fmt, encryption_algorithm=serialization.NoEncryption() ) diff --git a/jose/backends/pycrypto_backend.py b/jose/backends/pycrypto_backend.py index a888f3e2..665ab352 100644 --- a/jose/backends/pycrypto_backend.py +++ b/jose/backends/pycrypto_backend.py @@ -140,16 +140,15 @@ def public_key(self): return self return self.__class__(self.prepared_key.publickey(), self._algorithm) - def to_pem(self): - pem = self.prepared_key.exportKey('PEM', pkcs=1) - - # pycryptodome fix - begin = b'-----BEGIN RSA PUBLIC KEY-----' - end = b'-----END RSA PUBLIC KEY-----' - if pem.startswith(begin) and pem.strip().endswith(end): - pem = b'-----BEGIN PUBLIC KEY-----' + pem.strip()[len(begin):-len(end)] + b'-----END PUBLIC KEY-----' - if not pem.endswith(b'\n'): - pem = pem + b'\n' + def to_pem(self, pem_format='PKCS8'): + if pem_format == 'PKCS8': + pkcs = 8 + elif pem_format == 'PKCS1': + pkcs = 1 + else: + raise ValueError("Invalid pem format specified: %r" % (pem_format,)) + + pem = self.prepared_key.exportKey('PEM', pkcs=pkcs) return pem def to_dict(self): diff --git a/jose/backends/rsa_backend.py b/jose/backends/rsa_backend.py index 0deb8d9e..d5a15b64 100644 --- a/jose/backends/rsa_backend.py +++ b/jose/backends/rsa_backend.py @@ -1,4 +1,5 @@ import rsa as pyrsa +import rsa.pem as pyrsa_pem import six from jose.backends.base import Key @@ -7,6 +8,7 @@ from jose.utils import base64_to_long, long_to_base64 +PKCS8_RSA_HEADER = b'0\x82\x04\xbd\x02\x01\x000\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00' # Functions gcd and rsa_recover_prime_factors were copied from cryptography 1.9 # to enable pure python rsa module to be in compliance with section 6.3.1 of RFC7518 # which requires only private exponent (d) for private key. @@ -103,9 +105,20 @@ def __init__(self, key, algorithm): self._prepared_key = pyrsa.PublicKey.load_pkcs1(key) except ValueError: try: - self._prepared_key = pyrsa.PrivateKey.load_pkcs1(key) - except ValueError as e: - raise JWKError(e) + self._prepared_key = pyrsa.PublicKey.load_pkcs1_openssl_pem(key) + except ValueError: + try: + self._prepared_key = pyrsa.PrivateKey.load_pkcs1(key) + except ValueError: + try: + # python-rsa does not support PKCS8 yet so we have to manually remove OID + der = pyrsa_pem.load_pem(key, b'PRIVATE KEY') + header, der = der[:22], der[22:] + if header != PKCS8_RSA_HEADER: + raise ValueError("Invalid PKCS8 header") + self._prepared_key = pyrsa.PrivateKey._load_pkcs1_der(der) + except ValueError as e: + raise JWKError(e) return raise JWKError('Unable to parse an RSA_JWK from key: %s' % key) @@ -157,11 +170,17 @@ def public_key(self): return self return self.__class__(pyrsa.PublicKey(n=self._prepared_key.n, e=self._prepared_key.e), self._algorithm) - def to_pem(self): + def to_pem(self, pem_format='PKCS8'): import rsa.pem if isinstance(self._prepared_key, pyrsa.PrivateKey): - pem = self._prepared_key.save_pkcs1() + der = self._prepared_key.save_pkcs1(format='DER') + if pem_format == 'PKCS8': + pem = rsa.pem.save_pem(PKCS8_RSA_HEADER + der, pem_marker='PRIVATE KEY') + elif pem_format == 'PKCS1': + pem = rsa.pem.save_pem(der, pem_marker='RSA PRIVATE KEY') + else: + raise ValueError("Invalid pem format specified: %r" % (pem_format,)) else: # this is a PKCS#8 DER header to identify rsaEncryption header = b'0\x82\x04\xbd\x02\x01\x000\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00' diff --git a/tests/algorithms/test_RSA.py b/tests/algorithms/test_RSA.py index 5c936157..f9a2fc35 100644 --- a/tests/algorithms/test_RSA.py +++ b/tests/algorithms/test_RSA.py @@ -176,7 +176,13 @@ def test_get_public_key(self, Backend): @pytest.mark.parametrize("Backend", [RSAKey, CryptographyRSAKey, PurePythonRSAKey]) def test_to_pem(self, Backend): key = Backend(private_key, ALGORITHMS.RS256) - assert key.to_pem().strip() == private_key.strip() + assert key.to_pem(pem_format='PKCS1').strip() == private_key.strip() + + pkcs8 = key.to_pem(pem_format='PKCS8').strip() + assert pkcs8 != private_key.strip() + + newkey = Backend(pkcs8, ALGORITHMS.RS256) + assert newkey.to_pem(pem_format='PKCS1').strip() == private_key.strip() def assert_parameters(self, as_dict, private): assert isinstance(as_dict, dict) From 2c117c67e19ba1a9d3cc7e78fa7f6e1cb2dec41c Mon Sep 17 00:00:00 2001 From: Gasper Zejn Date: Fri, 26 Jan 2018 11:55:02 +0100 Subject: [PATCH 11/12] Add SubjectPublicKeyInfo format for PEM output and cross implementation tests. --- jose/backends/cryptography_backend.py | 11 ++++++-- jose/backends/pycrypto_backend.py | 11 +++++++- jose/backends/rsa_backend.py | 39 +++++++++++++++++++++------ tests/algorithms/test_RSA.py | 16 +++++++++++ 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/jose/backends/cryptography_backend.py b/jose/backends/cryptography_backend.py index 1794bf3f..d02384d5 100644 --- a/jose/backends/cryptography_backend.py +++ b/jose/backends/cryptography_backend.py @@ -284,10 +284,17 @@ def public_key(self): def to_pem(self, pem_format='PKCS8'): if self.is_public(): - return self.prepared_key.public_bytes( + if pem_format == 'PKCS8': + fmt = serialization.PublicFormat.SubjectPublicKeyInfo + elif pem_format == 'PKCS1': + fmt = serialization.PublicFormat.PKCS1 + else: + raise ValueError("Invalid format specified: %r" % pem_format) + pem = self.prepared_key.public_bytes( encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo + format=fmt ) + return pem if pem_format == 'PKCS8': fmt = serialization.PrivateFormat.PKCS8 diff --git a/jose/backends/pycrypto_backend.py b/jose/backends/pycrypto_backend.py index 665ab352..b70afad6 100644 --- a/jose/backends/pycrypto_backend.py +++ b/jose/backends/pycrypto_backend.py @@ -9,6 +9,7 @@ from Crypto.Util.asn1 import DerSequence from jose.backends.base import Key +from jose.backends.rsa_backend import pem_to_spki from jose.utils import base64_to_long, long_to_base64 from jose.constants import ALGORITHMS from jose.exceptions import JWKError @@ -148,7 +149,15 @@ def to_pem(self, pem_format='PKCS8'): else: raise ValueError("Invalid pem format specified: %r" % (pem_format,)) - pem = self.prepared_key.exportKey('PEM', pkcs=pkcs) + if self.is_public(): + pem = self.prepared_key.exportKey('PEM', pkcs=1) + if pkcs == 8: + pem = pem_to_spki(pem, fmt='PKCS8') + else: + pem = pem_to_spki(pem, fmt='PKCS1') + return pem + else: + pem = self.prepared_key.exportKey('PEM', pkcs=pkcs) return pem def to_dict(self): diff --git a/jose/backends/rsa_backend.py b/jose/backends/rsa_backend.py index d5a15b64..f66cf3a7 100644 --- a/jose/backends/rsa_backend.py +++ b/jose/backends/rsa_backend.py @@ -1,6 +1,10 @@ +import six +from pyasn1.codec.der import encoder +from pyasn1.type import univ + import rsa as pyrsa import rsa.pem as pyrsa_pem -import six +from rsa.asn1 import OpenSSLPubKey, AsnPubKey, PubKeyHeader from jose.backends.base import Key from jose.constants import ALGORITHMS @@ -73,6 +77,11 @@ def _rsa_recover_prime_factors(n, e, d): return (p, q) +def pem_to_spki(pem, fmt='PKCS8'): + key = RSAKey(pem, ALGORITHMS.RS256) + return key.to_pem(fmt) + + class RSAKey(Key): SHA256 = 'SHA-256' SHA384 = 'SHA-384' @@ -171,21 +180,35 @@ def public_key(self): return self.__class__(pyrsa.PublicKey(n=self._prepared_key.n, e=self._prepared_key.e), self._algorithm) def to_pem(self, pem_format='PKCS8'): - import rsa.pem if isinstance(self._prepared_key, pyrsa.PrivateKey): der = self._prepared_key.save_pkcs1(format='DER') if pem_format == 'PKCS8': - pem = rsa.pem.save_pem(PKCS8_RSA_HEADER + der, pem_marker='PRIVATE KEY') + pem = pyrsa_pem.save_pem(PKCS8_RSA_HEADER + der, pem_marker='PRIVATE KEY') elif pem_format == 'PKCS1': - pem = rsa.pem.save_pem(der, pem_marker='RSA PRIVATE KEY') + pem = pyrsa_pem.save_pem(der, pem_marker='RSA PRIVATE KEY') else: raise ValueError("Invalid pem format specified: %r" % (pem_format,)) else: - # this is a PKCS#8 DER header to identify rsaEncryption - header = b'0\x82\x04\xbd\x02\x01\x000\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00' - der = self._prepared_key.save_pkcs1(format='DER') - pem = rsa.pem.save_pem(header + der, pem_marker='PUBLIC KEY') + if pem_format == 'PKCS8': + asn_key = AsnPubKey() + asn_key.setComponentByName('modulus', self._prepared_key.n) + asn_key.setComponentByName('publicExponent', self._prepared_key.e) + der = encoder.encode(asn_key) + + header = PubKeyHeader() + header['oid'] = univ.ObjectIdentifier('1.2.840.113549.1.1.1') + pub_key = OpenSSLPubKey() + pub_key['header'] = header + pub_key['key'] = univ.BitString.fromOctetString(der) + + der = encoder.encode(pub_key) + pem = pyrsa_pem.save_pem(der, pem_marker='PUBLIC KEY') + elif pem_format == 'PKCS1': + der = self._prepared_key.save_pkcs1(format='DER') + pem = pyrsa_pem.save_pem(der, pem_marker='RSA PUBLIC KEY') + else: + raise ValueError("Invalid pem format specified: %r" % (pem_format,)) return pem def to_dict(self): diff --git a/tests/algorithms/test_RSA.py b/tests/algorithms/test_RSA.py index f9a2fc35..3cb74078 100644 --- a/tests/algorithms/test_RSA.py +++ b/tests/algorithms/test_RSA.py @@ -184,6 +184,22 @@ def test_to_pem(self, Backend): newkey = Backend(pkcs8, ALGORITHMS.RS256) assert newkey.to_pem(pem_format='PKCS1').strip() == private_key.strip() + @pytest.mark.parametrize("BackendFrom", [RSAKey, CryptographyRSAKey, PurePythonRSAKey]) + @pytest.mark.parametrize("BackendTo", [RSAKey, CryptographyRSAKey, PurePythonRSAKey]) + def test_public_key_to_pem(self, BackendFrom, BackendTo): + key = BackendFrom(private_key, ALGORITHMS.RS256) + pubkey = key.public_key() + + pkcs1_pub = pubkey.to_pem(pem_format='PKCS1').strip() + pkcs8_pub = pubkey.to_pem(pem_format='PKCS8').strip() + assert pkcs1_pub != pkcs8_pub, BackendFrom + + pub1 = BackendTo(pkcs1_pub, ALGORITHMS.RS256) + pub8 = BackendTo(pkcs8_pub, ALGORITHMS.RS256) + + assert pkcs8_pub == pub1.to_pem(pem_format='PKCS8').strip() + assert pkcs1_pub == pub8.to_pem(pem_format='PKCS1').strip() + def assert_parameters(self, as_dict, private): assert isinstance(as_dict, dict) From 873807d41d5d23d6f465d77017fb512a2f2f47a4 Mon Sep 17 00:00:00 2001 From: Gasper Zejn Date: Fri, 26 Jan 2018 20:18:17 +0100 Subject: [PATCH 12/12] Replace default backend for RSA to Python implementation provided by rsa package. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index c93cf0f1..aceeff76 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- import os -import sys import jose from setuptools import setup @@ -25,6 +24,7 @@ def get_packages(package): extras_require = { 'cryptography': ['cryptography'], 'pycrypto': ['pycrypto >=2.6.0, <2.7.0'], + 'pycryptodome': ['pycryptodome >=3.3.1, <4.0.0'], } @@ -55,5 +55,5 @@ def get_packages(package): 'Topic :: Utilities', ], extras_require=extras_require, - install_requires=['six <2.0', 'ecdsa <1.0', 'future <1.0', 'pycryptodome >=3.3.1, <4.0.0'] + install_requires=['six <2.0', 'ecdsa <1.0', 'rsa', 'future <1.0'] )