Skip to content

Commit 0d09a39

Browse files
committed
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
1 parent 5f8ff65 commit 0d09a39

File tree

4 files changed

+54
-19
lines changed

4 files changed

+54
-19
lines changed

CHANGES.rst

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
CHANGELOG for LaunchKey Python SDK
22
==================================
3+
3.8.1
4+
-----
5+
6+
* Fixed an issue where the SDK would improperly report which key was used when the encryption and signature keys differed
37

48
3.8.0
59
-----

launchkey/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""LaunchKey Service SDK module"""
2-
SDK_VERSION = '3.8.0'
2+
SDK_VERSION = '3.8.1-rc.1'
33
LAUNCHKEY_PRODUCTION = "https://api.launchkey.com"
44
VALID_JWT_ISSUER_LIST = ["svc", "dir", "org"]
55
JOSE_SUPPORTED_JWE_ALGS = ['RSA-OAEP']

launchkey/transports/jose_auth.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,19 @@ def __get_kid_from_api_response_headers(headers):
141141

142142
return kid
143143

144-
def _get_kid_from_api_response(self, response):
144+
@staticmethod
145+
def _get_kid_from_api_response(response):
145146
"""
146147
Gets `kid` property from a JWT token within a LaunchKey API
147148
response.
148149
:param response: Response object
149150
:return: string of the `kid`
150151
"""
151-
return self.__get_kid_from_api_response_headers(response.headers)
152+
try:
153+
return response.headers["X-IOV-KEY-ID"]
154+
except KeyError:
155+
raise UnexpectedAPIResponse("X-IOV-KEY-ID was missing or malformed"
156+
" in API response.")
152157

153158
@staticmethod
154159
def parse_api_time(api_time):

tests/test_jose_auth_transport.py

+42-16
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from ddt import data, ddt
55
from jwkest import JWKESTException
66
from jwkest.jws import JWS
7-
from jwkest.jwt import JWT
7+
from jwkest.jwt import JWT, BadSyntax
88
from mock import MagicMock, ANY, patch, call
99
from launchkey.transports import JOSETransport, RequestsTransport
1010
from launchkey.transports.base import APIResponse, APIErrorResponse
@@ -70,13 +70,15 @@
7070
"kid": faux_kid
7171
}
7272

73+
transport_request_headers = {"X-IOV-KEY-ID": faux_kid}
74+
7375

7476
class TestJOSETransport3rdParty(unittest.TestCase):
7577

7678
def setUp(self):
7779
self._transport = JOSETransport()
7880
self._transport.get = MagicMock(return_value=MagicMock(spec=APIResponse))
79-
public_key = APIResponse(valid_private_key, {}, 200)
81+
public_key = APIResponse(valid_public_key, transport_request_headers, 200)
8082
self._transport.get.return_value = public_key
8183
self._transport._server_time_difference = 0, time()
8284

@@ -152,7 +154,7 @@ class TestJWKESTSupportedAlgs(unittest.TestCase):
152154
def setUp(self):
153155
self._transport = JOSETransport()
154156
self._transport.get = MagicMock(return_value=MagicMock(spec=APIResponse))
155-
public_key = APIResponse(valid_private_key, {}, 200)
157+
public_key = APIResponse(valid_private_key, transport_request_headers, 200)
156158
self._transport.get.return_value = public_key
157159
self._transport._server_time_difference = 0, time()
158160

@@ -265,7 +267,7 @@ class TestJOSETransportJWTResponse(unittest.TestCase):
265267
def setUp(self):
266268
self._transport = JOSETransport()
267269
self._transport.get = MagicMock(return_value=MagicMock(spec=APIResponse))
268-
public_key = APIResponse(valid_private_key, {}, 200)
270+
public_key = APIResponse(valid_public_key, transport_request_headers, 200)
269271
self._transport.get.return_value = public_key
270272
self._transport._server_time_difference = 0, time()
271273
self.issuer = "svc"
@@ -421,7 +423,7 @@ class TestJOSETransportJWTRequest(unittest.TestCase):
421423
def setUp(self):
422424
self._transport = JOSETransport()
423425
self._transport.get = MagicMock(return_value=MagicMock(spec=APIResponse))
424-
public_key = APIResponse(valid_private_key, {}, 200)
426+
public_key = APIResponse(valid_private_key, transport_request_headers, 200)
425427
self._transport.get.return_value = public_key
426428
self._transport._server_time_difference = 0, time()
427429
self.issuer = "svc"
@@ -808,7 +810,7 @@ def setUp(self):
808810
}
809811

810812
self._requests_transport = MagicMock(spec=RequestsTransport)
811-
self._requests_transport.get.return_value = APIResponse(valid_private_key, {}, 200)
813+
self._requests_transport.get.return_value = APIResponse(valid_public_key, transport_request_headers, 200)
812814
self._transport = JOSETransport(http_client=self._requests_transport)
813815
self._import_rsa_key_patch = patch(
814816
"launchkey.transports.jose_auth.import_rsa_key",
@@ -850,8 +852,7 @@ def test_key_is_retrieved_by_id_when_key_changed(self):
850852
"kid": "jwt2keyid"
851853
}
852854

853-
self._jwt_patch.return_value.unpack.side_effect = [jwt1, jwt1, jwt2, jwt2]
854-
855+
self._jwt_patch.return_value.unpack.side_effect = [jwt1, jwt2]
855856
self._transport.verify_jwt_response(MagicMock(), self.jti, ANY, None)
856857
self._transport.verify_jwt_response(MagicMock(), self.jti, ANY, None)
857858
self._requests_transport.get.assert_has_calls([
@@ -865,15 +866,15 @@ def test_key_retrieved_is_used_to_verify_payload(self, rsa_key_patch):
865866
self._requests_transport.get.return_value.data = valid_public_key
866867
self._transport.verify_jwt_response(MagicMock(), self.jti, ANY, None)
867868

868-
# Verify that verify_compact is called one time with key created by our jwkest key patch
869-
self._jws_patch.return_value.verify_compact.assert_called_once_with(ANY, keys=[rsa_key_patch.return_value])
870-
871-
# Assert that the jwkest key patch is built using the import_rsa_key patch return value and the key id
872-
# from the header
873-
rsa_key_patch.assert_called_with(key=self._import_rsa_key_patch.return_value, kid=faux_kid)
869+
# Verify that verify_compact is called one time with key created
870+
# by our jwkest key patch
871+
self._jws_patch.return_value.verify_compact\
872+
.assert_called_once_with(ANY, keys=[rsa_key_patch.return_value])
874873

875-
# Verify that we used the correct key to retrieve the key id from the header
876-
self._requests_transport.get.return_value.headers.get.assert_called_with("X-IOV-JWT")
874+
# Assert that the jwkest key patch is built using the import_rsa_key
875+
# patch return value and the key id from the header
876+
rsa_key_patch.assert_called_with(
877+
key=self._import_rsa_key_patch.return_value, kid=faux_kid)
877878

878879
def test_raises_when_kid_header_is_missing(self):
879880
headers_without_kid = {"alg": "RS512", "typ": "JWT"}
@@ -883,6 +884,31 @@ def test_raises_when_kid_header_is_missing(self):
883884
with self.assertRaises(JWTValidationFailure):
884885
self._transport.verify_jwt_response(MagicMock(), self.jti, ANY, None)
885886

887+
def test_raises_when_jwt_unpack_returns_badsyntax(self):
888+
self._jwt_patch.return_value.unpack.side_effect = BadSyntax("test", "error")
889+
with self.assertRaises(UnexpectedAPIResponse):
890+
self._transport.verify_jwt_response(
891+
MagicMock(), self.jti, ANY, None)
892+
893+
def test_raises_when_jwt_unpack_returns_valueerror(self):
894+
self._jwt_patch.return_value.unpack.side_effect = ValueError()
895+
with self.assertRaises(UnexpectedAPIResponse):
896+
self._transport.verify_jwt_response(
897+
MagicMock(), self.jti, ANY, None)
898+
899+
def test_raises_when_jwt_unpack_returns_indexerror(self):
900+
self._jwt_patch.return_value.unpack.side_effect = IndexError()
901+
with self.assertRaises(UnexpectedAPIResponse):
902+
self._transport.verify_jwt_response(
903+
MagicMock(), self.jti, ANY, None)
904+
905+
def test_raises_when_kid_header_is_missing_from_http_headers(self):
906+
self._requests_transport.get.return_value = APIResponse(
907+
valid_public_key, {}, 200)
908+
with self.assertRaises(UnexpectedAPIResponse):
909+
self._transport.verify_jwt_response(
910+
MagicMock(), self.jti, ANY, None)
911+
886912
def test_raises_when_kid_header_is_malformed(self):
887913
headers_with_kid_of_wrong_type = {"alg": "RS512", "typ": "JWT", "kid": 1234}
888914
jwt = MagicMock()

0 commit comments

Comments
 (0)