1# Copyright 2021 Google LLC 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""Tests for tink.python.tink.jwt._jwt_hmac_key_manager.""" 15 16import base64 17import datetime 18 19from typing import cast, Any 20 21from absl.testing import absltest 22from absl.testing import parameterized 23 24from tink.proto import jwt_hmac_pb2 25from tink.proto import tink_pb2 26import tink 27from tink import jwt 28from tink.cc.pybind import tink_bindings 29from tink.jwt import _jwt_format 30from tink.jwt import _jwt_hmac_key_manager 31from tink.jwt import _jwt_mac 32 33 34DATETIME_1970 = datetime.datetime.fromtimestamp(12345, datetime.timezone.utc) 35DATETIME_2020 = datetime.datetime.fromtimestamp(1582230020, 36 datetime.timezone.utc) 37 38 39def setUpModule(): 40 _jwt_hmac_key_manager.register() 41 42 43def _fixed_key_data() -> tink_pb2.KeyData: 44 # test example in https://tools.ietf.org/html/rfc7515#appendix-A.1.1 45 key_encoded = (b'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_' 46 b'T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow') 47 padded_key_encoded = key_encoded + b'=' * (-len(key_encoded) % 4) 48 key_value = base64.urlsafe_b64decode(padded_key_encoded) 49 jwt_hmac_key = jwt_hmac_pb2.JwtHmacKey( 50 version=0, algorithm=jwt_hmac_pb2.HS256, key_value=key_value) 51 return tink_pb2.KeyData( 52 type_url='type.googleapis.com/google.crypto.tink.JwtHmacKey', 53 key_material_type=tink_pb2.KeyData.SYMMETRIC, 54 value=jwt_hmac_key.SerializeToString()) 55 56 57def _cc_mac() -> Any: 58 key_data = _fixed_key_data() 59 cc_key_manager = tink_bindings.MacKeyManager.from_cc_registry( 60 'type.googleapis.com/google.crypto.tink.JwtHmacKey') 61 return cc_key_manager.primitive(key_data.SerializeToString()) 62 63 64def create_fixed_jwt_hmac() -> _jwt_mac.JwtMacInternal: 65 key_data = _fixed_key_data() 66 key_manager = _jwt_hmac_key_manager.MacCcToPyJwtMacKeyManager() 67 return key_manager.primitive(key_data) 68 69 70def gen_token(json_header: str, json_payload: str) -> str: 71 cc_mac = _cc_mac() 72 unsigned_token = ( 73 _jwt_format.encode_header(json_header) + b'.' + 74 _jwt_format.encode_payload(json_payload)) 75 return _jwt_format.create_signed_compact(unsigned_token, 76 cc_mac.compute_mac(unsigned_token)) 77 78 79class JwtHmacKeyManagerTest(parameterized.TestCase): 80 81 def test_basic(self): 82 key_manager = _jwt_hmac_key_manager.MacCcToPyJwtMacKeyManager() 83 self.assertEqual(key_manager.primitive_class(), _jwt_mac.JwtMacInternal) 84 self.assertEqual(key_manager.key_type(), 85 'type.googleapis.com/google.crypto.tink.JwtHmacKey') 86 87 @parameterized.named_parameters([ 88 ('JWT_HS256', jwt.raw_jwt_hs256_template()), 89 ('JWT_HS384', jwt.raw_jwt_hs384_template()), 90 ('JWT_HS512', jwt.raw_jwt_hs512_template()), 91 ]) 92 def test_new_keydata_primitive_success(self, template): 93 key_manager = _jwt_hmac_key_manager.MacCcToPyJwtMacKeyManager() 94 key_data = key_manager.new_key_data(template) 95 jwt_hmac = key_manager.primitive(key_data) 96 97 raw_jwt = jwt.new_raw_jwt( 98 type_header='typeHeader', issuer='issuer', without_expiration=True) 99 validator = jwt.new_validator( 100 expected_type_header='typeHeader', 101 expected_issuer='issuer', 102 allow_missing_expiration=True, 103 fixed_now=DATETIME_1970) 104 105 token_with_kid = jwt_hmac.compute_mac_and_encode_with_kid( 106 raw_jwt, kid='kid-123') 107 token_without_kid = jwt_hmac.compute_mac_and_encode_with_kid( 108 raw_jwt, kid=None) 109 110 # Verification of a token with a kid only fails if the wrong kid is passed. 111 verified_jwt = jwt_hmac.verify_mac_and_decode_with_kid( 112 token_with_kid, validator, kid='kid-123') 113 self.assertEqual(verified_jwt.type_header(), 'typeHeader') 114 self.assertEqual(verified_jwt.issuer(), 'issuer') 115 jwt_hmac.verify_mac_and_decode_with_kid(token_with_kid, validator, kid=None) 116 with self.assertRaises(tink.TinkError): 117 jwt_hmac.verify_mac_and_decode_with_kid( 118 token_with_kid, validator, kid='other-kid') 119 120 # A token without kid is only valid if no kid is passed. 121 jwt_hmac.verify_mac_and_decode_with_kid( 122 token_without_kid, validator, kid=None) 123 with self.assertRaises(tink.TinkError): 124 jwt_hmac.verify_mac_and_decode_with_kid( 125 token_without_kid, validator, kid='kid-123') 126 127 def test_fixed_signed_compact(self): 128 signed_compact = ( 129 'eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleH' 130 'AiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.' 131 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk') 132 jwt_hmac = create_fixed_jwt_hmac() 133 verified_jwt = jwt_hmac.verify_mac_and_decode_with_kid( 134 signed_compact, 135 jwt.new_validator( 136 expected_type_header='JWT', 137 expected_issuer='joe', 138 fixed_now=DATETIME_1970), 139 kid=None) 140 self.assertEqual(verified_jwt.issuer(), 'joe') 141 self.assertEqual(verified_jwt.expiration().year, 2011) 142 self.assertCountEqual(verified_jwt.custom_claim_names(), 143 ['http://example.com/is_root']) 144 self.assertTrue(verified_jwt.custom_claim('http://example.com/is_root')) 145 self.assertTrue(verified_jwt.type_header(), 'JWT') 146 147 # fails because it is expired 148 with self.assertRaises(tink.TinkError): 149 jwt_hmac.verify_mac_and_decode_with_kid( 150 signed_compact, jwt.new_validator(fixed_now=DATETIME_2020), kid=None) 151 152 # fails with wrong issuer 153 with self.assertRaises(tink.TinkError): 154 jwt_hmac.verify_mac_and_decode_with_kid( 155 signed_compact, 156 jwt.new_validator(expected_issuer='jane', fixed_now=DATETIME_1970), 157 kid=None) 158 159 def test_weird_tokens_with_valid_macs(self): 160 jwt_hmac = create_fixed_jwt_hmac() 161 validator = jwt.new_validator( 162 expected_issuer='joe', allow_missing_expiration=True) 163 cc_mac = _cc_mac() 164 165 # Normal token. 166 valid_token = gen_token('{"alg":"HS256"}', '{"iss":"joe"}') 167 verified = jwt_hmac.verify_mac_and_decode_with_kid( 168 valid_token, validator, kid=None) 169 self.assertEqual(verified.issuer(), 'joe') 170 171 # Token with unknown header is valid. 172 token_with_unknown_header = gen_token( 173 '{"unknown_header":"123","alg":"HS256"}', '{"iss":"joe"}') 174 verified2 = jwt_hmac.verify_mac_and_decode_with_kid( 175 token_with_unknown_header, validator, kid=None) 176 self.assertEqual(verified2.issuer(), 'joe') 177 178 # Token with unknown kid is valid, since primitives with output prefix type 179 # RAW ignore kid headers. 180 token_with_unknown_kid = gen_token('{"kid":"unknown","alg":"HS256"}', 181 '{"iss":"joe"}') 182 verified2 = jwt_hmac.verify_mac_and_decode_with_kid( 183 token_with_unknown_kid, validator, kid=None) 184 self.assertEqual(verified2.issuer(), 'joe') 185 186 # Token with invalid alg header 187 alg_invalid = gen_token('{"alg":"HS384"}', '{"iss":"joe"}') 188 with self.assertRaises(tink.TinkError): 189 jwt_hmac.verify_mac_and_decode_with_kid(alg_invalid, validator, kid=None) 190 191 # Token with empty header 192 empty_header = gen_token('{}', '{"iss":"joe"}') 193 with self.assertRaises(tink.TinkError): 194 jwt_hmac.verify_mac_and_decode_with_kid(empty_header, validator, kid=None) 195 196 # Token header is not valid JSON 197 header_invalid = gen_token('{"alg":"HS256"', '{"iss":"joe"}') 198 with self.assertRaises(tink.TinkError): 199 jwt_hmac.verify_mac_and_decode_with_kid( 200 header_invalid, validator, kid=None) 201 202 # Token payload is not valid JSON 203 payload_invalid = gen_token('{"alg":"HS256"}', '{"iss":"joe"') 204 with self.assertRaises(tink.TinkError): 205 jwt_hmac.verify_mac_and_decode_with_kid( 206 payload_invalid, validator, kid=None) 207 208 # Token with whitespace in header JSON string is valid. 209 whitespace_in_header = gen_token(' {"alg": \n "HS256"} \n ', 210 '{"iss":"joe" }') 211 verified_jwt = jwt_hmac.verify_mac_and_decode_with_kid( 212 whitespace_in_header, validator, kid=None) 213 self.assertEqual(verified_jwt.issuer(), 'joe') 214 215 # Token with whitespace in payload JSON string is valid. 216 whitespace_in_payload = gen_token('{"alg":"HS256"}', 217 ' {"iss": \n"joe" } \n') 218 verified_jwt = jwt_hmac.verify_mac_and_decode_with_kid( 219 whitespace_in_payload, validator, kid=None) 220 self.assertEqual(verified_jwt.issuer(), 'joe') 221 222 # Token with whitespace in base64-encoded header is invalid. 223 with_whitespace_in_encoding = ( 224 _jwt_format.encode_header('{"alg":"HS256"}') + b' .' + 225 _jwt_format.encode_payload('{"iss":"joe"}')) 226 token_with_whitespace_in_encoding = _jwt_format.create_signed_compact( 227 with_whitespace_in_encoding, 228 cc_mac.compute_mac(with_whitespace_in_encoding)) 229 with self.assertRaises(tink.TinkError): 230 jwt_hmac.verify_mac_and_decode_with_kid( 231 token_with_whitespace_in_encoding, validator, kid=None) 232 233 # Token with invalid character is invalid. 234 with_invalid_char = ( 235 _jwt_format.encode_header('{"alg":"HS256"}') + b'.?' + 236 _jwt_format.encode_payload('{"iss":"joe"}')) 237 token_with_invalid_char = _jwt_format.create_signed_compact( 238 with_invalid_char, cc_mac.compute_mac(with_invalid_char)) 239 with self.assertRaises(tink.TinkError): 240 jwt_hmac.verify_mac_and_decode_with_kid( 241 token_with_invalid_char, validator, kid=None) 242 243 # Token with additional '.' is invalid. 244 with_dot = ( 245 _jwt_format.encode_header('{"alg":"HS256"}') + b'.' + 246 _jwt_format.encode_payload('{"iss":"joe"}') + b'.') 247 token_with_dot = _jwt_format.create_signed_compact( 248 with_dot, cc_mac.compute_mac(with_dot)) 249 with self.assertRaises(tink.TinkError): 250 jwt_hmac.verify_mac_and_decode_with_kid( 251 token_with_dot, validator, kid=None) 252 253 # num_recursions has been chosen such that parsing of this token fails 254 # in all languages. We want to make sure that the algorithm does not 255 # hang or crash in this case, but only returns a parsing error. 256 num_recursions = 10000 257 rec_payload = ('{"a":' * num_recursions) + '""' + ('}' * num_recursions) 258 rec_token = gen_token('{"alg":"HS256"}', rec_payload) 259 with self.assertRaises(tink.TinkError): 260 jwt_hmac.verify_mac_and_decode_with_kid( 261 rec_token, 262 validator=jwt.new_validator(allow_missing_expiration=True), 263 kid=None) 264 265 # test wrong types 266 with self.assertRaises(tink.TinkError): 267 jwt_hmac.verify_mac_and_decode_with_kid( 268 cast(str, None), validator, kid=None) 269 with self.assertRaises(tink.TinkError): 270 jwt_hmac.verify_mac_and_decode_with_kid( 271 cast(str, 123), validator, kid=None) 272 with self.assertRaises(tink.TinkError): 273 valid_token_bytes = valid_token.encode('utf8') 274 jwt_hmac.verify_mac_and_decode_with_kid( 275 cast(str, valid_token_bytes), validator, kid=None) 276 277 @parameterized.named_parameters([ 278 ('modified_signature', 279 ('eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleH' 280 'AiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.' 281 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXi')), 282 ('modified_payload', 283 ('eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLCJleHAiOj' 284 'EzMDA4MTkzODEsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.' 285 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk')), 286 ('modified_header', 287 ('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleH' 288 'AiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.' 289 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk')), 290 ('extra .', 'eyJhbGciOiJIUzI1NiJ9.e30.abc.'), 291 ('invalid_header_encoding', 'eyJhbGciOiJIUzI1NiJ9?.e30.abc'), 292 ('invalid_payload_encoding', 'eyJhbGciOiJIUzI1NiJ9.e30?.abc'), 293 ('invalid_mac_encoding', 'eyJhbGciOiJIUzI1NiJ9.e30.abc?'), 294 ('no_mac', 'eyJhbGciOiJIUzI1NiJ9.e30'), 295 ]) 296 def test_invalid_signed_compact(self, invalid_signed_compact): 297 jwt_hmac = create_fixed_jwt_hmac() 298 validator = jwt.new_validator( 299 expected_issuer='joe', 300 allow_missing_expiration=True, 301 fixed_now=DATETIME_1970) 302 303 with self.assertRaises(tink.TinkError): 304 jwt_hmac.verify_mac_and_decode_with_kid( 305 invalid_signed_compact, validator, kid=None) 306 307 308if __name__ == '__main__': 309 absltest.main() 310