From 04396cb767fcab81d26b61efa1042a2228d6e4ee Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Thu, 19 Sep 2024 16:18:21 +0200 Subject: [PATCH] Add sv2 noise protocol Co-Authored-By: Christopher Coverdale --- src/pubkey.h | 2 +- src/sv2/CMakeLists.txt | 1 + src/sv2/noise.cpp | 510 +++++++++++++++++++++++++++++++++++ src/sv2/noise.h | 299 ++++++++++++++++++++ src/test/CMakeLists.txt | 1 + src/test/fuzz/CMakeLists.txt | 1 + src/test/fuzz/sv2_noise.cpp | 168 ++++++++++++ src/test/sv2_noise_tests.cpp | 159 +++++++++++ 8 files changed, 1140 insertions(+), 1 deletion(-) create mode 100644 src/sv2/noise.cpp create mode 100644 src/sv2/noise.h create mode 100644 src/test/fuzz/sv2_noise.cpp create mode 100644 src/test/sv2_noise_tests.cpp diff --git a/src/pubkey.h b/src/pubkey.h index b4666aad228e1..798687de1fe5a 100644 --- a/src/pubkey.h +++ b/src/pubkey.h @@ -319,7 +319,7 @@ struct EllSwiftPubKey /** Construct a new ellswift public key from a given serialization. */ EllSwiftPubKey(Span ellswift) noexcept; - /** Decode to normal compressed CPubKey (for debugging purposes). */ + /** Decode to normal compressed CPubKey. */ CPubKey Decode() const; // Read-only access for serialization. diff --git a/src/sv2/CMakeLists.txt b/src/sv2/CMakeLists.txt index e02c4c01fa877..d6e44842e8c87 100644 --- a/src/sv2/CMakeLists.txt +++ b/src/sv2/CMakeLists.txt @@ -3,6 +3,7 @@ # file COPYING or https://opensource.org/license/mit/. add_library(bitcoin_sv2 STATIC EXCLUDE_FROM_ALL + noise.cpp ) target_link_libraries(bitcoin_sv2 diff --git a/src/sv2/noise.cpp b/src/sv2/noise.cpp new file mode 100644 index 0000000000000..62336e3715ac7 --- /dev/null +++ b/src/sv2/noise.cpp @@ -0,0 +1,510 @@ +// Copyright (c) 2023-present 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 + +Sv2SignatureNoiseMessage::Sv2SignatureNoiseMessage(uint16_t version, uint32_t valid_from, uint32_t valid_to, const XOnlyPubKey& static_key, const CKey& authority_key) : m_version{version}, m_valid_from{valid_from}, m_valid_to{valid_to}, m_static_key{static_key} +{ + SignSchnorr(authority_key, m_sig); +} + +uint256 Sv2SignatureNoiseMessage::GetHash() +{ + DataStream ss{}; + ss << m_version + << m_valid_from + << m_valid_to + << m_static_key; + + LogTrace(BCLog::SV2, "Certificate hashed data: %s\n", HexStr(ss)); + + CSHA256 hasher; + hasher.Write(reinterpret_cast(&(*ss.begin())), ss.end() - ss.begin()); + + uint256 hash_output; + hasher.Finalize(hash_output.begin()); + return hash_output; +} + +bool Sv2SignatureNoiseMessage::Validate(XOnlyPubKey authority_key) +{ + if (m_version > 0) { + LogTrace(BCLog::SV2, "Invalid certificate version: %d\n", m_version); + return false; + } + auto now{GetTime()}; + if (std::chrono::seconds{m_valid_from} > now) { + LogTrace(BCLog::SV2, "Certificate valid from is in the future: %d\n", m_valid_from); + return false; + } + if (std::chrono::seconds{m_valid_to} < now) { + LogTrace(BCLog::SV2, "Certificate expired: %d\n", m_valid_to); + return false; + } + + if (!authority_key.VerifySchnorr(this->GetHash(), m_sig)) { + LogTrace(BCLog::SV2, "Certificate signature is invalid\n"); + return false; + } + return true; +} + +void Sv2SignatureNoiseMessage::SignSchnorr(const CKey& authority_key, Span sig) +{ + authority_key.SignSchnorr(this->GetHash(), sig, nullptr, {}); +} + +Sv2CipherState::Sv2CipherState(NoiseHash&& key) : m_key(std::move(key)) {}; + +bool Sv2CipherState::DecryptWithAd(Span associated_data, Span ciphertext, Span plain) +{ + Assume(Sv2Cipher::EncryptedMessageSize(plain.size()) == ciphertext.size()); + + if (m_nonce == UINT64_MAX) { + // This nonce value is reserved, see chapter 5.1 of the Noise paper. + LogTrace(BCLog::SV2, "Nonce exceeds maximum value\n"); + return false; + } + AEADChaCha20Poly1305::Nonce96 nonce = {0, m_nonce}; + auto key = MakeByteSpan(m_key); + AEADChaCha20Poly1305 aead{key}; + if (!aead.Decrypt(ciphertext, associated_data, nonce, plain)) { + LogTrace(BCLog::SV2, "Message decryption failed\n"); + return false; + } + // Only increase nonce if decryption succeeded + m_nonce++; + return true; +} + +bool Sv2CipherState::EncryptWithAd(Span associated_data, Span plain, Span ciphertext) +{ + Assume(Sv2Cipher::EncryptedMessageSize(plain.size()) == ciphertext.size()); + + if (m_nonce == UINT64_MAX) { + // This nonce value is reserved, see chapter 5.1 of the Noise paper. + LogTrace(BCLog::SV2, "Nonce exceeds maximum value\n"); + return false; + } + AEADChaCha20Poly1305::Nonce96 nonce = {0, m_nonce++}; + auto key = MakeByteSpan(m_key); + AEADChaCha20Poly1305 aead{key}; + aead.Encrypt(plain, associated_data, nonce, ciphertext); + return true; +} + +bool Sv2CipherState::EncryptMessage(Span plain, Span ciphertext) +{ + Assume(ciphertext.size() == Sv2Cipher::EncryptedMessageSize(plain.size())); + + std::vector ad; // No associated data + + constexpr size_t max_chunk_size = NOISE_MAX_CHUNK_SIZE - Poly1305::TAGLEN; + size_t num_chunks = (plain.size() + max_chunk_size - 1) / max_chunk_size; + if (num_chunks > 1) { + LogTrace(BCLog::SV2, + "Split into %d chunks (max %d bytes)\n", + num_chunks, max_chunk_size); + } + + // Copy input bytes into output buffer + const std::vector padding(Poly1305::TAGLEN, std::byte(0)); + for (size_t i = 0; i < num_chunks; ++i) { + size_t chunk_start = i * max_chunk_size; + size_t chunk_end = std::min(chunk_start + max_chunk_size, plain.size()); + size_t chunk_size = chunk_end - chunk_start; + const auto encrypted_chunk_start = ciphertext.begin() + i * NOISE_MAX_CHUNK_SIZE; + std::copy(plain.begin() + chunk_start, plain.begin() + chunk_start + chunk_size, encrypted_chunk_start); + std::copy(padding.begin(), padding.end(), encrypted_chunk_start + chunk_size); + } + + // Encrypt each chunk + size_t bytes_written = 0; + for (size_t i = 0; i < num_chunks; ++i) { + size_t chunk_size = std::min(ciphertext.size() - bytes_written, NOISE_MAX_CHUNK_SIZE); + Span chunk = ciphertext.subspan(bytes_written, chunk_size); + Span chunk_plain = ciphertext.subspan(bytes_written, chunk_size - Poly1305::TAGLEN); + if (!EncryptWithAd(ad, chunk_plain, chunk)) { + return false; + } + bytes_written += chunk.size(); + } + + Assume(bytes_written == ciphertext.size()); + return true; +} + +bool Sv2CipherState::DecryptMessage(Span ciphertext, Span plain) +{ + Assume(Sv2Cipher::EncryptedMessageSize(plain.size()) == ciphertext.size()); + + size_t processed = 0; + size_t plain_position = 0; + std::vector ad; // No associated data + + while (processed < ciphertext.size()) { + size_t chunk_size = std::min(ciphertext.size() - processed, NOISE_MAX_CHUNK_SIZE); + Span chunk_cipher = ciphertext.subspan(processed, chunk_size); + Span chunk_plain = plain.subspan(plain_position, chunk_size - Poly1305::TAGLEN); + if (!DecryptWithAd(ad, chunk_cipher, chunk_plain)) return false; + processed += chunk_size; + plain_position += chunk_size - Poly1305::TAGLEN; + } + + return true; +} + +void Sv2SymmetricState::MixHash(const Span input) +{ + m_hash_output = (HashWriter{} << m_hash_output << input).GetSHA256(); +} + +void Sv2SymmetricState::MixKey(const Span input_key_material) +{ + NoiseHash out0; + NoiseHash out1; + HKDF2(input_key_material, out0, out1); + m_chaining_key = std::move(out0); + m_cipher_state = Sv2CipherState{std::move(out1)}; +} + +std::string Sv2SymmetricState::GetChainingKey() +{ + return HexStr(m_chaining_key); +} + +void Sv2SymmetricState::LogChainingKey() +{ + LogTrace(BCLog::SV2, "Chaining key: %s\n", GetChainingKey()); +} + +void Sv2SymmetricState::HKDF2(const Span input_key_material, NoiseHash& out0, NoiseHash& out1) +{ + NoiseHash tmp_key; + CHMAC_SHA256 tmp_mac(m_chaining_key.data(), m_chaining_key.size()); + tmp_mac.Write(UCharCast(input_key_material.data()), input_key_material.size()); + tmp_mac.Finalize(tmp_key.data()); + + CHMAC_SHA256 out0_mac(tmp_key.data(), tmp_key.size()); + uint8_t one[1]{0x1}; + out0_mac.Write(one, 1); + out0_mac.Finalize(out0.data()); + + std::vector in1; + in1.reserve(HASHLEN + 1); + std::copy(out0.begin(), out0.end(), std::back_inserter(in1)); + in1.push_back(0x02); + + CHMAC_SHA256 out1_mac(tmp_key.data(), tmp_key.size()); + out1_mac.Write(&in1[0], in1.size()); + out1_mac.Finalize(out1.data()); +} + +bool Sv2SymmetricState::EncryptAndHash(Span plain, Span ciphertext) +{ + Assume(Sv2Cipher::EncryptedMessageSize(plain.size()) == ciphertext.size()); + + if (!m_cipher_state.EncryptWithAd(MakeByteSpan(m_hash_output), plain, ciphertext)) { + return false; + } + MixHash(ciphertext); + return true; +} + +bool Sv2SymmetricState::DecryptAndHash(Span ciphertext, Span plain) +{ + Assume(Sv2Cipher::EncryptedMessageSize(plain.size()) == ciphertext.size()); + + // The handshake requires mix hashing the cipher text NOT the decrypted + // plaintext. + std::vector ciphertext_copy; + ciphertext_copy.assign(ciphertext.begin(), ciphertext.end()); + + bool res = m_cipher_state.DecryptWithAd(MakeByteSpan(m_hash_output), ciphertext, plain); + if (!res) return false; + MixHash(ciphertext_copy); + return true; +} + +std::array Sv2SymmetricState::Split() +{ + NoiseHash send_key; + NoiseHash recv_key; + HKDF2({}, send_key, recv_key); + return {Sv2CipherState{std::move(send_key)}, Sv2CipherState{std::move(recv_key)}}; +} + +uint256 Sv2SymmetricState::GetHashOutput() +{ + return m_hash_output; +} + +void Sv2HandshakeState::SetEphemeralKey(CKey&& key) +{ + m_ephemeral_key = key; + m_ephemeral_ellswift_pk = m_ephemeral_key.EllSwiftCreate(MakeByteSpan(GetRandHash())); +}; + +void Sv2HandshakeState::GenerateEphemeralKey() noexcept +{ + Assume(!m_ephemeral_key.size()); + LogTrace(BCLog::SV2, "Generate ephemeral key\n"); + SetEphemeralKey(GenerateRandomKey()); +}; + +void Sv2HandshakeState::WriteMsgEphemeralPK(Span msg) +{ + if (msg.size() < ELLSWIFT_PUB_KEY_SIZE) { + throw std::runtime_error(strprintf("Invalid message size: %d bytes < %d", msg.size(), ELLSWIFT_PUB_KEY_SIZE)); + } + + if (!m_ephemeral_key.IsValid()) { + GenerateEphemeralKey(); + } + + LogTrace(BCLog::SV2, "Write our ephemeral key\n"); + std::copy(m_ephemeral_ellswift_pk.begin(), m_ephemeral_ellswift_pk.end(), msg.begin()); + + m_symmetric_state.MixHash(msg.subspan(0, ELLSWIFT_PUB_KEY_SIZE)); + LogTrace(BCLog::SV2, "Mix hash: %s\n", HexStr(m_symmetric_state.GetHashOutput())); + + std::vector empty; + m_symmetric_state.MixHash(empty); +} + +void Sv2HandshakeState::ReadMsgEphemeralPK(Span msg) +{ + LogTrace(BCLog::SV2, "Read their ephemeral key\n"); + Assume(msg.size() == ELLSWIFT_PUB_KEY_SIZE); + m_remote_ephemeral_ellswift_pk = EllSwiftPubKey(msg); + + m_symmetric_state.MixHash(msg.subspan(0, ELLSWIFT_PUB_KEY_SIZE)); + LogTrace(BCLog::SV2, "Mix hash: %s\n", HexStr(m_symmetric_state.GetHashOutput())); + + std::vector empty; + m_symmetric_state.MixHash(empty); +} + +void Sv2HandshakeState::WriteMsgES(Span msg) +{ + if (msg.size() < HANDSHAKE_STEP2_SIZE) { + throw std::runtime_error(strprintf("Invalid message size: %d bytes < %d", msg.size(), HANDSHAKE_STEP2_SIZE)); + } + + ssize_t bytes_written = 0; + + if (!m_ephemeral_key.IsValid()) { + GenerateEphemeralKey(); + } + + // Send our ephemeral pk. + LogTrace(BCLog::SV2, "Write our ephemeral key\n"); + std::copy(m_ephemeral_ellswift_pk.begin(), m_ephemeral_ellswift_pk.end(), msg.begin()); + + m_symmetric_state.MixHash(msg.subspan(0, ELLSWIFT_PUB_KEY_SIZE)); + bytes_written += ELLSWIFT_PUB_KEY_SIZE; + + LogTrace(BCLog::SV2, "Mix hash: %s\n", HexStr(m_symmetric_state.GetHashOutput())); + + LogTrace(BCLog::SV2, "Perform ECDH with the remote ephemeral key\n"); + ECDHSecret ecdh_secret{m_ephemeral_key.ComputeBIP324ECDHSecret(m_remote_ephemeral_ellswift_pk, + m_ephemeral_ellswift_pk, + /*initiating=*/false)}; + + LogTrace(BCLog::SV2, "Mix key with ECDH result: ephemeral ours -- remote ephemeral\n"); + m_symmetric_state.MixKey(ecdh_secret); + m_symmetric_state.LogChainingKey(); + + // Send our static pk. + LogTrace(BCLog::SV2, "Encrypt and write our static key\n"); + + if (!m_symmetric_state.EncryptAndHash(m_static_ellswift_pk, msg.subspan(ELLSWIFT_PUB_KEY_SIZE, ELLSWIFT_PUB_KEY_SIZE + Poly1305::TAGLEN))) { + // This should never happen + Assume(false); + throw std::runtime_error("Failed to encrypt our ephemeral key\n"); + } + + bytes_written += ELLSWIFT_PUB_KEY_SIZE + Poly1305::TAGLEN; + + LogTrace(BCLog::SV2, "Mix hash: %s\n", HexStr(m_symmetric_state.GetHashOutput())); + + LogTrace(BCLog::SV2, "Perform ECDH between our static and remote ephemeral key\n"); + ECDHSecret ecdh_static_secret{m_static_key.ComputeBIP324ECDHSecret(m_remote_ephemeral_ellswift_pk, + m_static_ellswift_pk, + /*initiating=*/false)}; + LogTrace(BCLog::SV2, "ECDH result: %s\n", HexStr(ecdh_static_secret)); + + LogTrace(BCLog::SV2, "Mix key with ECDH result: static ours -- remote ephemeral\n"); + m_symmetric_state.MixKey(ecdh_static_secret); + m_symmetric_state.LogChainingKey(); + + // Serialize our digital signature noise message and encrypt. + DataStream ss{}; + Assume(m_certificate); + ss << m_certificate.value(); + Assume(ss.size() == Sv2SignatureNoiseMessage::SIZE); + + LogTrace(BCLog::SV2, "Encrypt certificate: %s\n", HexStr(ss)); + if (!m_symmetric_state.EncryptAndHash(ss, msg.subspan(bytes_written, Sv2SignatureNoiseMessage::SIZE + Poly1305::TAGLEN))) { + // This should never happen + Assume(false); + throw std::runtime_error("Failed to encrypt our certificate\n"); + } + + LogTrace(BCLog::SV2, "Mix hash: %s\n", HexStr(m_symmetric_state.GetHashOutput())); + + bytes_written += Sv2SignatureNoiseMessage::SIZE + Poly1305::TAGLEN; + Assume(bytes_written == HANDSHAKE_STEP2_SIZE); +} + +bool Sv2HandshakeState::ReadMsgES(Span msg) +{ + Assume(msg.size() == HANDSHAKE_STEP2_SIZE); + ssize_t bytes_read = 0; + + // Read the remote ephmeral key from the msg and decrypt. + LogTrace(BCLog::SV2, "Read remote ephemeral key\n"); + m_remote_ephemeral_ellswift_pk = EllSwiftPubKey(msg.subspan(0, ELLSWIFT_PUB_KEY_SIZE)); + bytes_read += ELLSWIFT_PUB_KEY_SIZE; + + m_symmetric_state.MixHash(m_remote_ephemeral_ellswift_pk); + LogTrace(BCLog::SV2, "Mix hash: %s\n", HexStr(m_symmetric_state.GetHashOutput())); + + LogTrace(BCLog::SV2, "Perform ECDH with the remote ephemeral key\n"); + ECDHSecret ecdh_secret{m_ephemeral_key.ComputeBIP324ECDHSecret(m_remote_ephemeral_ellswift_pk, + m_ephemeral_ellswift_pk, + /*initiating=*/true)}; + + LogTrace(BCLog::SV2, "Mix key with ECDH result: ephemeral ours -- remote ephemeral\n"); + m_symmetric_state.MixKey(ecdh_secret); + m_symmetric_state.LogChainingKey(); + + LogTrace(BCLog::SV2, "Decrypt remote static key\n"); + std::array remote_static_key_bytes; + bool res = m_symmetric_state.DecryptAndHash(msg.subspan(ELLSWIFT_PUB_KEY_SIZE, ELLSWIFT_PUB_KEY_SIZE + Poly1305::TAGLEN), remote_static_key_bytes); + if (!res) return false; + bytes_read += ELLSWIFT_PUB_KEY_SIZE + Poly1305::TAGLEN; + + LogTrace(BCLog::SV2, "Mix hash: %s\n", HexStr(m_symmetric_state.GetHashOutput())); + + // Load remote static key from the decryted msg + m_remote_static_ellswift_pk = EllSwiftPubKey(remote_static_key_bytes); + + LogTrace(BCLog::SV2, "Perform ECDH on the remote static key\n"); + ECDHSecret ecdh_static_secret{m_ephemeral_key.ComputeBIP324ECDHSecret(m_remote_static_ellswift_pk, + m_ephemeral_ellswift_pk, + /*initiating=*/true)}; + LogTrace(BCLog::SV2, "ECDH result: %s\n", HexStr(ecdh_static_secret)); + + LogTrace(BCLog::SV2, "Mix key with ECDH result: ephemeral ours -- remote static\n"); + m_symmetric_state.MixKey(ecdh_static_secret); + m_symmetric_state.LogChainingKey(); + + LogTrace(BCLog::SV2, "Decrypt remote certificate\n"); + std::array remote_cert_bytes; + res = m_symmetric_state.DecryptAndHash(msg.subspan(bytes_read, Sv2SignatureNoiseMessage::SIZE + Poly1305::TAGLEN), remote_cert_bytes); + if (!res) return false; + bytes_read += (Sv2SignatureNoiseMessage::SIZE + Poly1305::TAGLEN); + + LogTrace(BCLog::SV2, "Validate remote certificate\n"); + DataStream ss_cert(remote_cert_bytes); + Sv2SignatureNoiseMessage cert; + ss_cert >> cert; + cert.m_static_key = XOnlyPubKey(m_remote_static_ellswift_pk.Decode()); + Assume(m_authority_pubkey); + if (!cert.Validate(m_authority_pubkey.value())) { + // We initiated the connection, so it's safe to unconditionally log this: + LogWarning("Invalid certificate: %s\n", HexStr(remote_cert_bytes)); + return false; + } + + LogTrace(BCLog::SV2, "Mix hash: %s\n", HexStr(m_symmetric_state.GetHashOutput())); + + Assume(bytes_read == HANDSHAKE_STEP2_SIZE); + return true; +} + +std::array Sv2HandshakeState::SplitSymmetricState() +{ + return m_symmetric_state.Split(); +} + +uint256 Sv2HandshakeState::GetHashOutput() +{ + return m_symmetric_state.GetHashOutput(); +} + +Sv2Cipher::Sv2Cipher(CKey&& static_key, XOnlyPubKey authority_pubkey) +{ + m_handshake_state = std::make_unique(std::move(static_key), authority_pubkey); + m_initiator = true; +} + +Sv2Cipher::Sv2Cipher(CKey&& static_key, Sv2SignatureNoiseMessage&& certificate) +{ + m_handshake_state = std::make_unique(std::move(static_key), std::move(certificate)); + m_initiator = false; +} + +Sv2HandshakeState& Sv2Cipher::GetHandshakeState() +{ + Assume(m_handshake_state); + return *m_handshake_state; +} + +void Sv2Cipher::FinishHandshake() +{ + Assume(m_handshake_state); + + auto cipher_state{m_handshake_state->SplitSymmetricState()}; + + m_hash = m_handshake_state->GetHashOutput(); + + m_cs1 = std::move(cipher_state[0]); + m_cs2 = std::move(cipher_state[1]); + + m_handshake_state.reset(); +} + +size_t Sv2Cipher::EncryptedMessageSize(const size_t msg_len) +{ + constexpr size_t chunk_size = NOISE_MAX_CHUNK_SIZE - Poly1305::TAGLEN; + const size_t num_chunks = (msg_len + chunk_size - 1) / chunk_size; + return msg_len + (num_chunks * Poly1305::TAGLEN); +} + +bool Sv2Cipher::DecryptMessage(Span ciphertext, Span plain) +{ + Assume(EncryptedMessageSize(plain.size()) == ciphertext.size()); + + if (m_initiator) { + return m_cs2.DecryptMessage(ciphertext, plain); + } else { + return m_cs1.DecryptMessage(ciphertext, plain); + } +} + +bool Sv2Cipher::EncryptMessage(Span input, Span output) +{ + Assume(output.size() == Sv2Cipher::EncryptedMessageSize(input.size())); + + if (m_initiator) { + if (!m_cs1.EncryptMessage(input, output)) return false; + } else { + if (!m_cs2.EncryptMessage(input, output)) return false; + } + return true; +} + +uint256 Sv2Cipher::GetHash() const +{ + return m_hash; +} diff --git a/src/sv2/noise.h b/src/sv2/noise.h new file mode 100644 index 0000000000000..f72cf3df44e6d --- /dev/null +++ b/src/sv2/noise.h @@ -0,0 +1,299 @@ +// Copyright (c) 2023-present 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_SV2_NOISE_H +#define BITCOIN_SV2_NOISE_H + +#include +#include +#include +#include +#include +#include +#include + +/** The Noise Protocol Framework + * https://noiseprotocol.org/noise.html + * Revision 38, 2018-07-11 + * + * Stratum v2 handshake and cipher specification: + * https://github.com/stratum-mining/sv2-spec/blob/main/04-Protocol-Security.md + */ + +/** Section 3: All Noise messages are less than or equal to 65535 bytes in length. */ +static constexpr size_t NOISE_MAX_CHUNK_SIZE = 65535; + +static constexpr size_t HASHLEN{32}; +using NoiseHash = std::array; + +/** Simple certificate for the static key signed by the authority key. + * See 4.5.2 and 4.5.3 of the Stratum v2 spec. + */ +class Sv2SignatureNoiseMessage +{ +public: + /** Size of a Schnorr signature. */ + static constexpr size_t SCHNORR_SIGNATURE_SIZE = 64; + /** Size of serialized message, which does not include the static key. */ + static constexpr size_t SIZE = 2 + 4 + 4 + SCHNORR_SIGNATURE_SIZE; + +private: + uint16_t m_version = 0; + uint32_t m_valid_from = 0; + uint32_t m_valid_to = 0; + std::array m_sig; + + /** Hash of version, valid from/to and the static key. */ + uint256 GetHash(); + void SignSchnorr(const CKey& authority_key, Span sig); + +public: + Sv2SignatureNoiseMessage() = default; + Sv2SignatureNoiseMessage(uint16_t version, uint32_t valid_from, uint32_t valid_to, const XOnlyPubKey& static_key, const CKey& authority_key); + + /* The certificate serializes pubkeys in x-only format, not EllSwift. */ + XOnlyPubKey m_static_key = {}; + + [[nodiscard]] bool Validate(XOnlyPubKey authority_key); + + template + // The static_key is signed for, but not serialized. + void Serialize(Stream& s) const + { + s << m_version + << m_valid_from + << m_valid_to + << m_sig; + } + template + void Unserialize(Stream& s) + { + s >> m_version + >> m_valid_from + >> m_valid_to + >> m_sig; + } +}; + +/* + * The CipherState uses m_key (k) and m_nonce (n) to encrypt and decrypt ciphertexts. + * During the handshake phase each party has a single CipherState, but during + * the transport phase each party has two CipherState objects: one for sending, + * and one for receiving. + * + * See chapter "5. Processing rules" of the Noise paper. + */ +class Sv2CipherState +{ +public: + Sv2CipherState() = default; + explicit Sv2CipherState(NoiseHash&& key); + + /** Decrypt message + * @param[in] associated_data associated data + * @param[in] ciphertext message with encrypted and authenticated chunks. + * @param[out] plain message (defragmented) + * @returns whether decryption succeeded + */ + [[nodiscard]] bool DecryptWithAd(Span associated_data, Span ciphertext, Span plain); + + /** Encrypt message + * @param[in] associated_data associated data + * @param[in] plain message + * @param[out] ciphertext message with encrypted and authenticated chunks. + * @returns whether encryption succeeded + */ + [[nodiscard]] bool EncryptWithAd(Span associated_data, Span plain, Span ciphertext); + + /** The message will be chunked in NOISE_MAX_CHUNK_SIZE parts and expanded + * by 16 bytes per chunk for its MAC. + * + * @param[in] plain message. Can't point to the same memory location as ciphertext, + * because each encrypted message chunk would override the + * start of the next plain text chunk. + * @param[out] ciphertext message with encrypted and authenticated chunks + * @return whether encryption succeeded. Only fails if nonce is uint64_max. + */ + [[nodiscard]] bool EncryptMessage(Span plain, Span ciphertext); + + /** Decrypt message. + * + * @param[in] ciphertext encrypted message + * @param[out] plain decrypted message. May point to the same memory location + * as ciphertext. The result is defragmented. + */ + [[nodiscard]] bool DecryptMessage(Span ciphertext, Span plain); + +private: + NoiseHash m_key{0}; + uint64_t m_nonce = 0; +}; + +/* + * A SymmetricState object contains a CipherState plus m_chaining_key (ck) and + * m_hash_output (h) variables. It is so-named because it encapsulates all the + * "symmetric crypto" used by Noise. During the handshake phase each party has + * a single SymmetricState, which can be deleted once the handshake is finished. + * + * See chapter "5. Processing rules" of the Noise paper. + */ +class Sv2SymmetricState +{ +public: + // Sha256 hash of the ascii encoding - "Noise_NX_Secp256k1+EllSwift_ChaChaPoly_SHA256". + // This is the first step required when setting up the chaining key. + static constexpr NoiseHash PROTOCOL_NAME_HASH = { + 46, 180, 120, 129, 32, 142, 158, 238, 31, 102, 159, 103, 198, 110, 231, 14, + 169, 234, 136, 9, 13, 80, 63, 232, 48, 220, 75, 200, 62, 41, 191, 16}; + + // The double hash of protocol name "Noise_NX_EllSwiftXonly_ChaChaPoly_SHA256". + static constexpr NoiseHash PROTOCOL_NAME_DOUBLE_HASH = { + 146, 47, 163, 46, 79, 72, 124, 13, 89, 202, 163, 190, 215, 137, 156, 227, + 217, 141, 183, 225, 61, 189, 59, 124, 242, 210, 61, 212, 51, 220, 97, 4}; + + Sv2SymmetricState() : m_chaining_key{PROTOCOL_NAME_HASH} {} + + void MixHash(const Span input); + void MixKey(const Span input_key_material); + [[nodiscard]] bool EncryptAndHash(Span plain, Span ciphertext); + [[nodiscard]] bool DecryptAndHash(Span ciphertext, Span plain); + std::array Split(); + + uint256 GetHashOutput(); + + /* For testing */ + void LogChainingKey(); + std::string GetChainingKey(); + +private: + NoiseHash m_chaining_key; + uint256 m_hash_output{uint256(PROTOCOL_NAME_DOUBLE_HASH)}; + Sv2CipherState m_cipher_state; + + void HKDF2(const Span input_key_material, + NoiseHash& out0, + NoiseHash& out1); +}; + +/* + * A HandshakeState object contains a SymmetricState plus DH variables (s, e, rs, re) + * and a variable representing the handshake pattern. During the handshake phase + * each party has a single HandshakeState, which can be deleted once the handshake + * is finished. + * + * See chapter "5. Processing rules" of the Noise paper. + */ + +class Sv2HandshakeState +{ +public: + static constexpr size_t ELLSWIFT_PUB_KEY_SIZE{64}; + static constexpr size_t ECDH_OUTPUT_SIZE{32}; + + static constexpr size_t HANDSHAKE_STEP2_SIZE = ELLSWIFT_PUB_KEY_SIZE + ELLSWIFT_PUB_KEY_SIZE + + Poly1305::TAGLEN + Sv2SignatureNoiseMessage::SIZE + Poly1305::TAGLEN; + + /* + * If we are the initiator m_authority_pubkey must be set in order to verify + * the received certificate. + */ + Sv2HandshakeState(CKey&& static_key, + XOnlyPubKey authority_pubkey) : m_static_key{static_key}, + m_authority_pubkey{authority_pubkey} + { + m_static_ellswift_pk = static_key.EllSwiftCreate(MakeByteSpan(GetRandHash())); + }; + + /* + * If we are the responder, the certificate must be set + */ + Sv2HandshakeState(CKey&& static_key, + Sv2SignatureNoiseMessage&& certificate) : m_static_key{static_key}, + m_certificate{certificate} + { + m_static_ellswift_pk = static_key.EllSwiftCreate(MakeByteSpan(GetRandHash())); + }; + + /** Handshake step 1 for initiator: -> e */ + void WriteMsgEphemeralPK(Span msg); + /** Handshake step 1 for responder: -> e */ + void ReadMsgEphemeralPK(Span msg); + /** During handshake step 2, put our ephmeral key, static key + * and certificate in the buffer: <- e, ee, s, es, SIGNATURE_NOISE_MESSAGE + */ + void WriteMsgES(Span msg); + /** During handshake step 2, read the remote ephmeral key, static key + * and certificate. Verify their certificate. + * <- e, ee, s, es, SIGNATURE_NOISE_MESSAGE + */ + [[nodiscard]] bool ReadMsgES(Span msg); + + std::array SplitSymmetricState(); + uint256 GetHashOutput(); + + void SetEphemeralKey(CKey&& key); + +private: + /** Our static key (s) */ + CKey m_static_key; + /** EllSwift encoded static key, for optimized ECDH */ + EllSwiftPubKey m_static_ellswift_pk; + /** Our ephemeral key (e) */ + CKey m_ephemeral_key; + /** EllSwift encoded ephemeral key, for optimized ECDH */ + EllSwiftPubKey m_ephemeral_ellswift_pk; + /** Remote static key (rs) */ + EllSwiftPubKey m_remote_static_ellswift_pk; + /** Remote ephemeral key (re) */ + EllSwiftPubKey m_remote_ephemeral_ellswift_pk; + Sv2SymmetricState m_symmetric_state; + /** Certificate signed by m_authority_pubkey. */ + std::optional m_certificate; + /** Authority public key. */ + std::optional m_authority_pubkey; + + /** Generate ephemeral key, sets set m_ephemeral_key and m_ephemeral_ellswift_pk */ + void GenerateEphemeralKey() noexcept; +}; + +/** + * Interface somewhat similar to BIP324Cipher for use by a Transport class. + * The initiator and responder roles have their own constructor. + * FinishHandshake() must be called after all handshake bytes have been processed. + */ +class Sv2Cipher +{ +public: + Sv2Cipher(CKey&& static_key, XOnlyPubKey authority_pubkey); + Sv2Cipher(CKey&& static_key, Sv2SignatureNoiseMessage&& certificate); + + Sv2Cipher(bool initiator, std::unique_ptr handshake_state) : m_initiator{initiator}, m_handshake_state{std::move(handshake_state)} {}; + + Sv2HandshakeState& GetHandshakeState(); + /** + * Populates m_hash, m_cs1 and m_cs2 from m_handshake_state and deletes the latter. + */ + void FinishHandshake(); + + /** Decrypts a message. May only be called after FinishHandshake() */ + bool DecryptMessage(Span ciphertext, Span plain); + /** Encrypts a message. May only be called after FinishHandshake() */ + [[nodiscard]] bool EncryptMessage(Span input, Span output); + + /* Expected size after chunking and with MAC */ + static size_t EncryptedMessageSize(const size_t msg_len); + + /* Test only */ + uint256 GetHash() const; + +private: + bool m_initiator; + std::unique_ptr m_handshake_state; + + uint256 m_hash; + Sv2CipherState m_cs1; + Sv2CipherState m_cs2; +}; + +#endif // BITCOIN_SV2_NOISE_H diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index d5a197e498a4c..72284cd2dc4ff 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -176,6 +176,7 @@ endif() if(WITH_SV2) target_sources(test_bitcoin PRIVATE + sv2_noise_tests.cpp ) target_link_libraries(test_bitcoin bitcoin_sv2) endif() diff --git a/src/test/fuzz/CMakeLists.txt b/src/test/fuzz/CMakeLists.txt index 363e30f22a4f7..663f6526b186b 100644 --- a/src/test/fuzz/CMakeLists.txt +++ b/src/test/fuzz/CMakeLists.txt @@ -149,6 +149,7 @@ endif() if(WITH_SV2) target_sources(fuzz PRIVATE + sv2_noise.cpp ) target_link_libraries(fuzz bitcoin_sv2 diff --git a/src/test/fuzz/sv2_noise.cpp b/src/test/fuzz/sv2_noise.cpp new file mode 100644 index 0000000000000..465e4171d99a9 --- /dev/null +++ b/src/test/fuzz/sv2_noise.cpp @@ -0,0 +1,168 @@ +// Copyright (c) 2024 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() +{ + // Add test context for debugging. Usage: + // --debug=sv2 --loglevel=sv2:trace --printtoconsole=1 + static const auto testing_setup = std::make_unique(); +} +} // namespace + +bool MaybeDamage(FuzzedDataProvider& provider, std::vector& transport) +{ + if (transport.size() == 0) return false; + + // Optionally damage 1 bit in the ciphertext. + const bool damage = provider.ConsumeBool(); + if (damage) { + unsigned damage_bit = provider.ConsumeIntegralInRange(0, + transport.size() * 8U - 1U); + unsigned damage_pos = damage_bit >> 3; + LogTrace(BCLog::SV2, "Damage byte %d of %d\n", damage_pos, transport.size()); + std::byte damage_val{(uint8_t)(1U << (damage_bit & 7))}; + transport.at(damage_pos) ^= damage_val; + } + return damage; +} + +FUZZ_TARGET(sv2_noise_cipher_roundtrip, .init = Initialize) +{ + // Test that Sv2Noise's encryption and decryption agree. + + // To conserve fuzzer entropy, deterministically generate Alice and Bob keys. + FuzzedDataProvider provider(buffer.data(), buffer.size()); + auto seed_ent = provider.ConsumeBytes(32); + seed_ent.resize(32); + CExtKey seed; + seed.SetSeed(seed_ent); + + CExtKey tmp; + if (!seed.Derive(tmp, 0)) return; + CKey alice_authority_key{tmp.key}; + + if (!seed.Derive(tmp, 1)) return; + CKey alice_static_key{tmp.key}; + + if (!seed.Derive(tmp, 2)) return; + CKey alice_ephemeral_key{tmp.key}; + + if (!seed.Derive(tmp, 10)) return; + CKey bob_authority_key{tmp.key}; + + if (!seed.Derive(tmp, 11)) return; + CKey bob_static_key{tmp.key}; + + if (!seed.Derive(tmp, 12)) return; + CKey bob_ephemeral_key{tmp.key}; + + // Create certificate + // Pick random times in the past or future + uint32_t now = provider.ConsumeIntegralInRange(10000U, UINT32_MAX); + SetMockTime(now); + uint16_t version = provider.ConsumeBool() ? 0 : provider.ConsumeIntegral(); + uint32_t past = provider.ConsumeIntegralInRange(0, now); + uint32_t future = provider.ConsumeIntegralInRange(now, UINT32_MAX); + uint32_t valid_from = int32_t(provider.ConsumeBool() ? past : future); + uint32_t valid_to = int32_t(provider.ConsumeBool() ? future : past); + + auto bob_certificate = Sv2SignatureNoiseMessage(version, valid_from, valid_to, + XOnlyPubKey(bob_static_key.GetPubKey()), bob_authority_key); + + bool valid_certificate = version == 0 && + (valid_from <= now) && + (valid_to >= now); + + LogTrace(BCLog::SV2, "valid_certificate: %d - version %u, past: %u, now %u, future: %u\n", valid_certificate, version, past, now, future); + + // Alice's static is not used in the test + // Alice needs to verify Bob's certificate, so we pass his authority key + auto alice_handshake = std::make_unique(std::move(alice_static_key), XOnlyPubKey(bob_authority_key.GetPubKey())); + alice_handshake->SetEphemeralKey(std::move(alice_ephemeral_key)); + // Bob is the responder and does not receive (or verify) Alice's certificate, + // so we don't pass her authority key. + auto bob_handshake = std::make_unique(std::move(bob_static_key), std::move(bob_certificate)); + bob_handshake->SetEphemeralKey(std::move(bob_ephemeral_key)); + + // Handshake Act 1: e -> + + std::vector transport; + transport.resize(Sv2HandshakeState::ELLSWIFT_PUB_KEY_SIZE); + // Alice generates her ephemeral public key and write it into the buffer: + alice_handshake->WriteMsgEphemeralPK(transport); + + bool damage_e = MaybeDamage(provider, transport); + + // Bob reads the ephemeral key () + // With EllSwift encoding this step can't fail + bob_handshake->ReadMsgEphemeralPK(transport); + ClearShrink(transport); + + // Handshake Act 2: <- e, ee, s, es, SIGNATURE_NOISE_MESSAGE + transport.resize(Sv2HandshakeState::HANDSHAKE_STEP2_SIZE); + bob_handshake->WriteMsgES(transport); + + bool damage_es = MaybeDamage(provider, transport); + + // This ignores the remote possibility that the fuzzer finds two equivalent + // EllSwift encodings by flipping a single ephemeral key bit. + assert(alice_handshake->ReadMsgES(transport) == (valid_certificate && !damage_e && !damage_es)); + + if (!valid_certificate || damage_e || damage_es) return; + + // Construct Sv2Cipher from the Sv2HandshakeState and test transport + auto alice{Sv2Cipher(/*initiator=*/true, std::move(alice_handshake))}; + auto bob{Sv2Cipher(/*initiator=*/false, std::move(bob_handshake))}; + alice.FinishHandshake(); + bob.FinishHandshake(); + + // Use deterministic RNG to generate content rather than creating it from + // the fuzzer input. + InsecureRandomContext rng(provider.ConsumeIntegral()); + + LIMITED_WHILE(provider.remaining_bytes(), 1000) + { + ClearShrink(transport); + + // Alice or Bob sends a message + bool from_alice = provider.ConsumeBool(); + + // Set content length (slightly above NOISE_MAX_CHUNK_SIZE) + unsigned length = provider.ConsumeIntegralInRange(0, NOISE_MAX_CHUNK_SIZE + 100); + std::vector plain(length); + for (auto& val : plain) + val = std::byte{(uint8_t)rng()}; + + const size_t encrypted_size = Sv2Cipher::EncryptedMessageSize(plain.size()); + transport.resize(encrypted_size); + + assert((from_alice ? alice : bob).EncryptMessage(plain, transport)); + + const bool damage = MaybeDamage(provider, transport); + + std::vector plain_read; + plain_read.resize(plain.size()); + + bool ok = (from_alice ? bob : alice).DecryptMessage(transport, plain_read); + assert(!ok == damage); + if (!ok) break; + + assert(plain == plain_read); + } +} diff --git a/src/test/sv2_noise_tests.cpp b/src/test/sv2_noise_tests.cpp new file mode 100644 index 0000000000000..07da06e4e361d --- /dev/null +++ b/src/test/sv2_noise_tests.cpp @@ -0,0 +1,159 @@ +#include +#include +#include +#include +#include + +#include + +BOOST_FIXTURE_TEST_SUITE(sv2_noise_tests, BasicTestingSetup) + +BOOST_AUTO_TEST_CASE(MixKey_test) +{ + Sv2SymmetricState i_ss; + Sv2SymmetricState r_ss; + BOOST_CHECK_EQUAL(r_ss.GetChainingKey(), i_ss.GetChainingKey()); + + CKey initiator_key{GenerateRandomKey()}; + auto initiator_pk = initiator_key.EllSwiftCreate(MakeByteSpan(GetRandHash())); + + CKey responder_key{GenerateRandomKey()}; + auto responder_pk = responder_key.EllSwiftCreate(MakeByteSpan(GetRandHash())); + + auto ecdh_output_1 = initiator_key.ComputeBIP324ECDHSecret(responder_pk, initiator_pk, true); + auto ecdh_output_2 = responder_key.ComputeBIP324ECDHSecret(initiator_pk, responder_pk, false); + + BOOST_CHECK(std::memcmp(&ecdh_output_1[0], &ecdh_output_2[0], 32) == 0); + + i_ss.MixKey(ecdh_output_1); + r_ss.MixKey(ecdh_output_2); + + BOOST_CHECK_EQUAL(r_ss.GetChainingKey(), i_ss.GetChainingKey()); +} + +BOOST_AUTO_TEST_CASE(certificate_test) +{ + auto alice_static_key{GenerateRandomKey()}; + auto alice_authority_key{GenerateRandomKey()}; + + // Create certificate + auto epoch_now = std::chrono::system_clock::now().time_since_epoch(); + uint32_t now = static_cast(std::chrono::duration_cast(epoch_now).count()); + uint16_t version = 0; + uint32_t valid_from = now; + uint32_t valid_to = std::numeric_limits::max(); + + auto alice_certificate = Sv2SignatureNoiseMessage(version, valid_from, valid_to, + XOnlyPubKey(alice_static_key.GetPubKey()), alice_authority_key); + + BOOST_REQUIRE(alice_certificate.Validate(XOnlyPubKey(alice_authority_key.GetPubKey()))); + + auto malory_authority_key{GenerateRandomKey()}; + BOOST_REQUIRE(!alice_certificate.Validate(XOnlyPubKey(malory_authority_key.GetPubKey()))); + + // Check that certificate is not from the future + valid_from = now + 10000; + alice_certificate = Sv2SignatureNoiseMessage(version, valid_from, valid_to, + XOnlyPubKey(alice_static_key.GetPubKey()), alice_authority_key); + BOOST_REQUIRE(!alice_certificate.Validate(XOnlyPubKey(alice_authority_key.GetPubKey()))); + + valid_from = now; + + // Check certificate expiration + valid_to = now - 10000; + alice_certificate = Sv2SignatureNoiseMessage(version, valid_from, valid_to, + XOnlyPubKey(alice_static_key.GetPubKey()), alice_authority_key); + BOOST_REQUIRE(!alice_certificate.Validate(XOnlyPubKey(alice_authority_key.GetPubKey()))); + + valid_to = now; + + // Only version 0 is supported + version = 1; + alice_certificate = Sv2SignatureNoiseMessage(version, valid_from, valid_to, + XOnlyPubKey(alice_static_key.GetPubKey()), alice_authority_key); + BOOST_REQUIRE(!alice_certificate.Validate(XOnlyPubKey(alice_authority_key.GetPubKey()))); +} + +BOOST_AUTO_TEST_CASE(handshake_and_transport_test) +{ + // Alice initiates a handshake with Bob + + auto alice_static_key{GenerateRandomKey()}; + auto bob_static_key{GenerateRandomKey()}; + auto bob_authority_key{GenerateRandomKey()}; + + // Create certificates + auto epoch_now = std::chrono::system_clock::now().time_since_epoch(); + uint16_t version = 0; + uint32_t valid_from = static_cast(std::chrono::duration_cast(epoch_now).count()); + uint32_t valid_to = std::numeric_limits::max(); + + auto bob_certificate = Sv2SignatureNoiseMessage(version, valid_from, valid_to, + XOnlyPubKey(bob_static_key.GetPubKey()), + bob_authority_key); + + // Alice's static is not used in the test + // Alice needs to verify Bob's certificate, so we pass his authority key + auto alice_handshake = std::make_unique(std::move(alice_static_key), + XOnlyPubKey(bob_authority_key.GetPubKey())); + // Bob is the responder and does not receive (or verify) Alice's certificate, + // so we don't pass her authority key. + auto bob_handshake = std::make_unique(std::move(bob_static_key), + std::move(bob_certificate)); + + // Handshake Act 1: e -> + + std::vector transport; + transport.resize(Sv2HandshakeState::ELLSWIFT_PUB_KEY_SIZE); + // Alice generates her ephemeral public key and write it into the buffer: + alice_handshake->WriteMsgEphemeralPK(transport); + EllSwiftPubKey alice_pubkey(transport); + + // Bob reads the ephemeral key + bob_handshake->ReadMsgEphemeralPK(transport); + + ClearShrink(transport); + + // Handshake Act 2: <- e, ee, s, es, SIGNATURE_NOISE_MESSAGE + transport.resize(Sv2HandshakeState::HANDSHAKE_STEP2_SIZE); + bob_handshake->WriteMsgES(transport); + BOOST_REQUIRE(alice_handshake->ReadMsgES(transport)); + + // Construct Sv2Cipher from the Sv2HandshakeState and test transport + auto alice{Sv2Cipher(/*initiator=*/true, std::move(alice_handshake))}; + auto bob{Sv2Cipher(/*initiator=*/false, std::move(bob_handshake))}; + alice.FinishHandshake(); + bob.FinishHandshake(); + + ClearShrink(transport); + + constexpr std::array TEST{ + // hello world + 0x68, + 0x65, + 0x6C, + 0x6C, + 0x6F, + 0x20, + 0x77, + 0x6F, + 0x72, + 0x6C, + 0x64, + }; + + const size_t encrypted_size = Sv2Cipher::EncryptedMessageSize(TEST.size()); + BOOST_CHECK_EQUAL(encrypted_size, TEST.size() + Poly1305::TAGLEN); + + transport.resize(encrypted_size); + + auto plain_send{MakeByteSpan(TEST)}; + BOOST_TEST_CHECKPOINT("Alice encrypts message"); + BOOST_REQUIRE(alice.EncryptMessage(plain_send, transport)); + + std::vector plain_receive(TEST.size(), std::byte(0)); + BOOST_TEST_CHECKPOINT("Bob decrypts message"); + BOOST_REQUIRE(bob.DecryptMessage(transport, plain_receive)); + BOOST_CHECK_EQUAL(HexStr(plain_receive), HexStr(TEST)); +} +BOOST_AUTO_TEST_SUITE_END()