Revision control
Copy as Markdown
Other Tools
"""
(C) 2026 Jack Lloyd
(C) 2026 René Meusel, Rohde & Schwarz Cybersecurity
Botan is released under the Simplified BSD License (see license.txt)
"""
import binascii
import unittest
import botan3 as botan
from .wycheproof import WycheproofTests
_AEAD_CIPHER_ALIASES = {
"AES-GCM": "AES",
"AES-CCM": "AES",
"AEAD-AES-SIV-CMAC": "AES-SIV-CMAC",
"ARIA-GCM": "ARIA",
"ARIA-CCM": "ARIA",
"CAMELLIA-CCM": "Camellia",
"SEED-GCM": "SEED",
"SEED-CCM": "SEED",
"SM4-GCM": "SM4",
"SM4-CCM": "SM4",
}
def _aead_algorithm(
algorithm: str, key_size_bits: int, tag_size_bits: int | None, nonce_len: int
) -> str:
cipher = _AEAD_CIPHER_ALIASES.get(algorithm)
if cipher is None:
raise ValueError(f"Unsupported AEAD algorithm: {algorithm}")
tag_len_bytes = tag_size_bits // 8 if tag_size_bits is not None else None
if algorithm == "AEAD-AES-SIV-CMAC":
aes_size = key_size_bits // 2
return f"AES-{aes_size}/SIV"
if cipher in ("AES", "ARIA", "Camellia"):
cipher = f"{cipher}-{key_size_bits}"
if algorithm.endswith("GCM"):
suffix = f"/GCM({tag_len_bytes})" if tag_len_bytes is not None else "/GCM"
return f"{cipher}{suffix}"
if algorithm.endswith("CCM"):
l_val = 15 - nonce_len
if tag_len_bytes is None:
return f"{cipher}/CCM({l_val})"
return f"{cipher}/CCM({tag_len_bytes},{l_val})"
raise ValueError(f"Unhandled AEAD algorithm: {algorithm}")
def _encrypt(aead: botan.SymmetricCipher, iv: bytes, aad: bytes, msg: bytes) -> bytes:
aead.set_assoc_data(aad)
aead.start(iv)
return aead.finish(msg)
def _decrypt(
aead: botan.SymmetricCipher, iv: bytes, aad: bytes, ct_tag: bytes
) -> bytes:
aead.set_assoc_data(aad)
aead.start(iv)
return aead.finish(ct_tag)
class TestAEAD(WycheproofTests, unittest.TestCase):
def input_files(self) -> list[str]:
return [
"aes_ccm_test.json",
"aes_gcm_test.json",
"aria_ccm_test.json",
"aria_gcm_test.json",
"camellia_ccm_test.json",
"seed_ccm_test.json",
"seed_gcm_test.json",
"sm4_ccm_test.json",
"sm4_gcm_test.json",
"aead_aes_siv_cmac_test.json",
]
def run_test(self, data: dict, group: dict, test: dict) -> None:
algorithm = data["algorithm"]
is_siv = algorithm == "AEAD-AES-SIV-CMAC"
tag_size_bits = group.get("tagSize")
key_size_bits = group.get("keySize")
key = binascii.unhexlify(test["key"])
iv = binascii.unhexlify(test["iv"])
aad = binascii.unhexlify(test["aad"])
msg = binascii.unhexlify(test["msg"])
ct = binascii.unhexlify(test["ct"])
tag = binascii.unhexlify(test["tag"])
expected_ct_tag = (tag + ct) if is_siv else (ct + tag)
algo = _aead_algorithm(algorithm, key_size_bits, tag_size_bits, len(iv))
if test["result"] == "valid":
enc = botan.SymmetricCipher(algo, True)
enc.set_key(key)
enc_out = _encrypt(enc, iv, aad, msg)
self.assertEqual(enc_out, expected_ct_tag)
dec = botan.SymmetricCipher(algo, False)
dec.set_key(key)
dec_out = _decrypt(dec, iv, aad, expected_ct_tag)
self.assertEqual(dec_out, msg)
elif test["result"] in ("invalid", "acceptable"):
try:
dec = botan.SymmetricCipher(algo, False)
dec.set_key(key)
dec_out = _decrypt(dec, iv, aad, expected_ct_tag)
if test["result"] == "invalid":
self.assertNotEqual(dec_out, msg)
if test["result"] == "acceptable":
self.assertEqual(dec_out, msg)
except botan.BotanException:
# may fail, because test is "invalid" or "acceptable"
pass
else:
self.fail(f"Unknown test result: {test['result']}")