xref: /aosp_15_r20/external/tink/testing/cross_language/aead_test.py (revision e7b1675dde1b92d52ec075b0a92829627f2c52a5)
1# Copyright 2020 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"""Cross-language tests for the Aead primitive.
15
16These tests check some basic AEAD properties, and that all implementations can
17interoperate with each other.
18"""
19
20from typing import Iterable, List, Tuple
21
22from absl.testing import absltest
23from absl.testing import parameterized
24import tink
25from tink import aead
26
27from tink.proto import tink_pb2
28from tink.testing import keyset_builder
29import tink_config
30from util import testing_servers
31from util import utilities
32
33SUPPORTED_LANGUAGES = testing_servers.SUPPORTED_LANGUAGES_BY_PRIMITIVE['aead']
34
35
36def setUpModule():
37  aead.register()
38  testing_servers.start('aead')
39
40
41def tearDownModule():
42  testing_servers.stop()
43
44
45# Fake KMS keys are base64-encoded keysets. Each server must register a
46# fake KmsClient that can handle these keys.
47_FAKE_KMS_KEY_URI = (
48    'fake-kms://CM2b3_MDElQKSAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHRv'
49    'LnRpbmsuQWVzR2NtS2V5EhIaEIK75t5L-adlUwVhWvRuWUwYARABGM2b3_MDIAE')
50
51
52# maps from key_template_name to (key_template, supported_langs).  Contains all
53# templates we want to test which do not have a name in Tinkey.
54_ADDITIONAL_KEY_TEMPLATES = {
55    '_FAKE_KMS_AEAD': (
56        aead.aead_key_templates.create_kms_aead_key_template(_FAKE_KMS_KEY_URI),
57        tink_config.supported_languages_for_key_type('KmsAeadKey'),
58    ),
59    # Since the key type KmsEnvelopeAeadKey is supported by all languages, the
60    # following templates should be supported if the DEK Template is supported
61    # by the language.
62    '_FAKE_KMS_ENVELOPE_AEAD_WITH_AES128_GCM': (
63        aead.aead_key_templates.create_kms_envelope_aead_key_template(
64            _FAKE_KMS_KEY_URI, aead.aead_key_templates.AES128_GCM
65        ),
66        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME['AES128_GCM'],
67    ),
68    '_FAKE_KMS_ENVELOPE_AEAD_WITH_AES256_GCM': (
69        aead.aead_key_templates.create_kms_envelope_aead_key_template(
70            _FAKE_KMS_KEY_URI, aead.aead_key_templates.AES256_GCM
71        ),
72        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME['AES256_GCM'],
73    ),
74    '_FAKE_KMS_ENVELOPE_AEAD_WITH_AES128_EAX': (
75        aead.aead_key_templates.create_kms_envelope_aead_key_template(
76            _FAKE_KMS_KEY_URI, aead.aead_key_templates.AES128_EAX
77        ),
78        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME['AES128_EAX'],
79    ),
80    '_FAKE_KMS_ENVELOPE_AEAD_WITH_AES256_EAX': (
81        aead.aead_key_templates.create_kms_envelope_aead_key_template(
82            _FAKE_KMS_KEY_URI, aead.aead_key_templates.AES256_EAX
83        ),
84        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME['AES256_EAX'],
85    ),
86    '_FAKE_KMS_ENVELOPE_AEAD_WITH_AES128_GCM_SIV': (
87        aead.aead_key_templates.create_kms_envelope_aead_key_template(
88            _FAKE_KMS_KEY_URI, aead.aead_key_templates.AES128_GCM_SIV
89        ),
90        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME['AES128_GCM_SIV'],
91    ),
92    '_FAKE_KMS_ENVELOPE_AEAD_WITH_AES256_GCM_SIV': (
93        aead.aead_key_templates.create_kms_envelope_aead_key_template(
94            _FAKE_KMS_KEY_URI, aead.aead_key_templates.AES256_GCM_SIV
95        ),
96        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME['AES256_GCM_SIV'],
97    ),
98    '_FAKE_KMS_ENVELOPE_AEAD_WITH_AES128_CTR_HMAC_SHA256': (
99        aead.aead_key_templates.create_kms_envelope_aead_key_template(
100            _FAKE_KMS_KEY_URI, aead.aead_key_templates.AES128_CTR_HMAC_SHA256
101        ),
102        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
103            'AES128_CTR_HMAC_SHA256'
104        ],
105    ),
106    '_FAKE_KMS_ENVELOPE_AEAD_WITH_AES256_CTR_HMAC_SHA256': (
107        aead.aead_key_templates.create_kms_envelope_aead_key_template(
108            _FAKE_KMS_KEY_URI, aead.aead_key_templates.AES256_CTR_HMAC_SHA256
109        ),
110        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
111            'AES256_CTR_HMAC_SHA256'
112        ],
113    ),
114    '_FAKE_KMS_ENVELOPE_AEAD_WITH_CHACHA20_POLY1305': (
115        aead.aead_key_templates.create_kms_envelope_aead_key_template(
116            _FAKE_KMS_KEY_URI, utilities.KEY_TEMPLATE['CHACHA20_POLY1305']
117        ),
118        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME['CHACHA20_POLY1305'],
119    ),
120    '_FAKE_KMS_ENVELOPE_AEAD_WITH_XCHACHA20_POLY1305': (
121        aead.aead_key_templates.create_kms_envelope_aead_key_template(
122            _FAKE_KMS_KEY_URI, aead.aead_key_templates.XCHACHA20_POLY1305
123        ),
124        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME['XCHACHA20_POLY1305'],
125    ),
126}
127
128
129class AeadPythonTest(parameterized.TestCase):
130
131  def _create_aeads_ignore_errors(self,
132                                  keyset: bytes) -> List[Tuple[aead.Aead, str]]:
133    """Creates AEADs for the given keyset in each language.
134
135    Args:
136      keyset: A keyset as a serialized 'keyset' proto.
137
138    Returns:
139      A list of pairs (aead, language)
140    """
141
142    result = []
143    for lang in utilities.ALL_LANGUAGES:
144      try:
145        aead_p = testing_servers.remote_primitive(lang, keyset, aead.Aead)
146        result.append((aead_p, lang))
147      except tink.TinkError:
148        pass
149    return result
150
151  def _langs_from_key_template_name(self, key_template_name: str) -> List[str]:
152    if key_template_name in _ADDITIONAL_KEY_TEMPLATES:
153      _, supported_langs = _ADDITIONAL_KEY_TEMPLATES[key_template_name]
154      return supported_langs
155    else:
156      return utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[key_template_name]
157
158  def _as_proto_template(self, key_template_name: str) -> tink_pb2.KeyTemplate:
159    if key_template_name in _ADDITIONAL_KEY_TEMPLATES:
160      key_template, _ = _ADDITIONAL_KEY_TEMPLATES[key_template_name]
161      return key_template
162    else:
163      return utilities.KEY_TEMPLATE[key_template_name]
164
165  @parameterized.parameters([
166      *utilities.tinkey_template_names_for(aead.Aead),
167      *_ADDITIONAL_KEY_TEMPLATES.keys()
168  ])
169  def test_encrypt_decrypt(self, key_template_name):
170    langs = self._langs_from_key_template_name(key_template_name)
171    self.assertNotEmpty(langs)
172    proto_template = self._as_proto_template(key_template_name)
173    # Take the first supported language to generate the keyset.
174    keyset = testing_servers.new_keyset(langs[0], proto_template)
175
176    supported_aeads = self._create_aeads_ignore_errors(keyset)
177    self.assertEqual(set([lang for (_, lang) in supported_aeads]), set(langs))
178    for (p, lang) in supported_aeads:
179      plaintext = (
180          b'This is some plaintext message to be encrypted using key_template '
181          b'%s using %s for encryption.' %
182          (key_template_name.encode('utf8'), lang.encode('utf8')))
183      associated_data = (
184          b'Some associated data for %s using %s for encryption.' %
185          (key_template_name.encode('utf8'), lang.encode('utf8')))
186      ciphertext = p.encrypt(plaintext, associated_data)
187      for (p2, lang2) in supported_aeads:
188        output = p2.decrypt(ciphertext, associated_data)
189        self.assertEqual(
190            output, plaintext,
191            'While encrypting in %s an decrypting in %s' % (lang, lang2))
192
193  @parameterized.parameters(
194      tink_config.supported_languages_for_key_type('KmsEnvelopeAeadKey')
195  )
196  def test_envelope_encryption_rejects_envelope_templates_as_dek(self, lang):
197    dek_template = (
198        aead.aead_key_templates.create_kms_envelope_aead_key_template(
199            kek_uri=_FAKE_KMS_KEY_URI,
200            dek_template=aead.aead_key_templates.AES128_GCM,
201        )
202    )
203    template = aead.aead_key_templates.create_kms_envelope_aead_key_template(
204        kek_uri=_FAKE_KMS_KEY_URI, dek_template=dek_template
205    )
206    with self.assertRaises(tink.TinkError):
207      _ = testing_servers.new_keyset(lang, template)
208
209  @parameterized.parameters(
210      tink_config.supported_languages_for_key_type('KmsAeadKey')
211  )
212  def test_envelope_encryption_rejects_kms_templates_as_dek(self, lang):
213    dek_template = aead.aead_key_templates.create_kms_aead_key_template(
214        key_uri=_FAKE_KMS_KEY_URI
215    )
216    template = aead.aead_key_templates.create_kms_envelope_aead_key_template(
217        kek_uri=_FAKE_KMS_KEY_URI, dek_template=dek_template
218    )
219    with self.assertRaises(tink.TinkError):
220      _ = testing_servers.new_keyset(lang, template)
221
222
223# If the implementations work fine for keysets with single keys, then key
224# rotation should work if the primitive wrapper is implemented correctly.
225# These wrappers do not depend on the key type, so it should be fine to always
226# test with the same key type. Since the AEAD wrapper needs to treat keys
227# with output prefix RAW differently, we also include such a template for that.
228KEY_ROTATION_TEMPLATES = [
229    aead.aead_key_templates.AES128_CTR_HMAC_SHA256,
230    keyset_builder.raw_template(aead.aead_key_templates.AES128_CTR_HMAC_SHA256)
231]
232
233
234def key_rotation_test_cases(
235) -> Iterable[Tuple[str, str, tink_pb2.KeyTemplate, tink_pb2.KeyTemplate]]:
236  for enc_lang in SUPPORTED_LANGUAGES:
237    for dec_lang in SUPPORTED_LANGUAGES:
238      for old_key_tmpl in KEY_ROTATION_TEMPLATES:
239        for new_key_tmpl in KEY_ROTATION_TEMPLATES:
240          yield (enc_lang, dec_lang, old_key_tmpl, new_key_tmpl)
241
242
243class AeadKeyRotationTest(parameterized.TestCase):
244
245  @parameterized.parameters(key_rotation_test_cases())
246  def test_key_rotation(self, enc_lang, dec_lang, old_key_tmpl, new_key_tmpl):
247    # Do a key rotation from an old key generated from old_key_tmpl to a new
248    # key generated from new_key_tmpl. Encryption and decryption are done
249    # in languages enc_lang and dec_lang.
250    builder = keyset_builder.new_keyset_builder()
251    older_key_id = builder.add_new_key(old_key_tmpl)
252    builder.set_primary_key(older_key_id)
253    enc_aead1 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
254                                                 aead.Aead)
255    dec_aead1 = testing_servers.remote_primitive(dec_lang, builder.keyset(),
256                                                 aead.Aead)
257    newer_key_id = builder.add_new_key(new_key_tmpl)
258    enc_aead2 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
259                                                 aead.Aead)
260    dec_aead2 = testing_servers.remote_primitive(dec_lang, builder.keyset(),
261                                                 aead.Aead)
262
263    builder.set_primary_key(newer_key_id)
264    enc_aead3 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
265                                                 aead.Aead)
266    dec_aead3 = testing_servers.remote_primitive(dec_lang, builder.keyset(),
267                                                 aead.Aead)
268
269    builder.disable_key(older_key_id)
270    enc_aead4 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
271                                                 aead.Aead)
272    dec_aead4 = testing_servers.remote_primitive(dec_lang, builder.keyset(),
273                                                 aead.Aead)
274
275    self.assertNotEqual(older_key_id, newer_key_id)
276    # 1 encrypts with the older key. So 1, 2 and 3 can decrypt it, but not 4.
277    ciphertext1 = enc_aead1.encrypt(b'plaintext', b'ad')
278    self.assertEqual(dec_aead1.decrypt(ciphertext1, b'ad'), b'plaintext')
279    self.assertEqual(dec_aead2.decrypt(ciphertext1, b'ad'), b'plaintext')
280    self.assertEqual(dec_aead3.decrypt(ciphertext1, b'ad'), b'plaintext')
281    with self.assertRaises(tink.TinkError):
282      _ = dec_aead4.decrypt(ciphertext1, b'ad')
283
284    # 2 encrypts with the older key. So 1, 2 and 3 can decrypt it, but not 4.
285    ciphertext2 = enc_aead2.encrypt(b'plaintext', b'ad')
286    self.assertEqual(dec_aead1.decrypt(ciphertext2, b'ad'), b'plaintext')
287    self.assertEqual(dec_aead2.decrypt(ciphertext2, b'ad'), b'plaintext')
288    self.assertEqual(dec_aead3.decrypt(ciphertext2, b'ad'), b'plaintext')
289    with self.assertRaises(tink.TinkError):
290      _ = dec_aead4.decrypt(ciphertext2, b'ad')
291
292    # 3 encrypts with the newer key. So 2, 3 and 4 can decrypt it, but not 1.
293    ciphertext3 = enc_aead3.encrypt(b'plaintext', b'ad')
294    with self.assertRaises(tink.TinkError):
295      _ = dec_aead1.decrypt(ciphertext3, b'ad')
296    self.assertEqual(dec_aead2.decrypt(ciphertext3, b'ad'), b'plaintext')
297    self.assertEqual(dec_aead3.decrypt(ciphertext3, b'ad'), b'plaintext')
298    self.assertEqual(dec_aead4.decrypt(ciphertext3, b'ad'), b'plaintext')
299
300    # 4 encrypts with the newer key. So 2, 3 and 4 can decrypt it, but not 1.
301    ciphertext4 = enc_aead4.encrypt(b'plaintext', b'ad')
302    with self.assertRaises(tink.TinkError):
303      _ = dec_aead1.decrypt(ciphertext4, b'ad')
304    self.assertEqual(dec_aead2.decrypt(ciphertext4, b'ad'), b'plaintext')
305    self.assertEqual(dec_aead3.decrypt(ciphertext4, b'ad'), b'plaintext')
306    self.assertEqual(dec_aead4.decrypt(ciphertext4, b'ad'), b'plaintext')
307
308if __name__ == '__main__':
309  absltest.main()
310