diff --git a/doc/api_ref/ffi.rst b/doc/api_ref/ffi.rst index 30bfbce52cf..b1a86c0f5c2 100644 --- a/doc/api_ref/ffi.rst +++ b/doc/api_ref/ffi.rst @@ -544,6 +544,8 @@ Symmetric Ciphers .. cpp:function:: int botan_cipher_is_authenticated(botan_cipher_t cipher) +.. cpp:function:: int botan_cipher_requires_entire_message(botan_cipher_t cipher) + .. cpp:function:: int botan_cipher_get_tag_length(botan_cipher_t cipher, size_t* tag_len) Write the tag length of the cipher to ``tag_len``. This will be zero for non-authenticated diff --git a/src/lib/ffi/ffi.cpp b/src/lib/ffi/ffi.cpp index 4fa34d1b6a4..2fe79ce1c3d 100644 --- a/src/lib/ffi/ffi.cpp +++ b/src/lib/ffi/ffi.cpp @@ -221,6 +221,11 @@ uint32_t botan_ffi_api_version() { } int botan_ffi_supports_api(uint32_t api_version) { + // This is the API introduced in 3.4 + if(api_version == 20240408) { + return BOTAN_FFI_SUCCESS; + } + // This is the API introduced in 3.2 if(api_version == 20231009) { return BOTAN_FFI_SUCCESS; diff --git a/src/lib/ffi/ffi.h b/src/lib/ffi/ffi.h index b437d5c69de..506507a2b79 100644 --- a/src/lib/ffi/ffi.h +++ b/src/lib/ffi/ffi.h @@ -557,6 +557,13 @@ BOTAN_FFI_EXPORT(2, 0) int botan_cipher_get_tag_length(botan_cipher_t cipher, si */ BOTAN_FFI_EXPORT(3, 3) int botan_cipher_is_authenticated(botan_cipher_t cipher); +/** + * Returns 1 iff the cipher requires the entire message before any + * encryption or decryption can be performed. No output data will be produced + * in botan_cipher_update() until the final flag is set. + */ +BOTAN_FFI_EXPORT(3, 4) int botan_cipher_requires_entire_message(botan_cipher_t cipher); + /** * Get the default nonce length of this cipher */ diff --git a/src/lib/ffi/ffi_cipher.cpp b/src/lib/ffi/ffi_cipher.cpp index 3646613a79a..23239c0d82e 100644 --- a/src/lib/ffi/ffi_cipher.cpp +++ b/src/lib/ffi/ffi_cipher.cpp @@ -190,18 +190,19 @@ int botan_cipher_update(botan_cipher_t cipher_obj, size_t taken = 0, written = 0; while(input_size >= ud && output_size >= ud) { - // FIXME we can use process here and avoid the copy copy_mem(mbuf.data(), input, ud); - cipher.update(mbuf); + const size_t bytes_produced = cipher.process(mbuf); input_size -= ud; - copy_mem(output, mbuf.data(), ud); input += ud; taken += ud; - output_size -= ud; - output += ud; - written += ud; + if(bytes_produced > 0) { + copy_mem(output, mbuf.data(), bytes_produced); + output_size -= bytes_produced; + output += bytes_produced; + written += bytes_produced; + } } *output_written = written; @@ -245,6 +246,10 @@ int botan_cipher_is_authenticated(botan_cipher_t cipher) { return BOTAN_FFI_VISIT(cipher, [=](const auto& c) { return c.authenticated() ? 1 : 0; }); } +int botan_cipher_requires_entire_message(botan_cipher_t cipher) { + return BOTAN_FFI_VISIT(cipher, [=](const auto& c) { return c.requires_entire_message() ? 1 : 0; }); +} + int botan_cipher_name(botan_cipher_t cipher, char* name, size_t* name_len) { return BOTAN_FFI_VISIT(cipher, [=](const auto& c) { return write_str_output(name, name_len, c.name()); }); } diff --git a/src/lib/ffi/info.txt b/src/lib/ffi/info.txt index aac8028924a..19fdb9d538e 100644 --- a/src/lib/ffi/info.txt +++ b/src/lib/ffi/info.txt @@ -1,5 +1,5 @@ -FFI -> 20231009 +FFI -> 20240408 diff --git a/src/python/botan3.py b/src/python/botan3.py index 54bebf1d6db..95c392de5f6 100755 --- a/src/python/botan3.py +++ b/src/python/botan3.py @@ -26,7 +26,7 @@ from binascii import hexlify from datetime import datetime -BOTAN_FFI_VERSION = 20230403 +BOTAN_FFI_VERSION = 20240408 # # Base exception for all exceptions raised from this module @@ -176,7 +176,10 @@ def ffi_api(fn, args, allowed_errors=None): ffi_api(dll.botan_cipher_valid_nonce_length, [c_void_p, c_size_t]) ffi_api(dll.botan_cipher_get_tag_length, [c_void_p, POINTER(c_size_t)]) ffi_api(dll.botan_cipher_get_default_nonce_length, [c_void_p, POINTER(c_size_t)]) + ffi_api(dll.botan_cipher_is_authenticated, [c_void_p]) + ffi_api(dll.botan_cipher_requires_entire_message, [c_void_p]) ffi_api(dll.botan_cipher_get_update_granularity, [c_void_p, POINTER(c_size_t)]) + ffi_api(dll.botan_cipher_get_ideal_update_granularity, [c_void_p, POINTER(c_size_t)]) ffi_api(dll.botan_cipher_query_keylen, [c_void_p, POINTER(c_size_t), POINTER(c_size_t)]) ffi_api(dll.botan_cipher_get_keyspec, [c_void_p, POINTER(c_size_t), POINTER(c_size_t), POINTER(c_size_t)]) ffi_api(dll.botan_cipher_set_key, [c_void_p, c_char_p, c_size_t]) @@ -853,6 +856,11 @@ def update_granularity(self): _DLL.botan_cipher_get_update_granularity(self.__obj, byref(l)) return l.value + def ideal_update_granularity(self): + l = c_size_t(0) + _DLL.botan_cipher_get_ideal_update_granularity(self.__obj, byref(l)) + return l.value + def key_length(self): kmin = c_size_t(0) kmax = c_size_t(0) @@ -875,7 +883,8 @@ def tag_length(self): return l.value def is_authenticated(self): - return self.tag_length() > 0 + rc = _DLL.botan_cipher_is_authenticated(self.__obj) + return rc == 1 def valid_nonce_length(self, nonce_len): rc = _DLL.botan_cipher_valid_nonce_length(self.__obj, nonce_len) diff --git a/src/scripts/test_python.py b/src/scripts/test_python.py index 3fd54090598..a11a71bf626 100644 --- a/src/scripts/test_python.py +++ b/src/scripts/test_python.py @@ -212,14 +212,31 @@ def test_hash(self): def test_cipher(self): for mode in ['AES-128/CTR-BE', 'Serpent/GCM', 'ChaCha20Poly1305', 'AES-128/CBC/PKCS7']: - enc = botan.SymmetricCipher(mode, encrypt=True) + try: + enc = botan.SymmetricCipher(mode, encrypt=True) + except botan.BotanException as e: + raise RuntimeError("Failed to create encrypting cipher for " + mode) from e if mode == 'AES-128/CTR-BE': self.assertEqual(enc.algo_name(), 'CTR-BE(AES-128)') + self.assertFalse(enc.is_authenticated()) + self.assertEqual(enc.update_granularity(), 1) + self.assertGreater(enc.ideal_update_granularity(), 1) elif mode == 'Serpent/GCM': self.assertEqual(enc.algo_name(), 'Serpent/GCM(16)') - else: - self.assertEqual(enc.algo_name(), mode) + self.assertTrue(enc.is_authenticated()) + self.assertEqual(enc.update_granularity(), 16) + self.assertGreater(enc.ideal_update_granularity(), 16) + elif mode == 'ChaCha20Poly1305': + self.assertEqual(enc.algo_name(), 'ChaCha20Poly1305') + self.assertTrue(enc.is_authenticated()) + self.assertEqual(enc.update_granularity(), 1) + self.assertGreater(enc.ideal_update_granularity(), 1) + elif mode == 'AES-128/CBC/PKCS7': + self.assertEqual(enc.algo_name(), 'AES-128/CBC/PKCS7') + self.assertFalse(enc.is_authenticated()) + self.assertEqual(enc.update_granularity(), 16) + self.assertGreater(enc.ideal_update_granularity(), 16) (kmin, kmax) = enc.key_length() @@ -238,7 +255,10 @@ def test_cipher(self): ct = enc.finish(pt) - dec = botan.SymmetricCipher(mode, encrypt=False) + try: + dec = botan.SymmetricCipher(mode, encrypt=False) + except botan.BotanException as e: + raise RuntimeError("Failed to create decrypting cipher for " + mode) from e dec.set_key(key) dec.start(iv) decrypted = dec.finish(ct) diff --git a/src/tests/test_ffi.cpp b/src/tests/test_ffi.cpp index fcc70120d35..0ed73ee8b05 100644 --- a/src/tests/test_ffi.cpp +++ b/src/tests/test_ffi.cpp @@ -15,6 +15,7 @@ #if defined(BOTAN_HAS_FFI) #include #include + #include #include #include #endif @@ -973,6 +974,123 @@ class FFI_GCM_Test final : public FFI_Test { } }; +class FFI_ChaCha20Poly1305_Test final : public FFI_Test { + public: + std::string name() const override { return "FFI ChaCha20Poly1305"; } + + void ffi_test(Test::Result& result, botan_rng_t /*unused*/) override { + botan_cipher_t cipher_encrypt, cipher_decrypt; + + if(TEST_FFI_INIT(botan_cipher_init, (&cipher_encrypt, "ChaCha20Poly1305", BOTAN_CIPHER_INIT_FLAG_ENCRYPT))) { + std::array namebuf; + size_t name_len = 15; + TEST_FFI_FAIL("output buffer too short", botan_cipher_name, (cipher_encrypt, namebuf.data(), &name_len)); + result.test_eq("name len", name_len, 17); + + name_len = namebuf.size(); + if(TEST_FFI_OK(botan_cipher_name, (cipher_encrypt, namebuf.data(), &name_len))) { + result.test_eq("name len", name_len, 17); + result.test_eq("name", std::string(namebuf.data()), "ChaCha20Poly1305"); + } + + size_t min_keylen = 0; + size_t max_keylen = 0; + size_t nonce_len = 0; + size_t tag_len = 0; + + TEST_FFI_OK(botan_cipher_query_keylen, (cipher_encrypt, &min_keylen, &max_keylen)); + result.test_int_eq(min_keylen, 32, "Min key length"); + result.test_int_eq(max_keylen, 32, "Max key length"); + + TEST_FFI_OK(botan_cipher_get_default_nonce_length, (cipher_encrypt, &nonce_len)); + result.test_int_eq(nonce_len, 12, "Expected default ChaCha20Poly1305 nonce length"); + + TEST_FFI_OK(botan_cipher_get_tag_length, (cipher_encrypt, &tag_len)); + result.test_int_eq(tag_len, 16, "Expected Chacha20Poly1305 tag length"); + + TEST_FFI_RC(1, botan_cipher_is_authenticated, (cipher_encrypt)); + + // From RFC 7539 + const std::vector plaintext = Botan::hex_decode( + "4C616469657320616E642047656E746C656D656E206F662074686520636C617373206F66202739393A204966204920636F756C64206F6666657220796F75206F6E6C79206F6E652074697020666F7220746865206675747572652C2073756E73637265656E20776F756C642062652069742E"); + const std::vector symkey = + Botan::hex_decode("808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9F"); + const std::vector nonce = Botan::hex_decode("070000004041424344454647"); + const std::vector exp_ciphertext = Botan::hex_decode( + "D31A8D34648E60DB7B86AFBC53EF7EC2A4ADED51296E08FEA9E2B5A736EE62D63DBEA45E8CA9671282FAFB69DA92728B1A71DE0A9E060B2905D6A5B67ECD3B3692DDBD7F2D778B8C9803AEE328091B58FAB324E4FAD675945585808B4831D7BC3FF4DEF08E4B7A9DE576D26586CEC64B61161AE10B594F09E26A7E902ECBD0600691"); + const std::vector aad = Botan::hex_decode("50515253C0C1C2C3C4C5C6C7"); + + std::vector ciphertext(tag_len + plaintext.size()); + + size_t output_written = 0; + size_t input_consumed = 0; + + // Test that after clear or final the object can be reused + for(size_t r = 0; r != 2; ++r) { + TEST_FFI_OK(botan_cipher_set_key, (cipher_encrypt, symkey.data(), symkey.size())); + + // First use a nonce of the AAD, and ensure reset works + TEST_FFI_OK(botan_cipher_start, (cipher_encrypt, aad.data(), aad.size())); + TEST_FFI_OK(botan_cipher_reset, (cipher_encrypt)); + + TEST_FFI_OK(botan_cipher_start, (cipher_encrypt, nonce.data(), nonce.size())); + TEST_FFI_OK(botan_cipher_update, + (cipher_encrypt, + 0, + ciphertext.data(), + ciphertext.size(), + &output_written, + plaintext.data(), + plaintext.size(), + &input_consumed)); + TEST_FFI_OK(botan_cipher_clear, (cipher_encrypt)); + + TEST_FFI_OK(botan_cipher_set_key, (cipher_encrypt, symkey.data(), symkey.size())); + TEST_FFI_OK(botan_cipher_set_associated_data, (cipher_encrypt, aad.data(), aad.size())); + TEST_FFI_OK(botan_cipher_start, (cipher_encrypt, nonce.data(), nonce.size())); + TEST_FFI_OK(botan_cipher_update, + (cipher_encrypt, + BOTAN_CIPHER_UPDATE_FLAG_FINAL, + ciphertext.data(), + ciphertext.size(), + &output_written, + plaintext.data(), + plaintext.size(), + &input_consumed)); + + ciphertext.resize(output_written); + result.test_eq("AES/GCM ciphertext", ciphertext, exp_ciphertext); + + if(TEST_FFI_OK(botan_cipher_init, + (&cipher_decrypt, "ChaCha20Poly1305", BOTAN_CIPHER_INIT_FLAG_DECRYPT))) { + std::vector decrypted(plaintext.size()); + + TEST_FFI_OK(botan_cipher_set_key, (cipher_decrypt, symkey.data(), symkey.size())); + TEST_FFI_OK(botan_cipher_set_associated_data, (cipher_decrypt, aad.data(), aad.size())); + TEST_FFI_OK(botan_cipher_start, (cipher_decrypt, nonce.data(), nonce.size())); + TEST_FFI_OK(botan_cipher_update, + (cipher_decrypt, + BOTAN_CIPHER_UPDATE_FLAG_FINAL, + decrypted.data(), + decrypted.size(), + &output_written, + ciphertext.data(), + ciphertext.size(), + &input_consumed)); + + result.test_int_eq(input_consumed, ciphertext.size(), "All input consumed"); + result.test_int_eq(output_written, decrypted.size(), "Expected output size produced"); + result.test_eq("AES/GCM plaintext", decrypted, plaintext); + + TEST_FFI_OK(botan_cipher_destroy, (cipher_decrypt)); + } + } + + TEST_FFI_OK(botan_cipher_destroy, (cipher_encrypt)); + } + } +}; + class FFI_EAX_Test final : public FFI_Test { public: std::string name() const override { return "FFI EAX"; } @@ -1078,6 +1196,218 @@ class FFI_EAX_Test final : public FFI_Test { } }; +class FFI_AEAD_Test final : public FFI_Test { + public: + std::string name() const override { return "FFI AEAD"; } + + void ffi_test(Test::Result& merged_result, botan_rng_t rng) override { + botan_cipher_t cipher_encrypt, cipher_decrypt; + + std::array aeads = { + "AES-128/GCM", "ChaCha20Poly1305", "AES-128/EAX", "AES-256/SIV", "AES-128/CCM"}; + + for(const std::string& aead : aeads) { + Test::Result result(Botan::fmt("AEAD {}", aead)); + + if(!TEST_FFI_INIT(botan_cipher_init, (&cipher_encrypt, aead.c_str(), BOTAN_CIPHER_INIT_FLAG_ENCRYPT))) { + continue; + } + + if(!botan_cipher_is_authenticated(cipher_encrypt)) { + result.test_failure("Cipher " + aead + " claims is not authenticated"); + botan_cipher_destroy(cipher_encrypt); + continue; + } + + size_t min_keylen = 0; + size_t max_keylen = 0; + size_t ideal_granularity = 0; + size_t noncelen = 0; + size_t taglen = 0; + constexpr size_t pt_multiplier = 5; + TEST_FFI_OK(botan_cipher_query_keylen, (cipher_encrypt, &min_keylen, &max_keylen)); + TEST_FFI_OK(botan_cipher_get_ideal_update_granularity, (cipher_encrypt, &ideal_granularity)); + TEST_FFI_OK(botan_cipher_get_default_nonce_length, (cipher_encrypt, &noncelen)); + TEST_FFI_OK(botan_cipher_get_tag_length, (cipher_encrypt, &taglen)); + + std::vector key(max_keylen); + TEST_FFI_OK(botan_rng_get, (rng, key.data(), key.size())); + TEST_FFI_OK(botan_cipher_set_key, (cipher_encrypt, key.data(), key.size())); + + std::vector nonce(noncelen); + TEST_FFI_OK(botan_rng_get, (rng, nonce.data(), nonce.size())); + TEST_FFI_OK(botan_cipher_start, (cipher_encrypt, nonce.data(), nonce.size())); + + std::vector plaintext(ideal_granularity * pt_multiplier); + std::vector ciphertext(ideal_granularity * pt_multiplier + taglen); + TEST_FFI_OK(botan_rng_get, (rng, plaintext.data(), plaintext.size())); + + std::vector dummy_buffer(256); + TEST_FFI_OK(botan_rng_get, (rng, dummy_buffer.data(), dummy_buffer.size())); + std::vector dummy_buffer_reference = dummy_buffer; + + const bool requires_entire_message = botan_cipher_requires_entire_message(cipher_encrypt); + result.test_eq( + "requires entire message", requires_entire_message, (aead == "AES-256/SIV" || aead == "AES-128/CCM")); + + std::span pt_slicer(plaintext); + std::span ct_stuffer(ciphertext); + + // Process data that is explicitly a multiple of the ideal + // granularity and therefore should be aligned with the cipher's + // internal block size. + for(size_t i = 0; i < pt_multiplier; ++i) { + size_t output_written = 0; + size_t input_consumed = 0; + + auto pt_chunk = pt_slicer.first(ideal_granularity); + + // The existing implementation won't consume any bytes from the + // input if there is no space in the output buffer. Even when + // the cipher is a mode that won't produce any output until the + // entire message is processed. Hence, give it some dummy buffer. + auto ct_chunk = (requires_entire_message) ? std::span(dummy_buffer).first(ideal_granularity) + : ct_stuffer.first(ideal_granularity); + + TEST_FFI_OK(botan_cipher_update, + (cipher_encrypt, + 0 /* don't finalize */, + ct_chunk.data(), + ct_chunk.size(), + &output_written, + pt_chunk.data(), + pt_chunk.size(), + &input_consumed)); + + result.test_gt("some input consumed", input_consumed, 0); + result.test_lte("at most, all input consumed", input_consumed, pt_chunk.size()); + pt_slicer = pt_slicer.subspan(input_consumed); + + if(requires_entire_message) { + result.test_eq("no output produced", output_written, 0); + } else { + result.test_eq("all bytes produced", output_written, input_consumed); + ct_stuffer = ct_stuffer.subspan(output_written); + } + } + + // Trying to pull a part of the authentication tag should fail, + // as we must consume the entire tag in a single invocation to + // botan_cipher_update(). + size_t final_output_written = 42; + size_t final_input_consumed = 1337; + TEST_FFI_RC(BOTAN_FFI_ERROR_INVALID_INPUT, + botan_cipher_update, + (cipher_encrypt, + BOTAN_CIPHER_UPDATE_FLAG_FINAL, + dummy_buffer.data(), + 3, /* not enough to hold any reasonable auth'n tag */ + &final_output_written, + pt_slicer.data(), // remaining bytes (typically 0) + pt_slicer.size(), + &final_input_consumed)); + + const size_t expected_final_size = requires_entire_message ? ciphertext.size() : taglen + pt_slicer.size(); + + result.test_eq("remaining bytes consumed in bogus final", final_input_consumed, pt_slicer.size()); + result.test_eq("required buffer size is written in bogus final", final_output_written, expected_final_size); + + auto final_ct_chunk = ct_stuffer.first(expected_final_size); + + TEST_FFI_OK(botan_cipher_update, + (cipher_encrypt, + 0 /* explicitly not setting final flag, to fall into the 'input-size == 0' case */, + final_ct_chunk.data(), + final_ct_chunk.size(), + &final_output_written, + nullptr, // no more input + 0, + &final_input_consumed)); + + result.test_eq("no bytes consumed in final", final_input_consumed, 0); + result.test_eq("final bytes written", final_output_written, expected_final_size); + result.test_eq("dummy buffer unchanged", dummy_buffer, dummy_buffer_reference); + + TEST_FFI_OK(botan_cipher_destroy, (cipher_encrypt)); + + // ---------------------------------------------------------------- + + TEST_FFI_INIT(botan_cipher_init, (&cipher_decrypt, aead.c_str(), BOTAN_CIPHER_INIT_FLAG_DECRYPT)); + TEST_FFI_OK(botan_cipher_set_key, (cipher_decrypt, key.data(), key.size())); + TEST_FFI_OK(botan_cipher_start, (cipher_decrypt, nonce.data(), nonce.size())); + + std::vector decrypted(plaintext.size()); + + std::span ct_slicer(ciphertext); + std::span pt_stuffer(decrypted); + + // Process data that is explicitly a multiple of the ideal + // granularity and therefore should be aligned with the cipher's + // internal block size. + for(size_t i = 0; i < pt_multiplier; ++i) { + size_t output_written = 42; + size_t input_consumed = 1337; + + auto ct_chunk = ct_slicer.first(ideal_granularity); + + // The existing implementation won't consume any bytes from the + // input if there is no space in the output buffer. Even when + // the cipher is a mode that won't produce any output until the + // entire message is processed. Hence, give it some dummy buffer. + auto pt_chunk = (requires_entire_message) ? std::span(dummy_buffer).first(ideal_granularity) + : pt_stuffer.first(ideal_granularity); + + TEST_FFI_OK(botan_cipher_update, + (cipher_decrypt, + 0 /* don't finalize */, + pt_chunk.data(), + pt_chunk.size(), + &output_written, + ct_chunk.data(), + ct_chunk.size(), + &input_consumed)); + + result.test_gt("some input consumed", input_consumed, 0); + result.test_lte("at most, all input consumed", input_consumed, ct_chunk.size()); + ct_slicer = ct_slicer.subspan(input_consumed); + + if(requires_entire_message) { + result.test_eq("no output produced", output_written, 0); + } else { + result.test_eq("all bytes produced", output_written, input_consumed); + pt_stuffer = pt_stuffer.subspan(output_written); + } + } + + const size_t expected_final_size_dec = requires_entire_message ? plaintext.size() : pt_stuffer.size(); + auto pt_chunk = pt_stuffer.first(expected_final_size_dec); + + size_t final_output_written_dec = 42; + size_t final_input_consumed_dec = 1337; + + TEST_FFI_OK(botan_cipher_update, + (cipher_decrypt, + BOTAN_CIPHER_UPDATE_FLAG_FINAL, + pt_chunk.data(), + pt_chunk.size(), + &final_output_written_dec, + ct_slicer.data(), // remaining bytes (typically 0) + ct_slicer.size(), + &final_input_consumed_dec)); + + result.test_eq("remaining bytes consumed in final (decrypt)", final_input_consumed_dec, ct_slicer.size()); + result.test_eq("bytes written in final (decrypt)", final_output_written_dec, expected_final_size_dec); + result.test_eq("dummy buffer unchanged", dummy_buffer, dummy_buffer_reference); + + result.test_eq("decrypted plaintext", decrypted, plaintext); + + TEST_FFI_OK(botan_cipher_destroy, (cipher_decrypt)); + + merged_result.merge(result, true /* ignore names */); + } + } +}; + class FFI_StreamCipher_Test final : public FFI_Test { public: std::string name() const override { return "FFI stream ciphers"; } @@ -3142,7 +3472,9 @@ BOTAN_REGISTER_TEST("ffi", "ffi_ecdsa_certificate", FFI_ECDSA_Certificate_Test); BOTAN_REGISTER_TEST("ffi", "ffi_pkcs_hashid", FFI_PKCS_Hashid_Test); BOTAN_REGISTER_TEST("ffi", "ffi_cbc_cipher", FFI_CBC_Cipher_Test); BOTAN_REGISTER_TEST("ffi", "ffi_gcm", FFI_GCM_Test); +BOTAN_REGISTER_TEST("ffi", "ffi_chacha", FFI_ChaCha20Poly1305_Test); BOTAN_REGISTER_TEST("ffi", "ffi_eax", FFI_EAX_Test); +BOTAN_REGISTER_TEST("ffi", "ffi_aead", FFI_AEAD_Test); BOTAN_REGISTER_TEST("ffi", "ffi_streamcipher", FFI_StreamCipher_Test); BOTAN_REGISTER_TEST("ffi", "ffi_hashfunction", FFI_HashFunction_Test); BOTAN_REGISTER_TEST("ffi", "ffi_mac", FFI_MAC_Test);