From 9c4f3950d738bb5e0a35c4b40647e69eaca55b37 Mon Sep 17 00:00:00 2001 From: Branden Jordan Date: Mon, 5 Oct 2020 15:16:15 -0700 Subject: [PATCH 01/17] Properly Report Key Utilized in Signing/Encrypting (#103) * Fixed an issue where the SDK would improperly report the key used when the encryption and signature keys for API differed --- CHANGES.rst | 4 +++ launchkey/__init__.py | 2 +- launchkey/transports/jose_auth.py | 9 +++-- tests/test_jose_auth_transport.py | 58 ++++++++++++++++++++++--------- 4 files changed, 54 insertions(+), 19 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index c9fb1f3..47c4b9b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,9 @@ CHANGELOG for LaunchKey Python SDK ================================== +3.8.1 +----- + +* Fixed an issue where the SDK would improperly report which key was used when the encryption and signature keys differed 3.8.0 ----- diff --git a/launchkey/__init__.py b/launchkey/__init__.py index d383c7a..2ff83fe 100644 --- a/launchkey/__init__.py +++ b/launchkey/__init__.py @@ -1,5 +1,5 @@ """LaunchKey Service SDK module""" -SDK_VERSION = '3.8.0' +SDK_VERSION = '3.8.1-rc.1' LAUNCHKEY_PRODUCTION = "https://api.launchkey.com" VALID_JWT_ISSUER_LIST = ["svc", "dir", "org"] JOSE_SUPPORTED_JWE_ALGS = ['RSA-OAEP'] diff --git a/launchkey/transports/jose_auth.py b/launchkey/transports/jose_auth.py index ae5ce4b..af1c1a4 100644 --- a/launchkey/transports/jose_auth.py +++ b/launchkey/transports/jose_auth.py @@ -141,14 +141,19 @@ def __get_kid_from_api_response_headers(headers): return kid - def _get_kid_from_api_response(self, response): + @staticmethod + def _get_kid_from_api_response(response): """ Gets `kid` property from a JWT token within a LaunchKey API response. :param response: Response object :return: string of the `kid` """ - return self.__get_kid_from_api_response_headers(response.headers) + try: + return response.headers["X-IOV-KEY-ID"] + except KeyError: + raise UnexpectedAPIResponse("X-IOV-KEY-ID was missing or malformed" + " in API response.") @staticmethod def parse_api_time(api_time): diff --git a/tests/test_jose_auth_transport.py b/tests/test_jose_auth_transport.py index a2c2742..78990cb 100644 --- a/tests/test_jose_auth_transport.py +++ b/tests/test_jose_auth_transport.py @@ -4,7 +4,7 @@ from ddt import data, ddt from jwkest import JWKESTException from jwkest.jws import JWS -from jwkest.jwt import JWT +from jwkest.jwt import JWT, BadSyntax from mock import MagicMock, ANY, patch, call from launchkey.transports import JOSETransport, RequestsTransport from launchkey.transports.base import APIResponse, APIErrorResponse @@ -70,13 +70,15 @@ "kid": faux_kid } +transport_request_headers = {"X-IOV-KEY-ID": faux_kid} + class TestJOSETransport3rdParty(unittest.TestCase): def setUp(self): self._transport = JOSETransport() self._transport.get = MagicMock(return_value=MagicMock(spec=APIResponse)) - public_key = APIResponse(valid_private_key, {}, 200) + public_key = APIResponse(valid_public_key, transport_request_headers, 200) self._transport.get.return_value = public_key self._transport._server_time_difference = 0, time() @@ -152,7 +154,7 @@ class TestJWKESTSupportedAlgs(unittest.TestCase): def setUp(self): self._transport = JOSETransport() self._transport.get = MagicMock(return_value=MagicMock(spec=APIResponse)) - public_key = APIResponse(valid_private_key, {}, 200) + public_key = APIResponse(valid_private_key, transport_request_headers, 200) self._transport.get.return_value = public_key self._transport._server_time_difference = 0, time() @@ -265,7 +267,7 @@ class TestJOSETransportJWTResponse(unittest.TestCase): def setUp(self): self._transport = JOSETransport() self._transport.get = MagicMock(return_value=MagicMock(spec=APIResponse)) - public_key = APIResponse(valid_private_key, {}, 200) + public_key = APIResponse(valid_public_key, transport_request_headers, 200) self._transport.get.return_value = public_key self._transport._server_time_difference = 0, time() self.issuer = "svc" @@ -421,7 +423,7 @@ class TestJOSETransportJWTRequest(unittest.TestCase): def setUp(self): self._transport = JOSETransport() self._transport.get = MagicMock(return_value=MagicMock(spec=APIResponse)) - public_key = APIResponse(valid_private_key, {}, 200) + public_key = APIResponse(valid_private_key, transport_request_headers, 200) self._transport.get.return_value = public_key self._transport._server_time_difference = 0, time() self.issuer = "svc" @@ -808,7 +810,7 @@ def setUp(self): } self._requests_transport = MagicMock(spec=RequestsTransport) - self._requests_transport.get.return_value = APIResponse(valid_private_key, {}, 200) + self._requests_transport.get.return_value = APIResponse(valid_public_key, transport_request_headers, 200) self._transport = JOSETransport(http_client=self._requests_transport) self._import_rsa_key_patch = patch( "launchkey.transports.jose_auth.import_rsa_key", @@ -850,8 +852,7 @@ def test_key_is_retrieved_by_id_when_key_changed(self): "kid": "jwt2keyid" } - self._jwt_patch.return_value.unpack.side_effect = [jwt1, jwt1, jwt2, jwt2] - + self._jwt_patch.return_value.unpack.side_effect = [jwt1, jwt2] self._transport.verify_jwt_response(MagicMock(), self.jti, ANY, None) self._transport.verify_jwt_response(MagicMock(), self.jti, ANY, None) self._requests_transport.get.assert_has_calls([ @@ -865,15 +866,15 @@ def test_key_retrieved_is_used_to_verify_payload(self, rsa_key_patch): self._requests_transport.get.return_value.data = valid_public_key self._transport.verify_jwt_response(MagicMock(), self.jti, ANY, None) - # Verify that verify_compact is called one time with key created by our jwkest key patch - self._jws_patch.return_value.verify_compact.assert_called_once_with(ANY, keys=[rsa_key_patch.return_value]) - - # Assert that the jwkest key patch is built using the import_rsa_key patch return value and the key id - # from the header - rsa_key_patch.assert_called_with(key=self._import_rsa_key_patch.return_value, kid=faux_kid) + # Verify that verify_compact is called one time with key created + # by our jwkest key patch + self._jws_patch.return_value.verify_compact\ + .assert_called_once_with(ANY, keys=[rsa_key_patch.return_value]) - # Verify that we used the correct key to retrieve the key id from the header - self._requests_transport.get.return_value.headers.get.assert_called_with("X-IOV-JWT") + # Assert that the jwkest key patch is built using the import_rsa_key + # patch return value and the key id from the header + rsa_key_patch.assert_called_with( + key=self._import_rsa_key_patch.return_value, kid=faux_kid) def test_raises_when_kid_header_is_missing(self): headers_without_kid = {"alg": "RS512", "typ": "JWT"} @@ -883,6 +884,31 @@ def test_raises_when_kid_header_is_missing(self): with self.assertRaises(JWTValidationFailure): self._transport.verify_jwt_response(MagicMock(), self.jti, ANY, None) + def test_raises_when_jwt_unpack_returns_badsyntax(self): + self._jwt_patch.return_value.unpack.side_effect = BadSyntax("test", "error") + with self.assertRaises(UnexpectedAPIResponse): + self._transport.verify_jwt_response( + MagicMock(), self.jti, ANY, None) + + def test_raises_when_jwt_unpack_returns_valueerror(self): + self._jwt_patch.return_value.unpack.side_effect = ValueError() + with self.assertRaises(UnexpectedAPIResponse): + self._transport.verify_jwt_response( + MagicMock(), self.jti, ANY, None) + + def test_raises_when_jwt_unpack_returns_indexerror(self): + self._jwt_patch.return_value.unpack.side_effect = IndexError() + with self.assertRaises(UnexpectedAPIResponse): + self._transport.verify_jwt_response( + MagicMock(), self.jti, ANY, None) + + def test_raises_when_kid_header_is_missing_from_http_headers(self): + self._requests_transport.get.return_value = APIResponse( + valid_public_key, {}, 200) + with self.assertRaises(UnexpectedAPIResponse): + self._transport.verify_jwt_response( + MagicMock(), self.jti, ANY, None) + def test_raises_when_kid_header_is_malformed(self): headers_with_kid_of_wrong_type = {"alg": "RS512", "typ": "JWT", "kid": 1234} jwt = MagicMock() From 2ed457d7070e15ca17cb02b9098fceaf4ef64083 Mon Sep 17 00:00:00 2001 From: Branden Jordan Date: Tue, 6 Oct 2020 17:03:50 -0700 Subject: [PATCH 02/17] Fixed an issue where the SDK would fail to validate webhook signature verification if the signature key was not the one returned in the /public/v3/public-key endpoint (#104) --- CHANGES.rst | 1 + launchkey/transports/jose_auth.py | 5 ++++- tests/test_jose_auth_transport.py | 13 ++++++++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 47c4b9b..ea94bb9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,7 @@ CHANGELOG for LaunchKey Python SDK ----- * Fixed an issue where the SDK would improperly report which key was used when the encryption and signature keys differed +* Fixed an issue where the SDK would fail to validate webhook signature verification if the signature key was not the one returned in the public-key endpoint 3.8.0 ----- diff --git a/launchkey/transports/jose_auth.py b/launchkey/transports/jose_auth.py index af1c1a4..68877f9 100644 --- a/launchkey/transports/jose_auth.py +++ b/launchkey/transports/jose_auth.py @@ -572,7 +572,10 @@ def verify_jwt_request(self, compact_jwt, subject, method, path, body): :return: The claims of the JWT """ try: - self._set_current_kid() + # Ensure key exists in cache, fetch from API by ID otherwise + kid = JWT().unpack(compact_jwt).headers["kid"] + public_key = self._find_key_by_kid(kid) + self._cache_public_key(kid, public_key) payload = self._get_jwt_payload(compact_jwt) except JWKESTException as reason: raise JWTValidationFailure("Unable to parse JWT", reason=reason) diff --git a/tests/test_jose_auth_transport.py b/tests/test_jose_auth_transport.py index 78990cb..abdc996 100644 --- a/tests/test_jose_auth_transport.py +++ b/tests/test_jose_auth_transport.py @@ -453,7 +453,13 @@ def setUp(self): self._transport.content_hash_function().hexdigest.return_value = self._content_hash self._jwt_patch = patch("launchkey.transports.jose_auth.JWT", return_value=MagicMock(spec=JWT)).start() - self._jwt_patch.return_value.unpack.return_value.headers = faux_jwt_headers + self._faux_jwt_headers = { + "alg": "RS512", + "typ": "JWT", + # This should be different than the kid from the transport headers + "kid": "ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff" + } + self._jwt_patch.return_value.unpack.return_value.headers = self._faux_jwt_headers patcher = patch('launchkey.transports.jose_auth.sha256') patched = patcher.start() @@ -474,6 +480,11 @@ def test_none_path_returns_payload(self): actual = self._transport.verify_jwt_request(MagicMock(), self.jwt_payload['sub'], 'POST', None, MagicMock()) self.assertEqual(actual, self.jwt_payload) + def test_jwt_signed_with_key_not_in_cache_fetches_key(self): + self._verify_jwt_request() + self._transport.get.assert_called_once_with( + "/public/v3/public-key/%s" % self._faux_jwt_headers["kid"]) + def test_none_path_still_requires_jwt_request_path(self): del self.jwt_payload['request']['path'] with self.assertRaises(JWTValidationFailure): From a31946543a7211907356e72d452ed7d6cb2ea658 Mon Sep 17 00:00:00 2001 From: Branden Jordan Date: Tue, 6 Oct 2020 17:15:45 -0700 Subject: [PATCH 03/17] incremented rc version to 2 (#105) --- launchkey/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launchkey/__init__.py b/launchkey/__init__.py index 2ff83fe..9ab0c8b 100644 --- a/launchkey/__init__.py +++ b/launchkey/__init__.py @@ -1,5 +1,5 @@ """LaunchKey Service SDK module""" -SDK_VERSION = '3.8.1-rc.1' +SDK_VERSION = '3.8.1-rc.2' LAUNCHKEY_PRODUCTION = "https://api.launchkey.com" VALID_JWT_ISSUER_LIST = ["svc", "dir", "org"] JOSE_SUPPORTED_JWE_ALGS = ['RSA-OAEP'] From 3847199544334241e055286e681d3591e8ed7a4e Mon Sep 17 00:00:00 2001 From: Branden Jordan Date: Thu, 22 Oct 2020 13:58:17 -0700 Subject: [PATCH 04/17] Version bump to 3.9.0 --- launchkey/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launchkey/__init__.py b/launchkey/__init__.py index 9ab0c8b..63ad729 100644 --- a/launchkey/__init__.py +++ b/launchkey/__init__.py @@ -1,5 +1,5 @@ """LaunchKey Service SDK module""" -SDK_VERSION = '3.8.1-rc.2' +SDK_VERSION = '3.9.0' LAUNCHKEY_PRODUCTION = "https://api.launchkey.com" VALID_JWT_ISSUER_LIST = ["svc", "dir", "org"] JOSE_SUPPORTED_JWE_ALGS = ['RSA-OAEP'] From 738a398e3c5411b00b6d32579aaf6eacc8d221da Mon Sep 17 00:00:00 2001 From: Cody Moncur Date: Fri, 13 Nov 2020 15:06:35 -0800 Subject: [PATCH 05/17] Handle Single Purpose Keys * Handle Single Purpose Keys * Update version for RC, add unit test for invalid key type * Use Int validator instead of OneOf to validate key_type, update tests accordingly * Create KeyType enum type and ensure it is leveraged in PublicKey class * Capitalize enumerations and update language in feature test --- ...ory-client-service-public-keys-add.feature | 19 +++++ ...g-client-directory-public-keys-add.feature | 20 +++++ features/steps/directory_public_key_steps.py | 67 ++++++++++++++++- .../directory_service_public_key_steps.py | 74 ++++++++++++++++++- features/steps/managers/directory.py | 3 +- features/steps/managers/directory_service.py | 3 +- launchkey/__init__.py | 2 +- launchkey/clients/base.py | 7 +- launchkey/clients/organization.py | 8 +- launchkey/entities/shared.py | 21 ++++++ launchkey/entities/validation.py | 1 + tests/shared.py | 73 ++++++++++++++++++ tests/test_organization_client.py | 50 +++++++++++++ tests/test_validation.py | 51 ++++++++++++- 14 files changed, 391 insertions(+), 8 deletions(-) diff --git a/features/directory_client/services/public_keys/directory-client-service-public-keys-add.feature b/features/directory_client/services/public_keys/directory-client-service-public-keys-add.feature index 7ef7310..98051f5 100755 --- a/features/directory_client/services/public_keys/directory-client-service-public-keys-add.feature +++ b/features/directory_client/services/public_keys/directory-client-service-public-keys-add.feature @@ -12,6 +12,25 @@ Feature: Directory clients can add a Public Key to a Directory Service And I retrieve the current Directory Service's Public Keys Then the Directory Service Public Key is in the list of Public Keys for the Directory Service + Scenario Outline: I can add a Public Key with a key type to a Directory Service and the key type is present when the key is retrieved + When I add a Public Key with a type to the Directory Service + And I retrieve the current Directory Service's Public Keys + Then the Public Key is in the list of Public Keys for the Directory Service and has a key type + Examples: + | key_type | + | 0 | + | 1 | + | 2 | + + Scenario: Adding a Public Key to a Directory Service with an empty key type defaults to a dual use key type + When I add a Public Key to the Directory Service + And I retrieve the current Directory Service's Public Keys + Then the Public Key is in the list of Public Keys for the Directory Service and has a "0" key type + + Scenario: Adding a Public Key to a Directory Service with an invalid key type yields an error + When I attempt to add a Public Key with a "sup" type to the Directory Service + Then an InvalidParameters error occurs + Scenario: Adding multiple Public Keys to a Directory Service works When I add a Public Key to the Directory Service When I add another Public Key to the Directory Service diff --git a/features/org_client/directories/public_keys/org-client-directory-public-keys-add.feature b/features/org_client/directories/public_keys/org-client-directory-public-keys-add.feature index bdeb3f1..205763e 100755 --- a/features/org_client/directories/public_keys/org-client-directory-public-keys-add.feature +++ b/features/org_client/directories/public_keys/org-client-directory-public-keys-add.feature @@ -11,6 +11,26 @@ Feature: Organization clients can add a Public Key to a Directory And I retrieve the current Directory's Public Keys Then the Public Key is in the list of Public Keys for the Directory + + Scenario Outline: I can add a Public Key with a key type to a Directory and the key type is present when the key is retrieved + When I add a Public Key with a type to the Directory + And I retrieve the current Directory's Public Keys + Then the Public Key is in the list of Public Keys for the Directory and has a key type + Examples: + | key_type | + | 0 | + | 1 | + | 2 | + + Scenario: Adding a Public Key to a Directory with an empty key type defaults to a dual use key type + When I add a Public Key to the Directory + And I retrieve the current Directory's Public Keys + Then the Public Key is in the list of Public Keys for the Directory and has a "0" key type + + Scenario: Adding a Public Key to a Directory with an invalid key type yields an error + When I attempt to add a Public Key with a "sup" type to the Directory + Then an InvalidParameters error occurs + Scenario: Adding multiple Public Keys to a Directory works When I add a Public Key to the Directory When I add another Public Key to the Directory diff --git a/features/steps/directory_public_key_steps.py b/features/steps/directory_public_key_steps.py index 3963429..42b8cd0 100644 --- a/features/steps/directory_public_key_steps.py +++ b/features/steps/directory_public_key_steps.py @@ -3,7 +3,7 @@ from behave import given, when, then -from launchkey.entities.shared import PublicKey +from launchkey.entities.shared import KeyType, PublicKey # Add public keys @@ -25,6 +25,56 @@ def add_public_key_to_directory(context): ) +@when("I add a Public Key with a {key_type} type to the Directory") +def add_public_key_with_key_type_to_directory_service(context, key_type): + current_directory = context.entity_manager.get_current_directory() + + public_key = PublicKey({ + "id": context.keys_manager.alpha_md5_fingerprint, + "active": True, + "date_created": None, + "date_expires": None, + "public_key": context.keys_manager.alpha_public_key, + "key_type": KeyType(int(key_type)) + }) + + context.directory_manager.add_public_key_to_directory( + public_key, + current_directory.id + ) + + +@when("I attempt to add a Public Key with a \"{key_type}\" type to the Directory") +def add_public_key_with_key_type_to_directory_service(context, key_type): + current_directory = context.entity_manager.get_current_directory() + + try: + # Ensure that integers remain integers and are not recognized + # by Selenium as strings, and that strings remain strings + sanitized_key_type = int(key_type) + + except ValueError: + sanitized_key_type = key_type + + public_key = PublicKey({ + "id": context.keys_manager.alpha_md5_fingerprint, + "active": True, + "date_created": None, + "date_expires": None, + "public_key": context.keys_manager.alpha_public_key, + "key_type": sanitized_key_type + }) + + try: + context.directory_manager.add_public_key_to_directory( + public_key, + current_directory.id + ) + + except Exception as e: + context.current_exception = e + + @when("I attempt to add the same Public Key to the Directory") def attempt_to_add_public_key_to_directory_service(context): current_directory = context.entity_manager.get_current_directory() @@ -163,6 +213,21 @@ def verify_directory_public_key_is_in_list_of_public_keys(context): raise Exception("Unable to find the current directory public key") +@then("the Public Key is in the list of Public Keys for the Directory and has " + "a {key_type} key type") +@then("the Public Key is in the list of Public Keys for the Directory and has " + "a \"{key_type}\" key type") +def verify_directory_public_key_is_in_list_of_public_keys(context, key_type): + key_type_enum = KeyType(int(key_type)) + alpha_public_key = context.keys_manager.alpha_public_key + current_directory_public_keys = context.entity_manager. \ + get_current_directory_public_keys() + for key in current_directory_public_keys: + if key.public_key == alpha_public_key and key.key_type == key_type_enum: + return + raise Exception("Unable to find the current directory public key") + + @then("the other Public Key is in the list of Public Keys for the Directory") def verify_other_directory_public_key_is_in_list_of_public_keys(context): beta_public_key = context.keys_manager.beta_public_key diff --git a/features/steps/directory_service_public_key_steps.py b/features/steps/directory_service_public_key_steps.py index c835753..6962f0c 100644 --- a/features/steps/directory_service_public_key_steps.py +++ b/features/steps/directory_service_public_key_steps.py @@ -3,7 +3,7 @@ from behave import given, when, then -from launchkey.entities.shared import PublicKey +from launchkey.entities.shared import KeyType, PublicKey # Add public keys @@ -27,6 +27,62 @@ def add_public_key_to_directory_service(context): ) +@when("I add a Public Key with a {key_type} type to the Directory Service") +def add_public_key_with_key_type_to_directory_service(context, key_type): + current_service = context.entity_manager.get_current_directory_service() + current_directory = context.entity_manager.get_current_directory() + + public_key = PublicKey({ + "id": context.keys_manager.alpha_md5_fingerprint, + "active": True, + "date_created": None, + "date_expires": None, + "public_key": context.keys_manager.alpha_public_key, + "key_type": KeyType(int(key_type)) + }) + + context.directory_service_manager.add_public_key_to_service( + current_directory.id, + public_key, + current_service.id + ) + + +@when("I attempt to add a Public Key with a \"{key_type}\" type to the " + "Directory Service") +def attempt_add_public_key_with_key_type_to_directory_service(context, + key_type): + current_service = context.entity_manager.get_current_directory_service() + current_directory = context.entity_manager.get_current_directory() + + try: + # Ensure that integers remain integers and are not recognized + # by Selenium as strings, and that strings remain strings + sanitized_key_type = int(key_type) + + except ValueError: + sanitized_key_type = key_type + + public_key = PublicKey({ + "id": context.keys_manager.alpha_md5_fingerprint, + "active": True, + "date_created": None, + "date_expires": None, + "public_key": context.keys_manager.alpha_public_key, + "key_type": sanitized_key_type + }) + + try: + context.directory_service_manager.add_public_key_to_service( + current_directory.id, + public_key, + current_service.id + ) + + except Exception as e: + context.current_exception = e + + @when("I attempt to add the same Public Key to the Directory Service") def attempt_to_add_public_key_to_directory_service(context): current_service = context.entity_manager.get_current_directory_service() @@ -181,6 +237,22 @@ def verify_directory_service_public_key_is_in_list_of_public_keys(context): raise Exception("Unable to find the current directory public key") +@then("the Public Key is in the list of Public Keys for the Directory Service " + "and has a {key_type} key type") +@then("the Public Key is in the list of Public Keys for the Directory Service " + "and has a \"{key_type}\" key type") +def verify_directory_service_public_key_is_in_list_of_public_keys_with_key_type( + context, key_type): + key_type_enum = KeyType(int(key_type)) + alpha_public_key = context.keys_manager.alpha_public_key + current_directory_public_keys = context.entity_manager. \ + get_current_directory_service_public_keys() + for key in current_directory_public_keys: + if key.public_key == alpha_public_key and key.key_type == key_type_enum: + return + raise Exception("Unable to find the current directory public key") + + @then("the other Public Key is in the list of Public Keys for the Directory " "Service") def verify_other_directory_service_public_key_is_in_list_of_public_keys(context): diff --git a/features/steps/managers/directory.py b/features/steps/managers/directory.py index 00b2de3..d02a5df 100644 --- a/features/steps/managers/directory.py +++ b/features/steps/managers/directory.py @@ -129,7 +129,8 @@ def add_public_key_to_directory(self, public_key, directory_id): directory_id, public_key.public_key, expires=public_key.expires, - active=public_key.active + active=public_key.active, + key_type=public_key.key_type ) def retrieve_directory_public_keys(self, directory_id): diff --git a/features/steps/managers/directory_service.py b/features/steps/managers/directory_service.py index a6479ca..043caa1 100644 --- a/features/steps/managers/directory_service.py +++ b/features/steps/managers/directory_service.py @@ -87,7 +87,8 @@ def add_public_key_to_service(self, directory_id, public_key, service_id): service_id, public_key.public_key, expires=public_key.expires, - active=public_key.active + active=public_key.active, + key_type=public_key.key_type ) def update_public_key(self, directory_id, key_id, service_id, diff --git a/launchkey/__init__.py b/launchkey/__init__.py index 63ad729..58a49db 100644 --- a/launchkey/__init__.py +++ b/launchkey/__init__.py @@ -1,5 +1,5 @@ """LaunchKey Service SDK module""" -SDK_VERSION = '3.9.0' +SDK_VERSION = '3.9.0-rc.1' LAUNCHKEY_PRODUCTION = "https://api.launchkey.com" VALID_JWT_ISSUER_LIST = ["svc", "dir", "org"] JOSE_SUPPORTED_JWE_ALGS = ['RSA-OAEP'] diff --git a/launchkey/clients/base.py b/launchkey/clients/base.py index 248210e..1191d8b 100644 --- a/launchkey/clients/base.py +++ b/launchkey/clients/base.py @@ -267,7 +267,7 @@ def update_service(self, service_id, name=False, description=False, @api_call def add_service_public_key(self, service_id, public_key, expires=None, - active=None): + active=None, key_type=None): """ Adds a public key to a Service :param service_id: Unique Service ID @@ -276,6 +276,8 @@ def add_service_public_key(self, service_id, public_key, expires=None, key will no longer be valid :param active: Optional bool stating whether the key should be considered active and usable. + :param key_type: Optional KeyType enum to identify whether the key is + an encryption key, signature key, or a dual use key :raise: launchkey.exceptions.InvalidParameters - Input parameters were not correct :raise: launchkey.exceptions.InvalidPublicKey - The public key you @@ -291,6 +293,9 @@ def add_service_public_key(self, service_id, public_key, expires=None, kwargs['date_expires'] = iso_format(expires) if active is not None: kwargs['active'] = active + if key_type is not None: + kwargs['key_type'] = key_type.value + key_id = self._transport.post( "{}/keys".format(self.__service_base_path[0:-1]), self._subject, **kwargs).data['key_id'] diff --git a/launchkey/clients/organization.py b/launchkey/clients/organization.py index 6389ed4..77498b0 100644 --- a/launchkey/clients/organization.py +++ b/launchkey/clients/organization.py @@ -140,7 +140,7 @@ def get_directory_public_keys(self, directory_id): @api_call def add_directory_public_key(self, directory_id, public_key, expires=None, - active=None): + active=None, key_type=None): """ Adds a public key to an Directory :param directory_id: Unique Directory ID @@ -149,6 +149,8 @@ def add_directory_public_key(self, directory_id, public_key, expires=None, the key will no longer be valid :param active: Optional bool stating whether the key should be considered active and usable. + :param key_type: Optional KeyType enum to identify whether the key is + an encryption key, signature key, or a dual use key :raise: launchkey.exceptions.InvalidParameters - Input parameters were not correct :raise: launchkey.exceptions.InvalidPublicKey - The public key you @@ -166,6 +168,10 @@ def add_directory_public_key(self, directory_id, public_key, expires=None, kwargs['date_expires'] = iso_format(expires) if active is not None: kwargs['active'] = active + + if key_type is not None: + kwargs['key_type'] = key_type.value + return \ self._transport.post("/organization/v3/directory/keys", self._subject, diff --git a/launchkey/entities/shared.py b/launchkey/entities/shared.py index 961b994..6958821 100644 --- a/launchkey/entities/shared.py +++ b/launchkey/entities/shared.py @@ -2,6 +2,19 @@ # pylint: disable=invalid-name,too-few-public-methods +from enum import Enum + + +class KeyType(Enum): + """ + An enum that represents what a key is to be used for, i.e. signature, + encryption, or both. + """ + BOTH = 0 + ENCRYPTION = 1 + SIGNATURE = 2 + OTHER = -1 + class PublicKey(object): """ @@ -15,3 +28,11 @@ def __init__(self, public_key_data): self.created = public_key_data['date_created'] self.expires = public_key_data['date_expires'] self.public_key = public_key_data['public_key'] + + try: + self.key_type = KeyType.BOTH if not \ + public_key_data.get('key_type') else \ + KeyType(public_key_data['key_type']) + + except ValueError: + self.key_type = KeyType.OTHER diff --git a/launchkey/entities/validation.py b/launchkey/entities/validation.py index 01ab0e4..8c35d69 100644 --- a/launchkey/entities/validation.py +++ b/launchkey/entities/validation.py @@ -13,6 +13,7 @@ class PublicKeyValidator(Schema): date_created = ValidateISODate() date_expires = ValidateISODate() public_key = validators.String() + key_type = validators.Int(if_missing=0, if_empty=0) allow_extra_fields = True diff --git a/tests/shared.py b/tests/shared.py index dc95d48..f502b3a 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -6,6 +6,7 @@ from mock import ANY, patch from six import assertRaisesRegex +from launchkey.entities.shared import KeyType from launchkey.entities.service import Service, ServiceSecurityPolicy, TimeFence, GeoFence from launchkey.entities.service.policy import ConditionalGeoFencePolicy, \ FactorsPolicy, MethodAmountPolicy @@ -246,6 +247,19 @@ def test_add_service_public_key_all(self, active): public_key="public-key", date_expires="2017-10-03T22:50:15Z", active=active) + @data(KeyType.BOTH, KeyType.ENCRYPTION, KeyType.SIGNATURE) + def test_add_service_public_key_key_type(self, key_type): + self._response.data = {"key_id": ANY} + self._client.add_service_public_key( + "5e49fc4c-ddcb-48db-8473-a5f996b85fbc", "public-key", + active=True, key_type=key_type) + self._transport.post.assert_called_once_with( + self._expected_base_endpoint[0:-1] + "/keys", + self._expected_subject, + service_id="5e49fc4c-ddcb-48db-8473-a5f996b85fbc", + public_key="public-key", active=True, + key_type=key_type.value) + def test_add_service_public_key_invalid_params(self): self._transport.post.side_effect = LaunchKeyAPIException({"error_code": "ARG-001", "error_detail": ""}, 400) with self.assertRaises(InvalidParameters): @@ -295,6 +309,65 @@ def test_get_service_public_keys(self, active): second=15, tzinfo=pytz.timezone("UTC"))) self.assertEqual(key.public_key, "A Public Key") + @data(KeyType.BOTH, KeyType.ENCRYPTION, KeyType.SIGNATURE) + def test_get_service_public_keys_applies_key_type(self, key_type): + self._response.data = [ + { + "id": "ab:cd:ef:gh:ij:kl:mn:op:qr:st:uv:wx:yz", + "active": True, + "date_created": "2017-10-03T22:50:15Z", + "date_expires": "2018-10-03T22:50:15Z", + "public_key": "A Public Key", + "key_type": key_type.value + } + ] + + public_keys = self._client.get_service_public_keys( + "3559a520-180f-4fee-ad52-75959286340d") + + key = public_keys[0] + self.assertEqual(key.key_type, key_type) + + def test_get_service_public_keys_null_key_type_defaults_to_both(self): + actual = None + expected = KeyType.BOTH + + self._response.data = [ + { + "id": "ab:cd:ef:gh:ij:kl:mn:op:qr:st:uv:wx:yz", + "active": True, + "date_created": "2017-10-03T22:50:15Z", + "date_expires": "2018-10-03T22:50:15Z", + "public_key": "A Public Key", + "key_type": actual + } + ] + + public_keys = self._client.get_service_public_keys( + "3559a520-180f-4fee-ad52-75959286340d") + + key = public_keys[0] + self.assertEqual(key.key_type, expected) + + def test_get_service_public_keys_no_key_type_defaults_to_both_key(self): + expected = KeyType.BOTH + + self._response.data = [ + { + "id": "ab:cd:ef:gh:ij:kl:mn:op:qr:st:uv:wx:yz", + "active": True, + "date_created": "2017-10-03T22:50:15Z", + "date_expires": "2018-10-03T22:50:15Z", + "public_key": "A Public Key" + } + ] + + public_keys = self._client.get_service_public_keys( + "3559a520-180f-4fee-ad52-75959286340d") + + key = public_keys[0] + self.assertEqual(key.key_type, expected) + def test_get_service_public_keys_invalid_params(self): self._transport.post.side_effect = LaunchKeyAPIException({"error_code": "ARG-001", "error_detail": ""}, 400) diff --git a/tests/test_organization_client.py b/tests/test_organization_client.py index 6ff2130..ebd1746 100644 --- a/tests/test_organization_client.py +++ b/tests/test_organization_client.py @@ -4,6 +4,7 @@ from launchkey.clients import OrganizationClient from launchkey.clients.organization import Directory from launchkey.transports.base import APIResponse +from launchkey.entities.shared import KeyType from launchkey.exceptions import LaunchKeyAPIException, InvalidParameters, LastRemainingKey, PublicKeyDoesNotExist, \ InvalidPublicKey, PublicKeyAlreadyInUse, LastRemainingSDKKey, InvalidSDKKey, Forbidden, EntityNotFound from datetime import datetime @@ -271,6 +272,18 @@ def test_add_directory_public_key_all(self): public_key="public-key", date_expires="2017-10-03T22:50:15Z", active=True) + @data(KeyType.BOTH, KeyType.ENCRYPTION, KeyType.SIGNATURE) + def test_add_directory_public_key_key_type(self, key_type): + self._response.data = {"key_id": ANY} + self._organization_client.add_directory_public_key( + "5e49fc4c-ddcb-48db-8473-a5f996b85fbc", "public-key", active=True, + key_type=key_type) + self._transport.post.assert_called_once_with( + "/organization/v3/directory/keys", self._expected_subject, + directory_id="5e49fc4c-ddcb-48db-8473-a5f996b85fbc", + public_key="public-key", active=True, + key_type=key_type.value) + def test_add_directory_public_key_invalid_params(self): self._transport.post.side_effect = LaunchKeyAPIException({"error_code": "ARG-001", "error_detail": ""}, 400) with self.assertRaises(InvalidParameters): @@ -316,6 +329,43 @@ def test_get_directory_public_keys(self, active): self.assertEqual(key.expires, datetime(year=2018, month=10, day=3, hour=22, minute=50, second=15, tzinfo=pytz.timezone("UTC"))) self.assertEqual(key.public_key, "A Public Key") + self.assertEqual(key.key_type, KeyType.BOTH) + + @data(0, 1, 2) + def test_get_directory_public_keys_key_type_enum(self, key_type_int): + self._response.data = [ + { + "id": "ab:cd:ef:gh:ij:kl:mn:op:qr:st:uv:wx:yz", + "active": True, + "date_created": "2017-10-03T22:50:15Z", + "date_expires": "2018-10-03T22:50:15Z", + "public_key": "A Public Key", + "key_type": key_type_int + } + ] + + key = self._organization_client.get_directory_public_keys( + "a08eab76-4094-4d60-aca1-30efbab3179b")[0] + + self.assertEqual(key.key_type, KeyType(key_type_int)) + + @data(-1, 4, 100) + def test_get_directory_public_keys_key_type_other(self, key_type_int): + self._response.data = [ + { + "id": "ab:cd:ef:gh:ij:kl:mn:op:qr:st:uv:wx:yz", + "active": True, + "date_created": "2017-10-03T22:50:15Z", + "date_expires": "2018-10-03T22:50:15Z", + "public_key": "A Public Key", + "key_type": key_type_int + } + ] + + key = self._organization_client.get_directory_public_keys( + "a08eab76-4094-4d60-aca1-30efbab3179b")[0] + + self.assertEqual(key.key_type, KeyType.OTHER) def test_get_service_public_keys_invalid_params(self): self._transport.post.side_effect = LaunchKeyAPIException({"error_code": "ARG-001", "error_detail": ""}, diff --git a/tests/test_validation.py b/tests/test_validation.py index d400ca7..74b7798 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -9,10 +9,59 @@ from launchkey.entities.validation import AuthorizeValidator, \ ConditionalGeoFenceValidator, PolicyFenceValidator, FenceValidator, \ GeoFenceValidator, TerritoryFenceValidator, GeoCircleFenceValidator, \ - DirectoryUserTOTPValidator, ServiceTOTPVerificationValidator + DirectoryUserTOTPValidator, ServiceTOTPVerificationValidator, \ + PublicKeyValidator from launchkey.exceptions.validation import AuthorizationInProgressValidator +@ddt +class TestPublicKeyValidator(TestCase): + # TODO: There were previously no PublicKeyValidator tests. The only tests + # I'm adding here are for the new key_type field, but at some point + # we should go back and fill in tests for the other fields. + def setUp(self): + self._validator = PublicKeyValidator() + self._valid_key = { + "id": "ab:cd:ef:gh:ij:kl:mn:op:qr:st:uv:wx:yz", + "active": True, + "date_created": "2017-10-03T22:50:15Z", + "date_expires": "2018-10-03T22:50:15Z", + "public_key": "A Public Key", + "key_type": 0 + } + + def test_succeeds(self): + self._validator.to_python(self._valid_key) + + @data(0, 1, 2) + def test_valid_key_type(self, key_type): + self._valid_key["key_type"] = key_type + sanitized = self._validator.to_python(self._valid_key) + + self.assertEqual(sanitized["key_type"], key_type) + + @data(None, "") + def test_empty_key_type_defaults_to_zero(self, actual): + expected = 0 + self._valid_key["key_type"] = actual + sanitized = self._validator.to_python(self._valid_key) + + self.assertEqual(sanitized["key_type"], expected) + + @data("sup", [], {}) + def test_invalid_key_type_throws_invalid(self, invalid_key_type): + with self.assertRaises(Invalid): + self._valid_key["key_type"] = invalid_key_type + self._validator.to_python(self._valid_key) + + def test_absent_key_type_defaults_to_zero(self): + expected = 0 + del self._valid_key["key_type"] + sanitized = self._validator.to_python(self._valid_key) + + self.assertEqual(sanitized["key_type"], expected) + + @ddt class TestServiceTOTPVerificationValidator(TestCase): From 0a8d6ebe809dd16974a852132541532e9e9a7ba8 Mon Sep 17 00:00:00 2001 From: Brad Porter Date: Fri, 11 Dec 2020 11:18:16 -0800 Subject: [PATCH 06/17] Updated CLI to support separate encryption and signature keys (#111) Updated CLI to support separate encryption and signature keys --- CHANGES.rst | 6 ++++++ examples/cli/README.md | 13 +++++++++++++ examples/cli/cli.py | 21 ++++++++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index ea94bb9..cf2e3a8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,11 @@ CHANGELOG for LaunchKey Python SDK ================================== + +3.9.0 +----- + +* Updated CLI to support separate encryption and signature keys + 3.8.1 ----- diff --git a/examples/cli/README.md b/examples/cli/README.md index 47eb8f0..c518dff 100644 --- a/examples/cli/README.md +++ b/examples/cli/README.md @@ -4,6 +4,7 @@ * [Installation](#installation) * [Usage](#usage) * [Help](#help) + * [Using Multiple Keys](#multiple-keys) * [Commands](#commands) * [Service User Session Management](#service) * [Directory User Device Management](#directory) @@ -45,6 +46,18 @@ Help can be obtained by either executing the application without any parameters python cli.py --help ``` +### Using Multiple Keys + +If you wish to use separate encryption and signature keys, the --additional-key +option may be used to include supplementary keys that will be checked upon for +decrypting responses. + +The required PRIVATE_KEY argument will always be used for signing requests. + +``` +python cli.py --additional-key=/path/to/encryption.key --additional-key=/path/to/encryption2.key organization 86fc3e57-6f60-420c-9a4f-98b7b0f83000 /path/to/signature.key [COMMAND] +``` + ### Commands There are two sections of commands which have a number of actions they can perform. diff --git a/examples/cli/cli.py b/examples/cli/cli.py index acfa662..8dbb0d9 100644 --- a/examples/cli/cli.py +++ b/examples/cli/cli.py @@ -1,5 +1,6 @@ import random import string +import warnings import click import qrcode @@ -24,8 +25,15 @@ help="API URL to send auth requests. Defaults to " "https://api.launchkey.com", type=click.STRING) +@click.option('--additional-key', '-k', + help="Path to a supplemental key file. If a response is " + "returned in which the client cannot decrypt it will " + "attempt to use any given additional keys. This option " + "can be used multiple times.", + multiple=True, + type=click.File('rb')) @click.pass_context -def main(ctx, entity_type, entity_id, private_key, api): +def main(ctx, entity_type, entity_id, private_key, api, additional_key): """ ENTITY_TYPE: Entity type for the given ID. This can be a Service, Directory, or Organization. @@ -34,6 +42,9 @@ def main(ctx, entity_type, entity_id, private_key, api): Directory, or Organization. PRIVATE_KEY: Path to private key belonging to the given entity ID. + This key will be used for signing and decrypting requests. In the case that + you would like to separate signature and encryption keys the --additional-key + option may be used to add explicit encryption keys. IE: /path/to/my.key """ @@ -54,6 +65,14 @@ def main(ctx, entity_type, entity_id, private_key, api): raise TypeError("Input entity type is not valid. Should be one of: " "Organization, Directory, Service.") + for k in additional_key: + key = k.read() + if key == private_key: + warnings.warn("An additional key file was given that matches the " + "PRIVATE_KEY argument. Please verify this was " + "expected.", UserWarning) + ctx.obj['factory'].add_additional_private_key(key) + @main.command() @click.argument("service_id", type=click.STRING) From b41e21c362c994c4e09a321ea5767ba70a8b6fff Mon Sep 17 00:00:00 2001 From: Brad Porter Date: Mon, 14 Dec 2020 20:36:44 -0800 Subject: [PATCH 07/17] Fixed bug that was preventing public keys from being retrieved when cache time expired + updated encryption calls to only encrypt using the active API encryption key --- launchkey/transports/jose_auth.py | 16 ++++++++-------- tests/test_jose_auth_transport.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/launchkey/transports/jose_auth.py b/launchkey/transports/jose_auth.py index 68877f9..fc4411a 100644 --- a/launchkey/transports/jose_auth.py +++ b/launchkey/transports/jose_auth.py @@ -190,17 +190,15 @@ def api_public_keys(self): :return: List of RSAKeys """ if not self._public_key_cache: - self._set_current_kid() + self.update_and_return_active_encryption_kid() + return list(self._public_key_cache.values()) - rsa_keys = map(lambda kv: kv[1], self._public_key_cache.items()) - return list(rsa_keys) - - def _set_current_kid(self): + def update_and_return_active_encryption_kid(self): """ Determines whether a new current key is necessary, and if so, retrieves new `kid` and public key from LaunchKey API, sets the current `kid` with current timestamp, and caches the new key. - :return: + :return: Currently active key id """ now = int(time()) current_kid, current_kid_timestamp = self._current_kid @@ -210,6 +208,7 @@ def _set_current_kid(self): new_kid, new_public_key = self._get_current_kid_and_key() self._current_kid = new_kid, now self._cache_public_key(new_kid, new_public_key) + return self._current_kid[0] def _get_key_by_kid(self, kid): """ @@ -259,7 +258,6 @@ def _cache_public_key(self, kid, public_key): if not self._public_key_cache.get(kid): try: rsa_key = RSAKey(key=import_rsa_key(public_key), kid=kid) - except (TypeError, ValueError): raise UnexpectedAPIResponse("RSA parsing error for public key" ": %s" % public_key) @@ -412,7 +410,9 @@ def _encrypt_request(self, data): """ jwe = JWE(json.dumps(data), alg=self.jwe_cek_encryption, enc=self.jwe_claims_encryption) - return jwe.encrypt(keys=self.api_public_keys) + # Retrieve the active API encryption KID + current_kid = self.update_and_return_active_encryption_kid() + return jwe.encrypt(keys=[self._public_key_cache[current_kid]]) def _process_jose_request(self, method, path, subject, data=None): """ diff --git a/tests/test_jose_auth_transport.py b/tests/test_jose_auth_transport.py index abdc996..4a4d779 100644 --- a/tests/test_jose_auth_transport.py +++ b/tests/test_jose_auth_transport.py @@ -768,7 +768,7 @@ def test_api_public_keys_cache_expiration(self, rsa_key_patch, time_patch, jwt_p call_count = 10 time_patch.return_value = 0 for i in range(0, call_count): - self._transport._set_current_kid() + self._transport.update_and_return_active_encryption_kid() time_patch.return_value += API_CACHE_TIME + 1 self.assertEqual(self._transport.get.call_count, call_count) From 6e7e584a63d78bb381d378d42edebb5515c7d460 Mon Sep 17 00:00:00 2001 From: Brad Porter Date: Tue, 15 Dec 2020 13:07:47 -0800 Subject: [PATCH 08/17] Incremented rc version to 3.9.0-rc.2 --- launchkey/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launchkey/__init__.py b/launchkey/__init__.py index 58a49db..abd2e10 100644 --- a/launchkey/__init__.py +++ b/launchkey/__init__.py @@ -1,5 +1,5 @@ """LaunchKey Service SDK module""" -SDK_VERSION = '3.9.0-rc.1' +SDK_VERSION = '3.9.0-rc.2' LAUNCHKEY_PRODUCTION = "https://api.launchkey.com" VALID_JWT_ISSUER_LIST = ["svc", "dir", "org"] JOSE_SUPPORTED_JWE_ALGS = ['RSA-OAEP'] From 4f20060ee0944b1baf11418f3bd56b41ce1738e5 Mon Sep 17 00:00:00 2001 From: Brad Porter Date: Tue, 15 Dec 2020 16:01:58 -0800 Subject: [PATCH 09/17] Removed duplicate 3.8.1 changelog entry --- CHANGES.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b6c7d1f..cf2e3a8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,10 +1,5 @@ CHANGELOG for LaunchKey Python SDK ================================== -3.8.1 ------ - -* Fixed an issue where the SDK would improperly report which key was used when the encryption and signature keys differed -* Fixed an issue where the SDK would fail to validate webhook signature verification if the signature key was not the one returned in the public-key endpoint 3.9.0 ----- From 0604ef3ab7d569cba616455ad1dcfb829aa47472 Mon Sep 17 00:00:00 2001 From: Brad Porter Date: Tue, 15 Dec 2020 18:01:00 -0800 Subject: [PATCH 10/17] Added key addition methods to be more clear that they will be used for encryption, deprecated previously used methods, and also altered factory instantiated keys to be enforced for signing --- CHANGES.rst | 2 ++ README.rst | 41 +++++++++++++++++++++++++++++++ examples/cli/cli.py | 2 +- launchkey/__init__.py | 2 +- launchkey/factories/base.py | 34 +++++++++++++++++++++++-- launchkey/transports/jose_auth.py | 28 +++++++++++++++++---- tests/test_factories.py | 32 ++++++++++++++++++++---- tests/test_jose_auth_transport.py | 35 ++++++++++++++++++++------ 8 files changed, 154 insertions(+), 22 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index cf2e3a8..d511810 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,8 @@ CHANGELOG for LaunchKey Python SDK ----- * Updated CLI to support separate encryption and signature keys +* Altered JOSE transport to ensure only the designated signing key is used for signing requests +* Added the `add_encryption_private_key` method to all factories and deprecated `add_additional_private_key` 3.8.1 ----- diff --git a/README.rst b/README.rst index 4d986f5..ed28ccf 100644 --- a/README.rst +++ b/README.rst @@ -73,6 +73,47 @@ factory. | Service | No | No | Yes | +--------------+---------------------+------------------+----------------+ +**Utilizing Single Purpose Keys** + +In the case that separate encryption and signature keys are being used. The +initial key given to a factory will be used to sign requests, and any +additional keys can be added after instantiation. + +.. code-block:: python + + from launchkey.factories import OrganizationFactory + + organization_id = "37d98bb9-ac71-44b7-9ac0-5d75e31e627a" + organization_signature_private_key = open("organization_signature_private_key.key").read() + organization_encryption_private_key = open("organization_encryption_private_key.key").read() + + organization_factory = OrganizationFactory(organization_id, organization_signature_private_key) + organization_factory.add_encryption_private_key(organization_encryption_private_key) + + +.. code-block:: python + + from launchkey.factories import DirectoryFactory + + directory_id = "37d98bb9-ac71-44b7-9ac0-5d75e31e627a" + directory_signature_private_key = open("directory_signature_private_key.key").read() + directory_encryption_private_key = open("directory_encryption_private_key.key").read() + + directory_factory = DirectoryFactory(directory_id, directory_signature_private_key) + directory_factory.add_encryption_private_key(directory_encryption_private_key) + + +.. code-block:: python + + from launchkey.factories import ServiceFactory + + service_id = "37d98bb9-ac71-44b7-9ac0-5d75e31e627a" + service_signature_private_key = open("service_signature_private_key.key").read() + service_encryption_private_key = open("service_encryption_private_key.key").read() + + service_factory = ServiceFactory(organization_id, service_signature_private_key) + service_factory.add_encryption_private_key(service_encryption_private_key) + **Using individual clients** .. code-block:: python diff --git a/examples/cli/cli.py b/examples/cli/cli.py index 8dbb0d9..7176f62 100644 --- a/examples/cli/cli.py +++ b/examples/cli/cli.py @@ -71,7 +71,7 @@ def main(ctx, entity_type, entity_id, private_key, api, additional_key): warnings.warn("An additional key file was given that matches the " "PRIVATE_KEY argument. Please verify this was " "expected.", UserWarning) - ctx.obj['factory'].add_additional_private_key(key) + ctx.obj['factory'].add_encryption_private_key(key) @main.command() diff --git a/launchkey/__init__.py b/launchkey/__init__.py index abd2e10..ca8042c 100644 --- a/launchkey/__init__.py +++ b/launchkey/__init__.py @@ -1,5 +1,5 @@ """LaunchKey Service SDK module""" -SDK_VERSION = '3.9.0-rc.2' +SDK_VERSION = '3.9.0-rc.3' LAUNCHKEY_PRODUCTION = "https://api.launchkey.com" VALID_JWT_ISSUER_LIST = ["svc", "dir", "org"] JOSE_SUPPORTED_JWE_ALGS = ['RSA-OAEP'] diff --git a/launchkey/factories/base.py b/launchkey/factories/base.py index 55c516e..36406d9 100644 --- a/launchkey/factories/base.py +++ b/launchkey/factories/base.py @@ -2,6 +2,7 @@ # pylint: disable=too-many-arguments,too-few-public-methods +import warnings from ..transports import JOSETransport from ..utils.shared import UUIDHelper @@ -21,7 +22,12 @@ def __init__(self, issuer, issuer_id, private_key, url, testing, :param issuer: Issuer type that will be translated directly to the JOSE transport layer as an issuer. IE: svc, dir, org :param issuer_id: UUID of the issuer - :param private_key: PEM formatted private key string + :param private_key: PEM formatted private key string. This key will be + used for signing requests. It will also be used for + decrypting requests when a dual purpose key is + given. If a separate encryption key is desired it + can be added via the add_encryption_private_key + method. :param url: URL for the LaunchKey API :param testing: Boolean stating whether testing mode is being used. This will determine whether SSL validation @@ -31,14 +37,38 @@ def __init__(self, issuer, issuer_id, private_key, url, testing, self._transport = transport if transport is not None \ else JOSETransport() self._transport.set_url(url, testing) + # Set the issue which will set the given key as the signature key self._transport.set_issuer(issuer, issuer_id, private_key) + # Add the given key as an encryption key as well + self._transport.add_encryption_private_key(private_key) def add_additional_private_key(self, private_key): """ Adds an additional private key. This is to allow for key rotation. The default key to be used is the one set in the instantiation of the factory. + + This method is being deprecated in favor of add_encryption_private_key. + + :param private_key: + :return: + """ + warnings.warn( + "This method will be removed in the future. Please use " + "add_encryption_key instead.", + DeprecationWarning) + self.add_encryption_private_key(private_key) + + def add_encryption_private_key(self, private_key): + """ + Adds an additional encryption private key. This is to allow for + separating signature and encryption keys. Multiple encryption keys are + supported to allow for key rotation. + + When defining a signature key it should be done in the instantiation of + the factory. + :param private_key: :return: """ - self._transport.add_issuer_key(private_key) + self._transport.add_encryption_private_key(private_key) diff --git a/launchkey/transports/jose_auth.py b/launchkey/transports/jose_auth.py index fc4411a..a4101ec 100644 --- a/launchkey/transports/jose_auth.py +++ b/launchkey/transports/jose_auth.py @@ -60,6 +60,7 @@ def __init__(self, jwt_algorithm="RS512", jwe_cek_encryption="RSA-OAEP", """ self.issuer = None self.issuer_id = None + self.signing_key = None self.loaded_issuer_private_keys = {} self.issuer_private_keys = [] self._server_time_difference = None, None @@ -277,10 +278,9 @@ def _find_key_by_kid(self, kid): return key - def add_issuer_key(self, private_key): + def add_encryption_private_key(self, private_key): """ Adds a private key to the list of keys available for decryption - and signatures :return: Boolean - Whether the key is already in the list """ new_key = RSAKey(key=import_rsa_key(private_key), @@ -293,6 +293,17 @@ def add_issuer_key(self, private_key): RSA.importKey(private_key)) return True + def add_issuer_key(self, private_key): + """ + Adds a private key to the list of keys available for decryption + :return: Boolean - Whether the key is already in the list + """ + warnings.warn( + "This method will be removed in the future. Please use " + "add_encryption_key instead.", + DeprecationWarning) + return self.add_encryption_private_key(private_key) + def set_url(self, url, testing): """ Creates a new http_client using the input url and testing flag @@ -308,7 +319,8 @@ def set_issuer(self, issuer, issuer_id, private_key): Set the issuer credentials :param issuer: Issuer entity type (svc, dir, or org) :param issuer_id: Identifier for the issuer entity - :param private_key: PEM formatted private key for issuer entity + :param private_key: PEM formatted private key for issuer entity which + will be used for signing requests. :return: None :raises launchkey.exceptions.InvalidEntityID: when issuer_id is not valid @@ -326,7 +338,10 @@ def set_issuer(self, issuer, issuer_id, private_key): "The given id was invalid. Please ensure it is a UUID.") self.issuer = "%s:%s" % (issuer, issuer_id) try: - self.add_issuer_key(private_key) + self.signing_key = RSAKey( + key=import_rsa_key(private_key), + kid=self.__generate_key_id(private_key) + ) except ValueError: raise InvalidPrivateKey( "Invalid private key. Please ensure you are submitting " @@ -334,8 +349,11 @@ def set_issuer(self, issuer, issuer_id, private_key): def _get_jwt_signature(self, params): try: + if self.signing_key is None: + raise NoSuitableSigningKeys + jws = JWS(params, alg=self.jwt_algorithm) - return jws.sign_compact(keys=self.issuer_private_keys) + return jws.sign_compact(keys=[self.signing_key]) except NoSuitableSigningKeys: raise NoIssuerKey( "An issuer key wasn't loaded. Please run set_issuer() first.") diff --git a/tests/test_factories.py b/tests/test_factories.py index 6092392..6599e59 100644 --- a/tests/test_factories.py +++ b/tests/test_factories.py @@ -1,5 +1,5 @@ import unittest -from mock import MagicMock, ANY +from mock import MagicMock, ANY, patch from launchkey.factories.base import BaseFactory from launchkey.factories import DirectoryFactory, OrganizationFactory, ServiceFactory from launchkey.clients import DirectoryClient, OrganizationClient, ServiceClient @@ -12,10 +12,32 @@ class TestBaseFactory(unittest.TestCase): def setUp(self): - self._factory = BaseFactory(ANY, uuid1(), ANY, ANY, ANY, MagicMock(spec=JOSETransport)) - - def test_add_additional_private_key(self): - self._factory.add_additional_private_key(ANY) + self._transport = MagicMock(spec=JOSETransport) + self._factory = BaseFactory(ANY, uuid1(), ANY, ANY, ANY, self._transport) + + @patch("launchkey.factories.base.warnings") + def test_add_additional_private_key_makes_deprecation_warning(self, warnings_patch): + self._factory.add_additional_private_key("PRIVATE KEY") + warnings_patch.warn.assert_called_once_with( + "This method will be removed in the future. Please use " + "add_encryption_key instead.", + DeprecationWarning + ) + + @patch("launchkey.factories.base.warnings") + def test_add_additional_private_key_calls_transport(self, _): + encryption_key = "PRIVATE KEY" + self._factory.add_additional_private_key(encryption_key) + self._transport.add_encryption_private_key.assert_called_with( + encryption_key + ) + + def test_add_encryption_private_key_calls_transport(self): + encryption_key = "PRIVATE KEY" + self._factory.add_encryption_private_key(encryption_key) + self._transport.add_encryption_private_key.assert_called_with( + encryption_key + ) @data(uuid1(), uuid4()) def test_multiple_uuid_support(self, entity_id): diff --git a/tests/test_jose_auth_transport.py b/tests/test_jose_auth_transport.py index 4a4d779..c934fb6 100644 --- a/tests/test_jose_auth_transport.py +++ b/tests/test_jose_auth_transport.py @@ -93,7 +93,8 @@ def test_public_key_parse(self): self.assertEqual(len(keys), 1) self.assertIsInstance(keys[0], RSAKey) - def test_build_jwt_signature_no_key(self): + @patch.object(JWS, 'sign_compact') + def test_build_jwt_signature_no_key(self, _): with self.assertRaises(NoIssuerKey): self._transport._build_jwt_signature(MagicMock(spec=str), ANY, ANY, ANY, ANY) @@ -164,7 +165,7 @@ def setUp(self): self.addCleanup(patch.stopall) def _encrypt_decrypt(self): - self._transport.add_issuer_key(valid_private_key) + self._transport.add_encryption_private_key(valid_private_key) to_encrypt = {"tobe": "encrypted"} encrypted = self._transport._encrypt_request(to_encrypt) self.assertEqual(len(encrypted.split('.')), 5) @@ -642,21 +643,36 @@ class TestJOSETransportIssuers(unittest.TestCase): def setUp(self): self._transport = JOSETransport() - def test_add_issuer_key(self): + @patch("launchkey.transports.jose_auth.warnings") + def test_add_issuer_key_creates_warning(self, warnings_patch): + self._transport.add_issuer_key(valid_private_key) + warnings_patch.warn.assert_called_once_with( + "This method will be removed in the future. Please use " + "add_encryption_key instead.", + DeprecationWarning + ) + + @patch("launchkey.transports.jose_auth.warnings") + def test_add_issuer_key_creates_warning(self, _): self.assertEqual(len(self._transport.issuer_private_keys), 0) self._transport.add_issuer_key(valid_private_key) self.assertEqual(len(self._transport.issuer_private_keys), 1) + def test_add_issuer_key(self): + self.assertEqual(len(self._transport.issuer_private_keys), 0) + self._transport.add_encryption_private_key(valid_private_key) + self.assertEqual(len(self._transport.issuer_private_keys), 1) + def test_add_duplicate_issuer_key(self): self.assertEqual(len(self._transport.issuer_private_keys), 0) - self._transport.add_issuer_key(valid_private_key) + self._transport.add_encryption_private_key(valid_private_key) self.assertEqual(len(self._transport.issuer_private_keys), 1) - resp = self._transport.add_issuer_key(valid_private_key) + resp = self._transport.add_encryption_private_key(valid_private_key) self.assertFalse(resp) self.assertEqual(len(self._transport.issuer_private_keys), 1) def test_generate_key_id(self): - self._transport.add_issuer_key(valid_private_key) + self._transport.add_encryption_private_key(valid_private_key) self.assertEqual(self._transport.issuer_private_keys[0].kid, '59:12:e2:f6:3f:79:d5:1e:18:75:c5:25:ff:b3:b7:f2') @@ -677,12 +693,15 @@ def test_set_issuer_invalid_private_key(self): with self.assertRaises(InvalidPrivateKey): self._transport.set_issuer(ANY, uuid4(), "InvalidKey") + @patch("launchkey.transports.jose_auth.md5") + @patch("launchkey.transports.jose_auth.RSA") @patch("launchkey.transports.jose_auth.RSAKey") @patch("launchkey.transports.jose_auth.import_rsa_key") - def test_issuer_list(self, rsa_key_patch, import_key_patch): + def test_issuer_list(self, rsa_key_patch, import_key_patch, _, md5_patch): rsa_key_patch.return_value = MagicMock(spec=RSAKey) import_key_patch.return_value = MagicMock() - self._transport.add_issuer_key = MagicMock() + md5_patch.return_value.hexdigest.return_value = "9cdfb439c7876e703e307864c9167a15" + self._transport.add_encryption_private_key = MagicMock() for issuer in VALID_JWT_ISSUER_LIST: self._transport.set_issuer(issuer, uuid4(), ANY) From b7a7b73700831316fcac21147386e673eed5cfe6 Mon Sep 17 00:00:00 2001 From: Branden Jordan Date: Tue, 19 Jan 2021 17:28:51 -0800 Subject: [PATCH 11/17] Updated sample app manager to use the latest version of the sample application developed by the LK Android Mobile team --- features/steps/directory_device_steps.py | 8 +++-- .../steps/directory_service_policy_steps.py | 2 +- .../steps/managers/appium_device_manager.py | 3 ++ .../managers/sample_app_device_manager.py | 29 ++++++++++++++----- .../organization_service_policy_steps.py | 2 +- 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/features/steps/directory_device_steps.py b/features/steps/directory_device_steps.py index 5cd583c..75964b8 100644 --- a/features/steps/directory_device_steps.py +++ b/features/steps/directory_device_steps.py @@ -156,15 +156,19 @@ def link_device(context): @when("I link my device") def link_physical_device(context): sdk_key = context.entity_manager.get_current_directory_sdk_keys()[0] + context.sample_app_device_manager.set_sdk_key(sdk_key) linking_code = context.entity_manager.get_current_linking_response().code - context.sample_app_device_manager.link_device(sdk_key, linking_code) + context.sample_app_device_manager.link_device(linking_code) + + # We should now be on the home page if everything succeeded + context.appium_device_manager.get_scrollable_element_by_text("Auth Methods") @when("I link my physical device with the name \"{device_name}\"") def link_device_with_name(context, device_name): sdk_key = context.entity_manager.get_current_directory_sdk_keys()[0] linking_code = context.entity_manager.get_current_linking_response().code - context.sample_app_device_manager.link_device(sdk_key, linking_code, + context.sample_app_device_manager.link_device(linking_code, device_name=device_name) diff --git a/features/steps/directory_service_policy_steps.py b/features/steps/directory_service_policy_steps.py index 4e6a4b9..2caa06b 100644 --- a/features/steps/directory_service_policy_steps.py +++ b/features/steps/directory_service_policy_steps.py @@ -71,7 +71,7 @@ def verify_current_directory_service_policy_has_factor_count_requirement( context): current_policy = context.entity_manager.\ get_current_directory_service_policy() - if current_policy.minimum_amount is not 0: + if current_policy.minimum_amount != 0: raise Exception( "Expected minimum requirement amount to be 0 but it was %s" % current_policy.minimum_amount diff --git a/features/steps/managers/appium_device_manager.py b/features/steps/managers/appium_device_manager.py index a391dc8..efaf4d6 100644 --- a/features/steps/managers/appium_device_manager.py +++ b/features/steps/managers/appium_device_manager.py @@ -120,6 +120,9 @@ def long_press(self, id=None, text=None, duration=1000): def send_keys(self, keys, id=None, text=None): self.get_element(id=id, text=text).send_keys(keys) + def back(self): + self.driver.back() + def quit(self): """ Quits the driver and closes every associated window. diff --git a/features/steps/managers/sample_app_device_manager.py b/features/steps/managers/sample_app_device_manager.py index 3917fc5..a5aa234 100644 --- a/features/steps/managers/sample_app_device_manager.py +++ b/features/steps/managers/sample_app_device_manager.py @@ -10,29 +10,42 @@ def unlink_device(self): self.appium_device_manager.click(text="Unlink 2 (Custom UI)") def _open_linking_menu(self): - self.appium_device_manager.click(text="Link (Custom UI - Manual)") + self.appium_device_manager.click(text="Link (Default UI - Manual)") def _fill_linking_code(self, linking_code): - self.appium_device_manager.send_keys(linking_code, text="Linking code") + self.appium_device_manager.send_keys(linking_code, text="ABCD123") def _fill_authenticator_sdk_key(self, sdk_key): - self.appium_device_manager.send_keys(sdk_key, text="Auth SDK Key") + sdk_key_element = self.appium_device_manager.get_element_by_id(resource_id="configs_sdk_key") + sdk_key_element.click() + sdk_key_element.clear() + self.appium_device_manager.send_keys(sdk_key, id="configs_sdk_key") + self.appium_device_manager.back() + # Click the Re-Initialize Button to save the sdk_key + self.appium_device_manager.click(text="RE-INITIALIZE") + def _type_in_device_name(self, device_name): self.appium_device_manager.click(text="Use custom device name") self.appium_device_manager.send_keys(device_name, id="demo_link_edit_name") def _submit_linking_form(self): - self.appium_device_manager.click(id="demo_link_button") + self.appium_device_manager.click(id="pair_entercode_button_done") - def link_device(self, sdk_key, linking_code, device_name=None): - self._approve_alert() + def link_device(self, linking_code, device_name=None): self._open_linking_menu() self._fill_linking_code(linking_code) - self._fill_authenticator_sdk_key(sdk_key) + self._submit_linking_form() if device_name: self._type_in_device_name(device_name) - self._submit_linking_form() + self.appium_device_manager.click(text="OK") + + def set_sdk_key(self, sdk_key): + self._open_options() + self._fill_authenticator_sdk_key(sdk_key) + + def _open_options(self): + self.appium_device_manager.click(text="Config Testing") def _open_auth_menu(self): self.appium_device_manager.click(text="Check for Requests (XML)") diff --git a/features/steps/organization_service_policy_steps.py b/features/steps/organization_service_policy_steps.py index 8b50d17..fd6650b 100644 --- a/features/steps/organization_service_policy_steps.py +++ b/features/steps/organization_service_policy_steps.py @@ -69,7 +69,7 @@ def verify_current_organization_service_policy_has_factor_count_requirement( context): current_policy = context.entity_manager.\ get_current_organization_service_policy() - if current_policy.minimum_amount is not 0: + if current_policy.minimum_amount != 0: raise Exception( "Expected minimum requirement amount to be 0 but it was %s" % current_policy.minimum_amount From ef693442dd92d4ffbc97571b465d66be986072d9 Mon Sep 17 00:00:00 2001 From: Branden Jordan Date: Tue, 19 Jan 2021 17:37:07 -0800 Subject: [PATCH 12/17] Upgraded tox to next minor to upgrade Py from a vulnerable version --- Pipfile | 2 +- Pipfile.lock | 204 ++++++++++++++++++++------------------------------- 2 files changed, 80 insertions(+), 126 deletions(-) diff --git a/Pipfile b/Pipfile index a3f3f16..6d24789 100644 --- a/Pipfile +++ b/Pipfile @@ -18,7 +18,7 @@ flake8 = "~=3.8.3" pylint = "~=2.5.3" coverage = "~=4.5.2" # Tools for multi-environment testing before sending to CI -tox = "~=3.7.0" +tox = "~=3.8.0" behave = "~=1.2.6" appium-python-client = "*" pyhamcrest = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 6ab147c..8a09de5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5cd25530685add51917d930a5e47d159a4e08fa835af934efd3168e3abc48bd0" + "sha256": "b1d09097934c85c1af456a950c85df3a2fc3bd2a6bd445332beb85901b1d5edb" }, "pipfile-spec": 6, "requires": { @@ -18,17 +18,17 @@ "default": { "certifi": { "hashes": [ - "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", - "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" ], - "version": "==2020.6.20" + "version": "==2020.12.5" }, "chardet": { "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" ], - "version": "==3.0.4" + "version": "==4.0.0" }, "formencode": { "hashes": [ @@ -55,38 +55,43 @@ }, "pycryptodomex": { "hashes": [ - "sha256:06f5a458624c9b0e04c0086c7f84bcc578567dab0ddc816e0476b3057b18339f", - "sha256:1714675fb4ac29a26ced38ca22eb8ffd923ac851b7a6140563863194d7158422", - "sha256:17272d06e4b2f6455ee2cbe93e8eb50d9450a5dc6223d06862ee1ea5d1235861", - "sha256:2199708ebeed4b82eb45b10e1754292677f5a0df7d627ee91ea01290b9bab7e6", - "sha256:2275a663c9e744ee4eace816ef2d446b3060554c5773a92fbc79b05bf47debda", - "sha256:2710fc8d83b3352b370db932b3710033b9d630b970ff5aaa3e7458b5336e3b32", - "sha256:35b9c9177a9fe7288b19dd41554c9c8ca1063deb426dd5a02e7e2a7416b6bd11", - "sha256:3caa32cf807422adf33c10c88c22e9e2e08b9d9d042f12e1e25fe23113dd618f", - "sha256:48cc2cfc251f04a6142badeb666d1ff49ca6fdfc303fd72579f62b768aaa52b9", - "sha256:4ae6379350a09339109e9b6f419bb2c3f03d3e441f4b0f5b8ca699d47cc9ff7e", - "sha256:4e0b27697fa1621c6d3d3b4edeec723c2e841285de6a8d378c1962da77b349be", - "sha256:58e19560814dabf5d788b95a13f6b98279cf41a49b1e49ee6cf6c79a57adb4c9", - "sha256:8044eae59301dd392fbb4a7c5d64e1aea8ef0be2540549807ecbe703d6233d68", - "sha256:89be1bf55e50116fe7e493a7c0c483099770dd7f81b87ac8d04a43b1a203e259", - "sha256:8fcdda24dddf47f716400d54fc7f75cadaaba1dd47cc127e59d752c9c0fc3c48", - "sha256:914fbb18e29c54585e6aa39d300385f90d0fa3b3cc02ed829b08f95c1acf60c2", - "sha256:93a75d1acd54efed314b82c952b39eac96ce98d241ad7431547442e5c56138aa", - "sha256:9fd758e5e2fe02d57860b85da34a1a1e7037155c4eadc2326fc7af02f9cae214", - "sha256:a2bc4e1a2e6ca3a18b2e0be6131a23af76fecb37990c159df6edc7da6df913e3", - "sha256:a2ee8ba99d33e1a434fcd27d7d0aa7964163efeee0730fe2efc9d60edae1fc71", - "sha256:b2d756620078570d3f940c84bc94dd30aa362b795cce8b2723300a8800b87f1c", - "sha256:c0d085c8187a1e4d3402f626c9e438b5861151ab132d8761d9c5ce6491a87761", - "sha256:c990f2c58f7c67688e9e86e6557ed05952669ff6f1343e77b459007d85f7df00", - "sha256:ccbbec59bf4b74226170c54476da5780c9176bae084878fc94d9a2c841218e34", - "sha256:dc2bed32c7b138f1331794e454a953360c8cedf3ee62ae31f063822da6007489", - "sha256:e070a1f91202ed34c396be5ea842b886f6fa2b90d2db437dc9fb35a26c80c060", - "sha256:e42860fbe1292668b682f6dabd225fbe2a7a4fa1632f0c39881c019e93dea594", - "sha256:e4e1c486bf226822c8dceac81d0ec59c0a2399dbd1b9e04f03c3efa3605db677", - "sha256:ea4d4b58f9bc34e224ef4b4604a6be03d72ef1f8c486391f970205f6733dbc46", - "sha256:f60b3484ce4be04f5da3777c51c5140d3fe21cdd6674f2b6568f41c8130bcdeb" - ], - "version": "==3.9.8" + "sha256:15c03ffdac17731b126880622823d30d0a3cc7203cd219e6b9814140a44e7fab", + "sha256:20fb7f4efc494016eab1bc2f555bc0a12dd5ca61f35c95df8061818ffb2c20a3", + "sha256:28ee3bcb4d609aea3040cad995a8e2c9c6dc57c12183dadd69e53880c35333b9", + "sha256:305e3c46f20d019cd57543c255e7ba49e432e275d7c0de8913b6dbe57a851bc8", + "sha256:3547b87b16aad6afb28c9b3a9cd870e11b5e7b5ac649b74265258d96d8de1130", + "sha256:3642252d7bfc4403a42050e18ba748bedebd5a998a8cba89665a4f42aea4c380", + "sha256:404faa3e518f8bea516aae2aac47d4d960397199a15b4bd6f66cad97825469a0", + "sha256:42669638e4f7937b7141044a2fbd1019caca62bd2cdd8b535f731426ab07bde1", + "sha256:4632d55a140b28e20be3cd7a3057af52fb747298ff0fd3290d4e9f245b5004ba", + "sha256:4a88c9383d273bdce3afc216020282c9c5c39ec0bd9462b1a206af6afa377cf0", + "sha256:4ce1fc1e6d2fd2d6dc197607153327989a128c093e0e94dca63408f506622c3e", + "sha256:55cf4e99b3ba0122dee570dc7661b97bf35c16aab3e2ccb5070709d282a1c7ab", + "sha256:5e486cab2dfcfaec934dd4f5d5837f4a9428b690f4d92a3b020fd31d1497ca64", + "sha256:65ec88c8271448d2ea109d35c1f297b09b872c57214ab7e832e413090d3469a9", + "sha256:6c95a3361ce70068cf69526a58751f73ddac5ba27a3c2379b057efa2f5338c8c", + "sha256:73240335f4a1baf12880ebac6df66ab4d3a9212db9f3efe809c36a27280d16f8", + "sha256:7651211e15109ac0058a49159265d9f6e6423c8a81c65434d3c56d708417a05b", + "sha256:7b5b7c5896f8172ea0beb283f7f9428e0ab88ec248ce0a5b8c98d73e26267d51", + "sha256:836fe39282e75311ce4c38468be148f7fac0df3d461c5de58c5ff1ddb8966bac", + "sha256:871852044f55295449fbf225538c2c4118525093c32f0a6c43c91bed0452d7e3", + "sha256:892e93f3e7e10c751d6c17fa0dc422f7984cfd5eb6690011f9264dc73e2775fc", + "sha256:934e460c5058346c6f1d62fdf3db5680fbdfbfd212722d24d8277bf47cd9ebdc", + "sha256:9736f3f3e1761024200637a080a4f922f5298ad5d780e10dbb5634fe8c65b34c", + "sha256:a1d38a96da57e6103423a446079ead600b450cf0f8ebf56a231895abf77e7ffc", + "sha256:a385fceaa0cdb97f0098f1c1e9ec0b46cc09186ddf60ec23538e871b1dddb6dc", + "sha256:a7cf1c14e47027d9fb9d26aa62e5d603994227bd635e58a8df4b1d2d1b6a8ed7", + "sha256:a9aac1a30b00b5038d3d8e48248f3b58ea15c827b67325c0d18a447552e30fc8", + "sha256:b696876ee583d15310be57311e90e153a84b7913ac93e6b99675c0c9867926d0", + "sha256:bef9e9d39393dc7baec39ba4bac6c73826a4db02114cdeade2552a9d6afa16e2", + "sha256:c885fe4d5f26ce8ca20c97d02e88f5fdd92c01e1cc771ad0951b21e1641faf6d", + "sha256:d2d1388595cb5d27d9220d5cbaff4f37c6ec696a25882eb06d224d241e6e93fb", + "sha256:d2e853e0f9535e693fade97768cf7293f3febabecc5feb1e9b2ffdfe1044ab96", + "sha256:d62fbab185a6b01c5469eda9f0795f3d1a5bba24f5a5813f362e4b73a3c4dc70", + "sha256:f20a62397e09704049ce9007bea4f6bad965ba9336a760c6f4ef1b4192e12d6d", + "sha256:f81f7311250d9480e36dec819127897ae772e7e8de07abfabe931b8566770b8e" + ], + "version": "==3.9.9" }, "pyjwkest": { "hashes": [ @@ -103,17 +108,17 @@ }, "pytz": { "hashes": [ - "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", - "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" + "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4", + "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5" ], - "version": "==2020.1" + "version": "==2020.5" }, "requests": { "hashes": [ - "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", - "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" ], - "version": "==2.24.0" + "version": "==2.25.1" }, "six": { "hashes": [ @@ -124,11 +129,11 @@ }, "urllib3": { "hashes": [ - "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", - "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" + "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", + "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" ], "index": "pypi", - "version": "==1.25.10" + "version": "==1.26.2" } }, "develop": { @@ -223,27 +228,11 @@ }, "flake8": { "hashes": [ - "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", - "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" + "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839", + "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b" ], "index": "pypi", - "version": "==3.8.3" - }, - "importlib-metadata": { - "hashes": [ - "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83", - "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" - ], - "markers": "python_version < '3.8'", - "version": "==1.7.0" - }, - "importlib-resources": { - "hashes": [ - "sha256:19f745a6eca188b490b1428c8d1d4a0d2368759f32370ea8fb89cad2ab1106c3", - "sha256:d028f66b66c0d5732dae86ba4276999855e162a749c92620a38c1d779ed138a7" - ], - "markers": "python_version < '3.7'", - "version": "==3.0.0" + "version": "==3.8.4" }, "isort": { "hashes": [ @@ -311,9 +300,9 @@ }, "parse": { "hashes": [ - "sha256:cd89e57aed38dcf3e0ff8253f53121a3b23e6181758993323658bffc048a5c19" + "sha256:9ff82852bcb65d139813e2a5197627a94966245c897796760a3a2a8eb66f020b" ], - "version": "==1.16.0" + "version": "==1.19.0" }, "parse-type": { "hashes": [ @@ -324,10 +313,10 @@ }, "pbr": { "hashes": [ - "sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c", - "sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8" + "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9", + "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00" ], - "version": "==5.4.5" + "version": "==5.5.1" }, "pluggy": { "hashes": [ @@ -338,10 +327,10 @@ }, "py": { "hashes": [ - "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", - "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" + "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", + "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" ], - "version": "==1.9.0" + "version": "==1.10.0" }, "pycodestyle": { "hashes": [ @@ -375,11 +364,11 @@ }, "pyotp": { "hashes": [ - "sha256:01eceab573181188fe038d001e42777884a7f5367203080ef5bda0e30fe82d28", - "sha256:e96c2d725d0b613b793775bb184e90297fda5f4989e210e28acf08a09ac2cc83" + "sha256:038a3f70b34eaad3f72459e8b411662ef8dfcdd95f7d9203fa489e987a75584b", + "sha256:ef07c393660529261e66902e788b32e46260d2c29eb740978df778260a1c2b4c" ], "index": "pypi", - "version": "==2.4.0" + "version": "==2.4.1" }, "selenium": { "hashes": [ @@ -397,74 +386,39 @@ }, "toml": { "hashes": [ - "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "version": "==0.10.1" + "version": "==0.10.2" }, "tox": { "hashes": [ - "sha256:04f8f1aa05de8e76d7a266ccd14e0d665d429977cd42123bc38efa9b59964e9e", - "sha256:25ef928babe88c71e3ed3af0c464d1160b01fca2dd1870a5bb26c2dea61a17fc" + "sha256:69620e19de33a6b7ee8aeda5478791b3618ff58f0b869dbd0319fb71aa903deb", + "sha256:e5cdb1653aa27b3e46b5c390de6b6d51d31afcfdbd9d1222d82d76b82ad03d9b" ], "index": "pypi", - "version": "==3.7.0" - }, - "typed-ast": { - "hashes": [ - "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", - "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", - "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", - "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", - "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", - "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", - "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", - "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", - "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", - "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", - "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", - "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", - "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", - "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", - "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", - "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", - "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", - "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", - "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", - "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", - "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" - ], - "markers": "implementation_name == 'cpython' and python_version < '3.8'", - "version": "==1.4.1" + "version": "==3.8.6" }, "urllib3": { "hashes": [ - "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", - "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" + "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", + "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" ], "index": "pypi", - "version": "==1.25.10" + "version": "==1.26.2" }, "virtualenv": { "hashes": [ - "sha256:7b54fd606a1b85f83de49ad8d80dbec08e983a2d2f96685045b262ebc7481ee5", - "sha256:8cd7b2a4850b003a11be2fc213e206419efab41115cc14bca20e69654f2ac08e" + "sha256:219ee956e38b08e32d5639289aaa5bd190cfbe7dafcb8fa65407fca08e808f9c", + "sha256:227a8fed626f2f20a6cdb0870054989f82dd27b2560a911935ba905a2a5e0034" ], - "version": "==20.0.30" + "version": "==20.4.0" }, "wrapt": { "hashes": [ "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" ], "version": "==1.12.1" - }, - "zipp": { - "hashes": [ - "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", - "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" - ], - "markers": "python_version < '3.8'", - "version": "==3.1.0" } } } From 77dd34af2fe736a9cc05d0ac6df3f5dc4e763dd1 Mon Sep 17 00:00:00 2001 From: Branden Jordan Date: Wed, 20 Jan 2021 11:28:07 -0800 Subject: [PATCH 13/17] made sure python specific version dependencies for CI related tasks were properly included in the lockfile --- Pipfile.lock | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/Pipfile.lock b/Pipfile.lock index 8a09de5..f000966 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -234,6 +234,22 @@ "index": "pypi", "version": "==3.8.4" }, + "importlib-metadata": { + "hashes": [ + "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771", + "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d" + ], + "markers": "python_version < '3.8'", + "version": "==3.4.0" + }, + "importlib-resources": { + "hashes": [ + "sha256:885b8eae589179f661c909d699a546cf10d83692553e34dca1bf5eb06f7f6217", + "sha256:bfdad047bce441405a49cf8eb48ddce5e56c696e185f59147a8b79e75e9e6380" + ], + "markers": "python_version < '3.7'", + "version": "==5.1.0" + }, "isort": { "hashes": [ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", @@ -399,6 +415,51 @@ "index": "pypi", "version": "==3.8.6" }, + "typed-ast": { + "hashes": [ + "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1", + "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d", + "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6", + "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd", + "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37", + "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151", + "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07", + "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440", + "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70", + "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496", + "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea", + "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400", + "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc", + "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606", + "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc", + "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581", + "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412", + "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a", + "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2", + "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787", + "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f", + "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937", + "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64", + "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487", + "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b", + "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41", + "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a", + "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3", + "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166", + "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10" + ], + "markers": "implementation_name == 'cpython' and python_version < '3.8'", + "version": "==1.4.2" + }, + "typing-extensions": { + "hashes": [ + "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", + "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", + "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" + ], + "markers": "python_version < '3.8'", + "version": "==3.7.4.3" + }, "urllib3": { "hashes": [ "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", @@ -419,6 +480,14 @@ "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" ], "version": "==1.12.1" + }, + "zipp": { + "hashes": [ + "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108", + "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb" + ], + "markers": "python_version < '3.8'", + "version": "==3.4.0" } } } From 3010bde43d1c995f0ead2dd0663fc1b3bc500846 Mon Sep 17 00:00:00 2001 From: Branden Jordan Date: Thu, 21 Jan 2021 10:27:29 -0800 Subject: [PATCH 14/17] Make Travis CI ignore the pipfile and use the lockfile for running CI tests --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index dbe4b16..4347d0e 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ test: dependencies: pip install --upgrade pipenv - pipenv install --three --dev + pipenv install --three --dev --ignore-pipfile coverage: pipenv run coverage run --source="launchkey" setup.py nosetests From 4b3143b77c57444bcfffab3837b9ecfaf37dae98 Mon Sep 17 00:00:00 2001 From: Branden Jordan Date: Thu, 21 Jan 2021 18:01:20 -0800 Subject: [PATCH 15/17] set version to 3.9.0 from RC release --- launchkey/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launchkey/__init__.py b/launchkey/__init__.py index ca8042c..63ad729 100644 --- a/launchkey/__init__.py +++ b/launchkey/__init__.py @@ -1,5 +1,5 @@ """LaunchKey Service SDK module""" -SDK_VERSION = '3.9.0-rc.3' +SDK_VERSION = '3.9.0' LAUNCHKEY_PRODUCTION = "https://api.launchkey.com" VALID_JWT_ISSUER_LIST = ["svc", "dir", "org"] JOSE_SUPPORTED_JWE_ALGS = ['RSA-OAEP'] From d18c7eed2039b21fc79908ee44a619cc91e36086 Mon Sep 17 00:00:00 2001 From: Branden Jordan Date: Mon, 25 Jan 2021 12:04:06 -0800 Subject: [PATCH 16/17] Phase out travis to test GitHub actions (#119) * Added github actions to run Unit Tests and full CI * Removed EOL versions of Python from github actions, Travis-CI, and tox * completely remove Travis in favor of github actions --- .github/workflows/test_python_versions.yml | 48 ++++++++++++++++++++++ .travis.yml | 39 ------------------ Makefile | 6 --- README.rst | 2 +- tox.ini | 2 +- tox_integration.ini | 2 +- 6 files changed, 51 insertions(+), 48 deletions(-) create mode 100644 .github/workflows/test_python_versions.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/test_python_versions.yml b/.github/workflows/test_python_versions.yml new file mode 100644 index 0000000..1216ca1 --- /dev/null +++ b/.github/workflows/test_python_versions.yml @@ -0,0 +1,48 @@ +name: Test Python Versions + +on: [pull_request, push] + +jobs: + run_tests: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8, 3.9, pypy-3.6] + fail-fast: false + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Run Unit Tests + run: | + python -m pip install --upgrade pip wheel + python setup.py test + run_ci: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.6] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip wheel + python -m pip install --upgrade pipenv + pipenv install --three --dev --ignore-pipfile + - name: Run Coverage + run: | + pipenv run coverage run --source="launchkey" setup.py nosetests + pipenv run coverage report --fail-under=100 + - name: Run Linters + run: | + pipenv run flake8 launchkey + pipenv run pylint launchkey + - name: Dependency Check + run: | + pipenv check \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b17dbc1..0000000 --- a/.travis.yml +++ /dev/null @@ -1,39 +0,0 @@ -dist: xenial - -language: python -python: - - "2.7" - - "3.5" - - "3.5-dev" - - "3.6" - - "3.6-dev" - - "3.7" - - "3.7-dev" - - "3.8-dev" - - "nightly" - - "pypy3.5" - -env: - - MAKE_RUN=test - -matrix: - fast_finish: true - allow_failures: - - python: 3.5-dev - - python: 3.6-dev - - python: 3.7-dev - - python: 3.8-dev - - python: nightly - - include: - - name: CI - python: "3.6" - env: - - MAKE_RUN=ci PIPENV_IGNORE_VIRTUALENVS=1 - -notifications: - email: - recipients: - - secure: "bdFDE8d50zWO9XWc1gQ4OPDrbvjZXOum1AIchDqwYXr8C7iluJcS2q+z5mczdvHlW3XflcjALl+x81hekdBk+8L6AJuAj5MhTF+HvjFL2EkY7q0V+seLTltYhoqnnlf0BlYGwoILBbDX2p/GYHNUOqWvCtGlJGMpy0qM3R8t5Vw=" - -script: make ${MAKE_RUN} diff --git a/Makefile b/Makefile index 4347d0e..8f7fab7 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,3 @@ -ci-py27: test - -ci-pypy: test - -ci-py35: test - ci-py36: ci ci-py37: test diff --git a/README.rst b/README.rst index ed28ccf..61c6c74 100644 --- a/README.rst +++ b/README.rst @@ -310,7 +310,7 @@ Tests require a number of Python versions. The best way to manage these versions is with pyenv_. You will need to register all of the versions with pyenv. There are a couple ways to do that. An example of doing it globally is:: - pyenv global 2.7.15 3.4.9 3.5.6 3.6.6 3.7.0 3.8-dev pypy3.5-6.0.0 pypy2.7-6.0.0 + pyenv local 3.6.6 3.7.0 3.8-dev pypy3.5-6.0.0 Install dependencies via Pipenv diff --git a/tox.ini b/tox.ini index 4daf8b4..205dc84 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py35,py36,py37,py38,pypy3,pypy +envlist = py36,py37,py38,pypy3 [testenv] whitelist_externals=make diff --git a/tox_integration.ini b/tox_integration.ini index e8c567b..2b3cf09 100644 --- a/tox_integration.ini +++ b/tox_integration.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py35,py36,py37,py38,pypy3,pypy +envlist = py36,py37,py38,pypy3 [testenv] whitelist_externals=make From 19565f264f4610e62047818b1c1baa70beb23509 Mon Sep 17 00:00:00 2001 From: Cody Moncur Date: Tue, 26 Jan 2021 14:20:25 -0800 Subject: [PATCH 17/17] Update README to reflect changes in 3.9 --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d511810..e4e1b06 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,9 @@ CHANGELOG for LaunchKey Python SDK * Updated CLI to support separate encryption and signature keys * Altered JOSE transport to ensure only the designated signing key is used for signing requests * Added the `add_encryption_private_key` method to all factories and deprecated `add_additional_private_key` +* Added `KeyType` Enum +* Added additional `key_type` parameter to `add_service_public_key` +* Added additional `key_type` parameter to `add_directory_public_key` 3.8.1 -----