From c807f3322897ca8c0da114556e5936e389da5059 Mon Sep 17 00:00:00 2001 From: stratospher <44024636+stratospher@users.noreply.github.com> Date: Sun, 13 Aug 2023 09:29:28 +0530 Subject: [PATCH 1/2] Add fuzz test for AEADChacha20Poly1305 --- src/Makefile.test.include | 1 + src/test/fuzz/crypto_chacha20poly1305.cpp | 103 ++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 src/test/fuzz/crypto_chacha20poly1305.cpp diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 8651eea9424..0993a65efff 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -309,6 +309,7 @@ test_fuzz_fuzz_SOURCES = \ test/fuzz/crypto_aes256.cpp \ test/fuzz/crypto_aes256cbc.cpp \ test/fuzz/crypto_chacha20.cpp \ + test/fuzz/crypto_chacha20poly1305.cpp \ test/fuzz/crypto_common.cpp \ test/fuzz/crypto_diff_fuzz_chacha20.cpp \ test/fuzz/crypto_hkdf_hmac_sha256_l32.cpp \ diff --git a/src/test/fuzz/crypto_chacha20poly1305.cpp b/src/test/fuzz/crypto_chacha20poly1305.cpp new file mode 100644 index 00000000000..96188d2230d --- /dev/null +++ b/src/test/fuzz/crypto_chacha20poly1305.cpp @@ -0,0 +1,103 @@ +// Copyright (c) 2020-2021 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +FUZZ_TARGET(crypto_aeadchacha20poly1305) +{ + FuzzedDataProvider provider{buffer.data(), buffer.size()}; + + auto key = provider.ConsumeBytes(32); + key.resize(32); + AEADChaCha20Poly1305 aead(key); + + // Initialize RNG deterministically, to generate contents and AAD. We assume that there are no + // (potentially buggy) edge cases triggered by specific values of contents/AAD, so we can avoid + // reading the actual data for those from the fuzzer input (which would need large amounts of + // data). + InsecureRandomContext rng(provider.ConsumeIntegral()); + + LIMITED_WHILE(provider.ConsumeBool(), 10000) + { + // Mode: + // - Bit 0: whether to use single-plain Encrypt/Decrypt; otherwise use a split at prefix. + // - Bit 2: whether this ciphertext will be corrupted (making it the last sent one) + // - Bit 3-4: controls the maximum aad length (max 511 bytes) + // - Bit 5-7: controls the maximum content length (max 16383 bytes, for performance reasons) + unsigned mode = provider.ConsumeIntegral(); + bool use_splits = mode & 1; + bool damage = mode & 4; + unsigned aad_length_bits = 3 * ((mode >> 3) & 3); + unsigned aad_length = provider.ConsumeIntegralInRange(0, (1 << aad_length_bits) - 1); + unsigned length_bits = 2 * ((mode >> 5) & 7); + unsigned length = provider.ConsumeIntegralInRange(0, (1 << length_bits) - 1); + // Generate aad and content. + auto aad = rng.randbytes(aad_length); + auto plain = rng.randbytes(length); + std::vector cipher(length + AEADChaCha20Poly1305::EXPANSION); + // Generate nonce + AEADChaCha20Poly1305::Nonce96 nonce = {(uint32_t)rng(), rng()}; + + if (use_splits && length > 0) { + size_t split_index = provider.ConsumeIntegralInRange(1, length); + aead.Encrypt(Span{plain}.first(split_index), Span{plain}.subspan(split_index), aad, nonce, cipher); + } else { + aead.Encrypt(plain, aad, nonce, cipher); + } + + // Test Keystream output + std::vector keystream(length); + aead.Keystream(nonce, keystream); + for (size_t i = 0; i < length; ++i) { + assert((plain[i] ^ keystream[i]) == cipher[i]); + } + + std::vector decrypted_contents(length); + bool ok{false}; + + // damage the key + unsigned key_position = provider.ConsumeIntegralInRange(0, 31); + std::byte damage_val{(uint8_t)(1U << (key_position & 7))}; + std::vector bad_key = key; + bad_key[key_position] ^= damage_val; + + AEADChaCha20Poly1305 bad_aead(bad_key); + ok = bad_aead.Decrypt(cipher, aad, nonce, decrypted_contents); + assert(!ok); + + // Optionally damage 1 bit in either the cipher (corresponding to a change in transit) + // or the aad (to make sure that decryption will fail if the AAD mismatches). + if (damage) { + unsigned damage_bit = provider.ConsumeIntegralInRange(0, (cipher.size() + aad.size()) * 8U - 1U); + unsigned damage_pos = damage_bit >> 3; + std::byte damage_val{(uint8_t)(1U << (damage_bit & 7))}; + if (damage_pos >= cipher.size()) { + aad[damage_pos - cipher.size()] ^= damage_val; + } else { + cipher[damage_pos] ^= damage_val; + } + } + + if (use_splits && length > 0) { + size_t split_index = provider.ConsumeIntegralInRange(1, length); + ok = aead.Decrypt(cipher, aad, nonce, Span{decrypted_contents}.first(split_index), Span{decrypted_contents}.subspan(split_index)); + } else { + ok = aead.Decrypt(cipher, aad, nonce, decrypted_contents); + } + + // Decryption *must* fail if the packet was damaged, and succeed if it wasn't. + assert(!ok == damage); + if (!ok) break; + assert(decrypted_contents == plain); + } +} From 8607773750e60931e51a33e48cd077a1dedf9db3 Mon Sep 17 00:00:00 2001 From: stratospher <44024636+stratospher@users.noreply.github.com> Date: Sun, 13 Aug 2023 09:30:19 +0530 Subject: [PATCH 2/2] Add fuzz test for FSChaCha20Poly1305 --- src/test/fuzz/crypto_chacha20poly1305.cpp | 97 +++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/test/fuzz/crypto_chacha20poly1305.cpp b/src/test/fuzz/crypto_chacha20poly1305.cpp index 96188d2230d..2b39a060948 100644 --- a/src/test/fuzz/crypto_chacha20poly1305.cpp +++ b/src/test/fuzz/crypto_chacha20poly1305.cpp @@ -13,6 +13,18 @@ #include #include +constexpr static inline void crypt_till_rekey(FSChaCha20Poly1305& aead, int rekey_interval, bool encrypt) +{ + for (int i = 0; i < rekey_interval; ++i) { + std::byte dummy_tag[FSChaCha20Poly1305::EXPANSION] = {{}}; + if (encrypt) { + aead.Encrypt(Span{dummy_tag}.first(0), Span{dummy_tag}.first(0), dummy_tag); + } else { + aead.Decrypt(dummy_tag, Span{dummy_tag}.first(0), Span{dummy_tag}.first(0)); + } + } +} + FUZZ_TARGET(crypto_aeadchacha20poly1305) { FuzzedDataProvider provider{buffer.data(), buffer.size()}; @@ -101,3 +113,88 @@ FUZZ_TARGET(crypto_aeadchacha20poly1305) assert(decrypted_contents == plain); } } + +FUZZ_TARGET(crypto_fschacha20poly1305) +{ + FuzzedDataProvider provider{buffer.data(), buffer.size()}; + + uint32_t rekey_interval = provider.ConsumeIntegralInRange(32, 512); + auto key = provider.ConsumeBytes(32); + key.resize(32); + FSChaCha20Poly1305 enc_aead(key, rekey_interval); + FSChaCha20Poly1305 dec_aead(key, rekey_interval); + + // Initialize RNG deterministically, to generate contents and AAD. We assume that there are no + // (potentially buggy) edge cases triggered by specific values of contents/AAD, so we can avoid + // reading the actual data for those from the fuzzer input (which would need large amounts of + // data). + InsecureRandomContext rng(provider.ConsumeIntegral()); + + LIMITED_WHILE(provider.ConsumeBool(), 10000) + { + // Mode: + // - Bit 0: whether to use single-plain Encrypt/Decrypt; otherwise use a split at prefix. + // - Bit 2: whether this ciphertext will be corrupted (making it the last sent one) + // - Bit 3-4: controls the maximum aad length (max 511 bytes) + // - Bit 5-7: controls the maximum content length (max 16383 bytes, for performance reasons) + unsigned mode = provider.ConsumeIntegral(); + bool use_splits = mode & 1; + bool damage = mode & 4; + unsigned aad_length_bits = 3 * ((mode >> 3) & 3); + unsigned aad_length = provider.ConsumeIntegralInRange(0, (1 << aad_length_bits) - 1); + unsigned length_bits = 2 * ((mode >> 5) & 7); + unsigned length = provider.ConsumeIntegralInRange(0, (1 << length_bits) - 1); + // Generate aad and content. + auto aad = rng.randbytes(aad_length); + auto plain = rng.randbytes(length); + std::vector cipher(length + FSChaCha20Poly1305::EXPANSION); + + crypt_till_rekey(enc_aead, rekey_interval, true); + if (use_splits && length > 0) { + size_t split_index = provider.ConsumeIntegralInRange(1, length); + enc_aead.Encrypt(Span{plain}.first(split_index), Span{plain}.subspan(split_index), aad, cipher); + } else { + enc_aead.Encrypt(plain, aad, cipher); + } + + std::vector decrypted_contents(length); + bool ok{false}; + + // damage the key + unsigned key_position = provider.ConsumeIntegralInRange(0, 31); + std::byte damage_val{(uint8_t)(1U << (key_position & 7))}; + std::vector bad_key = key; + bad_key[key_position] ^= damage_val; + + FSChaCha20Poly1305 bad_fs_aead(bad_key, rekey_interval); + crypt_till_rekey(bad_fs_aead, rekey_interval, false); + ok = bad_fs_aead.Decrypt(cipher, aad, decrypted_contents); + assert(!ok); + + // Optionally damage 1 bit in either the cipher (corresponding to a change in transit) + // or the aad (to make sure that decryption will fail if the AAD mismatches). + if (damage) { + unsigned damage_bit = provider.ConsumeIntegralInRange(0, (cipher.size() + aad.size()) * 8U - 1U); + unsigned damage_pos = damage_bit >> 3; + std::byte damage_val{(uint8_t)(1U << (damage_bit & 7))}; + if (damage_pos >= cipher.size()) { + aad[damage_pos - cipher.size()] ^= damage_val; + } else { + cipher[damage_pos] ^= damage_val; + } + } + + crypt_till_rekey(dec_aead, rekey_interval, false); + if (use_splits && length > 0) { + size_t split_index = provider.ConsumeIntegralInRange(1, length); + ok = dec_aead.Decrypt(cipher, aad, Span{decrypted_contents}.first(split_index), Span{decrypted_contents}.subspan(split_index)); + } else { + ok = dec_aead.Decrypt(cipher, aad, decrypted_contents); + } + + // Decryption *must* fail if the packet was damaged, and succeed if it wasn't. + assert(!ok == damage); + if (!ok) break; + assert(decrypted_contents == plain); + } +}