mirror of https://github.com/bitcoin/bitcoin
Co-authored-by: dhruv <856960+dhruv@users.noreply.github.com>pull/28008/head
parent
c91cedf281
commit
990f0f8da9
@ -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 <bip324.h>
|
||||
|
||||
#include <chainparams.h>
|
||||
#include <crypto/chacha20.h>
|
||||
#include <crypto/chacha20poly1305.h>
|
||||
#include <crypto/hkdf_sha256_32.h>
|
||||
#include <random.h>
|
||||
#include <span.h>
|
||||
#include <support/cleanse.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <assert.h>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
|
||||
BIP324Cipher::BIP324Cipher() noexcept
|
||||
{
|
||||
m_key.MakeNewKey(true);
|
||||
uint256 entropy = GetRandHash();
|
||||
m_our_pubkey = m_key.EllSwiftCreate(MakeByteSpan(entropy));
|
||||
}
|
||||
|
||||
BIP324Cipher::BIP324Cipher(const CKey& key, Span<const std::byte> 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<std::byte, 32> 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<const std::byte> contents, Span<const std::byte> aad, bool ignore, Span<std::byte> 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<const std::byte> 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<const std::byte> input, Span<const std::byte> aad, bool& ignore, Span<std::byte> 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;
|
||||
}
|
@ -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 <cstddef>
|
||||
#include <optional>
|
||||
|
||||
#include <crypto/chacha20.h>
|
||||
#include <crypto/chacha20poly1305.h>
|
||||
#include <key.h>
|
||||
#include <pubkey.h>
|
||||
#include <span.h>
|
||||
|
||||
/** 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<FSChaCha20> m_send_l_cipher;
|
||||
std::optional<FSChaCha20> m_recv_l_cipher;
|
||||
std::optional<FSChaCha20Poly1305> m_send_p_cipher;
|
||||
std::optional<FSChaCha20Poly1305> m_recv_p_cipher;
|
||||
|
||||
CKey m_key;
|
||||
EllSwiftPubKey m_our_pubkey;
|
||||
|
||||
std::array<std::byte, SESSION_ID_LEN> m_session_id;
|
||||
std::array<std::byte, GARBAGE_TERMINATOR_LEN> m_send_garbage_terminator;
|
||||
std::array<std::byte, GARBAGE_TERMINATOR_LEN> 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<const std::byte> 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<const std::byte> contents, Span<const std::byte> aad, bool ignore, Span<std::byte> output) noexcept;
|
||||
|
||||
/** Decrypt the length of a packet. Only after Initialize().
|
||||
*
|
||||
* It must hold that input.size() == LENGTH_LEN.
|
||||
*/
|
||||
unsigned DecryptLength(Span<const std::byte> 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<const std::byte> input, Span<const std::byte> aad, bool& ignore, Span<std::byte> contents) noexcept;
|
||||
|
||||
/** Get the Session ID. Only after Initialize(). */
|
||||
Span<const std::byte> GetSessionID() const noexcept { return m_session_id; }
|
||||
|
||||
/** Get the Garbage Terminator to send. Only after Initialize(). */
|
||||
Span<const std::byte> GetSendGarbageTerminator() const noexcept { return m_send_garbage_terminator; }
|
||||
|
||||
/** Get the expected Garbage Terminator to receive. Only after Initialize(). */
|
||||
Span<const std::byte> GetReceiveGarbageTerminator() const noexcept { return m_recv_garbage_terminator; }
|
||||
};
|
||||
|
||||
#endif // BITCOIN_BIP324_H
|
File diff suppressed because one or more lines are too long
@ -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 <bip324.h>
|
||||
#include <chainparams.h>
|
||||
#include <span.h>
|
||||
#include <test/fuzz/FuzzedDataProvider.h>
|
||||
#include <test/fuzz/fuzz.h>
|
||||
#include <test/fuzz/util.h>
|
||||
#include <test/util/xoroshiro128plusplus.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
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<unsigned char>(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<std::byte>(32);
|
||||
init_ent.resize(32);
|
||||
// Responder key
|
||||
auto resp_key_data = provider.ConsumeBytes<unsigned char>(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<std::byte>(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<uint64_t>());
|
||||
|
||||
// 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<uint8_t>();
|
||||
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<unsigned>(0, (1 << aad_length_bits) - 1);
|
||||
unsigned length_bits = 2 * ((mode >> 5) & 7);
|
||||
unsigned length = provider.ConsumeIntegralInRange<unsigned>(0, (1 << length_bits) - 1);
|
||||
// Generate aad and content.
|
||||
std::vector<std::byte> aad(aad_length);
|
||||
for (auto& val : aad) val = std::byte{(uint8_t)rng()};
|
||||
std::vector<std::byte> 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<std::byte> 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<unsigned>(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<std::byte> 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);
|
||||
}
|
||||
}
|
Loading…
Reference in new issue