From 6c1c1b76bb8fa91180820247d2eee4ec613fe626 Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Mon, 11 May 2026 11:47:56 +0100 Subject: [PATCH] Fix AesSiv silently mangling associated data AesSiv._prepare_associated_data only checked for str and bytes when deciding whether the input was a single associated-data block, so bytearray and memoryview fell through to the "list of blocks" branch. Iterating those types yields integers, which t2b() then turned into ASCII decimal byte-strings (b'16', b'17', ...), producing many bogus blocks instead of one. The C SIV computation succeeded over wrong associated data, so encryption/decryption silently produced an incorrect tag rather than raising. This broke interoperability between callers passing the same content as different buffer types. Match the set of types accepted by t2b() by including bytearray and memoryview in the isinstance check. Add a parametrized test that reuses the OpenSSL KAT vectors with bytes, bytearray, and memoryview wrappers; a round-trip test would not have caught this since both sides mangle identically. F-1981 --- tests/test_ciphers.py | 29 +++++++++++++++++++++++++++++ wolfcrypt/ciphers.py | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/tests/test_ciphers.py b/tests/test_ciphers.py index c8b1e25..4615b07 100644 --- a/tests/test_ciphers.py +++ b/tests/test_ciphers.py @@ -938,6 +938,35 @@ def test_aessiv_decrypt_kat_openssl(): assert plaintext == TEST_VECTOR_PLAINTEXT_OPENSSL +@pytest.mark.skipif(not _lib.AES_SIV_ENABLED, reason="AES-SIV not enabled") +@pytest.mark.parametrize("wrap", [bytes, bytearray, memoryview], + ids=["bytes", "bytearray", "memoryview"]) +def test_aessiv_associated_data_accepts_buffer_types(wrap): + """ + Single-block associated_data passed as bytes, bytearray, or memoryview + must all produce the same SIV/ciphertext as the OpenSSL KAT. A previous + bug treated bytearray/memoryview as a sequence of int blocks, producing + a different (incorrect) tag without raising. + """ + aessiv = AesSiv(TEST_VECTOR_KEY_OPENSSL) + associated_data = wrap(TEST_VECTOR_ASSOCIATED_DATA_OPENSSL) + siv, ciphertext = aessiv.encrypt( + associated_data, + TEST_VECTOR_NONCE_OPENSSL, + TEST_VECTOR_PLAINTEXT_OPENSSL + ) + assert siv == TEST_VECTOR_SIV_OPENSSL + assert ciphertext == TEST_VECTOR_CIPHERTEXT_OPENSSL + + plaintext = aessiv.decrypt( + wrap(TEST_VECTOR_ASSOCIATED_DATA_OPENSSL), + TEST_VECTOR_NONCE_OPENSSL, + TEST_VECTOR_SIV_OPENSSL, + TEST_VECTOR_CIPHERTEXT_OPENSSL + ) + assert plaintext == TEST_VECTOR_PLAINTEXT_OPENSSL + + if _lib.DES3_ENABLED: def test_des3_rejects_mode_ctr(): key = b"\x01\x23\x45\x67\x89\xab\xcd\xef" * 3 diff --git a/wolfcrypt/ciphers.py b/wolfcrypt/ciphers.py index b3899b5..af8a7aa 100644 --- a/wolfcrypt/ciphers.py +++ b/wolfcrypt/ciphers.py @@ -363,7 +363,7 @@ if _lib.AES_SIV_ENABLED: C function has been called, in order to make sure that the memory is not freed by the FFI garbage collector before the data is read. """ - if isinstance(associated_data, str) or isinstance(associated_data, bytes): + if isinstance(associated_data, (str, bytes, bytearray, memoryview)): # A single block is provided. # Make sure we have bytes. associated_data = t2b(associated_data)