xref: /aosp_15_r20/external/tink/python/tink/jwt/_jwt_hmac_key_manager_test.py (revision e7b1675dde1b92d52ec075b0a92829627f2c52a5)
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