diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bf7c60f6b22..d14393896bd 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -167,7 +167,6 @@ set(TILEDB_UNIT_TEST_SOURCES src/unit-compression-dd.cc src/unit-compression-delta.cc src/unit-compression-rle.cc - src/unit-crypto.cc src/unit-ctx.cc src/unit-current-domain-rest.cc src/unit-dense-reader.cc diff --git a/tiledb/sm/crypto/CMakeLists.txt b/tiledb/sm/crypto/CMakeLists.txt index 1647dca95a0..5dd81745aa4 100644 --- a/tiledb/sm/crypto/CMakeLists.txt +++ b/tiledb/sm/crypto/CMakeLists.txt @@ -37,8 +37,9 @@ commence(object_library tiledb_crypto) this_target_link_libraries(bcrypt) else() find_package(OpenSSL REQUIRED) - #this_target_link_libraries(OpenSSL::Crypto) - target_link_libraries(tiledb_crypto PRIVATE OpenSSL::Crypto) + # We cannot use this_target_link_libraries, because it links with PUBLIC + # visibility, and we use OpenSSL only as an internal implementation detail. + target_link_libraries(tiledb_crypto PRIVATE OpenSSL::Crypto) endif() # OpenSSL-3 deprecates MD5 if(MSVC) @@ -47,3 +48,5 @@ commence(object_library tiledb_crypto) set_source_files_properties(crypto_openssl.cc PROPERTIES COMPILE_OPTIONS "-Wno-deprecated-declarations") endif() conclude(object_library) + +add_test_subdirectory() diff --git a/tiledb/sm/crypto/crypto.cc b/tiledb/sm/crypto/crypto.cc index 790d8b3e654..1e7f4687b92 100644 --- a/tiledb/sm/crypto/crypto.cc +++ b/tiledb/sm/crypto/crypto.cc @@ -36,8 +36,10 @@ #ifdef _WIN32 #include "tiledb/sm/crypto/crypto_win32.h" +using PlatformCrypto = tiledb::sm::Win32CNG; #else #include "tiledb/sm/crypto/crypto_openssl.h" +using PlatformCrypto = tiledb::sm::OpenSSL; #endif using namespace tiledb::common; @@ -89,11 +91,7 @@ Status Crypto::decrypt_aes256gcm( return LOG_STATUS( Status_EncryptionError("AES-256-GCM error; invalid tag.")); -#ifdef _WIN32 - return Win32CNG::decrypt_aes256gcm(key, iv, tag, input, output); -#else - return OpenSSL::decrypt_aes256gcm(key, iv, tag, input, output); -#endif + return PlatformCrypto::decrypt_aes256gcm(key, iv, tag, input, output); } Status Crypto::md5(ConstBuffer* input, Buffer* output) { @@ -107,11 +105,7 @@ Status Crypto::md5( Status Crypto::md5( const void* input, uint64_t input_read_size, Buffer* output) { -#ifdef _WIN32 - return Win32CNG::md5(input, input_read_size, output); -#else - return OpenSSL::md5(input, input_read_size, output); -#endif + return PlatformCrypto::md5(input, input_read_size, output); } Status Crypto::sha256(ConstBuffer* input, Buffer* output) { @@ -125,11 +119,7 @@ Status Crypto::sha256( Status Crypto::sha256( const void* input, uint64_t input_read_size, Buffer* output) { -#ifdef _WIN32 - return Win32CNG::sha256(input, input_read_size, output); -#else - return OpenSSL::sha256(input, input_read_size, output); -#endif + return PlatformCrypto::sha256(input, input_read_size, output); } } // namespace sm diff --git a/tiledb/sm/crypto/crypto_openssl.cc b/tiledb/sm/crypto/crypto_openssl.cc index d5a64a4a563..1d3234669a6 100644 --- a/tiledb/sm/crypto/crypto_openssl.cc +++ b/tiledb/sm/crypto/crypto_openssl.cc @@ -48,20 +48,19 @@ using namespace tiledb::common; namespace tiledb { namespace sm { -Status OpenSSL::get_random_bytes(unsigned num_bytes, Buffer* output) { - if (output->free_space() < num_bytes) - RETURN_NOT_OK(output->realloc(output->alloced_size() + num_bytes)); - - int rc = RAND_bytes((unsigned char*)output->cur_data(), num_bytes); - if (rc < 1) { - char err_msg[256]; - ERR_error_string_n(ERR_get_error(), err_msg, sizeof(err_msg)); - return Status_EncryptionError( - "Cannot generate random bytes with OpenSSL: " + std::string(err_msg)); +static Status get_random_bytes(span buffer) { + while (!buffer.empty()) { + int num_bytes = + int(std::min(buffer.size(), size_t(std::numeric_limits::max()))); + int rc = RAND_bytes(buffer.data(), num_bytes); + if (rc < 1) { + char err_msg[256]; + ERR_error_string_n(ERR_get_error(), err_msg, sizeof(err_msg)); + return Status_EncryptionError( + "Cannot generate random bytes with OpenSSL: " + std::string(err_msg)); + } + buffer = buffer.subspan(num_bytes); } - output->advance_size(num_bytes); - output->advance_offset(num_bytes); - return Status::Ok(); } @@ -83,11 +82,11 @@ Status OpenSSL::encrypt_aes256gcm( RETURN_NOT_OK(output->realloc(output->alloced_size() + required_space)); // Generate IV if the given IV buffer is null. - Buffer generated_iv; + std::array generated_iv; int iv_len; unsigned char* iv_buf; if (iv == nullptr || iv->data() == nullptr) { - RETURN_NOT_OK(get_random_bytes(Crypto::AES256GCM_IV_BYTES, &generated_iv)); + RETURN_NOT_OK(get_random_bytes(generated_iv)); iv_len = (int)generated_iv.size(); iv_buf = (unsigned char*)generated_iv.data(); } else { diff --git a/tiledb/sm/crypto/crypto_openssl.h b/tiledb/sm/crypto/crypto_openssl.h index 12a4d73d3a7..1151ab09f77 100644 --- a/tiledb/sm/crypto/crypto_openssl.h +++ b/tiledb/sm/crypto/crypto_openssl.h @@ -107,16 +107,6 @@ class OpenSSL { */ static Status sha256( const void* input, uint64_t input_read_size, Buffer* output); - - private: - /** - * Generates a number of cryptographically random bytes. - * - * @param num_bytes Number of bytes to generate. - * @param output Buffer to store random bytes. - * @return Status - */ - static Status get_random_bytes(unsigned num_bytes, Buffer* output); }; } // namespace sm diff --git a/tiledb/sm/crypto/crypto_win32.cc b/tiledb/sm/crypto/crypto_win32.cc index acac8bec24f..16db0b52dcd 100644 --- a/tiledb/sm/crypto/crypto_win32.cc +++ b/tiledb/sm/crypto/crypto_win32.cc @@ -33,6 +33,8 @@ #ifdef _WIN32 #include "tiledb/sm/crypto/crypto_win32.h" + +#include #include "tiledb/common/heap_memory.h" #include "tiledb/common/logger.h" #include "tiledb/sm/buffer/buffer.h" @@ -47,28 +49,26 @@ using namespace tiledb::common; namespace tiledb { namespace sm { -Status Win32CNG::get_random_bytes(unsigned num_bytes, Buffer* output) { - if (output->free_space() < num_bytes) - RETURN_NOT_OK(output->realloc(output->alloced_size() + num_bytes)); - +static Status get_random_bytes(span buffer) { BCRYPT_ALG_HANDLE alg_handle; if (!NT_SUCCESS(BCryptOpenAlgorithmProvider( &alg_handle, BCRYPT_RNG_ALGORITHM, nullptr, 0))) return Status_EncryptionError( "Win32CNG error; generating random bytes: error opening algorithm."); - if (!NT_SUCCESS(BCryptGenRandom( - alg_handle, (unsigned char*)output->cur_data(), num_bytes, 0))) { - BCryptCloseAlgorithmProvider(alg_handle, 0); - return Status_EncryptionError( - "Win32CNG error; generating random bytes: error generating bytes."); + while (!buffer.empty()) { + ULONG num_bytes = ULONG( + std::min(buffer.size(), size_t(std::numeric_limits::max()))); + if (!NT_SUCCESS(BCryptGenRandom(alg_handle, buffer.data(), num_bytes, 0))) { + BCryptCloseAlgorithmProvider(alg_handle, 0); + return Status_EncryptionError( + "Win32CNG error; generating random bytes: error generating bytes."); + } + buffer = buffer.subspan(num_bytes); } BCryptCloseAlgorithmProvider(alg_handle, 0); - output->advance_size(num_bytes); - output->advance_offset(num_bytes); - return Status::Ok(); } @@ -87,11 +87,11 @@ Status Win32CNG::encrypt_aes256gcm( // Generate IV if the given IV buffer is null. ULONG iv_len; unsigned char* iv_buf; - Buffer generated_iv; + std::array generated_iv; if (iv == nullptr || iv->data() == nullptr) { - RETURN_NOT_OK(get_random_bytes(Crypto::AES256GCM_IV_BYTES, &generated_iv)); + RETURN_NOT_OK(get_random_bytes(generated_iv)); iv_len = (ULONG)generated_iv.size(); - iv_buf = (unsigned char*)generated_iv.data(); + iv_buf = generated_iv.data(); } else { iv_len = (ULONG)iv->size(); iv_buf = (unsigned char*)iv->data(); @@ -294,16 +294,6 @@ Status Win32CNG::decrypt_aes256gcm( return Status::Ok(); } -Status Win32CNG::md5( - const void* input, uint64_t input_read_size, Buffer* output) { - return hash_bytes(input, input_read_size, output, BCRYPT_MD5_ALGORITHM); -} - -Status Win32CNG::sha256( - const void* input, uint64_t input_read_size, Buffer* output) { - return hash_bytes(input, input_read_size, output, BCRYPT_SHA256_ALGORITHM); -} - Status Win32CNG::hash_bytes( const void* input, uint64_t input_read_size, diff --git a/tiledb/sm/crypto/crypto_win32.h b/tiledb/sm/crypto/crypto_win32.h index 9ac945d19cc..78446abc289 100644 --- a/tiledb/sm/crypto/crypto_win32.h +++ b/tiledb/sm/crypto/crypto_win32.h @@ -41,6 +41,7 @@ #include #include + #include "tiledb/common/status.h" using namespace tiledb::common; @@ -101,7 +102,9 @@ class Win32CNG { * @return Status */ static Status md5( - const void* input, uint64_t input_read_size, Buffer* output); + const void* input, uint64_t input_read_size, Buffer* output) { + return hash_bytes(input, input_read_size, output, BCRYPT_MD5_ALGORITHM); + } /** * Compute sha256 checksum of data @@ -112,11 +115,14 @@ class Win32CNG { * @return Status */ static Status sha256( - const void* input, uint64_t input_read_size, Buffer* output); + const void* input, uint64_t input_read_size, Buffer* output) { + return hash_bytes(input, input_read_size, output, BCRYPT_SHA256_ALGORITHM); + } + private: /** * - * Compute a has using Win32CNG functions + * Compute a hash using Win32CNG functions * * @param input Plaintext to compute hash of * @param input_read_size size of input to read for hash @@ -129,16 +135,6 @@ class Win32CNG { uint64_t input_read_size, Buffer* output, LPCWSTR hash_algorithm); - - private: - /** - * Generates a number of cryptographically random bytes. - * - * @param num_bytes Number of bytes to generate. - * @param output Buffer to store random bytes. - * @return Status - */ - static Status get_random_bytes(unsigned num_bytes, Buffer* output); }; } // namespace sm diff --git a/tiledb/sm/crypto/test/CMakeLists.txt b/tiledb/sm/crypto/test/CMakeLists.txt new file mode 100644 index 00000000000..148bc59176a --- /dev/null +++ b/tiledb/sm/crypto/test/CMakeLists.txt @@ -0,0 +1,32 @@ +# +# tiledb/sm/crypto/test/CMakeLists.txt +# +# The MIT License +# +# Copyright (c) 2024 TileDB, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +include(unit_test) + +commence(unit_test tiledb_crypto) + this_target_sources(unit_tiledb_crypto.cc) + this_target_object_libraries(tiledb_crypto) +conclude(unit_test) diff --git a/tiledb/sm/crypto/test/compile_crypto_main.cc b/tiledb/sm/crypto/test/compile_crypto_main.cc deleted file mode 100644 index ea3eef1c579..00000000000 --- a/tiledb/sm/crypto/test/compile_crypto_main.cc +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @file compile_crypto_main.cc - * - * @section LICENSE - * - * The MIT License - * - * @copyright Copyright (c) 2021 TileDB, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -#include "../crypto.h" - -int main() { - (void)sizeof(tiledb::sm::Crypto); - return 0; -} diff --git a/test/src/unit-crypto.cc b/tiledb/sm/crypto/test/unit_tiledb_crypto.cc similarity index 76% rename from test/src/unit-crypto.cc rename to tiledb/sm/crypto/test/unit_tiledb_crypto.cc index de90e64637b..fab112d61af 100644 --- a/test/src/unit-crypto.cc +++ b/tiledb/sm/crypto/test/unit_tiledb_crypto.cc @@ -1,5 +1,5 @@ /** - * @file unit-crypto.cc + * @file unit_tiledb_crypto.cc * * @section LICENSE * @@ -29,12 +29,33 @@ * @section DESCRIPTION * * Tests the `Crypto` class + * + * @section Test plan + * + * The random number generator is tested by generating two 64-byte buffers with + * cryptographically random data and checking that their content is not the + * same. The probability of having the same content is vanishingly small. + * + * The algorithms are tested using official test vectors: + * + * AES-GCM test vectors are taken from gcmEncryptExtIV256.rsp, from "GCM Test + * Vectors (SP 800-38D)" + * (https://csrc.nist.gov/Projects/cryptographic-algorithm-validation-program/cavp-testing-block-cipher-modes). + * We also test that modifying the ciphertext causes failures in decrypting it. + * + * MD5 test vectors are taken from RFC 1321's section A.5 + * (https://www.ietf.org/rfc/rfc1321.txt) + * + * SHA-256 test vectors were taken from SHA256ShortMsg.rsp, from "SHA Test + * Vectors for Hashing Bit-Oriented Messages" + * (https://csrc.nist.gov/Projects/cryptographic-algorithm-validation-program/Secure-Hashing) */ #include "tiledb/sm/buffer/buffer.h" #include "tiledb/sm/crypto/crypto.h" #include +#include #include using namespace tiledb::sm; @@ -214,28 +235,29 @@ TEST_CASE("Crypto: Test AES-256-GCM", "[crypto][aes]") { // These are test vectors where: // Keylen = 256, IVlen = 96, PTlen = 408, AADlen = 0, Taglen = 128. struct TestCase { - char key[64 + 1]; - char iv[24 + 1]; - char pt[102 + 1]; - char ct[102 + 1]; - char tag[32 + 1]; - - TestCase( - const char* key_arg, - const char* iv_arg, - const char* pt_arg, - const char* ct_arg, - const char* tag_arg) { - std::memcpy(key, key_arg, sizeof(key)); - std::memcpy(iv, iv_arg, sizeof(iv)); - std::memcpy(pt, pt_arg, sizeof(pt)); - std::memcpy(ct, ct_arg, sizeof(ct)); - std::memcpy(tag, tag_arg, sizeof(tag)); + span key; + span iv; + span plaintext; + span ciphertext; + span tag; + + constexpr TestCase( + decltype(key) key, + decltype(iv) iv, + decltype(plaintext) pt, + decltype(ciphertext) ct, + decltype(tag) tag) + : key(key) + , iv(iv) + , plaintext(pt) + , ciphertext(ct) + , tag(tag) { } - Buffer get_buffer( - unsigned buf_size, unsigned num_chars, const char* field) const { + static Buffer get_buffer(span field) { Buffer result; + auto num_chars = field.size() - 1; // Exclude null terminator. + auto buf_size = num_chars / 2; REQUIRE(result.realloc(buf_size).ok()); for (unsigned i = 0; i < num_chars; i += 2) { char byte_str[3] = {field[i], field[i + 1], '\0'}; @@ -246,27 +268,27 @@ TEST_CASE("Crypto: Test AES-256-GCM", "[crypto][aes]") { } Buffer get_key() const { - return get_buffer(256 / 8, 64, key); + return get_buffer(key); } Buffer get_iv() const { - return get_buffer(96 / 8, 24, iv); + return get_buffer(iv); } Buffer get_plaintext() const { - return get_buffer(408 / 8, 102, pt); + return get_buffer(plaintext); } - Buffer get_tag() const { - return get_buffer(128 / 8, 32, tag); + Buffer get_ciphertext() const { + return get_buffer(ciphertext); } - Buffer get_ciphertext() const { - return get_buffer(408 / 8, 102, ct); + Buffer get_tag() const { + return get_buffer(tag); } }; - std::vector tests = { + static constexpr auto tests = { TestCase( "1fded32d5999de4a76e0f8082108823aef60417e1896cf4218a2fa90f632ec8a", "1f3afa4711e9474f32e70462", @@ -493,50 +515,102 @@ TEST_CASE("Crypto: Test AES-256-GCM", "[crypto][aes]") { } } +static std::string from_hex(const std::string_view& str) { + std::string output; + for (size_t i = 0; i < str.length(); i += 2) { + char byte_str[3] = {str[i], str[i + 1], '\0'}; + output.push_back((uint8_t)std::strtoul(byte_str, nullptr, 16)); + } + return output; +} + +static std::string to_hex(span data) { + std::stringstream ss; + for (size_t i = 0; i < data.size(); i++) { + ss << std::hex << std::setw(2) << std::setfill('0') << (int)data[i]; + } + return ss.str(); +} + +/** + * Test that the given input (optionally in hex) has the expected hash value in + * hex. The function is generic over the hash and the input size. + */ +template +static void test_expected_hash_value( + const std::string_view& input, + const std::string_view& expected_value, + bool hex) { + REQUIRE(expected_value.length() == digest_bytes * 2); + + const std::string& processed_input = + hex ? from_hex(input) : std::string{input}; + + Buffer hash_buf(digest_bytes); + CHECK( + (hash(processed_input.data(), processed_input.length(), &hash_buf)).ok()); + + // Compare the strings for a better error message in case of failure. + CHECK( + to_hex( + {(uint8_t*)hash_buf.data(), + static_cast(hash_buf.alloced_size())}) == expected_value); +} + TEST_CASE("Crypto: Test MD5", "[crypto][md5]") { - SECTION("- Basic") { - std::string expected_checksum = "e99a18c428cb38d5f260853678922e03"; - std::string text_to_checksum = "abc123"; - ConstBuffer input_buffer( - text_to_checksum.data(), text_to_checksum.length()); - Buffer output_buffer; - CHECK(output_buffer.realloc(Crypto::MD5_DIGEST_BYTES).ok()); - CHECK(Crypto::md5(&input_buffer, &output_buffer).ok()); - - unsigned char* digest = - reinterpret_cast(output_buffer.data()); - char md5string[33]; - for (uint64_t i = 0; i < output_buffer.alloced_size(); ++i) { - sprintf(&md5string[i * 2], "%02x", (unsigned int)digest[i]); - } - CHECK( - memcmp( - expected_checksum.data(), md5string, expected_checksum.length()) == - 0); + auto test_md5 = + test_expected_hash_value; + static const std::vector> + test_cases{ + {"", "d41d8cd98f00b204e9800998ecf8427e"}, + {"a", "0cc175b9c0f1b6a831c399e269772661"}, + {"abc", "900150983cd24fb0d6963f7d28e17f72"}, + {"message digest", "f96b697d7cb7938d525a2f31aaf161d0"}, + {"abcdefghijklmnopqrstuvwxyz", "c3fcd3d76192e4007dfb496cca67e13b"}, + {"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + "d174ab98d277d9f5a5611c2c9f419d9f"}, + {"1234567890123456789012345678901234567890123456789012345678901234567" + "890" + "1234567890", + "57edf4a22be3c955ac49da2e2107b67a"}}; + + for (auto& [input, expected_hash] : test_cases) { + test_md5(input, expected_hash, false); } } TEST_CASE("Crypto: Test SHA256", "[crypto][sha256]") { - SECTION("- Basic") { - std::string expected_checksum = - "6ca13d52ca70c883e0f0bb101e425a89e8624de51db2d2392593af6a84118090"; - std::string text_to_checksum = "abc123"; - ConstBuffer input_buffer( - text_to_checksum.data(), text_to_checksum.length()); - Buffer output_buffer; - CHECK(output_buffer.realloc(Crypto::SHA256_DIGEST_BYTES).ok()); - CHECK(Crypto::sha256(&input_buffer, &output_buffer).ok()); - - unsigned char* digest = - reinterpret_cast(output_buffer.data()); - char shastring[65]; - for (uint64_t i = 0; i < output_buffer.alloced_size(); ++i) { - sprintf(&shastring[i * 2], "%02x", (unsigned int)digest[i]); - } - - CHECK( - memcmp( - expected_checksum.data(), shastring, expected_checksum.length()) == - 0); + auto test_sha256 = + test_expected_hash_value; + std::vector> test_cases{ + // Len = 0 + {"", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, + // Len = 64 + {"5738c929c4f4ccb6", + "963bb88f27f512777aab6c8b1a02c70ec0ad651d428f870036e1917120fb48bf"}, + // Len = 128 + {"0a27847cdc98bd6f62220b046edd762b", + "80c25ec1600587e7f28b18b1b18e3cdc89928e39cab3bc25e4d4a4c139bcedc4"}, + // Len = 192 + {"47991301156d1d977c0338efbcad41004133aefbca6bcf7e", + "feeb4b2b59fec8fdb1e55194a493d8c871757b5723675e93d3ac034b380b7fc9"}, + // Len = 256 + {"09fc1accc230a205e4a208e64a8f204291f581a12756392da4b8c0cf5ef02b95", + "4f44c1c7fbebb6f9601829f3897bfd650c56fa07844be76489076356ac1886a4"}, + // Len = 384 + {"4eef5107459bddf8f24fc7656fd4896da8711db50400c0164847f692b886ce8d7f4d67" + "395090b3534efd7b0d298da34b", + "7c5d14ed83dab875ac25ce7feed6ef837d58e79dc601fb3c1fca48d4464e8b83"}, + // Len = 448 + {"2d52447d1244d2ebc28650e7b05654bad35b3a68eedc7f8515306b496d75f3e73385dd" + "1b002625024b81a02f2fd6dffb6e6d561cb7d0bd7a", + "cfb88d6faf2de3a69d36195acec2e255e2af2b7d933997f348e09f6ce5758360"}, + // Len = 512 + {"5a86b737eaea8ee976a0a24da63e7ed7eefad18a101c1211e2b3650c5187c2a8a65054" + "7208251f6d4237e661c7bf4c77f335390394c37fa1a9f9be836ac28509", + "42e61e174fbb3897d6dd6cef3dd2802fe67b331953b06114a65c772859dfc1aa"}}; + + for (auto& [input, expected_hash] : test_cases) { + test_sha256(input, expected_hash, true); } }