From 9fd085a1a49d317abcaf1492b71c48bf1a1b3007 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Wed, 28 Jun 2023 14:09:53 -0400 Subject: [PATCH 1/8] crypto: remove outdated variant of ChaCha20Poly1305 AEAD Remove the variant of ChaCha20Poly1305 AEAD that was previously added in anticipation of BIP324 using it. BIP324 was updated to instead use rekeying wrappers around otherwise unmodified versions of the ChaCha20 stream cipher and the ChaCha20Poly1305 AEAD as specified in RFC8439. --- src/Makefile.am | 2 - src/Makefile.bench.include | 1 - src/Makefile.test.include | 1 - src/bench/chacha_poly_aead.cpp | 126 --------------- src/crypto/chacha_poly_aead.cpp | 132 ---------------- src/crypto/chacha_poly_aead.h | 146 ------------------ src/test/crypto_tests.cpp | 126 +-------------- .../fuzz/crypto_chacha20_poly1305_aead.cpp | 72 --------- 8 files changed, 1 insertion(+), 605 deletions(-) delete mode 100644 src/bench/chacha_poly_aead.cpp delete mode 100644 src/crypto/chacha_poly_aead.cpp delete mode 100644 src/crypto/chacha_poly_aead.h delete mode 100644 src/test/fuzz/crypto_chacha20_poly1305_aead.cpp diff --git a/src/Makefile.am b/src/Makefile.am index dfea7146aad..d8ea4bbd285 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -546,8 +546,6 @@ crypto_libbitcoin_crypto_base_la_LDFLAGS = $(AM_LDFLAGS) -static crypto_libbitcoin_crypto_base_la_SOURCES = \ crypto/aes.cpp \ crypto/aes.h \ - crypto/chacha_poly_aead.h \ - crypto/chacha_poly_aead.cpp \ crypto/chacha20.h \ crypto/chacha20.cpp \ crypto/common.h \ diff --git a/src/Makefile.bench.include b/src/Makefile.bench.include index 10c8389c800..25a17c43ed3 100644 --- a/src/Makefile.bench.include +++ b/src/Makefile.bench.include @@ -22,7 +22,6 @@ bench_bench_bitcoin_SOURCES = \ bench/block_assemble.cpp \ bench/ccoins_caching.cpp \ bench/chacha20.cpp \ - bench/chacha_poly_aead.cpp \ bench/checkblock.cpp \ bench/checkqueue.cpp \ bench/crypto_hash.cpp \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 224f1fe301a..d3d2464ce67 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -261,7 +261,6 @@ test_fuzz_fuzz_SOURCES = \ test/fuzz/crypto_aes256.cpp \ test/fuzz/crypto_aes256cbc.cpp \ test/fuzz/crypto_chacha20.cpp \ - test/fuzz/crypto_chacha20_poly1305_aead.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/bench/chacha_poly_aead.cpp b/src/bench/chacha_poly_aead.cpp deleted file mode 100644 index 9149eb683a5..00000000000 --- a/src/bench/chacha_poly_aead.cpp +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) 2019-2022 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 // for the POLY1305_TAGLEN constant -#include - -#include -#include - -/* Number of bytes to process per iteration */ -static constexpr uint64_t BUFFER_SIZE_TINY = 64; -static constexpr uint64_t BUFFER_SIZE_SMALL = 256; -static constexpr uint64_t BUFFER_SIZE_LARGE = 1024 * 1024; - -static const unsigned char k1[32] = {0}; -static const unsigned char k2[32] = {0}; - -static ChaCha20Poly1305AEAD aead(k1, 32, k2, 32); - -static void CHACHA20_POLY1305_AEAD(benchmark::Bench& bench, size_t buffersize, bool include_decryption) -{ - std::vector in(buffersize + CHACHA20_POLY1305_AEAD_AAD_LEN + Poly1305::TAGLEN, 0); - std::vector out(buffersize + CHACHA20_POLY1305_AEAD_AAD_LEN + Poly1305::TAGLEN, 0); - uint64_t seqnr_payload = 0; - uint64_t seqnr_aad = 0; - int aad_pos = 0; - uint32_t len = 0; - bench.batch(buffersize).unit("byte").run([&] { - // encrypt or decrypt the buffer with a static key - const bool crypt_ok_1 = aead.Crypt(seqnr_payload, seqnr_aad, aad_pos, out.data(), out.size(), in.data(), buffersize, true); - assert(crypt_ok_1); - - if (include_decryption) { - // if we decrypt, include the GetLength - const bool get_length_ok = aead.GetLength(&len, seqnr_aad, aad_pos, in.data()); - assert(get_length_ok); - const bool crypt_ok_2 = aead.Crypt(seqnr_payload, seqnr_aad, aad_pos, out.data(), out.size(), in.data(), buffersize, true); - assert(crypt_ok_2); - } - - // increase main sequence number - seqnr_payload++; - // increase aad position (position in AAD keystream) - aad_pos += CHACHA20_POLY1305_AEAD_AAD_LEN; - if (aad_pos + CHACHA20_POLY1305_AEAD_AAD_LEN > CHACHA20_ROUND_OUTPUT) { - aad_pos = 0; - seqnr_aad++; - } - if (seqnr_payload + 1 == std::numeric_limits::max()) { - // reuse of nonce+key is okay while benchmarking. - seqnr_payload = 0; - seqnr_aad = 0; - aad_pos = 0; - } - }); -} - -static void CHACHA20_POLY1305_AEAD_64BYTES_ONLY_ENCRYPT(benchmark::Bench& bench) -{ - CHACHA20_POLY1305_AEAD(bench, BUFFER_SIZE_TINY, false); -} - -static void CHACHA20_POLY1305_AEAD_256BYTES_ONLY_ENCRYPT(benchmark::Bench& bench) -{ - CHACHA20_POLY1305_AEAD(bench, BUFFER_SIZE_SMALL, false); -} - -static void CHACHA20_POLY1305_AEAD_1MB_ONLY_ENCRYPT(benchmark::Bench& bench) -{ - CHACHA20_POLY1305_AEAD(bench, BUFFER_SIZE_LARGE, false); -} - -static void CHACHA20_POLY1305_AEAD_64BYTES_ENCRYPT_DECRYPT(benchmark::Bench& bench) -{ - CHACHA20_POLY1305_AEAD(bench, BUFFER_SIZE_TINY, true); -} - -static void CHACHA20_POLY1305_AEAD_256BYTES_ENCRYPT_DECRYPT(benchmark::Bench& bench) -{ - CHACHA20_POLY1305_AEAD(bench, BUFFER_SIZE_SMALL, true); -} - -static void CHACHA20_POLY1305_AEAD_1MB_ENCRYPT_DECRYPT(benchmark::Bench& bench) -{ - CHACHA20_POLY1305_AEAD(bench, BUFFER_SIZE_LARGE, true); -} - -// Add Hash() (dbl-sha256) bench for comparison - -static void HASH(benchmark::Bench& bench, size_t buffersize) -{ - uint8_t hash[CHash256::OUTPUT_SIZE]; - std::vector in(buffersize,0); - bench.batch(in.size()).unit("byte").run([&] { - CHash256().Write(in).Finalize(hash); - }); -} - -static void HASH_64BYTES(benchmark::Bench& bench) -{ - HASH(bench, BUFFER_SIZE_TINY); -} - -static void HASH_256BYTES(benchmark::Bench& bench) -{ - HASH(bench, BUFFER_SIZE_SMALL); -} - -static void HASH_1MB(benchmark::Bench& bench) -{ - HASH(bench, BUFFER_SIZE_LARGE); -} - -BENCHMARK(CHACHA20_POLY1305_AEAD_64BYTES_ONLY_ENCRYPT, benchmark::PriorityLevel::HIGH); -BENCHMARK(CHACHA20_POLY1305_AEAD_256BYTES_ONLY_ENCRYPT, benchmark::PriorityLevel::HIGH); -BENCHMARK(CHACHA20_POLY1305_AEAD_1MB_ONLY_ENCRYPT, benchmark::PriorityLevel::HIGH); -BENCHMARK(CHACHA20_POLY1305_AEAD_64BYTES_ENCRYPT_DECRYPT, benchmark::PriorityLevel::HIGH); -BENCHMARK(CHACHA20_POLY1305_AEAD_256BYTES_ENCRYPT_DECRYPT, benchmark::PriorityLevel::HIGH); -BENCHMARK(CHACHA20_POLY1305_AEAD_1MB_ENCRYPT_DECRYPT, benchmark::PriorityLevel::HIGH); -BENCHMARK(HASH_64BYTES, benchmark::PriorityLevel::HIGH); -BENCHMARK(HASH_256BYTES, benchmark::PriorityLevel::HIGH); -BENCHMARK(HASH_1MB, benchmark::PriorityLevel::HIGH); diff --git a/src/crypto/chacha_poly_aead.cpp b/src/crypto/chacha_poly_aead.cpp deleted file mode 100644 index 0d82cf3d741..00000000000 --- a/src/crypto/chacha_poly_aead.cpp +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) 2019-2022 The Bitcoin Core developers -// Distributed under the MIT software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. - -#if defined(HAVE_CONFIG_H) -#include -#endif - -#include - -#include -#include - -#include -#include - -#include -#include - -#ifndef HAVE_TIMINGSAFE_BCMP - -int timingsafe_bcmp(const unsigned char* b1, const unsigned char* b2, size_t n) -{ - const unsigned char *p1 = b1, *p2 = b2; - int ret = 0; - - for (; n > 0; n--) - ret |= *p1++ ^ *p2++; - return (ret != 0); -} - -#endif // TIMINGSAFE_BCMP - -ChaCha20Poly1305AEAD::ChaCha20Poly1305AEAD(const unsigned char* K_1, size_t K_1_len, const unsigned char* K_2, size_t K_2_len) -{ - assert(K_1_len == CHACHA20_POLY1305_AEAD_KEY_LEN); - assert(K_2_len == CHACHA20_POLY1305_AEAD_KEY_LEN); - - static_assert(CHACHA20_POLY1305_AEAD_KEY_LEN == 32); - m_chacha_header.SetKey32(K_1); - m_chacha_main.SetKey32(K_2); - - // set the cached sequence number to uint64 max which hints for an unset cache. - // we can't hit uint64 max since the rekey rule (which resets the sequence number) is 1GB - m_cached_aad_seqnr = std::numeric_limits::max(); -} - -bool ChaCha20Poly1305AEAD::Crypt(uint64_t seqnr_payload, uint64_t seqnr_aad, int aad_pos, unsigned char* dest, size_t dest_len /* length of the output buffer for sanity checks */, const unsigned char* src, size_t src_len, bool is_encrypt) -{ - // check buffer boundaries - if ( - // if we encrypt, make sure the source contains at least the expected AAD and the destination has at least space for the source + MAC - (is_encrypt && (src_len < CHACHA20_POLY1305_AEAD_AAD_LEN || dest_len < src_len + Poly1305::TAGLEN)) || - // if we decrypt, make sure the source contains at least the expected AAD+MAC and the destination has at least space for the source - MAC - (!is_encrypt && (src_len < CHACHA20_POLY1305_AEAD_AAD_LEN + Poly1305::TAGLEN || dest_len < src_len - Poly1305::TAGLEN))) { - return false; - } - - unsigned char expected_tag[Poly1305::TAGLEN], poly_key[Poly1305::KEYLEN]; - memset(poly_key, 0, sizeof(poly_key)); - - // block counter 0 for the poly1305 key - // use lower 32bytes for the poly1305 key - // (throws away 32 unused bytes (upper 32) from this ChaCha20 round) - m_chacha_main.Seek64({0, seqnr_payload}, 0); - m_chacha_main.Crypt(poly_key, poly_key, sizeof(poly_key)); - - // if decrypting, verify the tag prior to decryption - if (!is_encrypt) { - const unsigned char* tag = src + src_len - Poly1305::TAGLEN; - Poly1305{MakeByteSpan(poly_key)} - .Update(AsBytes(Span{src, src_len - Poly1305::TAGLEN})) - .Finalize(MakeWritableByteSpan(expected_tag)); - - // constant time compare the calculated MAC with the provided MAC - if (timingsafe_bcmp(expected_tag, tag, Poly1305::TAGLEN) != 0) { - memory_cleanse(expected_tag, sizeof(expected_tag)); - memory_cleanse(poly_key, sizeof(poly_key)); - return false; - } - memory_cleanse(expected_tag, sizeof(expected_tag)); - // MAC has been successfully verified, make sure we don't convert it in decryption - src_len -= Poly1305::TAGLEN; - } - - // calculate and cache the next 64byte keystream block if requested sequence number is not yet the cache - if (m_cached_aad_seqnr != seqnr_aad) { - m_cached_aad_seqnr = seqnr_aad; - m_chacha_header.Seek64({0, seqnr_aad}, 0); - m_chacha_header.Keystream(m_aad_keystream_buffer, CHACHA20_ROUND_OUTPUT); - } - // crypt the AAD (3 bytes message length) with given position in AAD cipher instance keystream - dest[0] = src[0] ^ m_aad_keystream_buffer[aad_pos]; - dest[1] = src[1] ^ m_aad_keystream_buffer[aad_pos + 1]; - dest[2] = src[2] ^ m_aad_keystream_buffer[aad_pos + 2]; - - // Set the playload ChaCha instance block counter to 1 and crypt the payload - m_chacha_main.Seek64({0, seqnr_payload}, 1); - m_chacha_main.Crypt(src + CHACHA20_POLY1305_AEAD_AAD_LEN, dest + CHACHA20_POLY1305_AEAD_AAD_LEN, src_len - CHACHA20_POLY1305_AEAD_AAD_LEN); - - // If encrypting, calculate and append tag - if (is_encrypt) { - // the poly1305 tag expands over the AAD (3 bytes length) & encrypted payload - Poly1305{MakeByteSpan(poly_key)} - .Update(AsBytes(Span{dest, src_len})) - .Finalize(AsWritableBytes(Span{dest + src_len, Poly1305::TAGLEN})); - } - - // cleanse no longer required MAC and polykey - memory_cleanse(poly_key, sizeof(poly_key)); - return true; -} - -bool ChaCha20Poly1305AEAD::GetLength(uint32_t* len24_out, uint64_t seqnr_aad, int aad_pos, const uint8_t* ciphertext) -{ - // enforce valid aad position to avoid accessing outside of the 64byte keystream cache - // (there is space for 21 times 3 bytes) - assert(aad_pos >= 0 && aad_pos < CHACHA20_ROUND_OUTPUT - CHACHA20_POLY1305_AEAD_AAD_LEN); - if (m_cached_aad_seqnr != seqnr_aad) { - // we need to calculate the 64 keystream bytes since we reached a new aad sequence number - m_cached_aad_seqnr = seqnr_aad; - m_chacha_header.Seek64({0, seqnr_aad}, 0); // use LE for the nonce - m_chacha_header.Keystream(m_aad_keystream_buffer, CHACHA20_ROUND_OUTPUT); // write keystream to the cache - } - - // decrypt the ciphertext length by XORing the right position of the 64byte keystream cache with the ciphertext - *len24_out = (ciphertext[0] ^ m_aad_keystream_buffer[aad_pos + 0]) | - (ciphertext[1] ^ m_aad_keystream_buffer[aad_pos + 1]) << 8 | - (ciphertext[2] ^ m_aad_keystream_buffer[aad_pos + 2]) << 16; - - return true; -} diff --git a/src/crypto/chacha_poly_aead.h b/src/crypto/chacha_poly_aead.h deleted file mode 100644 index 5d57b5a5e21..00000000000 --- a/src/crypto/chacha_poly_aead.h +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) 2019-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. - -#ifndef BITCOIN_CRYPTO_CHACHA_POLY_AEAD_H -#define BITCOIN_CRYPTO_CHACHA_POLY_AEAD_H - -#include - -#include - -static constexpr int CHACHA20_POLY1305_AEAD_KEY_LEN = 32; -static constexpr int CHACHA20_POLY1305_AEAD_AAD_LEN = 3; /* 3 bytes length */ -static constexpr int CHACHA20_ROUND_OUTPUT = 64; /* 64 bytes per round */ -static constexpr int AAD_PACKAGES_PER_ROUND = 21; /* 64 / 3 round down*/ - -/* A AEAD class for ChaCha20-Poly1305@bitcoin. - * - * ChaCha20 is a stream cipher designed by Daniel Bernstein and described in - * [https://cr.yp.to/chacha/chacha-20080128.pdf ChaCha20]. It operates - * by permuting 128 fixed bits, 128 or 256 bits of key, a 64 bit nonce and a 64 - * bit counter into 64 bytes of output. This output is used as a keystream, with - * any unused bytes simply discarded. - * - * Poly1305 [https://cr.yp.to/mac/poly1305-20050329.pdf Poly1305], also - * by Daniel Bernstein, is a one-time Carter-Wegman MAC that computes a 128 bit - * integrity tag given a message and a single-use 256 bit secret key. - * - * The chacha20-poly1305@bitcoin combines these two primitives into an - * authenticated encryption mode. The construction used is based on that proposed - * for TLS by Adam Langley in - * [http://tools.ietf.org/html/draft-agl-tls-chacha20poly1305-03 "ChaCha20 - * and Poly1305 based Cipher Suites for TLS", Adam Langley], but differs in - * the layout of data passed to the MAC and in the addition of encryption of the - * packet lengths. - * - * ==== Detailed Construction ==== - * - * The chacha20-poly1305@bitcoin cipher requires two 256 bits of key material as - * output from the key exchange. Each key (K_1 and K_2) are used by two separate - * instances of chacha20. - * - * The instance keyed by K_1 is a stream cipher that is used only to encrypt the 3 - * byte packet length field and has its own sequence number. The second instance, - * keyed by K_2, is used in conjunction with poly1305 to build an AEAD - * (Authenticated Encryption with Associated Data) that is used to encrypt and - * authenticate the entire packet. - * - * Two separate cipher instances are used here so as to keep the packet lengths - * confidential but not create an oracle for the packet payload cipher by - * decrypting and using the packet length prior to checking the MAC. By using an - * independently-keyed cipher instance to encrypt the length, an active attacker - * seeking to exploit the packet input handling as a decryption oracle can learn - * nothing about the payload contents or its MAC (assuming key derivation, - * ChaCha20 and Poly1305 are secure). - * - * The AEAD is constructed as follows: for each packet, generate a Poly1305 key by - * taking the first 256 bits of ChaCha20 stream output generated using K_2, an IV - * consisting of the packet sequence number encoded as an LE uint64 and a ChaCha20 - * block counter of zero. The K_2 ChaCha20 block counter is then set to the - * little-endian encoding of 1 (i.e. {1, 0, 0, 0, 0, 0, 0, 0}) and this instance - * is used for encryption of the packet payload. - * - * ==== Packet Handling ==== - * - * When receiving a packet, the length must be decrypted first. When 3 bytes of - * ciphertext length have been received, they may be decrypted. - * - * A ChaCha20 round always calculates 64bytes which is sufficient to crypt 21 - * times a 3 bytes length field (21*3 = 63). The length field sequence number can - * thus be used 21 times (keystream caching). - * - * The length field must be enc-/decrypted with the ChaCha20 keystream keyed with - * K_1 defined by block counter 0, the length field sequence number in little - * endian and a keystream position from 0 to 60. - * - * Once the entire packet has been received, the MAC MUST be checked before - * decryption. A per-packet Poly1305 key is generated as described above and the - * MAC tag calculated using Poly1305 with this key over the ciphertext of the - * packet length and the payload together. The calculated MAC is then compared in - * constant time with the one appended to the packet and the packet decrypted - * using ChaCha20 as described above (with K_2, the packet sequence number as - * nonce and a starting block counter of 1). - * - * Detection of an invalid MAC MUST lead to immediate connection termination. - * - * To send a packet, first encode the 3 byte length and encrypt it using K_1 as - * described above. Encrypt the packet payload (using K_2) and append it to the - * encrypted length. Finally, calculate a MAC tag and append it. - * - * The initiating peer MUST use K_1_A, K_2_A to encrypt messages on - * the send channel, K_1_B, K_2_B MUST be used to decrypt messages on - * the receive channel. - * - * The responding peer MUST use K_1_A, K_2_A to decrypt messages on - * the receive channel, K_1_B, K_2_B MUST be used to encrypt messages - * on the send channel. - * - * Optimized implementations of ChaCha20-Poly1305@bitcoin are relatively fast in - * general, therefore it is very likely that encrypted messages require not more - * CPU cycles per bytes then the current unencrypted p2p message format - * (ChaCha20/Poly1305 versus double SHA256). - * - * The initial packet sequence numbers are 0. - * - * K_2 ChaCha20 cipher instance (payload) must never reuse a {key, nonce} for - * encryption nor may it be used to encrypt more than 2^70 bytes under the same - * {key, nonce}. - * - * K_1 ChaCha20 cipher instance (length field/AAD) must never reuse a {key, nonce, - * position-in-keystream} for encryption nor may it be used to encrypt more than - * 2^70 bytes under the same {key, nonce}. - * - * We use message sequence numbers for both communication directions. - */ - -class ChaCha20Poly1305AEAD -{ -private: - ChaCha20 m_chacha_header; // AAD cipher instance (encrypted length) and poly1305 key-derivation cipher instance - ChaCha20 m_chacha_main; // payload - unsigned char m_aad_keystream_buffer[CHACHA20_ROUND_OUTPUT]; // aad keystream cache - uint64_t m_cached_aad_seqnr; // aad keystream cache hint - -public: - ChaCha20Poly1305AEAD(const unsigned char* K_1, size_t K_1_len, const unsigned char* K_2, size_t K_2_len); - - explicit ChaCha20Poly1305AEAD(const ChaCha20Poly1305AEAD&) = delete; - - /** Encrypts/decrypts a packet - seqnr_payload, the message sequence number - seqnr_aad, the messages AAD sequence number which allows reuse of the AAD keystream - aad_pos, position to use in the AAD keystream to encrypt the AAD - dest, output buffer, must be of a size equal or larger then CHACHA20_POLY1305_AEAD_AAD_LEN + payload (+ POLY1305_TAG_LEN in encryption) bytes - destlen, length of the destination buffer - src, the AAD+payload to encrypt or the AAD+payload+MAC to decrypt - src_len, the length of the source buffer - is_encrypt, set to true if we encrypt (creates and appends the MAC instead of verifying it) - */ - bool Crypt(uint64_t seqnr_payload, uint64_t seqnr_aad, int aad_pos, unsigned char* dest, size_t dest_len, const unsigned char* src, size_t src_len, bool is_encrypt); - - /** decrypts the 3 bytes AAD data and decodes it into a uint32_t field */ - bool GetLength(uint32_t* len24_out, uint64_t seqnr_aad, int aad_pos, const uint8_t* ciphertext); -}; - -#endif // BITCOIN_CRYPTO_CHACHA_POLY_AEAD_H diff --git a/src/test/crypto_tests.cpp b/src/test/crypto_tests.cpp index 8332f545910..809b8979985 100644 --- a/src/test/crypto_tests.cpp +++ b/src/test/crypto_tests.cpp @@ -4,7 +4,6 @@ #include #include -#include #include #include #include @@ -686,7 +685,7 @@ BOOST_AUTO_TEST_CASE(chacha20_midblock) ChaCha20 c20{key.data()}; // get one block of keystream unsigned char block[64]; - c20.Keystream(block, CHACHA20_ROUND_OUTPUT); + c20.Keystream(block, sizeof(block)); unsigned char b1[5], b2[7], b3[52]; c20 = ChaCha20{key.data()}; c20.Keystream(b1, 5); @@ -839,129 +838,6 @@ BOOST_AUTO_TEST_CASE(hkdf_hmac_sha256_l32_tests) "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d"); } -static void TestChaCha20Poly1305AEAD(bool must_succeed, unsigned int expected_aad_length, const std::string& hex_m, const std::string& hex_k1, const std::string& hex_k2, const std::string& hex_aad_keystream, const std::string& hex_encrypted_message, const std::string& hex_encrypted_message_seq_999) -{ - // we need two sequence numbers, one for the payload cipher instance... - uint32_t seqnr_payload = 0; - // ... and one for the AAD (length) cipher instance - uint32_t seqnr_aad = 0; - // we need to keep track of the position in the AAD cipher instance - // keystream since we use the same 64byte output 21 times - // (21 times 3 bytes length < 64) - int aad_pos = 0; - - std::vector aead_K_1 = ParseHex(hex_k1); - std::vector aead_K_2 = ParseHex(hex_k2); - std::vector plaintext_buf = ParseHex(hex_m); - std::vector expected_aad_keystream = ParseHex(hex_aad_keystream); - std::vector expected_ciphertext_and_mac = ParseHex(hex_encrypted_message); - std::vector expected_ciphertext_and_mac_sequence999 = ParseHex(hex_encrypted_message_seq_999); - - std::vector ciphertext_buf(plaintext_buf.size() + Poly1305::TAGLEN, 0); - std::vector plaintext_buf_new(plaintext_buf.size(), 0); - std::vector cmp_ctx_buffer(64); - uint32_t out_len = 0; - - // create the AEAD instance - ChaCha20Poly1305AEAD aead(aead_K_1.data(), aead_K_1.size(), aead_K_2.data(), aead_K_2.size()); - - // create a chacha20 instance to compare against - ChaCha20 cmp_ctx(aead_K_1.data()); - - // encipher - bool res = aead.Crypt(seqnr_payload, seqnr_aad, aad_pos, ciphertext_buf.data(), ciphertext_buf.size(), plaintext_buf.data(), plaintext_buf.size(), true); - // make sure the operation succeeded if expected to succeed - BOOST_CHECK_EQUAL(res, must_succeed); - if (!res) return; - - // verify ciphertext & mac against the test vector - BOOST_CHECK_EQUAL(expected_ciphertext_and_mac.size(), ciphertext_buf.size()); - BOOST_CHECK(memcmp(ciphertext_buf.data(), expected_ciphertext_and_mac.data(), ciphertext_buf.size()) == 0); - - // manually construct the AAD keystream - cmp_ctx.Seek64({0, seqnr_aad}, 0); - cmp_ctx.Keystream(cmp_ctx_buffer.data(), 64); - BOOST_CHECK(memcmp(expected_aad_keystream.data(), cmp_ctx_buffer.data(), expected_aad_keystream.size()) == 0); - // crypt the 3 length bytes and compare the length - uint32_t len_cmp = 0; - len_cmp = (ciphertext_buf[0] ^ cmp_ctx_buffer[aad_pos + 0]) | - (ciphertext_buf[1] ^ cmp_ctx_buffer[aad_pos + 1]) << 8 | - (ciphertext_buf[2] ^ cmp_ctx_buffer[aad_pos + 2]) << 16; - BOOST_CHECK_EQUAL(len_cmp, expected_aad_length); - - // encrypt / decrypt 1000 packets - for (size_t i = 0; i < 1000; ++i) { - res = aead.Crypt(seqnr_payload, seqnr_aad, aad_pos, ciphertext_buf.data(), ciphertext_buf.size(), plaintext_buf.data(), plaintext_buf.size(), true); - BOOST_CHECK(res); - BOOST_CHECK(aead.GetLength(&out_len, seqnr_aad, aad_pos, ciphertext_buf.data())); - BOOST_CHECK_EQUAL(out_len, expected_aad_length); - res = aead.Crypt(seqnr_payload, seqnr_aad, aad_pos, plaintext_buf_new.data(), plaintext_buf_new.size(), ciphertext_buf.data(), ciphertext_buf.size(), false); - BOOST_CHECK(res); - - // make sure we repetitive get the same plaintext - BOOST_CHECK(memcmp(plaintext_buf.data(), plaintext_buf_new.data(), plaintext_buf.size()) == 0); - - // compare sequence number 999 against the test vector - if (seqnr_payload == 999) { - BOOST_CHECK(memcmp(ciphertext_buf.data(), expected_ciphertext_and_mac_sequence999.data(), expected_ciphertext_and_mac_sequence999.size()) == 0); - } - // set nonce and block counter, output the keystream - cmp_ctx.Seek64({0, seqnr_aad}, 0); - cmp_ctx.Keystream(cmp_ctx_buffer.data(), 64); - - // crypt the 3 length bytes and compare the length - len_cmp = 0; - len_cmp = (ciphertext_buf[0] ^ cmp_ctx_buffer[aad_pos + 0]) | - (ciphertext_buf[1] ^ cmp_ctx_buffer[aad_pos + 1]) << 8 | - (ciphertext_buf[2] ^ cmp_ctx_buffer[aad_pos + 2]) << 16; - BOOST_CHECK_EQUAL(len_cmp, expected_aad_length); - - // increment the sequence number(s) - // always increment the payload sequence number - // increment the AAD keystream position by its size (3) - // increment the AAD sequence number if we would hit the 64 byte limit - seqnr_payload++; - aad_pos += CHACHA20_POLY1305_AEAD_AAD_LEN; - if (aad_pos + CHACHA20_POLY1305_AEAD_AAD_LEN > CHACHA20_ROUND_OUTPUT) { - aad_pos = 0; - seqnr_aad++; - } - } -} - -BOOST_AUTO_TEST_CASE(chacha20_poly1305_aead_testvector) -{ - /* test chacha20poly1305@bitcoin AEAD */ - - // must fail with no message - TestChaCha20Poly1305AEAD(false, 0, - "", - "0000000000000000000000000000000000000000000000000000000000000000", - "0000000000000000000000000000000000000000000000000000000000000000", "", "", ""); - - TestChaCha20Poly1305AEAD(true, 0, - /* m */ "0000000000000000000000000000000000000000000000000000000000000000", - /* k1 (AAD) */ "0000000000000000000000000000000000000000000000000000000000000000", - /* k2 (payload) */ "0000000000000000000000000000000000000000000000000000000000000000", - /* AAD keystream */ "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586", - /* encrypted message & MAC */ "76b8e09f07e7be5551387a98ba977c732d080dcb0f29a048e3656912c6533e32d2fc11829c1b6c1df1f551cd6131ff08", - /* encrypted message & MAC at sequence 999 */ "b0a03d5bd2855d60699e7d3a3133fa47be740fe4e4c1f967555e2d9271f31c3aaa7aa16ec62c5e24f040c08bb20c3598"); - TestChaCha20Poly1305AEAD(true, 1, - "0100000000000000000000000000000000000000000000000000000000000000", - "0000000000000000000000000000000000000000000000000000000000000000", - "0000000000000000000000000000000000000000000000000000000000000000", - "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586", - "77b8e09f07e7be5551387a98ba977c732d080dcb0f29a048e3656912c6533e32baf0c85b6dff8602b06cf52a6aefc62e", - "b1a03d5bd2855d60699e7d3a3133fa47be740fe4e4c1f967555e2d9271f31c3a8bd94d54b5ecabbc41ffbb0c90924080"); - TestChaCha20Poly1305AEAD(true, 255, - "ff0000f195e66982105ffb640bb7757f579da31602fc93ec01ac56f85ac3c134a4547b733b46413042c9440049176905d3be59ea1c53f15916155c2be8241a38008b9a26bc35941e2444177c8ade6689de95264986d95889fb60e84629c9bd9a5acb1cc118be563eb9b3a4a472f82e09a7e778492b562ef7130e88dfe031c79db9d4f7c7a899151b9a475032b63fc385245fe054e3dd5a97a5f576fe064025d3ce042c566ab2c507b138db853e3d6959660996546cc9c4a6eafdc777c040d70eaf46f76dad3979e5c5360c3317166a1c894c94a371876a94df7628fe4eaaf2ccb27d5aaae0ad7ad0f9d4b6ad3b54098746d4524d38407a6deb3ab78fab78c9", - "ff0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", - "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", - "c640c1711e3ee904ac35c57ab9791c8a1c408603a90b77a83b54f6c844cb4b06d94e7fc6c800e165acd66147e80ec45a567f6ce66d05ec0cae679dceeb890017", - "3940c1e92da4582ff6f92a776aeb14d014d384eeb30f660dacf70a14a23fd31e91212701334e2ce1acf5199dc84f4d61ddbe6571bca5af874b4c9226c26e650995d157644e1848b96ed6c2102d5489a050e71d29a5a66ece11de5fb5c9558d54da28fe45b0bc4db4e5b88030bfc4a352b4b7068eccf656bae7ad6a35615315fc7c49d4200388d5eca67c2e822e069336c69b40db67e0f3c81209c50f3216a4b89fb3ae1b984b7851a2ec6f68ab12b101ab120e1ea7313bb93b5a0f71185c7fea017ddb92769861c29dba4fbc432280d5dff21b36d1c4c790128b22699950bb18bf74c448cdfe547d8ed4f657d8005fdc0cd7a050c2d46050a44c4376355858981fbe8b184288276e7a93eabc899c4a", - "f039c6689eaeef0456685200feaab9d54bbd9acde4410a3b6f4321296f4a8ca2604b49727d8892c57e005d799b2a38e85e809f20146e08eec75169691c8d4f54a0d51a1e1c7b381e0474eb02f994be9415ef3ffcbd2343f0601e1f3b172a1d494f838824e4df570f8e3b0c04e27966e36c82abd352d07054ef7bd36b84c63f9369afe7ed79b94f953873006b920c3fa251a771de1b63da927058ade119aa898b8c97e42a606b2f6df1e2d957c22f7593c1e2002f4252f4c9ae4bf773499e5cfcfe14dfc1ede26508953f88553bf4a76a802f6a0068d59295b01503fd9a600067624203e880fdf53933b96e1f4d9eb3f4e363dd8165a278ff667a41ee42b9892b077cefff92b93441f7be74cf10e6cd"); -} - BOOST_AUTO_TEST_CASE(countbits_tests) { FastRandomContext ctx; diff --git a/src/test/fuzz/crypto_chacha20_poly1305_aead.cpp b/src/test/fuzz/crypto_chacha20_poly1305_aead.cpp deleted file mode 100644 index 84ac65dd882..00000000000 --- a/src/test/fuzz/crypto_chacha20_poly1305_aead.cpp +++ /dev/null @@ -1,72 +0,0 @@ -// 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 -#include - -FUZZ_TARGET(crypto_chacha20_poly1305_aead) -{ - FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; - - const std::vector k1 = ConsumeFixedLengthByteVector(fuzzed_data_provider, CHACHA20_POLY1305_AEAD_KEY_LEN); - const std::vector k2 = ConsumeFixedLengthByteVector(fuzzed_data_provider, CHACHA20_POLY1305_AEAD_KEY_LEN); - - ChaCha20Poly1305AEAD aead(k1.data(), k1.size(), k2.data(), k2.size()); - uint64_t seqnr_payload = 0; - uint64_t seqnr_aad = 0; - int aad_pos = 0; - size_t buffer_size = fuzzed_data_provider.ConsumeIntegralInRange(0, 4096); - std::vector in(buffer_size + CHACHA20_POLY1305_AEAD_AAD_LEN + Poly1305::TAGLEN, 0); - std::vector out(buffer_size + CHACHA20_POLY1305_AEAD_AAD_LEN + Poly1305::TAGLEN, 0); - bool is_encrypt = fuzzed_data_provider.ConsumeBool(); - LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000) { - CallOneOf( - fuzzed_data_provider, - [&] { - buffer_size = fuzzed_data_provider.ConsumeIntegralInRange(64, 4096); - in = std::vector(buffer_size + CHACHA20_POLY1305_AEAD_AAD_LEN + Poly1305::TAGLEN, 0); - out = std::vector(buffer_size + CHACHA20_POLY1305_AEAD_AAD_LEN + Poly1305::TAGLEN, 0); - }, - [&] { - (void)aead.Crypt(seqnr_payload, seqnr_aad, aad_pos, out.data(), out.size(), in.data(), buffer_size, is_encrypt); - }, - [&] { - uint32_t len = 0; - const bool ok = aead.GetLength(&len, seqnr_aad, aad_pos, in.data()); - assert(ok); - }, - [&] { - if (AdditionOverflow(seqnr_payload, static_cast(1))) { - return; - } - seqnr_payload += 1; - aad_pos += CHACHA20_POLY1305_AEAD_AAD_LEN; - if (aad_pos + CHACHA20_POLY1305_AEAD_AAD_LEN > CHACHA20_ROUND_OUTPUT) { - aad_pos = 0; - if (AdditionOverflow(seqnr_aad, static_cast(1))) { - return; - } - seqnr_aad += 1; - } - }, - [&] { - seqnr_payload = fuzzed_data_provider.ConsumeIntegral(); - }, - [&] { - seqnr_aad = fuzzed_data_provider.ConsumeIntegral(); - }, - [&] { - is_encrypt = fuzzed_data_provider.ConsumeBool(); - }); - } -} From 9ff0768bdcca06836ccc673eacfa648e801930cb Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Fri, 30 Jun 2023 00:30:34 -0400 Subject: [PATCH 2/8] crypto: add the ChaCha20Poly1305 AEAD as specified in RFC8439 This adds an implementation of the ChaCha20Poly1305 AEAD exactly matching the version specified in RFC8439 section 2.8, including tests and official test vectors. --- src/Makefile.am | 2 + src/crypto/chacha20poly1305.cpp | 101 ++++++++++++++++++++++++++++++++ src/crypto/chacha20poly1305.h | 51 ++++++++++++++++ src/test/crypto_tests.cpp | 77 ++++++++++++++++++++++++ 4 files changed, 231 insertions(+) create mode 100644 src/crypto/chacha20poly1305.cpp create mode 100644 src/crypto/chacha20poly1305.h diff --git a/src/Makefile.am b/src/Makefile.am index d8ea4bbd285..b6fe2563350 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -548,6 +548,8 @@ crypto_libbitcoin_crypto_base_la_SOURCES = \ crypto/aes.h \ crypto/chacha20.h \ crypto/chacha20.cpp \ + crypto/chacha20poly1305.h \ + crypto/chacha20poly1305.cpp \ crypto/common.h \ crypto/hkdf_sha256_32.cpp \ crypto/hkdf_sha256_32.h \ diff --git a/src/crypto/chacha20poly1305.cpp b/src/crypto/chacha20poly1305.cpp new file mode 100644 index 00000000000..5f27857213c --- /dev/null +++ b/src/crypto/chacha20poly1305.cpp @@ -0,0 +1,101 @@ +// Copyright (c) 2023 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 + +AEADChaCha20Poly1305::AEADChaCha20Poly1305(Span key) noexcept : m_chacha20(UCharCast(key.data())) +{ + assert(key.size() == KEYLEN); +} + +void AEADChaCha20Poly1305::SetKey(Span key) noexcept +{ + assert(key.size() == KEYLEN); + m_chacha20.SetKey32(UCharCast(key.data())); +} + +namespace { + +#ifndef HAVE_TIMINGSAFE_BCMP +#define HAVE_TIMINGSAFE_BCMP + +int timingsafe_bcmp(const unsigned char* b1, const unsigned char* b2, size_t n) noexcept +{ + const unsigned char *p1 = b1, *p2 = b2; + int ret = 0; + for (; n > 0; n--) + ret |= *p1++ ^ *p2++; + return (ret != 0); +} + +#endif + +/** Compute poly1305 tag. chacha20 must be set to the right nonce, block 0. Will be at block 1 after. */ +void ComputeTag(ChaCha20& chacha20, Span aad, Span cipher, Span tag) noexcept +{ + static const std::byte PADDING[16] = {{}}; + + // Get block of keystream (use a full 64 byte buffer to avoid the need for chacha20's own buffering). + std::byte first_block[64]; + chacha20.Keystream(UCharCast(first_block), sizeof(first_block)); + + // Use the first 32 bytes of the first keystream block as poly1305 key. + Poly1305 poly1305{Span{first_block}.first(Poly1305::KEYLEN)}; + + // Compute tag: + // - Process the padded AAD with Poly1305. + const unsigned aad_padding_length = (16 - (aad.size() % 16)) % 16; + poly1305.Update(aad).Update(Span{PADDING}.first(aad_padding_length)); + // - Process the padded ciphertext with Poly1305. + const unsigned cipher_padding_length = (16 - (cipher.size() % 16)) % 16; + poly1305.Update(cipher).Update(Span{PADDING}.first(cipher_padding_length)); + // - Process the AAD and plaintext length with Poly1305. + std::byte length_desc[Poly1305::TAGLEN]; + WriteLE64(UCharCast(length_desc), aad.size()); + WriteLE64(UCharCast(length_desc + 8), cipher.size()); + poly1305.Update(length_desc); + + // Output tag. + poly1305.Finalize(tag); +} + +} // namespace + +void AEADChaCha20Poly1305::Encrypt(Span plain, Span aad, Nonce96 nonce, Span cipher) noexcept +{ + assert(cipher.size() == plain.size() + EXPANSION); + + // Encrypt using ChaCha20 (starting at block 1). + m_chacha20.Seek64(nonce, 1); + m_chacha20.Crypt(UCharCast(plain.data()), UCharCast(cipher.data()), plain.size()); + + // Seek to block 0, and compute tag using key drawn from there. + m_chacha20.Seek64(nonce, 0); + ComputeTag(m_chacha20, aad, cipher.first(plain.size()), cipher.last(EXPANSION)); +} + +bool AEADChaCha20Poly1305::Decrypt(Span cipher, Span aad, Nonce96 nonce, Span plain) noexcept +{ + assert(cipher.size() == plain.size() + EXPANSION); + + // Verify tag (using key drawn from block 0). + m_chacha20.Seek64(nonce, 0); + std::byte expected_tag[EXPANSION]; + ComputeTag(m_chacha20, aad, cipher.first(plain.size()), expected_tag); + if (timingsafe_bcmp(UCharCast(expected_tag), UCharCast(cipher.data() + plain.size()), EXPANSION)) return false; + + // Decrypt (starting at block 1). + m_chacha20.Crypt(UCharCast(cipher.data()), UCharCast(plain.data()), plain.size()); + return true; +} diff --git a/src/crypto/chacha20poly1305.h b/src/crypto/chacha20poly1305.h new file mode 100644 index 00000000000..0007ee63a96 --- /dev/null +++ b/src/crypto/chacha20poly1305.h @@ -0,0 +1,51 @@ +// Copyright (c) 2023 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_CRYPTO_CHACHA20POLY1305_H +#define BITCOIN_CRYPTO_CHACHA20POLY1305_H + +#include +#include +#include + +#include +#include +#include + +/** The AEAD_CHACHA20_POLY1305 authenticated encryption algorithm from RFC8439 section 2.8. */ +class AEADChaCha20Poly1305 +{ + /** Internal stream cipher. */ + ChaCha20 m_chacha20; + +public: + /** Expected size of key argument in constructor. */ + static constexpr unsigned KEYLEN = 32; + + /** Expansion when encrypting. */ + static constexpr unsigned EXPANSION = Poly1305::TAGLEN; + + /** Initialize an AEAD instance with a specified 32-byte key. */ + AEADChaCha20Poly1305(Span key) noexcept; + + /** Switch to another 32-byte key. */ + void SetKey(Span key) noexcept; + + /** 96-bit nonce type. */ + using Nonce96 = ChaCha20::Nonce96; + + /** Encrypt a message with a specified 96-bit nonce and aad. + * + * Requires cipher.size() = plain.size() + EXPANSION. + */ + void Encrypt(Span plain, Span aad, Nonce96 nonce, Span cipher) noexcept; + + /** Decrypt a message with a specified 96-bit nonce and aad. Returns true if valid. + * + * Requires cipher.size() = plain.size() + EXPANSION. + */ + bool Decrypt(Span cipher, Span aad, Nonce96 nonce, Span plain) noexcept; +}; + +#endif // BITCOIN_CRYPTO_CHACHA20POLY1305_H diff --git a/src/test/crypto_tests.cpp b/src/test/crypto_tests.cpp index 809b8979985..f62072f5bbe 100644 --- a/src/test/crypto_tests.cpp +++ b/src/test/crypto_tests.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -207,6 +208,24 @@ static void TestPoly1305(const std::string &hexmessage, const std::string &hexke } } +static void TestChaCha20Poly1305(const std::string& plain_hex, const std::string& aad_hex, const std::string& key_hex, ChaCha20::Nonce96 nonce, const std::string& cipher_hex) +{ + auto plain = ParseHex(plain_hex); + auto aad = ParseHex(aad_hex); + auto key = ParseHex(key_hex); + auto expected_cipher = ParseHex(cipher_hex); + + std::vector cipher(plain.size() + AEADChaCha20Poly1305::EXPANSION); + AEADChaCha20Poly1305 aead{key}; + aead.Encrypt(plain, aad, nonce, cipher); + BOOST_CHECK(cipher == expected_cipher); + + std::vector decipher(cipher.size() - AEADChaCha20Poly1305::EXPANSION); + bool ret = aead.Decrypt(cipher, aad, nonce, decipher); + BOOST_CHECK(ret); + BOOST_CHECK(decipher == plain); +} + static void TestHKDF_SHA256_32(const std::string &ikm_hex, const std::string &salt_hex, const std::string &info_hex, const std::string &okm_check_hex) { std::vector initial_key_material = ParseHex(ikm_hex); std::vector salt = ParseHex(salt_hex); @@ -818,6 +837,64 @@ BOOST_AUTO_TEST_CASE(poly1305_testvector) "0e410fa9d7a40ac582e77546be9a72bb"); } +BOOST_AUTO_TEST_CASE(chacha20poly1305_testvectors) +{ + // Note that in our implementation, the authentication is suffixed to the ciphertext. + // The RFC test vectors specify them separately. + + // RFC 8439 Example from section 2.8.2 + TestChaCha20Poly1305("4c616469657320616e642047656e746c656d656e206f662074686520636c6173" + "73206f66202739393a204966204920636f756c64206f6666657220796f75206f" + "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73" + "637265656e20776f756c642062652069742e", + "50515253c0c1c2c3c4c5c6c7", + "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", + {7, 0x4746454443424140}, + "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6" + "3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36" + "92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc" + "3ff4def08e4b7a9de576d26586cec64b61161ae10b594f09e26a7e902ecbd060" + "0691"); + + // RFC 8439 Test vector A.5 + TestChaCha20Poly1305("496e7465726e65742d4472616674732061726520647261667420646f63756d65" + "6e74732076616c696420666f722061206d6178696d756d206f6620736978206d" + "6f6e74687320616e64206d617920626520757064617465642c207265706c6163" + "65642c206f72206f62736f6c65746564206279206f7468657220646f63756d65" + "6e747320617420616e792074696d652e20497420697320696e617070726f7072" + "6961746520746f2075736520496e7465726e65742d4472616674732061732072" + "65666572656e6365206d6174657269616c206f7220746f206369746520746865" + "6d206f74686572207468616e206173202fe2809c776f726b20696e2070726f67" + "726573732e2fe2809d", + "f33388860000000000004e91", + "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0", + {0, 0x0807060504030201}, + "64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb2" + "4c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf" + "332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c855" + "9797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4" + "b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523e" + "af4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a" + "0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a10" + "49e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29" + "a6ad5cb4022b02709beead9d67890cbb22392336fea1851f38"); + + // Test vectors exercising aad and plaintext which are multiples of 16 bytes. + TestChaCha20Poly1305("8d2d6a8befd9716fab35819eaac83b33269afb9f1a00fddf66095a6c0cd91951" + "a6b7ad3db580be0674c3f0b55f618e34", + "", + "72ddc73f07101282bbbcf853b9012a9f9695fc5d36b303a97fd0845d0314e0c3", + {0x3432b75f, 0xb3585537eb7f4024}, + "f760b8224fb2a317b1b07875092606131232a5b86ae142df5df1c846a7f6341a" + "f2564483dd77f836be45e6230808ffe402a6f0a3e8be074b3d1f4ea8a7b09451"); + TestChaCha20Poly1305("", + "36970d8a704c065de16250c18033de5a400520ac1b5842b24551e5823a3314f3" + "946285171e04a81ebfbe3566e312e74ab80e94c7dd2ff4e10de0098a58d0f503", + "77adda51d6730b9ad6c995658cbd49f581b2547e7c0c08fcc24ceec797461021", + {0x1f90da88, 0x75dafa3ef84471a4}, + "aaae5bb81e8407c94b2ae86ae0c7efbe"); +} + BOOST_AUTO_TEST_CASE(hkdf_hmac_sha256_l32_tests) { // Use rfc5869 test vectors but truncated to 32 bytes (our implementation only support length 32) From 0fee267792eb8cbdd48ad78f1712420b5d8d905b Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Wed, 28 Jun 2023 18:20:30 -0400 Subject: [PATCH 3/8] crypto: add FSChaCha20, a rekeying wrapper around ChaCha20 This adds the FSChaCha20 stream cipher as specified in BIP324, a wrapper around the ChaCha20 stream cipher (specified in RFC8439 section 2.4) which automatically rekeys every N messages, and manages the nonces used for encryption. Co-authored-by: dhruv <856960+dhruv@users.noreply.github.com> --- src/crypto/chacha20.cpp | 41 +++++++++++++++++++++++ src/crypto/chacha20.h | 49 ++++++++++++++++++++++++++++ src/test/crypto_tests.cpp | 54 +++++++++++++++++++++++++++++++ src/test/fuzz/crypto_chacha20.cpp | 20 ++++++++++++ 4 files changed, 164 insertions(+) diff --git a/src/crypto/chacha20.cpp b/src/crypto/chacha20.cpp index fafd783ab17..469b280494a 100644 --- a/src/crypto/chacha20.cpp +++ b/src/crypto/chacha20.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -42,6 +43,11 @@ ChaCha20Aligned::ChaCha20Aligned() memset(input, 0, sizeof(input)); } +ChaCha20Aligned::~ChaCha20Aligned() +{ + memory_cleanse(input, sizeof(input)); +} + ChaCha20Aligned::ChaCha20Aligned(const unsigned char* key32) { SetKey32(key32); @@ -318,3 +324,38 @@ void ChaCha20::Crypt(const unsigned char* m, unsigned char* c, size_t bytes) m_bufleft = 64 - bytes; } } + +ChaCha20::~ChaCha20() +{ + memory_cleanse(m_buffer, sizeof(m_buffer)); +} + +FSChaCha20::FSChaCha20(Span key, uint32_t rekey_interval) noexcept : + m_chacha20(UCharCast(key.data())), m_rekey_interval(rekey_interval) +{ + assert(key.size() == KEYLEN); +} + +void FSChaCha20::Crypt(Span input, Span output) noexcept +{ + assert(input.size() == output.size()); + + // Invoke internal stream cipher for actual encryption/decryption. + m_chacha20.Crypt(UCharCast(input.data()), UCharCast(output.data()), input.size()); + + // Rekey after m_rekey_interval encryptions/decryptions. + if (++m_chunk_counter == m_rekey_interval) { + // Get new key from the stream cipher. + std::byte new_key[KEYLEN]; + m_chacha20.Keystream(UCharCast(new_key), sizeof(new_key)); + // Update its key. + m_chacha20.SetKey32(UCharCast(new_key)); + // Wipe the key (a copy remains inside m_chacha20, where it'll be wiped on the next rekey + // or on destruction). + memory_cleanse(new_key, sizeof(new_key)); + // Set the nonce for the new section of output. + m_chacha20.Seek64({0, ++m_rekey_counter}, 0); + // Reset the chunk counter. + m_chunk_counter = 0; + } +} diff --git a/src/crypto/chacha20.h b/src/crypto/chacha20.h index f2ec21d82e2..d1b2094e7e8 100644 --- a/src/crypto/chacha20.h +++ b/src/crypto/chacha20.h @@ -5,6 +5,10 @@ #ifndef BITCOIN_CRYPTO_CHACHA20_H #define BITCOIN_CRYPTO_CHACHA20_H +#include + +#include +#include #include #include #include @@ -29,6 +33,9 @@ public: /** Initialize a cipher with specified 32-byte key. */ ChaCha20Aligned(const unsigned char* key32); + /** Destructor to clean up private memory. */ + ~ChaCha20Aligned(); + /** set 32-byte key. */ void SetKey32(const unsigned char* key32); @@ -72,6 +79,9 @@ public: /** Initialize a cipher with specified 32-byte key. */ ChaCha20(const unsigned char* key32) : m_aligned(key32) {} + /** Destructor to clean up private memory. */ + ~ChaCha20(); + /** set 32-byte key. */ void SetKey32(const unsigned char* key32) { @@ -98,4 +108,43 @@ public: void Crypt(const unsigned char* input, unsigned char* output, size_t bytes); }; +/** Forward-secure ChaCha20 + * + * This implements a stream cipher that automatically transitions to a new stream with a new key + * and new nonce after a predefined number of encryptions or decryptions. + * + * See BIP324 for details. + */ +class FSChaCha20 +{ +private: + /** Internal stream cipher. */ + ChaCha20 m_chacha20; + + /** The number of encryptions/decryptions before a rekey happens. */ + const uint32_t m_rekey_interval; + + /** The number of encryptions/decryptions since the last rekey. */ + uint32_t m_chunk_counter{0}; + + /** The number of rekey operations that have happened. */ + uint64_t m_rekey_counter{0}; + +public: + /** Length of keys expected by the constructor. */ + static constexpr unsigned KEYLEN = 32; + + // No copy or move to protect the secret. + FSChaCha20(const FSChaCha20&) = delete; + FSChaCha20(FSChaCha20&&) = delete; + FSChaCha20& operator=(const FSChaCha20&) = delete; + FSChaCha20& operator=(FSChaCha20&&) = delete; + + /** Construct an FSChaCha20 cipher that rekeys every rekey_interval Crypt() calls. */ + FSChaCha20(Span key, uint32_t rekey_interval) noexcept; + + /** Encrypt or decrypt a chunk. */ + void Crypt(Span input, Span output) noexcept; +}; + #endif // BITCOIN_CRYPTO_CHACHA20_H diff --git a/src/test/crypto_tests.cpp b/src/test/crypto_tests.cpp index f62072f5bbe..d50596c2045 100644 --- a/src/test/crypto_tests.cpp +++ b/src/test/crypto_tests.cpp @@ -182,6 +182,46 @@ static void TestChaCha20(const std::string &hex_message, const std::string &hexk } } +static void TestFSChaCha20(const std::string& hex_plaintext, const std::string& hexkey, uint32_t rekey_interval, const std::string& ciphertext_after_rotation) +{ + auto key = ParseHex(hexkey); + BOOST_CHECK_EQUAL(FSChaCha20::KEYLEN, key.size()); + + auto plaintext = ParseHex(hex_plaintext); + + auto fsc20 = FSChaCha20{key, rekey_interval}; + auto c20 = ChaCha20{UCharCast(key.data())}; + + std::vector fsc20_output; + fsc20_output.resize(plaintext.size()); + + std::vector c20_output; + c20_output.resize(plaintext.size()); + + for (size_t i = 0; i < rekey_interval; i++) { + fsc20.Crypt(plaintext, fsc20_output); + c20.Crypt(UCharCast(plaintext.data()), UCharCast(c20_output.data()), plaintext.size()); + BOOST_CHECK(c20_output == fsc20_output); + } + + // At the rotation interval, the outputs will no longer match + fsc20.Crypt(plaintext, fsc20_output); + auto c20_copy = c20; + c20.Crypt(UCharCast(plaintext.data()), UCharCast(c20_output.data()), plaintext.size()); + BOOST_CHECK(c20_output != fsc20_output); + + std::byte new_key[FSChaCha20::KEYLEN]; + c20_copy.Keystream(UCharCast(new_key), sizeof(new_key)); + c20.SetKey32(UCharCast(new_key)); + c20.Seek64({0, 1}, 0); + + // Outputs should match again after simulating key rotation + c20.Crypt(UCharCast(plaintext.data()), UCharCast(c20_output.data()), plaintext.size()); + BOOST_CHECK(c20_output == fsc20_output); + + BOOST_CHECK_EQUAL(HexStr(fsc20_output), ciphertext_after_rotation); +} + static void TestPoly1305(const std::string &hexmessage, const std::string &hexkey, const std::string& hextag) { auto key = ParseHex(hexkey); @@ -696,6 +736,20 @@ BOOST_AUTO_TEST_CASE(chacha20_testvector) "fd565dea5addbdb914208fde7950f23e0385f9a727143f6a6ac51d84b1c0fb3e" "2e3b00b63d6841a1cc6d1538b1d3a74bef1eb2f54c7b7281e36e484dba89b351" "c8f572617e61e342879f211b0e4c515df50ea9d0771518fad96cd0baee62deb6"); + + // Forward secure ChaCha20 + TestFSChaCha20("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "0000000000000000000000000000000000000000000000000000000000000000", + 256, + "a93df4ef03011f3db95f60d996e1785df5de38fc39bfcb663a47bb5561928349"); + TestFSChaCha20("01", + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + 5, + "ea"); + TestFSChaCha20("e93fdb5c762804b9a706816aca31e35b11d2aa3080108ef46a5b1f1508819c0a", + "8ec4c3ccdaea336bdeb245636970be01266509b33f3d2642504eaf412206207a", + 4096, + "8bfaa4eacff308fdb4a94a5ff25bd9d0c1f84b77f81239f67ff39d6e1ac280c9"); } BOOST_AUTO_TEST_CASE(chacha20_midblock) diff --git a/src/test/fuzz/crypto_chacha20.cpp b/src/test/fuzz/crypto_chacha20.cpp index 63c7bf3b45b..76370b4e57a 100644 --- a/src/test/fuzz/crypto_chacha20.cpp +++ b/src/test/fuzz/crypto_chacha20.cpp @@ -8,6 +8,8 @@ #include #include +#include +#include #include #include @@ -151,3 +153,21 @@ FUZZ_TARGET(chacha20_split_keystream) FuzzedDataProvider provider{buffer.data(), buffer.size()}; ChaCha20SplitFuzz(provider); } + +FUZZ_TARGET(crypto_fschacha20) +{ + FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; + + auto key = fuzzed_data_provider.ConsumeBytes(FSChaCha20::KEYLEN); + key.resize(FSChaCha20::KEYLEN); + + auto fsc20 = FSChaCha20{key, fuzzed_data_provider.ConsumeIntegralInRange(1, 1024)}; + + LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000) + { + auto input = fuzzed_data_provider.ConsumeBytes(fuzzed_data_provider.ConsumeIntegralInRange(0, 4096)); + std::vector output; + output.resize(input.size()); + fsc20.Crypt(input, output); + } +} From aa8cee93342ee857931afec9af3ff5dbd8ce7749 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Thu, 29 Jun 2023 14:55:16 -0400 Subject: [PATCH 4/8] crypto: add FSChaCha20Poly1305, rekeying wrapper around ChaCha20Poly1305 This adds the FSChaCha20Poly1305 AEAD as specified in BIP324, a wrapper around the ChaCha20Poly1305 AEAD (as specified in RFC8439 section 2.8) which automatically rekeys every N messages, and automatically increments the nonce every message. --- src/crypto/chacha20poly1305.cpp | 39 +++++++++++++++++++++ src/crypto/chacha20poly1305.h | 62 +++++++++++++++++++++++++++++++++ src/test/crypto_tests.cpp | 58 ++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+) diff --git a/src/crypto/chacha20poly1305.cpp b/src/crypto/chacha20poly1305.cpp index 5f27857213c..c3f8fe9e640 100644 --- a/src/crypto/chacha20poly1305.cpp +++ b/src/crypto/chacha20poly1305.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -99,3 +100,41 @@ bool AEADChaCha20Poly1305::Decrypt(Span cipher, Span keystream) noexcept +{ + // Skip the first output block, as it's used for generating the poly1305 key. + m_chacha20.Seek64(nonce, 1); + m_chacha20.Keystream(UCharCast(keystream.data()), keystream.size()); +} + +void FSChaCha20Poly1305::NextPacket() noexcept +{ + if (++m_packet_counter == m_rekey_interval) { + // Generate a full block of keystream, to avoid needing the ChaCha20 buffer, even though + // we only need KEYLEN (32) bytes. + std::byte one_block[64]; + m_aead.Keystream({0xFFFFFFFF, m_rekey_counter}, one_block); + // Switch keys. + m_aead.SetKey(Span{one_block}.first(KEYLEN)); + // Wipe the generated keystream (a copy remains inside m_aead, which will be cleaned up + // once it cycles again, or is destroyed). + memory_cleanse(one_block, sizeof(one_block)); + // Update counters. + m_packet_counter = 0; + ++m_rekey_counter; + } +} + +void FSChaCha20Poly1305::Encrypt(Span plain, Span aad, Span cipher) noexcept +{ + m_aead.Encrypt(plain, aad, {m_packet_counter, m_rekey_counter}, cipher); + NextPacket(); +} + +bool FSChaCha20Poly1305::Decrypt(Span cipher, Span aad, Span plain) noexcept +{ + bool ret = m_aead.Decrypt(cipher, aad, {m_packet_counter, m_rekey_counter}, plain); + NextPacket(); + return ret; +} diff --git a/src/crypto/chacha20poly1305.h b/src/crypto/chacha20poly1305.h index 0007ee63a96..c4ec978df86 100644 --- a/src/crypto/chacha20poly1305.h +++ b/src/crypto/chacha20poly1305.h @@ -46,6 +46,68 @@ public: * Requires cipher.size() = plain.size() + EXPANSION. */ bool Decrypt(Span cipher, Span aad, Nonce96 nonce, Span plain) noexcept; + + /** Get a number of keystream bytes from the underlying stream cipher. + * + * This is equivalent to Encrypt() with plain set to that many zero bytes, and dropping the + * last EXPANSION bytes off the result. + */ + void Keystream(Nonce96 nonce, Span keystream) noexcept; +}; + +/** Forward-secure wrapper around AEADChaCha20Poly1305. + * + * This implements an AEAD which automatically increments the nonce on every encryption or + * decryption, and cycles keys after a predetermined number of encryptions or decryptions. + * + * See BIP324 for details. + */ +class FSChaCha20Poly1305 +{ +private: + /** Internal AEAD. */ + AEADChaCha20Poly1305 m_aead; + + /** Every how many iterations this cipher rekeys. */ + const uint32_t m_rekey_interval; + + /** The number of encryptions/decryptions since the last rekey. */ + uint32_t m_packet_counter{0}; + + /** The number of rekeys performed so far. */ + uint64_t m_rekey_counter{0}; + + /** Update counters (and if necessary, key) to transition to the next message. */ + void NextPacket() noexcept; + +public: + /** Length of keys expected by the constructor. */ + static constexpr auto KEYLEN = AEADChaCha20Poly1305::KEYLEN; + + /** Expansion when encrypting. */ + static constexpr auto EXPANSION = AEADChaCha20Poly1305::EXPANSION; + + // No copy or move to protect the secret. + FSChaCha20Poly1305(const FSChaCha20Poly1305&) = delete; + FSChaCha20Poly1305(FSChaCha20Poly1305&&) = delete; + FSChaCha20Poly1305& operator=(const FSChaCha20Poly1305&) = delete; + FSChaCha20Poly1305& operator=(FSChaCha20Poly1305&&) = delete; + + /** Construct an FSChaCha20Poly1305 cipher that rekeys every rekey_interval operations. */ + FSChaCha20Poly1305(Span key, uint32_t rekey_interval) noexcept : + m_aead(key), m_rekey_interval(rekey_interval) {} + + /** Encrypt a message with a specified aad. + * + * Requires cipher.size() = plain.size() + EXPANSION. + */ + void Encrypt(Span plain, Span aad, Span cipher) noexcept; + + /** Decrypt a message with a specified aad. Returns true if valid. + * + * Requires cipher.size() = plain.size() + EXPANSION. + */ + bool Decrypt(Span cipher, Span aad, Span plain) noexcept; }; #endif // BITCOIN_CRYPTO_CHACHA20POLY1305_H diff --git a/src/test/crypto_tests.cpp b/src/test/crypto_tests.cpp index d50596c2045..149a8cf7bf3 100644 --- a/src/test/crypto_tests.cpp +++ b/src/test/crypto_tests.cpp @@ -264,6 +264,41 @@ static void TestChaCha20Poly1305(const std::string& plain_hex, const std::string bool ret = aead.Decrypt(cipher, aad, nonce, decipher); BOOST_CHECK(ret); BOOST_CHECK(decipher == plain); + + std::vector keystream(plain.size()); + aead.Keystream(nonce, keystream); + for (size_t i = 0; i < plain.size(); ++i) { + BOOST_CHECK_EQUAL(plain[i] ^ keystream[i], cipher[i]); + } +} + +static void TestFSChaCha20Poly1305(const std::string& plain_hex, const std::string& aad_hex, const std::string& key_hex, uint64_t msg_idx, const std::string& cipher_hex) +{ + auto plain = ParseHex(plain_hex); + auto aad = ParseHex(aad_hex); + auto key = ParseHex(key_hex); + auto expected_cipher = ParseHex(cipher_hex); + std::vector cipher(plain.size() + FSChaCha20Poly1305::EXPANSION); + + FSChaCha20Poly1305 enc_aead{key, 224}; + for (uint64_t i = 0; i < msg_idx; ++i) { + std::byte dummy_tag[FSChaCha20Poly1305::EXPANSION] = {{}}; + enc_aead.Encrypt(Span{dummy_tag}.first(0), Span{dummy_tag}.first(0), dummy_tag); + } + + enc_aead.Encrypt(plain, aad, cipher); + BOOST_CHECK(cipher == expected_cipher); + + FSChaCha20Poly1305 dec_aead{key, 224}; + for (uint64_t i = 0; i < msg_idx; ++i) { + std::byte dummy_tag[FSChaCha20Poly1305::EXPANSION] = {{}}; + dec_aead.Decrypt(dummy_tag, Span{dummy_tag}.first(0), Span{dummy_tag}.first(0)); + } + + std::vector decipher(cipher.size() - AEADChaCha20Poly1305::EXPANSION); + bool ret = dec_aead.Decrypt(cipher, aad, decipher); + BOOST_CHECK(ret); + BOOST_CHECK(decipher == plain); } static void TestHKDF_SHA256_32(const std::string &ikm_hex, const std::string &salt_hex, const std::string &info_hex, const std::string &okm_check_hex) { @@ -947,6 +982,29 @@ BOOST_AUTO_TEST_CASE(chacha20poly1305_testvectors) "77adda51d6730b9ad6c995658cbd49f581b2547e7c0c08fcc24ceec797461021", {0x1f90da88, 0x75dafa3ef84471a4}, "aaae5bb81e8407c94b2ae86ae0c7efbe"); + + // FSChaCha20Poly1305 tests. + TestFSChaCha20Poly1305("d6a4cb04ef0f7c09c1866ed29dc24d820e75b0491032a51b4c3366f9ca35c19e" + "a3047ec6be9d45f9637b63e1cf9eb4c2523a5aab7b851ebeba87199db0e839cf" + "0d5c25e50168306377aedbe9089fd2463ded88b83211cf51b73b150608cc7a60" + "0d0f11b9a742948482e1b109d8faf15b450aa7322e892fa2208c6691e3fecf4c" + "711191b14d75a72147", + "786cb9b6ebf44288974cf0", + "5c9e1c3951a74fba66708bf9d2c217571684556b6a6a3573bff2847d38612654", + 500, + "9dcebbd3281ea3dd8e9a1ef7d55a97abd6743e56ebc0c190cb2c4e14160b385e" + "0bf508dddf754bd02c7c208447c131ce23e47a4a14dfaf5dd8bc601323950f75" + "4e05d46e9232f83fc5120fbbef6f5347a826ec79a93820718d4ec7a2b7cfaaa4" + "4b21e16d726448b62f803811aff4f6d827ed78e738ce8a507b81a8ae13131192" + "8039213de18a5120dc9b7370baca878f50ff254418de3da50c"); + TestFSChaCha20Poly1305("8349b7a2690b63d01204800c288ff1138a1d473c832c90ea8b3fc102d0bb3adc" + "44261b247c7c3d6760bfbe979d061c305f46d94c0582ac3099f0bf249f8cb234", + "", + "3bd2093fcbcb0d034d8c569583c5425c1a53171ea299f8cc3bbf9ae3530adfce", + 60000, + "30a6757ff8439b975363f166a0fa0e36722ab35936abd704297948f45083f4d4" + "99433137ce931f7fca28a0acd3bc30f57b550acbc21cbd45bbef0739d9caf30c" + "14b94829deb27f0b1923a2af704ae5d6"); } BOOST_AUTO_TEST_CASE(hkdf_hmac_sha256_l32_tests) From af2b44c76e5de8ce880381e5535ead37ab0b3ba9 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Fri, 30 Jun 2023 13:06:54 -0400 Subject: [PATCH 5/8] bench: add benchmark for FSChaCha20Poly1305 Add a benchmark for FSChaCha20Poly1305 encryption, so the overhead of key generation and authentication can be observed for various message sizes. --- src/bench/chacha20.cpp | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/bench/chacha20.cpp b/src/bench/chacha20.cpp index 3b57e29f395..d8bebf9319a 100644 --- a/src/bench/chacha20.cpp +++ b/src/bench/chacha20.cpp @@ -5,6 +5,7 @@ #include #include +#include /* Number of bytes to process per iteration */ static const uint64_t BUFFER_SIZE_TINY = 64; @@ -23,6 +24,18 @@ static void CHACHA20(benchmark::Bench& bench, size_t buffersize) }); } +static void FSCHACHA20POLY1305(benchmark::Bench& bench, size_t buffersize) +{ + std::vector key(32); + FSChaCha20Poly1305 ctx(key, 224); + std::vector in(buffersize); + std::vector aad; + std::vector out(buffersize + FSChaCha20Poly1305::EXPANSION); + bench.batch(in.size()).unit("byte").run([&] { + ctx.Encrypt(in, aad, out); + }); +} + static void CHACHA20_64BYTES(benchmark::Bench& bench) { CHACHA20(bench, BUFFER_SIZE_TINY); @@ -38,6 +51,24 @@ static void CHACHA20_1MB(benchmark::Bench& bench) CHACHA20(bench, BUFFER_SIZE_LARGE); } +static void FSCHACHA20POLY1305_64BYTES(benchmark::Bench& bench) +{ + FSCHACHA20POLY1305(bench, BUFFER_SIZE_TINY); +} + +static void FSCHACHA20POLY1305_256BYTES(benchmark::Bench& bench) +{ + FSCHACHA20POLY1305(bench, BUFFER_SIZE_SMALL); +} + +static void FSCHACHA20POLY1305_1MB(benchmark::Bench& bench) +{ + FSCHACHA20POLY1305(bench, BUFFER_SIZE_LARGE); +} + BENCHMARK(CHACHA20_64BYTES, benchmark::PriorityLevel::HIGH); BENCHMARK(CHACHA20_256BYTES, benchmark::PriorityLevel::HIGH); BENCHMARK(CHACHA20_1MB, benchmark::PriorityLevel::HIGH); +BENCHMARK(FSCHACHA20POLY1305_64BYTES, benchmark::PriorityLevel::HIGH); +BENCHMARK(FSCHACHA20POLY1305_256BYTES, benchmark::PriorityLevel::HIGH); +BENCHMARK(FSCHACHA20POLY1305_1MB, benchmark::PriorityLevel::HIGH); From c91cedf281e5207fb5fd2ca81feec9760f7c2ed0 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Thu, 6 Jul 2023 20:40:20 -0400 Subject: [PATCH 6/8] crypto: support split plaintext in ChaCha20Poly1305 Encrypt/Decrypt --- src/crypto/chacha20poly1305.cpp | 28 ++++++----- src/crypto/chacha20poly1305.h | 44 ++++++++++++++-- src/test/crypto_tests.cpp | 89 +++++++++++++++++++++++---------- 3 files changed, 117 insertions(+), 44 deletions(-) diff --git a/src/crypto/chacha20poly1305.cpp b/src/crypto/chacha20poly1305.cpp index c3f8fe9e640..c936dd22659 100644 --- a/src/crypto/chacha20poly1305.cpp +++ b/src/crypto/chacha20poly1305.cpp @@ -73,31 +73,33 @@ void ComputeTag(ChaCha20& chacha20, Span aad, Span plain, Span aad, Nonce96 nonce, Span cipher) noexcept +void AEADChaCha20Poly1305::Encrypt(Span plain1, Span plain2, Span aad, Nonce96 nonce, Span cipher) noexcept { - assert(cipher.size() == plain.size() + EXPANSION); + assert(cipher.size() == plain1.size() + plain2.size() + EXPANSION); // Encrypt using ChaCha20 (starting at block 1). m_chacha20.Seek64(nonce, 1); - m_chacha20.Crypt(UCharCast(plain.data()), UCharCast(cipher.data()), plain.size()); + m_chacha20.Crypt(UCharCast(plain1.data()), UCharCast(cipher.data()), plain1.size()); + m_chacha20.Crypt(UCharCast(plain2.data()), UCharCast(cipher.data() + plain1.size()), plain2.size()); // Seek to block 0, and compute tag using key drawn from there. m_chacha20.Seek64(nonce, 0); - ComputeTag(m_chacha20, aad, cipher.first(plain.size()), cipher.last(EXPANSION)); + ComputeTag(m_chacha20, aad, cipher.first(cipher.size() - EXPANSION), cipher.last(EXPANSION)); } -bool AEADChaCha20Poly1305::Decrypt(Span cipher, Span aad, Nonce96 nonce, Span plain) noexcept +bool AEADChaCha20Poly1305::Decrypt(Span cipher, Span aad, Nonce96 nonce, Span plain1, Span plain2) noexcept { - assert(cipher.size() == plain.size() + EXPANSION); + assert(cipher.size() == plain1.size() + plain2.size() + EXPANSION); // Verify tag (using key drawn from block 0). m_chacha20.Seek64(nonce, 0); std::byte expected_tag[EXPANSION]; - ComputeTag(m_chacha20, aad, cipher.first(plain.size()), expected_tag); - if (timingsafe_bcmp(UCharCast(expected_tag), UCharCast(cipher.data() + plain.size()), EXPANSION)) return false; + ComputeTag(m_chacha20, aad, cipher.first(cipher.size() - EXPANSION), expected_tag); + if (timingsafe_bcmp(UCharCast(expected_tag), UCharCast(cipher.data() + cipher.size() - EXPANSION), EXPANSION)) return false; // Decrypt (starting at block 1). - m_chacha20.Crypt(UCharCast(cipher.data()), UCharCast(plain.data()), plain.size()); + m_chacha20.Crypt(UCharCast(cipher.data()), UCharCast(plain1.data()), plain1.size()); + m_chacha20.Crypt(UCharCast(cipher.data() + plain1.size()), UCharCast(plain2.data()), plain2.size()); return true; } @@ -126,15 +128,15 @@ void FSChaCha20Poly1305::NextPacket() noexcept } } -void FSChaCha20Poly1305::Encrypt(Span plain, Span aad, Span cipher) noexcept +void FSChaCha20Poly1305::Encrypt(Span plain1, Span plain2, Span aad, Span cipher) noexcept { - m_aead.Encrypt(plain, aad, {m_packet_counter, m_rekey_counter}, cipher); + m_aead.Encrypt(plain1, plain2, aad, {m_packet_counter, m_rekey_counter}, cipher); NextPacket(); } -bool FSChaCha20Poly1305::Decrypt(Span cipher, Span aad, Span plain) noexcept +bool FSChaCha20Poly1305::Decrypt(Span cipher, Span aad, Span plain1, Span plain2) noexcept { - bool ret = m_aead.Decrypt(cipher, aad, {m_packet_counter, m_rekey_counter}, plain); + bool ret = m_aead.Decrypt(cipher, aad, {m_packet_counter, m_rekey_counter}, plain1, plain2); NextPacket(); return ret; } diff --git a/src/crypto/chacha20poly1305.h b/src/crypto/chacha20poly1305.h index c4ec978df86..dc8fb1cc13f 100644 --- a/src/crypto/chacha20poly1305.h +++ b/src/crypto/chacha20poly1305.h @@ -39,13 +39,31 @@ public: * * Requires cipher.size() = plain.size() + EXPANSION. */ - void Encrypt(Span plain, Span aad, Nonce96 nonce, Span cipher) noexcept; + void Encrypt(Span plain, Span aad, Nonce96 nonce, Span cipher) noexcept + { + Encrypt(plain, {}, aad, nonce, cipher); + } + + /** Encrypt a message (given split into plain1 + plain2) with a specified 96-bit nonce and aad. + * + * Requires cipher.size() = plain1.size() + plain2.size() + EXPANSION. + */ + void Encrypt(Span plain1, Span plain2, Span aad, Nonce96 nonce, Span cipher) noexcept; /** Decrypt a message with a specified 96-bit nonce and aad. Returns true if valid. * * Requires cipher.size() = plain.size() + EXPANSION. */ - bool Decrypt(Span cipher, Span aad, Nonce96 nonce, Span plain) noexcept; + bool Decrypt(Span cipher, Span aad, Nonce96 nonce, Span plain) noexcept + { + return Decrypt(cipher, aad, nonce, plain, {}); + } + + /** Decrypt a message with a specified 96-bit nonce and aad and split the result. Returns true if valid. + * + * Requires cipher.size() = plain1.size() + plain2.size() + EXPANSION. + */ + bool Decrypt(Span cipher, Span aad, Nonce96 nonce, Span plain1, Span plain2) noexcept; /** Get a number of keystream bytes from the underlying stream cipher. * @@ -101,13 +119,31 @@ public: * * Requires cipher.size() = plain.size() + EXPANSION. */ - void Encrypt(Span plain, Span aad, Span cipher) noexcept; + void Encrypt(Span plain, Span aad, Span cipher) noexcept + { + Encrypt(plain, {}, aad, cipher); + } + + /** Encrypt a message (given split into plain1 + plain2) with a specified aad. + * + * Requires cipher.size() = plain.size() + EXPANSION. + */ + void Encrypt(Span plain1, Span plain2, Span aad, Span cipher) noexcept; /** Decrypt a message with a specified aad. Returns true if valid. * * Requires cipher.size() = plain.size() + EXPANSION. */ - bool Decrypt(Span cipher, Span aad, Span plain) noexcept; + bool Decrypt(Span cipher, Span aad, Span plain) noexcept + { + return Decrypt(cipher, aad, plain, {}); + } + + /** Decrypt a message with a specified aad and split the result. Returns true if valid. + * + * Requires cipher.size() = plain1.size() + plain2.size() + EXPANSION. + */ + bool Decrypt(Span cipher, Span aad, Span plain1, Span plain2) noexcept; }; #endif // BITCOIN_CRYPTO_CHACHA20POLY1305_H diff --git a/src/test/crypto_tests.cpp b/src/test/crypto_tests.cpp index 149a8cf7bf3..6663c914a9a 100644 --- a/src/test/crypto_tests.cpp +++ b/src/test/crypto_tests.cpp @@ -255,20 +255,37 @@ static void TestChaCha20Poly1305(const std::string& plain_hex, const std::string auto key = ParseHex(key_hex); auto expected_cipher = ParseHex(cipher_hex); - std::vector cipher(plain.size() + AEADChaCha20Poly1305::EXPANSION); - AEADChaCha20Poly1305 aead{key}; - aead.Encrypt(plain, aad, nonce, cipher); - BOOST_CHECK(cipher == expected_cipher); + for (int i = 0; i < 10; ++i) { + // During i=0, use single-plain Encrypt/Decrypt; others use a split at prefix. + size_t prefix = i ? InsecureRandRange(plain.size() + 1) : plain.size(); + // Encrypt. + std::vector cipher(plain.size() + AEADChaCha20Poly1305::EXPANSION); + AEADChaCha20Poly1305 aead{key}; + if (i == 0) { + aead.Encrypt(plain, aad, nonce, cipher); + } else { + aead.Encrypt(Span{plain}.first(prefix), Span{plain}.subspan(prefix), aad, nonce, cipher); + } + BOOST_CHECK(cipher == expected_cipher); - std::vector decipher(cipher.size() - AEADChaCha20Poly1305::EXPANSION); - bool ret = aead.Decrypt(cipher, aad, nonce, decipher); - BOOST_CHECK(ret); - BOOST_CHECK(decipher == plain); + // Decrypt. + std::vector decipher(cipher.size() - AEADChaCha20Poly1305::EXPANSION); + bool ret{false}; + if (i == 0) { + ret = aead.Decrypt(cipher, aad, nonce, decipher); + } else { + ret = aead.Decrypt(cipher, aad, nonce, Span{decipher}.first(prefix), Span{decipher}.subspan(prefix)); + } + BOOST_CHECK(ret); + BOOST_CHECK(decipher == plain); + } + // Test Keystream output. std::vector keystream(plain.size()); + AEADChaCha20Poly1305 aead{key}; aead.Keystream(nonce, keystream); for (size_t i = 0; i < plain.size(); ++i) { - BOOST_CHECK_EQUAL(plain[i] ^ keystream[i], cipher[i]); + BOOST_CHECK_EQUAL(plain[i] ^ keystream[i], expected_cipher[i]); } } @@ -280,25 +297,43 @@ static void TestFSChaCha20Poly1305(const std::string& plain_hex, const std::stri auto expected_cipher = ParseHex(cipher_hex); std::vector cipher(plain.size() + FSChaCha20Poly1305::EXPANSION); - FSChaCha20Poly1305 enc_aead{key, 224}; - for (uint64_t i = 0; i < msg_idx; ++i) { - std::byte dummy_tag[FSChaCha20Poly1305::EXPANSION] = {{}}; - enc_aead.Encrypt(Span{dummy_tag}.first(0), Span{dummy_tag}.first(0), dummy_tag); + for (int it = 0; it < 10; ++it) { + // During it==0 we use the single-plain Encrypt/Decrypt; others use a split at prefix. + size_t prefix = it ? InsecureRandRange(plain.size() + 1) : plain.size(); + + // Do msg_idx dummy encryptions to seek to the correct packet. + FSChaCha20Poly1305 enc_aead{key, 224}; + for (uint64_t i = 0; i < msg_idx; ++i) { + std::byte dummy_tag[FSChaCha20Poly1305::EXPANSION] = {{}}; + enc_aead.Encrypt(Span{dummy_tag}.first(0), Span{dummy_tag}.first(0), dummy_tag); + } + + // Invoke single-plain or plain1/plain2 Encrypt. + if (it == 0) { + enc_aead.Encrypt(plain, aad, cipher); + } else { + enc_aead.Encrypt(Span{plain}.first(prefix), Span{plain}.subspan(prefix), aad, cipher); + } + BOOST_CHECK(cipher == expected_cipher); + + // Do msg_idx dummy decryptions to seek to the correct packet. + FSChaCha20Poly1305 dec_aead{key, 224}; + for (uint64_t i = 0; i < msg_idx; ++i) { + std::byte dummy_tag[FSChaCha20Poly1305::EXPANSION] = {{}}; + dec_aead.Decrypt(dummy_tag, Span{dummy_tag}.first(0), Span{dummy_tag}.first(0)); + } + + // Invoke single-plain or plain1/plain2 Decrypt. + std::vector decipher(cipher.size() - AEADChaCha20Poly1305::EXPANSION); + bool ret{false}; + if (it == 0) { + ret = dec_aead.Decrypt(cipher, aad, decipher); + } else { + ret = dec_aead.Decrypt(cipher, aad, Span{decipher}.first(prefix), Span{decipher}.subspan(prefix)); + } + BOOST_CHECK(ret); + BOOST_CHECK(decipher == plain); } - - enc_aead.Encrypt(plain, aad, cipher); - BOOST_CHECK(cipher == expected_cipher); - - FSChaCha20Poly1305 dec_aead{key, 224}; - for (uint64_t i = 0; i < msg_idx; ++i) { - std::byte dummy_tag[FSChaCha20Poly1305::EXPANSION] = {{}}; - dec_aead.Decrypt(dummy_tag, Span{dummy_tag}.first(0), Span{dummy_tag}.first(0)); - } - - std::vector decipher(cipher.size() - AEADChaCha20Poly1305::EXPANSION); - bool ret = dec_aead.Decrypt(cipher, aad, decipher); - BOOST_CHECK(ret); - BOOST_CHECK(decipher == plain); } static void TestHKDF_SHA256_32(const std::string &ikm_hex, const std::string &salt_hex, const std::string &info_hex, const std::string &okm_check_hex) { From 990f0f8da92a2d11828a7c05ca93bf0520b2a95e Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Thu, 6 Jul 2023 14:19:29 -0400 Subject: [PATCH 7/8] Add BIP324Cipher, encapsulating key agreement, derivation, and stream/AEAD ciphers Co-authored-by: dhruv <856960+dhruv@users.noreply.github.com> --- src/Makefile.am | 2 + src/Makefile.test.include | 2 + src/bip324.cpp | 110 +++++++++++++++++ src/bip324.h | 90 ++++++++++++++ src/pubkey.h | 3 + src/test/bip324_tests.cpp | 242 ++++++++++++++++++++++++++++++++++++++ src/test/fuzz/bip324.cpp | 137 +++++++++++++++++++++ 7 files changed, 586 insertions(+) create mode 100644 src/bip324.cpp create mode 100644 src/bip324.h create mode 100644 src/test/bip324_tests.cpp create mode 100644 src/test/fuzz/bip324.cpp diff --git a/src/Makefile.am b/src/Makefile.am index b6fe2563350..fecf86498f7 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -124,6 +124,7 @@ BITCOIN_CORE_H = \ banman.h \ base58.h \ bech32.h \ + bip324.h \ blockencodings.h \ blockfilter.h \ chain.h \ @@ -376,6 +377,7 @@ libbitcoin_node_a_SOURCES = \ addrdb.cpp \ addrman.cpp \ banman.cpp \ + bip324.cpp \ blockencodings.cpp \ blockfilter.cpp \ chain.cpp \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index d3d2464ce67..37de6237eca 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -74,6 +74,7 @@ BITCOIN_TESTS =\ test/base64_tests.cpp \ test/bech32_tests.cpp \ test/bip32_tests.cpp \ + test/bip324_tests.cpp \ test/blockchain_tests.cpp \ test/blockencodings_tests.cpp \ test/blockfilter_index_tests.cpp \ @@ -246,6 +247,7 @@ test_fuzz_fuzz_SOURCES = \ test/fuzz/banman.cpp \ test/fuzz/base_encode_decode.cpp \ test/fuzz/bech32.cpp \ + test/fuzz/bip324.cpp \ test/fuzz/bitdeque.cpp \ test/fuzz/block.cpp \ test/fuzz/block_header.cpp \ diff --git a/src/bip324.cpp b/src/bip324.cpp new file mode 100644 index 00000000000..eb223f1f048 --- /dev/null +++ b/src/bip324.cpp @@ -0,0 +1,110 @@ +// Copyright (c) 2023 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 +#include +#include +#include + +BIP324Cipher::BIP324Cipher() noexcept +{ + m_key.MakeNewKey(true); + uint256 entropy = GetRandHash(); + m_our_pubkey = m_key.EllSwiftCreate(MakeByteSpan(entropy)); +} + +BIP324Cipher::BIP324Cipher(const CKey& key, Span ent32) noexcept : + m_key(key) +{ + m_our_pubkey = m_key.EllSwiftCreate(ent32); +} + +BIP324Cipher::BIP324Cipher(const CKey& key, const EllSwiftPubKey& pubkey) noexcept : + m_key(key), m_our_pubkey(pubkey) {} + +void BIP324Cipher::Initialize(const EllSwiftPubKey& their_pubkey, bool initiator) noexcept +{ + // Determine salt (fixed string + network magic bytes) + const auto& message_header = Params().MessageStart(); + std::string salt = std::string{"bitcoin_v2_shared_secret"} + std::string(std::begin(message_header), std::end(message_header)); + + // Perform ECDH to derive shared secret. + ECDHSecret ecdh_secret = m_key.ComputeBIP324ECDHSecret(their_pubkey, m_our_pubkey, initiator); + + // Derive encryption keys from shared secret, and initialize stream ciphers and AEADs. + CHKDF_HMAC_SHA256_L32 hkdf(UCharCast(ecdh_secret.data()), ecdh_secret.size(), salt); + std::array hkdf_32_okm; + hkdf.Expand32("initiator_L", UCharCast(hkdf_32_okm.data())); + (initiator ? m_send_l_cipher : m_recv_l_cipher).emplace(hkdf_32_okm, REKEY_INTERVAL); + hkdf.Expand32("initiator_P", UCharCast(hkdf_32_okm.data())); + (initiator ? m_send_p_cipher : m_recv_p_cipher).emplace(hkdf_32_okm, REKEY_INTERVAL); + hkdf.Expand32("responder_L", UCharCast(hkdf_32_okm.data())); + (initiator ? m_recv_l_cipher : m_send_l_cipher).emplace(hkdf_32_okm, REKEY_INTERVAL); + hkdf.Expand32("responder_P", UCharCast(hkdf_32_okm.data())); + (initiator ? m_recv_p_cipher : m_send_p_cipher).emplace(hkdf_32_okm, REKEY_INTERVAL); + + // Derive garbage terminators from shared secret. + hkdf.Expand32("garbage_terminators", UCharCast(hkdf_32_okm.data())); + std::copy(std::begin(hkdf_32_okm), std::begin(hkdf_32_okm) + GARBAGE_TERMINATOR_LEN, + (initiator ? m_send_garbage_terminator : m_recv_garbage_terminator).begin()); + std::copy(std::end(hkdf_32_okm) - GARBAGE_TERMINATOR_LEN, std::end(hkdf_32_okm), + (initiator ? m_recv_garbage_terminator : m_send_garbage_terminator).begin()); + + // Derive session id from shared secret. + hkdf.Expand32("session_id", UCharCast(m_session_id.data())); + + // Wipe all variables that contain information which could be used to re-derive encryption keys. + memory_cleanse(ecdh_secret.data(), ecdh_secret.size()); + memory_cleanse(hkdf_32_okm.data(), sizeof(hkdf_32_okm)); + memory_cleanse(&hkdf, sizeof(hkdf)); + m_key = CKey(); +} + +void BIP324Cipher::Encrypt(Span contents, Span aad, bool ignore, Span output) noexcept +{ + assert(output.size() == contents.size() + EXPANSION); + + // Encrypt length. + std::byte len[LENGTH_LEN]; + len[0] = std::byte{(uint8_t)(contents.size() & 0xFF)}; + len[1] = std::byte{(uint8_t)((contents.size() >> 8) & 0xFF)}; + len[2] = std::byte{(uint8_t)((contents.size() >> 16) & 0xFF)}; + m_send_l_cipher->Crypt(len, output.first(LENGTH_LEN)); + + // Encrypt plaintext. + std::byte header[HEADER_LEN] = {ignore ? IGNORE_BIT : std::byte{0}}; + m_send_p_cipher->Encrypt(header, contents, aad, output.subspan(LENGTH_LEN)); +} + +uint32_t BIP324Cipher::DecryptLength(Span input) noexcept +{ + assert(input.size() == LENGTH_LEN); + + std::byte buf[LENGTH_LEN]; + // Decrypt length + m_recv_l_cipher->Crypt(input, buf); + // Convert to number. + return uint32_t(buf[0]) + (uint32_t(buf[1]) << 8) + (uint32_t(buf[2]) << 16); +} + +bool BIP324Cipher::Decrypt(Span input, Span aad, bool& ignore, Span contents) noexcept +{ + assert(input.size() + LENGTH_LEN == contents.size() + EXPANSION); + + std::byte header[HEADER_LEN]; + if (!m_recv_p_cipher->Decrypt(input, aad, header, contents)) return false; + + ignore = (header[0] & IGNORE_BIT) == IGNORE_BIT; + return true; +} diff --git a/src/bip324.h b/src/bip324.h new file mode 100644 index 00000000000..1738356ee51 --- /dev/null +++ b/src/bip324.h @@ -0,0 +1,90 @@ +// Copyright (c) 2023 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_BIP324_H +#define BITCOIN_BIP324_H + +#include +#include + +#include +#include +#include +#include +#include + +/** The BIP324 packet cipher, encapsulating its key derivation, stream cipher, and AEAD. */ +class BIP324Cipher +{ +public: + static constexpr unsigned SESSION_ID_LEN{32}; + static constexpr unsigned GARBAGE_TERMINATOR_LEN{16}; + static constexpr unsigned REKEY_INTERVAL{224}; + static constexpr unsigned LENGTH_LEN{3}; + static constexpr unsigned HEADER_LEN{1}; + static constexpr unsigned EXPANSION = LENGTH_LEN + HEADER_LEN + FSChaCha20Poly1305::EXPANSION; + static constexpr std::byte IGNORE_BIT{0x80}; + +private: + std::optional m_send_l_cipher; + std::optional m_recv_l_cipher; + std::optional m_send_p_cipher; + std::optional m_recv_p_cipher; + + CKey m_key; + EllSwiftPubKey m_our_pubkey; + + std::array m_session_id; + std::array m_send_garbage_terminator; + std::array m_recv_garbage_terminator; + +public: + /** Initialize a BIP324 cipher with securely generated random keys. */ + BIP324Cipher() noexcept; + + /** Initialize a BIP324 cipher with specified key and encoding entropy (testing only). */ + BIP324Cipher(const CKey& key, Span ent32) noexcept; + + /** Initialize a BIP324 cipher with specified key (testing only). */ + BIP324Cipher(const CKey& key, const EllSwiftPubKey& pubkey) noexcept; + + /** Retrieve our public key. */ + const EllSwiftPubKey& GetOurPubKey() const noexcept { return m_our_pubkey; } + + /** Initialize when the other side's public key is received. Can only be called once. */ + void Initialize(const EllSwiftPubKey& their_pubkey, bool initiator) noexcept; + + /** Determine whether this cipher is fully initialized. */ + explicit operator bool() const noexcept { return m_send_l_cipher.has_value(); } + + /** Encrypt a packet. Only after Initialize(). + * + * It must hold that output.size() == contents.size() + EXPANSION. + */ + void Encrypt(Span contents, Span aad, bool ignore, Span output) noexcept; + + /** Decrypt the length of a packet. Only after Initialize(). + * + * It must hold that input.size() == LENGTH_LEN. + */ + unsigned DecryptLength(Span input) noexcept; + + /** Decrypt a packet. Only after Initialize(). + * + * It must hold that input.size() + LENGTH_LEN == contents.size() + EXPANSION. + * Contents.size() must equal the length returned by DecryptLength. + */ + bool Decrypt(Span input, Span aad, bool& ignore, Span contents) noexcept; + + /** Get the Session ID. Only after Initialize(). */ + Span GetSessionID() const noexcept { return m_session_id; } + + /** Get the Garbage Terminator to send. Only after Initialize(). */ + Span GetSendGarbageTerminator() const noexcept { return m_send_garbage_terminator; } + + /** Get the expected Garbage Terminator to receive. Only after Initialize(). */ + Span GetReceiveGarbageTerminator() const noexcept { return m_recv_garbage_terminator; } +}; + +#endif // BITCOIN_BIP324_H diff --git a/src/pubkey.h b/src/pubkey.h index 7d37504b012..00defa25a01 100644 --- a/src/pubkey.h +++ b/src/pubkey.h @@ -299,6 +299,9 @@ private: std::array m_pubkey; public: + /** Default constructor creates all-zero pubkey (which is valid). */ + EllSwiftPubKey() noexcept = default; + /** Construct a new ellswift public key from a given serialization. */ EllSwiftPubKey(const std::array& ellswift) : m_pubkey(ellswift) {} diff --git a/src/test/bip324_tests.cpp b/src/test/bip324_tests.cpp new file mode 100644 index 00000000000..3b0ec8513e8 --- /dev/null +++ b/src/test/bip324_tests.cpp @@ -0,0 +1,242 @@ +// Copyright (c) 2023 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 +#include + +#include + +namespace { + +void TestBIP324PacketVector( + uint32_t in_idx, + const std::string& in_priv_ours_hex, + const std::string& in_ellswift_ours_hex, + const std::string& in_ellswift_theirs_hex, + bool in_initiating, + const std::string& in_contents_hex, + uint32_t in_multiply, + const std::string& in_aad_hex, + bool in_ignore, + const std::string& mid_send_garbage_hex, + const std::string& mid_recv_garbage_hex, + const std::string& out_session_id_hex, + const std::string& out_ciphertext_hex, + const std::string& out_ciphertext_endswith_hex) +{ + // Convert input from hex to char/byte vectors/arrays. + const auto in_priv_ours = ParseHex(in_priv_ours_hex); + const auto in_ellswift_ours_vec = ParseHex(in_ellswift_ours_hex); + assert(in_ellswift_ours_vec.size() == 64); + std::array in_ellswift_ours; + std::copy(in_ellswift_ours_vec.begin(), in_ellswift_ours_vec.end(), in_ellswift_ours.begin()); + const auto in_ellswift_theirs_vec = ParseHex(in_ellswift_theirs_hex); + assert(in_ellswift_theirs_vec.size() == 64); + std::array in_ellswift_theirs; + std::copy(in_ellswift_theirs_vec.begin(), in_ellswift_theirs_vec.end(), in_ellswift_theirs.begin()); + const auto in_contents = ParseHex(in_contents_hex); + const auto in_aad = ParseHex(in_aad_hex); + const auto mid_send_garbage = ParseHex(mid_send_garbage_hex); + const auto mid_recv_garbage = ParseHex(mid_recv_garbage_hex); + const auto out_session_id = ParseHex(out_session_id_hex); + const auto out_ciphertext = ParseHex(out_ciphertext_hex); + const auto out_ciphertext_endswith = ParseHex(out_ciphertext_endswith_hex); + + // Load keys + CKey key; + key.Set(in_priv_ours.begin(), in_priv_ours.end(), true); + EllSwiftPubKey ellswift_ours(in_ellswift_ours); + EllSwiftPubKey ellswift_theirs(in_ellswift_theirs); + + // Instantiate encryption BIP324 cipher. + BIP324Cipher cipher(key, ellswift_ours); + BOOST_CHECK(!cipher); + BOOST_CHECK(cipher.GetOurPubKey() == ellswift_ours); + cipher.Initialize(ellswift_theirs, in_initiating); + BOOST_CHECK(cipher); + + // Compare session variables. + BOOST_CHECK(Span{out_session_id} == cipher.GetSessionID()); + BOOST_CHECK(Span{mid_send_garbage} == cipher.GetSendGarbageTerminator()); + BOOST_CHECK(Span{mid_recv_garbage} == cipher.GetReceiveGarbageTerminator()); + + // Seek to the numbered packet. + for (uint32_t i = 0; i < in_idx; ++i) { + std::vector dummy(cipher.EXPANSION); + cipher.Encrypt({}, {}, false, dummy); + } + + // Construct contents and encrypt it. + std::vector contents; + for (uint32_t i = 0; i < in_multiply; ++i) { + contents.insert(contents.end(), in_contents.begin(), in_contents.end()); + } + std::vector ciphertext(contents.size() + cipher.EXPANSION); + cipher.Encrypt(contents, in_aad, in_ignore, ciphertext); + + // Verify ciphertext. Note that the test vectors specify either out_ciphertext (for short + // messages) or out_ciphertext_endswith (for long messages), so only check the relevant one. + if (!out_ciphertext.empty()) { + BOOST_CHECK(out_ciphertext == ciphertext); + } else { + BOOST_CHECK(ciphertext.size() >= out_ciphertext_endswith.size()); + BOOST_CHECK(Span{out_ciphertext_endswith} == Span{ciphertext}.last(out_ciphertext_endswith.size())); + } + + // Note that we don't test decryption here, as the test vectors don't provide the other party's + // private key, so we cannot act like them. See the bip324_cipher_roundtrip fuzz test for a test + // that does cover decryption. +} + +} // namespace + +BOOST_FIXTURE_TEST_SUITE(bip324_tests, BasicTestingSetup) + +BOOST_AUTO_TEST_CASE(packet_test_vectors) { + // BIP324 key derivation uses network magic in the HKDF process. We use mainnet params here + // as that is what the test vectors are written for. + SelectParams(ChainType::MAIN); + + // The test vectors are converted using the following Python code in the BIP bip-0324/ directory: + // + // import sys + // import csv + // with open('packet_encoding_test_vectors.csv', newline='', encoding='utf-8') as csvfile: + // reader = csv.DictReader(csvfile) + // quote = lambda x: "\"" + x + "\"" + // for row in reader: + // args = [ + // row['in_idx'], + // quote(row['in_priv_ours']), + // quote(row['in_ellswift_ours']), + // quote(row['in_ellswift_theirs']), + // "true" if int(row['in_initiating']) else "false", + // quote(row['in_contents']), + // row['in_multiply'], + // quote(row['in_aad']), + // "true" if int(row['in_ignore']) else "false", + // quote(row['mid_send_garbage_terminator']), + // quote(row['mid_recv_garbage_terminator']), + // quote(row['out_session_id']), + // quote(row['out_ciphertext']), + // quote(row['out_ciphertext_endswith']) + // ] + // print(" TestBIP324PacketVector(\n " + ",\n ".join(args) + ");") + TestBIP324PacketVector( + 1, + "61062ea5071d800bbfd59e2e8b53d47d194b095ae5a4df04936b49772ef0d4d7", + "ec0adff257bbfe500c188c80b4fdd640f6b45a482bbc15fc7cef5931deff0aa186f6eb9bba7b85dc4dcc28b28722de1e3d9108b985e2967045668f66098e475b", + "a4a94dfce69b4a2a0a099313d10f9f7e7d649d60501c9e1d274c300e0d89aafaffffffffffffffffffffffffffffffffffffffffffffffffffffffff8faf88d5", + true, + "8e", + 1, + "", + false, + "faef555dfcdb936425d84aba524758f3", + "02cb8ff24307a6e27de3b4e7ea3fa65b", + "ce72dffb015da62b0d0f5474cab8bc72605225b0cee3f62312ec680ec5f41ba5", + "7530d2a18720162ac09c25329a60d75adf36eda3c3", + ""); + TestBIP324PacketVector( + 999, + "1f9c581b35231838f0f17cf0c979835baccb7f3abbbb96ffcc318ab71e6e126f", + "a1855e10e94e00baa23041d916e259f7044e491da6171269694763f018c7e63693d29575dcb464ac816baa1be353ba12e3876cba7628bd0bd8e755e721eb0140", + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f0000000000000000000000000000000000000000000000000000000000000000", + false, + "3eb1d4e98035cfd8eeb29bac969ed3824a", + 1, + "", + false, + "efb64fd80acd3825ac9bc2a67216535a", + "b3cb553453bceb002897e751ff7588bf", + "9267c54560607de73f18c563b76a2442718879c52dd39852885d4a3c9912c9ea", + "1da1bcf589f9b61872f45b7fa5371dd3f8bdf5d515b0c5f9fe9f0044afb8dc0aa1cd39a8c4", + ""); + TestBIP324PacketVector( + 0, + "0286c41cd30913db0fdff7a64ebda5c8e3e7cef10f2aebc00a7650443cf4c60d", + "d1ee8a93a01130cbf299249a258f94feb5f469e7d0f2f28f69ee5e9aa8f9b54a60f2c3ff2d023634ec7f4127a96cc11662e402894cf1f694fb9a7eaa5f1d9244", + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffff22d5e441524d571a52b3def126189d3f416890a99d4da6ede2b0cde1760ce2c3f98457ae", + true, + "054290a6c6ba8d80478172e89d32bf690913ae9835de6dcf206ff1f4d652286fe0ddf74deba41d55de3edc77c42a32af79bbea2c00bae7492264c60866ae5a", + 1, + "84932a55aac22b51e7b128d31d9f0550da28e6a3f394224707d878603386b2f9d0c6bcd8046679bfed7b68c517e7431e75d9dd34605727d2ef1c2babbf680ecc8d68d2c4886e9953a4034abde6da4189cd47c6bb3192242cf714d502ca6103ee84e08bc2ca4fd370d5ad4e7d06c7fbf496c6c7cc7eb19c40c61fb33df2a9ba48497a96c98d7b10c1f91098a6b7b16b4bab9687f27585ade1491ae0dba6a79e1e2d85dd9d9d45c5135ca5fca3f0f99a60ea39edbc9efc7923111c937913f225d67788d5f7e8852b697e26b92ec7bfcaa334a1665511c2b4c0a42d06f7ab98a9719516c8fd17f73804555ee84ab3b7d1762f6096b778d3cb9c799cbd49a9e4a325197b4e6cc4a5c4651f8b41ff88a92ec428354531f970263b467c77ed11312e2617d0d53fe9a8707f51f9f57a77bfb49afe3d89d85ec05ee17b9186f360c94ab8bb2926b65ca99dae1d6ee1af96cad09de70b6767e949023e4b380e66669914a741ed0fa420a48dbc7bfae5ef2019af36d1022283dd90655f25eec7151d471265d22a6d3f91dc700ba749bb67c0fe4bc0888593fbaf59d3c6fff1bf756a125910a63b9682b597c20f560ecb99c11a92c8c8c3f7fbfaa103146083a0ccaecf7a5f5e735a784a8820155914a289d57d8141870ffcaf588882332e0bcd8779efa931aa108dab6c3cce76691e345df4a91a03b71074d66333fd3591bff071ea099360f787bbe43b7b3dff2a59c41c7642eb79870222ad1c6f2e5a191ed5acea51134679587c9cf71c7d8ee290be6bf465c4ee47897a125708704ad610d8d00252d01959209d7cd04d5ecbbb1419a7e84037a55fefa13dee464b48a35c96bcb9a53e7ed461c3a1607ee00c3c302fd47cd73fda7493e947c9834a92d63dcfbd65aa7c38c3e3a2748bb5d9a58e7495d243d6b741078c8f7ee9c8813e473a323375702702b0afae1550c8341eedf5247627343a95240cb02e3e17d5dca16f8d8d3b2228e19c06399f8ec5c5e9dbe4caef6a0ea3ffb1d3c7eac03ae030e791fa12e537c80d56b55b764cadf27a8701052df1282ba8b5e3eb62b5dc7973ac40160e00722fa958d95102fc25c549d8c0e84bed95b7acb61ba65700c4de4feebf78d13b9682c52e937d23026fb4c6193e6644e2d3c99f91f4f39a8b9fc6d013f89c3793ef703987954dc0412b550652c01d922f525704d32d70d6d4079bc3551b563fb29577b3aecdc9505011701dddfd94830431e7a4918927ee44fb3831ce8c4513839e2deea1287f3fa1ab9b61a256c09637dbc7b4f0f8fbb783840f9c24526da883b0df0c473cf231656bd7bc1aaba7f321fec0971c8c2c3444bff2f55e1df7fea66ec3e440a612db9aa87bb505163a59e06b96d46f50d8120b92814ac5ab146bc78dbbf91065af26107815678ce6e33812e6bf3285d4ef3b7b04b076f21e7820dcbfdb4ad5218cf4ff6a65812d8fcb98ecc1e95e2fa58e3efe4ce26cd0bd400d6036ab2ad4f6c713082b5e3f1e04eb9e3b6c8f63f57953894b9e220e0130308e1fd91f72d398c1e7962ca2c31be83f31d6157633581a0a6910496de8d55d3d07090b6aa087159e388b7e7dec60f5d8a60d93ca2ae91296bd484d916bfaaa17c8f45ea4b1a91b37c82821199a2b7596672c37156d8701e7352aa48671d3b1bbbd2bd5f0a2268894a25b0cb2514af39c8743f8cce8ab4b523053739fd8a522222a09acf51ac704489cf17e4b7125455cb8f125b4d31af1eba1f8cf7f81a5a100a141a7ee72e8083e065616649c241f233645c5fc865d17f0285f5c52d9f45312c979bfb3ce5f2a1b951deddf280ffb3f370410cffd1583bfa90077835aa201a0712d1dcd1293ee177738b14e6b5e2a496d05220c3253bb6578d6aff774be91946a614dd7e879fb3dcf7451e0b9adb6a8c44f53c2c464bcc0019e9fad89cac7791a0a3f2974f759a9856351d4d2d7c5612c17cfc50f8479945df57716767b120a590f4bf656f4645029a525694d8a238446c5f5c2c1c995c09c1405b8b1eb9e0352ffdf766cc964f8dcf9f8f043dfab6d102cf4b298021abd78f1d9025fa1f8e1d710b38d9d1652f2d88d1305874ec41609b6617b65c5adb19b6295dc5c5da5fdf69f28144ea12f17c3c6fcce6b9b5157b3dfc969d6725fa5b098a4d9b1d31547ed4c9187452d281d0a5d456008caf1aa251fac8f950ca561982dc2dc908d3691ee3b6ad3ae3d22d002577264ca8e49c523bd51c4846be0d198ad9407bf6f7b82c79893eb2c05fe9981f687a97a4f01fe45ff8c8b7ecc551135cd960a0d6001ad35020be07ffb53cb9e731522ca8ae9364628914b9b8e8cc2f37f03393263603cc2b45295767eb0aac29b0930390eb89587ab2779d2e3decb8042acece725ba42eda650863f418f8d0d50d104e44fbbe5aa7389a4a144a8cecf00f45fb14c39112f9bfb56c0acbd44fa3ff261f5ce4acaa5134c2c1d0cca447040820c81ab1bcdc16aa075b7c68b10d06bbb7ce08b5b805e0238f24402cf24a4b4e00701935a0c68add3de090903f9b85b153cb179a582f57113bfc21c2093803f0cfa4d9d4672c2b05a24f7e4c34a8e9101b70303a7378b9c50b6cddd46814ef7fd73ef6923feceab8fc5aa8b0d185f2e83c7a99dcb1077c0ab5c1f5d5f01ba2f0420443f75c4417db9ebf1665efbb33dca224989920a64b44dc26f682cc77b4632c8454d49135e52503da855bc0f6ff8edc1145451a9772c06891f41064036b66c3119a0fc6e80dffeb65dc456108b7ca0296f4175fff3ed2b0f842cd46bd7e86f4c62dfaf1ddbf836263c00b34803de164983d0811cebfac86e7720c726d3048934c36c23189b02386a722ca9f0fe00233ab50db928d3bccea355cc681144b8b7edcaae4884d5a8f04425c0890ae2c74326e138066d8c05f4c82b29df99b034ea727afde590a1f2177ace3af99cfb1729d6539ce7f7f7314b046aab74497e63dd399e1f7d5f16517c23bd830d1fdee810f3c3b77573dd69c4b97d80d71fb5a632e00acdfa4f8e829faf3580d6a72c40b28a82172f8dcd4627663ebf6069736f21735fd84a226f427cd06bb055f94e7c92f31c48075a2955d82a5b9d2d0198ce0d4e131a112570a8ee40fb80462a81436a58e7db4e34b6e2c422e82f934ecda9949893da5730fc5c23c7c920f363f85ab28cc6a4206713c3152669b47efa8238fa826735f17b4e78750276162024ec85458cd5808e06f40dd9fd43775a456a3ff6cae90550d76d8b2899e0762ad9a371482b3e38083b1274708301d6346c22fea9bb4b73db490ff3ab05b2f7f9e187adef139a7794454b7300b8cc64d3ad76c0e4bc54e08833a4419251550655380d675bc91855aeb82585220bb97f03e976579c08f321b5f8f70988d3061f41465517d53ac571dbf1b24b94443d2e9a8e8a79b392b3d6a4ecdd7f626925c365ef6221305105ce9b5f5b6ecc5bed3d702bd4b7f5008aa8eb8c7aa3ade8ecf6251516fbefeea4e1082aa0e1848eddb31ffe44b04792d296054402826e4bd054e671f223e5557e4c94f89ca01c25c44f1a2ff2c05a70b43408250705e1b858bf0670679fdcd379203e36be3500dd981b1a6422c3cf15224f7fefdef0a5f225c5a09d15767598ecd9e262460bb33a4b5d09a64591efabc57c923d3be406979032ae0bc0997b65336a06dd75b253332ad6a8b63ef043f780a1b3fb6d0b6cad98b1ef4a02535eb39e14a866cfc5fc3a9c5deb2261300d71280ebe66a0776a151469551c3c5fa308757f956655278ec6330ae9e3625468c5f87e02cd9a6489910d4143c1f4ee13aa21a6859d907b788e28572fecee273d44e4a900fa0aa668dd861a60fb6b6b12c2c5ef3c8df1bd7ef5d4b0d1cdb8c15fffbb365b9784bd94abd001c6966216b9b67554ad7cb7f958b70092514f7800fc40244003e0fd1133a9b850fb17f4fcafde07fc87b07fb510670654a5d2d6fc9876ac74728ea41593beef003d6858786a52d3a40af7529596767c17000bfaf8dc52e871359f4ad8bf6e7b2853e5229bdf39657e213580294a5317c5df172865e1e17fe37093b585e04613f5f078f761b2b1752eb32983afda24b523af8851df9a02b37e77f543f18888a782a994a50563334282bf9cdfccc183fdf4fcd75ad86ee0d94f91ee2300a5befbccd14e03a77fc031a8cfe4f01e4c5290f5ac1da0d58ea054bd4837cfd93e5e34fc0eb16e48044ba76131f228d16cde9b0bb978ca7cdcd10653c358bdb26fdb723a530232c32ae0a4cecc06082f46e1c1d596bfe60621ad1e354e01e07b040cc7347c016653f44d926d13ca74e6cbc9d4ab4c99f4491c95c76fff5076b3936eb9d0a286b97c035ca88a3c6309f5febfd4cdaac869e4f58ed409b1e9eb4192fb2f9c2f12176d460fd98286c9d6df84598f260119fd29c63f800c07d8df83d5cc95f8c2fea2812e7890e8a0718bb1e031ecbebc0436dcf3e3b9a58bcc06b4c17f711f80fe1dffc3326a6eb6e00283055c6dabe20d311bfd5019591b7954f8163c9afad9ef8390a38f3582e0a79cdf0353de8eeb6b5f9f27b16ffdef7dd62869b4840ee226ccdce95e02c4545eb981b60571cd83f03dc5eaf8c97a0829a4318a9b3dc06c0e003db700b2260ff1fa8fee66890e637b109abb03ec901b05ca599775f48af50154c0e67d82bf0f558d7d3e0778dc38bea1eb5f74dc8d7f90abdf5511a424be66bf8b6a3cacb477d2e7ef4db68d2eba4d5289122d851f9501ba7e9c4957d8eba3be3fc8e785c4265a1d65c46f2809b70846c693864b169c9dcb78be26ea14b8613f145b01887222979a9e67aee5f800caa6f5c4229bdeefc901232ace6143c9865e4d9c07f51aa200afaf7e48a7d1d8faf366023beab12906ffcb3eaf72c0eb68075e4daf3c080e0c31911befc16f0cc4a09908bb7c1e26abab38bd7b788e1a09c0edf1a35a38d2ff1d3ed47fcdaae2f0934224694f5b56705b9409b6d3d64f3833b686f7576ec64bbdd6ff174e56c2d1edac0011f904681a73face26573fbba4e34652f7ae84acfb2fa5a5b3046f98178cd0831df7477de70e06a4c00e305f31aafc026ef064dd68fd3e4252b1b91d617b26c6d09b6891a00df68f105b5962e7f9d82da101dd595d286da721443b72b2aba2377f6e7772e33b3a5e3753da9c2578c5d1daab80187f55518c72a64ee150a7cb5649823c08c9f62cd7d020b45ec2cba8310db1a7785a46ab24785b4d54ff1660b5ca78e05a9a55edba9c60bf044737bc468101c4e8bd1480d749be5024adefca1d998abe33eaeb6b11fbb39da5d905fdd3f611b2e51517ccee4b8af72c2d948573505590d61a6783ab7278fc43fe55b1fcc0e7216444d3c8039bb8145ef1ce01c50e95a3f3feab0aee883fdb94cc13ee4d21c542aa795e18932228981690f4d4c57ca4db6eb5c092e29d8a05139d509a8aeb48baa1eb97a76e597a32b280b5e9d6c36859064c98ff96ef5126130264fa8d2f49213870d9fb036cff95da51f270311d9976208554e48ffd486470d0ecdb4e619ccbd8226147204baf8e235f54d8b1cba8fa34a9a4d055de515cdf180d2bb6739a175183c472e30b5c914d09eeb1b7dafd6872b38b48c6afc146101200e6e6a44fe5684e220adc11f5c403ddb15df8051e6bdef09117a3a5349938513776286473a3cf1d2788bb875052a2e6459fa7926da33380149c7f98d7700528a60c954e6f5ecb65842fde69d614be69eaa2040a4819ae6e756accf936e14c1e894489744a79c1f2c1eb295d13e2d767c09964b61f9cfe497649f712", + false, + "d4e3f18ac2e2095edb5c3b94236118ad", + "4faa6c4233d9fd53d170ede4172142a8", + "23f154ac43cfc59c4243e9fc68aeec8f19ad3942d74108e833b36f0dd3dcd357", + "8da7de6ea7bf2a81a396a42880ba1f5756734c4821309ac9aeffa2a26ce86873b9dc4935a772de6ec5162c6d075b14536800fb174841153511bfb597e992e2fe8a450c4bce102cc550bb37fd564c4d60bf884e", + ""); + TestBIP324PacketVector( + 223, + "6c77432d1fda31e9f942f8af44607e10f3ad38a65f8a4bddae823e5eff90dc38", + "d2685070c1e6376e633e825296634fd461fa9e5bdf2109bcebd735e5a91f3e587c5cb782abb797fbf6bb5074fd1542a474f2a45b673763ec2db7fb99b737bbb9", + "56bd0c06f10352c3a1a9f4b4c92f6fa2b26df124b57878353c1fc691c51abea77c8817daeeb9fa546b77c8daf79d89b22b0e1b87574ece42371f00237aa9d83a", + false, + "7e0e78eb6990b059e6cf0ded66ea93ef82e72aa2f18ac24f2fc6ebab561ae557420729da103f64cecfa20527e15f9fb669a49bbbf274ef0389b3e43c8c44e5f60bf2ac38e2b55e7ec4273dba15ba41d21f8f5b3ee1688b3c29951218caf847a97fb50d75a86515d445699497d968164bf740012679b8962de573be941c62b7ef", + 1, + "", + true, + "cf2e25f23501399f30738d7eee652b90", + "225a477a28a54ea7671d2b217a9c29db", + "7ec02fea8c1484e3d0875f978c5f36d63545e2e4acf56311394422f4b66af612", + "", + "729847a3e9eba7a5bff454b5de3b393431ee360736b6c030d7a5bd01d1203d2e98f528543fd2bf886ccaa1ada5e215a730a36b3f4abfc4e252c89eb01d9512f94916dae8a76bf16e4da28986ffe159090fe5267ee3394300b7ccf4dfad389a26321b3a3423e4594a82ccfbad16d6561ecb8772b0cb040280ff999a29e3d9d4fd"); + TestBIP324PacketVector( + 448, + "a6ec25127ca1aa4cf16b20084ba1e6516baae4d32422288e9b36d8bddd2de35a", + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffff053d7ecca53e33e185a8b9be4e7699a97c6ff4c795522e5918ab7cd6b6884f67e683f3dc", + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffa7730be30000000000000000000000000000000000000000000000000000000000000000", + true, + "00cf68f8f7ac49ffaa02c4864fdf6dfe7bbf2c740b88d98c50ebafe32c92f3427f57601ffcb21a3435979287db8fee6c302926741f9d5e464c647eeb9b7acaeda46e00abd7506fc9a719847e9a7328215801e96198dac141a15c7c2f68e0690dd1176292a0dded04d1f548aad88f1aebdc0a8f87da4bb22df32dd7c160c225b843e83f6525d6d484f502f16d923124fc538794e21da2eb689d18d87406ecced5b9f92137239ed1d37bcfa7836641a83cf5e0a1cf63f51b06f158e499a459ede41c", + 1, + "", + false, + "fead69be77825a23daec377c362aa560", + "511d4980526c5e64aa7187462faeafdd", + "acb8f084ea763ddd1b92ac4ed23bf44de20b84ab677d4e4e6666a6090d40353d", + "", + "77b4656934a82de1a593d8481f020194ddafd8cac441f9d72aeb8721e6a14f49698ca6d9b2b6d59d07a01aa552fd4d5b68d0d1617574c77dea10bfadbaa31b83885b7ceac2fd45e3e4a331c51a74e7b1698d81b64c87c73c5b9258b4d83297f9debc2e9aa07f8572ff434dc792b83ecf07b3197de8dc9cf7be56acb59c66cff5"); + TestBIP324PacketVector( + 673, + "0af952659ed76f80f585966b95ab6e6fd68654672827878684c8b547b1b94f5a", + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffc81017fd92fd31637c26c906b42092e11cc0d3afae8d9019d2578af22735ce7bc469c72d", + "9652d78baefc028cd37a6a92625b8b8f85fde1e4c944ad3f20e198bef8c02f19fffffffffffffffffffffffffffffffffffffffffffffffffffffffff2e91870", + false, + "5c6272ee55da855bbbf7b1246d9885aa7aa601a715ab86fa46c50da533badf82b97597c968293ae04e", + 97561, + "", + false, + "5e2375ac629b8df1e4ff3617c6255a70", + "70bcbffcb62e4d29d2605d30bceef137", + "7332e92a3f9d2792c4d444fac5ed888c39a073043a65eefb626318fd649328f8", + "", + "657a4a19711ce593c3844cb391b224f60124aba7e04266233bc50cafb971e26c7716b76e98376448f7d214dd11e629ef9a974d60e3770a695810a61c4ba66d78b936ee7892b98f0b48ddae9fcd8b599dca1c9b43e9b95e0226cf8d4459b8a7c2c4e6db80f1d58c7b20dd7208fa5c1057fb78734223ee801dbd851db601fee61e"); + TestBIP324PacketVector( + 1024, + "f90e080c64b05824c5a24b2501d5aeaf08af3872ee860aa80bdcd430f7b63494", + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffff115173765dc202cf029ad3f15479735d57697af12b0131dd21430d5772e4ef11474d58b9", + "12a50f3fafea7c1eeada4cf8d33777704b77361453afc83bda91eef349ae044d20126c6200547ea5a6911776c05dee2a7f1a9ba7dfbabbbd273c3ef29ef46e46", + true, + "5f67d15d22ca9b2804eeab0a66f7f8e3a10fa5de5809a046084348cbc5304e843ef96f59a59c7d7fdfe5946489f3ea297d941bac326225df316a25fc90f0e65b0d31a9c497e960fdbf8c482516bc8a9c1c77b7f6d0e1143810c737f76f9224e6f2c9af5186b4f7259c7e8d165b6e4fe3d38a60bdbdd4d06ecdcaaf62086070dbb68686b802d53dfd7db14b18743832605f5461ad81e2af4b7e8ff0eff0867a25b93cec7becf15c43131895fed09a83bf1ee4a87d44dd0f02a837bf5a1232e201cb882734eb9643dc2dc4d4e8b5690840766212c7ac8f38ad8a9ec47c7a9b3e022ae3eb6a32522128b518bd0d0085dd81c5", + 69615, + "", + true, + "b709dea25e0be287c50e3603482c2e98", + "1f677e9d7392ebe3633fd82c9efb0f16", + "889f339285564fd868401fac8380bb9887925122ec8f31c8ae51ce067def103b", + "", + "7c4b9e1e6c1ce69da7b01513cdc4588fd93b04dafefaf87f31561763d906c672bac3dfceb751ebd126728ac017d4d580e931b8e5c7d5dfe0123be4dc9b2d2238b655c8a7fadaf8082c31e310909b5b731efc12f0a56e849eae6bfeedcc86dd27ef9b91d159256aa8e8d2b71a311f73350863d70f18d0d7302cf551e4303c7733"); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/fuzz/bip324.cpp b/src/test/fuzz/bip324.cpp new file mode 100644 index 00000000000..359de6c66a3 --- /dev/null +++ b/src/test/fuzz/bip324.cpp @@ -0,0 +1,137 @@ +// Copyright (c) 2023 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 +#include + +namespace { + +void Initialize() +{ + ECC_Start(); + SelectParams(ChainType::MAIN); +} + +} // namespace + +FUZZ_TARGET(bip324_cipher_roundtrip, .init=Initialize) +{ + // Test that BIP324Cipher's encryption and decryption agree. + + // Load keys from fuzzer. + FuzzedDataProvider provider(buffer.data(), buffer.size()); + // Initiator key + auto init_key_data = provider.ConsumeBytes(32); + init_key_data.resize(32); + CKey init_key; + init_key.Set(init_key_data.begin(), init_key_data.end(), true); + if (!init_key.IsValid()) return; + // Initiator entropy + auto init_ent = provider.ConsumeBytes(32); + init_ent.resize(32); + // Responder key + auto resp_key_data = provider.ConsumeBytes(32); + resp_key_data.resize(32); + CKey resp_key; + resp_key.Set(resp_key_data.begin(), resp_key_data.end(), true); + if (!resp_key.IsValid()) return; + // Responder entropy + auto resp_ent = provider.ConsumeBytes(32); + resp_ent.resize(32); + + // Initialize ciphers by exchanging public keys. + BIP324Cipher initiator(init_key, init_ent); + assert(!initiator); + BIP324Cipher responder(resp_key, resp_ent); + assert(!responder); + initiator.Initialize(responder.GetOurPubKey(), true); + assert(initiator); + responder.Initialize(initiator.GetOurPubKey(), false); + assert(responder); + + // 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). + XoRoShiRo128PlusPlus rng(provider.ConsumeIntegral()); + + // Compare session IDs and garbage terminators. + assert(initiator.GetSessionID() == responder.GetSessionID()); + assert(initiator.GetSendGarbageTerminator() == responder.GetReceiveGarbageTerminator()); + assert(initiator.GetReceiveGarbageTerminator() == responder.GetSendGarbageTerminator()); + + LIMITED_WHILE(provider.remaining_bytes(), 1000) { + // Mode: + // - Bit 0: whether the ignore bit is set in message + // - Bit 1: whether the responder (0) or initiator (1) sends + // - 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 ignore = mode & 1; + bool from_init = mode & 2; + 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. + std::vector aad(aad_length); + for (auto& val : aad) val = std::byte{(uint8_t)rng()}; + std::vector contents(length); + for (auto& val : contents) val = std::byte{(uint8_t)rng()}; + + // Pick sides. + auto& sender{from_init ? initiator : responder}; + auto& receiver{from_init ? responder : initiator}; + + // Encrypt + std::vector ciphertext(length + initiator.EXPANSION); + sender.Encrypt(contents, aad, ignore, ciphertext); + + // Optionally damage 1 bit in either the ciphertext (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, + (ciphertext.size() + aad.size()) * 8U - 1U); + unsigned damage_pos = damage_bit >> 3; + std::byte damage_val{(uint8_t)(1U << (damage_bit & 3))}; + if (damage_pos >= ciphertext.size()) { + aad[damage_pos - ciphertext.size()] ^= damage_val; + } else { + ciphertext[damage_pos] ^= damage_val; + } + } + + // Decrypt length + uint32_t dec_length = receiver.DecryptLength(Span{ciphertext}.first(initiator.LENGTH_LEN)); + if (!damage) { + assert(dec_length == length); + } else { + // For performance reasons, don't try to decode if length got increased too much. + if (dec_length > 16384 + length) break; + // Otherwise, just append zeros if dec_length > length. + ciphertext.resize(dec_length + initiator.EXPANSION); + } + + // Decrypt + std::vector decrypt(dec_length); + bool dec_ignore{false}; + bool ok = receiver.Decrypt(Span{ciphertext}.subspan(initiator.LENGTH_LEN), aad, dec_ignore, decrypt); + // Decryption *must* fail if the packet was damaged, and succeed if it wasn't. + assert(!ok == damage); + if (!ok) break; + assert(ignore == dec_ignore); + assert(decrypt == contents); + } +} From 1c7582ead6e1119899922041c1af2b4169b0bc74 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Mon, 10 Jul 2023 10:58:20 -0400 Subject: [PATCH 8/8] tests: add decryption test to bip324_tests --- src/bip324.cpp | 11 +++--- src/bip324.h | 8 +++-- src/test/bip324_tests.cpp | 72 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 79 insertions(+), 12 deletions(-) diff --git a/src/bip324.cpp b/src/bip324.cpp index eb223f1f048..7ed99e5585d 100644 --- a/src/bip324.cpp +++ b/src/bip324.cpp @@ -33,7 +33,7 @@ BIP324Cipher::BIP324Cipher(const CKey& key, Span ent32) noexcep BIP324Cipher::BIP324Cipher(const CKey& key, const EllSwiftPubKey& pubkey) noexcept : m_key(key), m_our_pubkey(pubkey) {} -void BIP324Cipher::Initialize(const EllSwiftPubKey& their_pubkey, bool initiator) noexcept +void BIP324Cipher::Initialize(const EllSwiftPubKey& their_pubkey, bool initiator, bool self_decrypt) noexcept { // Determine salt (fixed string + network magic bytes) const auto& message_header = Params().MessageStart(); @@ -43,16 +43,17 @@ void BIP324Cipher::Initialize(const EllSwiftPubKey& their_pubkey, bool initiator ECDHSecret ecdh_secret = m_key.ComputeBIP324ECDHSecret(their_pubkey, m_our_pubkey, initiator); // Derive encryption keys from shared secret, and initialize stream ciphers and AEADs. + bool side = (initiator != self_decrypt); CHKDF_HMAC_SHA256_L32 hkdf(UCharCast(ecdh_secret.data()), ecdh_secret.size(), salt); std::array hkdf_32_okm; hkdf.Expand32("initiator_L", UCharCast(hkdf_32_okm.data())); - (initiator ? m_send_l_cipher : m_recv_l_cipher).emplace(hkdf_32_okm, REKEY_INTERVAL); + (side ? m_send_l_cipher : m_recv_l_cipher).emplace(hkdf_32_okm, REKEY_INTERVAL); hkdf.Expand32("initiator_P", UCharCast(hkdf_32_okm.data())); - (initiator ? m_send_p_cipher : m_recv_p_cipher).emplace(hkdf_32_okm, REKEY_INTERVAL); + (side ? m_send_p_cipher : m_recv_p_cipher).emplace(hkdf_32_okm, REKEY_INTERVAL); hkdf.Expand32("responder_L", UCharCast(hkdf_32_okm.data())); - (initiator ? m_recv_l_cipher : m_send_l_cipher).emplace(hkdf_32_okm, REKEY_INTERVAL); + (side ? m_recv_l_cipher : m_send_l_cipher).emplace(hkdf_32_okm, REKEY_INTERVAL); hkdf.Expand32("responder_P", UCharCast(hkdf_32_okm.data())); - (initiator ? m_recv_p_cipher : m_send_p_cipher).emplace(hkdf_32_okm, REKEY_INTERVAL); + (side ? m_recv_p_cipher : m_send_p_cipher).emplace(hkdf_32_okm, REKEY_INTERVAL); // Derive garbage terminators from shared secret. hkdf.Expand32("garbage_terminators", UCharCast(hkdf_32_okm.data())); diff --git a/src/bip324.h b/src/bip324.h index 1738356ee51..8d025c2ee37 100644 --- a/src/bip324.h +++ b/src/bip324.h @@ -52,8 +52,12 @@ public: /** Retrieve our public key. */ const EllSwiftPubKey& GetOurPubKey() const noexcept { return m_our_pubkey; } - /** Initialize when the other side's public key is received. Can only be called once. */ - void Initialize(const EllSwiftPubKey& their_pubkey, bool initiator) noexcept; + /** Initialize when the other side's public key is received. Can only be called once. + * + * self_decrypt is only for testing, and swaps encryption/decryption keys, so that encryption + * and decryption can be tested without knowing the other side's private key. + */ + void Initialize(const EllSwiftPubKey& their_pubkey, bool initiator, bool self_decrypt = false) noexcept; /** Determine whether this cipher is fully initialized. */ explicit operator bool() const noexcept { return m_send_l_cipher.has_value(); } diff --git a/src/test/bip324_tests.cpp b/src/test/bip324_tests.cpp index 3b0ec8513e8..ccb9e59e589 100644 --- a/src/test/bip324_tests.cpp +++ b/src/test/bip324_tests.cpp @@ -70,10 +70,13 @@ void TestBIP324PacketVector( BOOST_CHECK(Span{mid_send_garbage} == cipher.GetSendGarbageTerminator()); BOOST_CHECK(Span{mid_recv_garbage} == cipher.GetReceiveGarbageTerminator()); + // Vector of encrypted empty messages, encrypted in order to seek to the right position. + std::vector> dummies(in_idx); + // Seek to the numbered packet. for (uint32_t i = 0; i < in_idx; ++i) { - std::vector dummy(cipher.EXPANSION); - cipher.Encrypt({}, {}, false, dummy); + dummies[i].resize(cipher.EXPANSION); + cipher.Encrypt({}, {}, true, dummies[i]); } // Construct contents and encrypt it. @@ -93,9 +96,68 @@ void TestBIP324PacketVector( BOOST_CHECK(Span{out_ciphertext_endswith} == Span{ciphertext}.last(out_ciphertext_endswith.size())); } - // Note that we don't test decryption here, as the test vectors don't provide the other party's - // private key, so we cannot act like them. See the bip324_cipher_roundtrip fuzz test for a test - // that does cover decryption. + for (unsigned error = 0; error <= 12; ++error) { + // error selects a type of error introduced: + // - error=0: no errors, decryption should be successful + // - error=1: wrong side + // - error=2..9: bit error in ciphertext + // - error=10: bit error in aad + // - error=11: extra 0x00 at end of aad + // - error=12: message index wrong + + // Instantiate self-decrypting BIP324 cipher. + BIP324Cipher dec_cipher(key, ellswift_ours); + BOOST_CHECK(!dec_cipher); + BOOST_CHECK(dec_cipher.GetOurPubKey() == ellswift_ours); + dec_cipher.Initialize(ellswift_theirs, (error == 1) ^ in_initiating, /*self_decrypt=*/true); + BOOST_CHECK(dec_cipher); + + // Compare session variables. + BOOST_CHECK((Span{out_session_id} == dec_cipher.GetSessionID()) == (error != 1)); + BOOST_CHECK((Span{mid_send_garbage} == dec_cipher.GetSendGarbageTerminator()) == (error != 1)); + BOOST_CHECK((Span{mid_recv_garbage} == dec_cipher.GetReceiveGarbageTerminator()) == (error != 1)); + + // Seek to the numbered packet. + if (in_idx == 0 && error == 12) continue; + uint32_t dec_idx = in_idx ^ (error == 12 ? (1U << InsecureRandRange(16)) : 0); + for (uint32_t i = 0; i < dec_idx; ++i) { + unsigned use_idx = i < in_idx ? i : 0; + bool dec_ignore{false}; + dec_cipher.DecryptLength(Span{dummies[use_idx]}.first(cipher.LENGTH_LEN)); + dec_cipher.Decrypt(Span{dummies[use_idx]}.subspan(cipher.LENGTH_LEN), {}, dec_ignore, {}); + } + + // Construct copied (and possibly damaged) copy of ciphertext. + // Decrypt length + auto to_decrypt = ciphertext; + if (error >= 2 && error <= 9) { + to_decrypt[InsecureRandRange(to_decrypt.size())] ^= std::byte(1U << InsecureRandRange(8)); + } + + // Decrypt length and resize ciphertext to accomodate. + uint32_t dec_len = dec_cipher.DecryptLength(MakeByteSpan(to_decrypt).first(cipher.LENGTH_LEN)); + to_decrypt.resize(dec_len + cipher.EXPANSION); + + // Construct copied (and possibly damaged) copy of aad. + auto dec_aad = in_aad; + if (error == 10) { + if (in_aad.size() == 0) continue; + dec_aad[InsecureRandRange(dec_aad.size())] ^= std::byte(1U << InsecureRandRange(8)); + } + if (error == 11) dec_aad.push_back({}); + + // Decrypt contents. + std::vector decrypted(dec_len); + bool dec_ignore{false}; + bool dec_ok = dec_cipher.Decrypt(Span{to_decrypt}.subspan(cipher.LENGTH_LEN), dec_aad, dec_ignore, decrypted); + + // Verify result. + BOOST_CHECK(dec_ok == !error); + if (dec_ok) { + BOOST_CHECK(decrypted == contents); + BOOST_CHECK(dec_ignore == in_ignore); + } + } } } // namespace