diff --git a/README.md b/README.md index bb4cd4a..9f3d13e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# BIP324 Encrypted Transport Protocol +# BIP-324 Encrypted Transport Protocol -[BIP324](https://github.com/bitcoin/bips/blob/master/bip-0324.mediawiki) describes the V2 encrypted communication protocol for the bitcoin P2P network. +[BIP-324](https://github.com/bitcoin/bips/blob/master/bip-0324.mediawiki) describes the V2 encrypted communication protocol for the bitcoin P2P network. ## Motivation @@ -10,9 +10,9 @@ Bitcoin's original P2P protocol, "V1", was designed without any encryption. Even * Plaintext message tampering, without detection, is trivial for a man in the middle (MitM) attacker. * Nefarious actors may associate metadata, such as IP addresses and transaction origins, without explicitly having to connect directly to peers. -BIP 324 - "V2" - encrypted communication protects against the above issues increasing the privacy and censorship-resistance of the bitcoin ecosystem. Any applications communicating with bitcoin nodes, including light clients, should make use of the V2 protocol. +BIP-324 - "V2" - encrypted communication protects against the above issues increasing the privacy and censorship-resistance of the bitcoin ecosystem. Any applications communicating with bitcoin nodes, including light clients, should make use of the V2 protocol. ## Packages -* `protocol` - Exports the `BIP324` client library. +* `protocol` - Exports the `bip324` client library. * `proxy` - A small side-car application to enable V2 communication for V1-only applications. diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index a8e4343..bd80276 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -27,6 +27,6 @@ bitcoin_hashes = { version ="0.15.0", default-features = false } chacha20-poly1305 = { version = "0.1.1", default-features = false } [dev-dependencies] -# bitcoind version 26.0 includes support for BIP324's V2 protocol, but it is disabled by default. +# bitcoind version 26.0 includes support for BIP-324's V2 protocol, but it is disabled by default. bitcoind = { package = "corepc-node", version = "0.7.1", default-features = false, features = ["26_0","download"] } hex = { package = "hex-conservative", version = "0.2.0" } diff --git a/protocol/README.md b/protocol/README.md index d4db084..32404c2 100644 --- a/protocol/README.md +++ b/protocol/README.md @@ -1,6 +1,6 @@ # Protocol -A BIP324 library to establish and communicate over an encrypted channel. +A BIP-324 library to establish and communicate over an encrypted channel. The library is designed with a bare `no_std` and "Sans I/O" interface to keep it as agnostic as possible to application runtimes, but higher level interfaces are exposed for ease of use. diff --git a/protocol/doc/DESIGN.md b/protocol/doc/DESIGN.md index 44f3ad0..b4cdf04 100644 --- a/protocol/doc/DESIGN.md +++ b/protocol/doc/DESIGN.md @@ -8,6 +8,6 @@ With Bob's public key, Alice derives the shared secret and ensures the decrypted ## ChaCha20Poly1305 -BIP324 elects to use the ChaCha20Poly1305 Authenticated Encryption with Addition Data (AEAD) algorithm under the hood. This is a combination of the ChaCha20 stream cipher and the Poly1305 message authentication code (MAC). In this context, "authentication" refers to the encrypted message's integrity, not to the identity of either party communicating. +BIP-324 elects to use the ChaCha20Poly1305 Authenticated Encryption with Addition Data (AEAD) algorithm under the hood. This is a combination of the ChaCha20 stream cipher and the Poly1305 message authentication code (MAC). In this context, "authentication" refers to the encrypted message's integrity, not to the identity of either party communicating. Poly1305 is a purpose-built MAC, as opposed to something like an HMAC using SHA256 which leverages an existing hash scheme to build a message authentication code. Purpose-built introduces new complexity, but also allows for increased performance. diff --git a/protocol/src/fschacha20poly1305.rs b/protocol/src/fschacha20poly1305.rs index 8d0b4e5..d7e17a5 100644 --- a/protocol/src/fschacha20poly1305.rs +++ b/protocol/src/fschacha20poly1305.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: CC0-1.0 //! Wrap ciphers with automatic re-keying in order to provide [forward secrecy](https://eprint.iacr.org/2001/035.pdf) within a session. -//! Logic is covered by the BIP324 test vectors. +//! Logic is covered by the BIP-324 test vectors. //! //! ## Performance Considerations //! @@ -51,7 +51,7 @@ impl std::error::Error for Error { /// A wrapper over ChaCha20Poly1305 AEAD stream cipher which handles automatically changing /// nonces and re-keying, providing forward secrecy within the session. /// -/// FSChaCha20Poly1305 is used for message packets in BIP324. +/// FSChaCha20Poly1305 is used for message packets in BIP-324. #[derive(Clone)] pub struct FSChaCha20Poly1305 { key: Key, @@ -138,7 +138,7 @@ impl FSChaCha20Poly1305 { /// A wrapper over ChaCha20 (unauthenticated) stream cipher which handles automatically changing /// nonces and re-keying, providing forward secrecy within the session. /// -/// FSChaCha20 is used for lengths in BIP324. Should be noted that the lengths are still +/// FSChaCha20 is used for lengths in BIP-324. Should be noted that the lengths are still /// implicitly authenticated by the message packets. #[derive(Clone)] pub struct FSChaCha20 { diff --git a/protocol/src/io.rs b/protocol/src/io.rs index 726bd5d..0e172b1 100644 --- a/protocol/src/io.rs +++ b/protocol/src/io.rs @@ -1,51 +1,80 @@ // SPDX-License-Identifier: CC0-1.0 -//! High-level interfaces for establishing and using BIP324 encrypted +//! High-level interfaces for establishing and using BIP-324 encrypted //! connections over Read/Write transports. use core::fmt; -#[cfg(feature = "tokio")] +use std::io::{Chain, Cursor, Read, Write}; use std::vec; use std::vec::Vec; -use crate::{Error, PacketType}; - -#[cfg(feature = "tokio")] use bitcoin::Network; -#[cfg(feature = "tokio")] use crate::{ handshake::{self, GarbageResult, VersionResult}, - Handshake, InboundCipher, OutboundCipher, Role, NUM_ELLIGATOR_SWIFT_BYTES, - NUM_GARBAGE_TERMINTOR_BYTES, + Error, Handshake, InboundCipher, OutboundCipher, PacketType, Role, NUM_ELLIGATOR_SWIFT_BYTES, + NUM_GARBAGE_TERMINTOR_BYTES, NUM_LENGTH_BYTES, }; #[cfg(feature = "tokio")] use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -/// A decrypted BIP324 payload with its packet type. +/// A decrypted BIP-324 payload. +/// +/// # Invariants +/// +/// The internal data vector must always contain at least one byte (the header byte). +/// This invariant is maintained by the decrypt functions which validate that +/// ciphertext contains at least `NUM_TAG_BYTES + NUM_HEADER_BYTES` before +/// attempting decryption. pub struct Payload { - contents: Vec, - packet_type: PacketType, + data: Vec, } impl Payload { - /// Create a new payload. - pub fn new(contents: Vec, packet_type: PacketType) -> Self { - Self { - contents, - packet_type, - } + /// Create a new payload from complete decrypted data (including header byte). + /// + /// The data must contain at least one byte (the header). This is guaranteed + /// by the decrypt functions, but can be asserted in debug builds. + pub fn new(data: Vec) -> Self { + debug_assert!( + !data.is_empty(), + "Payload data must contain at least the header byte" + ); + Self { data } } - /// Access the decrypted payload contents. + /// Access just the message contents (excluding header byte). pub fn contents(&self) -> &[u8] { - &self.contents + &self.data[1..] } - /// Access the packet type. + /// Extract the packet type from the header byte. pub fn packet_type(&self) -> PacketType { - self.packet_type + PacketType::from_byte(&self.data[0]) + } +} + +/// A reader for BIP-324 session data that handles any buffered handshake overflow. +/// +/// This reader ensures seamless transition from handshake to session data by +/// first consuming any data that was read during handshake but belongs to the +/// session stream. +pub struct SessionReader { + inner: Chain>, R>, +} + +impl SessionReader { + fn new(buffer: Vec, reader: R) -> Self { + Self { + inner: std::io::Read::chain(Cursor::new(buffer), reader), + } + } +} + +impl Read for SessionReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.inner.read(buf) } } @@ -98,7 +127,6 @@ impl ProtocolError { /// /// This is used when the remote peer closes the connection during handshake, /// which often indicates they don't support the V2 protocol. - #[cfg(feature = "tokio")] fn eof() -> Self { ProtocolError::Io( std::io::Error::new( @@ -235,14 +263,14 @@ impl AsyncProtocol { let mut version_buffer = garbage_buffer[garbage_bytes..].to_vec(); loop { // Decrypt packet length. - if version_buffer.len() < 3 { + if version_buffer.len() < NUM_LENGTH_BYTES { let old_len = version_buffer.len(); - version_buffer.resize(3, 0); + version_buffer.resize(NUM_LENGTH_BYTES, 0); reader.read_exact(&mut version_buffer[old_len..]).await?; } - let packet_len = - handshake.decrypt_packet_len(version_buffer[..3].try_into().unwrap())?; - version_buffer.drain(..3); + let packet_len = handshake + .decrypt_packet_len(version_buffer[..NUM_LENGTH_BYTES].try_into().unwrap())?; + version_buffer.drain(..NUM_LENGTH_BYTES); // Process packet. if version_buffer.len() < packet_len { @@ -293,7 +321,7 @@ impl AsyncProtocol { #[derive(Debug)] enum DecryptState { ReadingLength { - length_bytes: [u8; 3], + length_bytes: [u8; NUM_LENGTH_BYTES], bytes_read: usize, }, ReadingPayload { @@ -307,7 +335,7 @@ impl DecryptState { /// Transition state to reading the length bytes. fn init_reading_length() -> Self { DecryptState::ReadingLength { - length_bytes: [0u8; 3], + length_bytes: [0u8; NUM_LENGTH_BYTES], bytes_read: 0, } } @@ -354,7 +382,7 @@ impl AsyncProtocolReader { length_bytes, bytes_read, } => { - while *bytes_read < 3 { + while *bytes_read < NUM_LENGTH_BYTES { *bytes_read += buffer.read(&mut length_bytes[*bytes_read..]).await?; } @@ -371,12 +399,10 @@ impl AsyncProtocolReader { let plaintext_len = InboundCipher::decryption_buffer_len(packet_bytes.len()); let mut plaintext_buffer = vec![0u8; plaintext_len]; - let packet_type = - self.inbound_cipher - .decrypt(packet_bytes, &mut plaintext_buffer, None)?; + self.inbound_cipher + .decrypt(packet_bytes, &mut plaintext_buffer, None)?; self.state = DecryptState::init_reading_length(); - // Skip the header byte (first byte) which contains the packet type - return Ok(Payload::new(plaintext_buffer[1..].to_vec(), packet_type)); + return Ok(Payload::new(plaintext_buffer)); } } } @@ -431,3 +457,484 @@ impl AsyncProtocolWriter { self.outbound_cipher } } + +/// Perform a BIP-324 handshake and return ready-to-use session components. +/// +/// This function handles the complete handshake process and returns the +/// cryptographic ciphers and a session reader prepared for encrypted communication. +/// +/// # Arguments +/// +/// * `network` - Network which both parties are operating on. +/// * `role` - Role in handshake, initiator or responder. +/// * `garbage` - Optional garbage bytes to send in handshake. +/// * `decoys` - Optional decoy packet contents bytes to send in handshake. +/// * `reader` - Buffer to read packets sent by peer (takes ownership). +/// * `writer` - Buffer to write packets to peer (takes mutable reference). +/// +/// # Reader Transformation +/// +/// The I/O reader is transformed in order to handle possible over-read +/// scenarios while attempting to detect the remote's garbage terminator. +/// +/// # Returns +/// +/// A `Result` containing: +/// * `Ok((InboundCipher, OutboundCipher, SessionReader))`: Ready-to-use session components. +/// * `Err(ProtocolError)`: An error that occurred during the handshake. +/// +/// # Errors +/// +/// * `Io` - Includes a flag for if the remote probably only understands the V1 protocol. +pub fn handshake( + network: Network, + role: Role, + garbage: Option<&[u8]>, + decoys: Option<&[&[u8]]>, + reader: R, + writer: &mut W, +) -> Result<(InboundCipher, OutboundCipher, SessionReader), ProtocolError> +where + R: Read, + W: Write, +{ + let handshake = Handshake::::new(network, role)?; + handshake_with_initialized(handshake, garbage, decoys, reader, writer) +} + +/// Internal handshake implementation that accepts an already-initialized handshake. +/// +/// This allows for testing with deterministic handshake states. +fn handshake_with_initialized( + handshake: Handshake, + garbage: Option<&[u8]>, + decoys: Option<&[&[u8]]>, + mut reader: R, + writer: &mut W, +) -> Result<(InboundCipher, OutboundCipher, SessionReader), ProtocolError> +where + R: Read, + W: Write, +{ + // Send local public key and optional garbage. + let key_buffer_len = Handshake::::send_key_len(garbage); + let mut key_buffer = vec![0u8; key_buffer_len]; + let handshake = handshake.send_key(garbage, &mut key_buffer)?; + writer.write_all(&key_buffer)?; + writer.flush()?; + + // Read remote's public key. + let mut remote_ellswift_buffer = [0u8; NUM_ELLIGATOR_SWIFT_BYTES]; + reader.read_exact(&mut remote_ellswift_buffer)?; + let handshake = handshake.receive_key(remote_ellswift_buffer)?; + + // Send garbage terminator, decoys, and version. + let version_buffer_len = Handshake::::send_version_len(decoys); + let mut version_buffer = vec![0u8; version_buffer_len]; + let handshake = handshake.send_version(&mut version_buffer, decoys)?; + writer.write_all(&version_buffer)?; + writer.flush()?; + + // Receive and process garbage terminator + let mut garbage_buffer = vec![0u8; NUM_GARBAGE_TERMINTOR_BYTES]; + reader.read_exact(&mut garbage_buffer)?; + + let mut handshake = handshake; + let (mut handshake, garbage_bytes) = loop { + match handshake.receive_garbage(&garbage_buffer) { + Ok(GarbageResult::FoundGarbage { + handshake, + consumed_bytes, + }) => { + break (handshake, consumed_bytes); + } + Ok(GarbageResult::NeedMoreData(h)) => { + handshake = h; + // The 256 bytes is a bit arbitrary. There is a max of 4095, but not sure + // all of that should be allocated right away. + let mut temp = vec![0u8; 256]; + match reader.read(&mut temp) { + Ok(0) => return Err(ProtocolError::eof()), + Ok(n) => { + garbage_buffer.extend_from_slice(&temp[..n]); + } + Err(e) => return Err(ProtocolError::from(e)), + } + } + Err(e) => return Err(ProtocolError::Internal(e)), + } + }; + + // Process remaining bytes for decoy packets and version. + let mut session_reader = SessionReader::new(garbage_buffer[garbage_bytes..].to_vec(), reader); + loop { + // Decrypt packet length. + let mut length_bytes = [0u8; NUM_LENGTH_BYTES]; + session_reader.read_exact(&mut length_bytes)?; + let packet_len = handshake.decrypt_packet_len(length_bytes)?; + + // Process packet. + let mut packet_bytes = vec![0u8; packet_len]; + session_reader.read_exact(&mut packet_bytes)?; + match handshake.receive_version(&mut packet_bytes) { + Ok(VersionResult::Complete { cipher }) => { + let (inbound_cipher, outbound_cipher) = cipher.into_split(); + return Ok((inbound_cipher, outbound_cipher, session_reader)); + } + Ok(VersionResult::Decoy(h)) => { + handshake = h; + } + Err(e) => return Err(ProtocolError::Internal(e)), + } + } +} + +/// A synchronous protocol session with handshake and send/receive packet management. +pub struct Protocol { + reader: ProtocolReader>, + writer: ProtocolWriter, +} + +impl Protocol +where + R: Read, + W: Write, +{ + /// New protocol session which completes the initial handshake and returns a handler. + /// + /// # Arguments + /// + /// * `network` - Network which both parties are operating on. + /// * `role` - Role in handshake, initiator or responder. + /// * `garbage` - Optional garbage bytes to send in handshake. + /// * `decoys` - Optional decoy packet contents bytes to send in handshake. + /// * `reader` - Buffer to read packets sent by peer (takes ownership). + /// * `writer` - Buffer to write packets to peer (takes ownership). + /// + /// # Returns + /// + /// A `Result` containing: + /// * `Ok(Protocol)`: An initialized protocol handler. + /// * `Err(ProtocolError)`: An error that occurred during the handshake. + /// + /// # Errors + /// + /// * `Io` - Includes a flag for if the remote probably only understands the V1 protocol. + pub fn new<'a>( + network: Network, + role: Role, + garbage: Option<&'a [u8]>, + decoys: Option<&'a [&'a [u8]]>, + reader: R, + mut writer: W, + ) -> Result { + let (inbound_cipher, outbound_cipher, session_reader) = + handshake(network, role, garbage, decoys, reader, &mut writer)?; + + Ok(Self { + reader: ProtocolReader { + inbound_cipher, + reader: session_reader, + }, + writer: ProtocolWriter { + outbound_cipher, + writer, + }, + }) + } + + /// Split the protocol into a separate reader and writer. + pub fn into_split(self) -> (ProtocolReader>, ProtocolWriter) { + (self.reader, self.writer) + } + + /// Read and decrypt a packet from the underlying reader. + /// + /// This is a convenience method that calls read on the internal reader. + /// + /// # Returns + /// + /// A `Result` containing: + /// * `Ok(Payload)`: A decrypted payload with packet type. + /// * `Err(ProtocolError)`: An error that occurred during the read or decryption. + pub fn read(&mut self) -> Result { + self.reader.read() + } + + /// Encrypt and write a packet to the underlying writer. + /// + /// This is a convenience method that calls write on the internal writer. + /// + /// # Arguments + /// + /// * `plaintext` - The data to encrypt and send. + /// + /// # Returns + /// + /// A `Result` containing: + /// * `Ok()`: On successful contents encryption and packet send. + /// * `Err(ProtocolError)`: An error that occurred during the encryption or write. + pub fn write(&mut self, plaintext: &[u8]) -> Result<(), ProtocolError> { + self.writer.write(plaintext) + } +} + +/// Manages a buffer to automatically decrypt contents of received packets. +pub struct ProtocolReader { + inbound_cipher: InboundCipher, + reader: R, +} + +impl ProtocolReader +where + R: Read, +{ + /// Decrypt contents of received packet from the internal reader. + /// + /// # Returns + /// + /// A `Result` containing: + /// * `Ok(Payload)`: A decrypted payload with packet type. + /// * `Err(ProtocolError)`: An error that occurred during the read or decryption. + pub fn read(&mut self) -> Result { + // Read packet length. + let mut length_bytes = [0u8; NUM_LENGTH_BYTES]; + self.reader.read_exact(&mut length_bytes)?; + let packet_bytes_len = self.inbound_cipher.decrypt_packet_len(length_bytes); + + // Read packet data. + let mut packet_bytes = vec![0u8; packet_bytes_len]; + self.reader.read_exact(&mut packet_bytes)?; + let (_, plaintext_buffer) = self.inbound_cipher.decrypt_to_vec(&packet_bytes, None)?; + + Ok(Payload::new(plaintext_buffer)) + } + + /// Consume the protocol reader in exchange for the underlying reader and cipher. + pub fn into_inner(self) -> (InboundCipher, R) { + (self.inbound_cipher, self.reader) + } +} + +/// Manages a buffer to automatically encrypt and send contents in packets. +pub struct ProtocolWriter { + outbound_cipher: OutboundCipher, + writer: W, +} + +impl ProtocolWriter +where + W: Write, +{ + /// Encrypt contents and write packet to the internal writer. + /// + /// # Arguments + /// + /// * `plaintext` - The data to encrypt and send. + /// + /// # Returns + /// + /// A `Result` containing: + /// * `Ok()`: On successful contents encryption and packet send. + /// * `Err(ProtocolError)`: An error that occurred during the encryption or write. + pub fn write(&mut self, plaintext: &[u8]) -> Result<(), ProtocolError> { + let packet_buffer = + self.outbound_cipher + .encrypt_to_vec(plaintext, PacketType::Genuine, None); + self.writer.write_all(&packet_buffer)?; + self.writer.flush()?; + Ok(()) + } + + /// Consume the protocol writer in exchange for the underlying writer and cipher. + pub fn into_inner(self) -> (OutboundCipher, W) { + (self.outbound_cipher, self.writer) + } +} + +/// Extension trait to convert duplex streams into BIP-324 Protocol instances. +/// +/// # Example +/// +/// ```rust +/// use std::net::TcpStream; +/// use bip324::io::IntoBip324; +/// use bip324::{Network, Role}; +/// +/// # fn example() -> Result<(), Box> { +/// let stream = TcpStream::connect("127.0.0.1:8333")?; +/// let protocol = stream.into_bip324( +/// Network::Bitcoin, +/// Role::Initiator, +/// None, // no garbage +/// None, // no decoys +/// )?; +/// # Ok(()) +/// # } +/// ``` +pub trait IntoBip324 { + /// Convert this stream into a BIP-324 Protocol after performing the handshake. + /// + /// # Arguments + /// + /// * `network` - Network which both parties are operating on. + /// * `role` - Role in handshake, initiator or responder. + /// * `garbage` - Optional garbage bytes to send in handshake. + /// * `decoys` - Optional decoy packet contents bytes to send in handshake. + /// + /// # Returns + /// + /// A `Result` containing: + /// * `Ok(Protocol)`: An initialized protocol handler. + /// * `Err(ProtocolError)`: An error that occurred during the handshake or stream cloning. + /// + /// # Errors + /// + /// * `Io` - Includes errors from stream cloning and handshake I/O operations. + /// * `Internal` - Protocol-specific errors during handshake. + fn into_bip324( + self, + network: Network, + role: Role, + garbage: Option<&[u8]>, + decoys: Option<&[&[u8]]>, + ) -> Result, ProtocolError>; +} + +impl IntoBip324 for std::net::TcpStream { + fn into_bip324( + self, + network: Network, + role: Role, + garbage: Option<&[u8]>, + decoys: Option<&[&[u8]]>, + ) -> Result, ProtocolError> { + let reader = self.try_clone()?; + let writer = self; + Protocol::new(network, role, garbage, decoys, reader, writer) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::{rngs::StdRng, SeedableRng}; + use std::io::Cursor; + + /// Generate deterministic handshake messages for testing. + /// Returns the complete handshake message (key + garbage + version) for the specified role. + fn generate_handshake_messages( + local_seed: u64, + remote_seed: u64, + role: Role, + garbage: Option<&[u8]>, + decoys: Option<&[&[u8]]>, + ) -> Vec { + let secp = bitcoin::secp256k1::Secp256k1::new(); + + // Create both parties. + let mut local_rng = StdRng::seed_from_u64(local_seed); + let local_handshake = Handshake::::new_with_rng( + Network::Bitcoin, + role, + &mut local_rng, + &secp, + ) + .unwrap(); + + let mut remote_rng = StdRng::seed_from_u64(remote_seed); + let remote_role = match role { + Role::Initiator => Role::Responder, + Role::Responder => Role::Initiator, + }; + let remote_handshake = Handshake::::new_with_rng( + Network::Bitcoin, + remote_role, + &mut remote_rng, + &secp, + ) + .unwrap(); + + // Exchange keys. + let mut local_key_buffer = + vec![0u8; Handshake::::send_key_len(garbage)]; + let local_handshake = local_handshake + .send_key(garbage, &mut local_key_buffer) + .unwrap(); + + let mut remote_key_buffer = vec![0u8; NUM_ELLIGATOR_SWIFT_BYTES]; + remote_handshake + .send_key(None, &mut remote_key_buffer) + .unwrap(); + + let local_handshake = local_handshake + .receive_key( + remote_key_buffer[..NUM_ELLIGATOR_SWIFT_BYTES] + .try_into() + .unwrap(), + ) + .unwrap(); + + let mut local_version_buffer = + vec![0u8; Handshake::::send_version_len(decoys)]; + local_handshake + .send_version(&mut local_version_buffer, decoys) + .unwrap(); + + // Return complete message: key + garbage + version. + let garbage_bytes = garbage.map(|g| g.to_vec()).unwrap_or_default(); + [ + &local_key_buffer[..NUM_ELLIGATOR_SWIFT_BYTES], + &garbage_bytes[..], + &local_version_buffer[..], + ] + .concat() + } + + #[test] + fn test_handshake_session_reader() { + let mut init_rng = StdRng::seed_from_u64(42); + let secp = bitcoin::secp256k1::Secp256k1::new(); + let init_handshake = Handshake::::new_with_rng( + Network::Bitcoin, + Role::Initiator, + &mut init_rng, + &secp, + ) + .unwrap(); + + // Generate responder messages with garbage and decoys. + let resp_garbage = b"responder garbage"; + let resp_decoys: &[&[u8]] = &[b"decoy1", b"another decoy packet"]; + let mut messages = generate_handshake_messages( + 1042, + 42, + Role::Responder, + Some(resp_garbage), + Some(resp_decoys), + ); + + // Add one extra byte that should be left for the session reader. + let session_byte = 0x42u8; + messages.push(session_byte); + + let reader = Cursor::new(messages); + let mut writer = Vec::new(); + + let result = handshake_with_initialized(init_handshake, None, None, reader, &mut writer); + + // Verify the session reader contains exactly the extra byte we added. + let (_, _, mut session_reader) = result.unwrap(); + let mut buffer = [0u8; 1]; + match session_reader.read(&mut buffer) { + Ok(1) => { + assert_eq!( + buffer[0], session_byte, + "Session reader should contain the extra byte" + ); + } + Ok(n) => panic!("Expected to read 1 byte but read {}", n), + Err(e) => panic!("Unexpected error reading from SessionReader: {}", e), + } + } +} diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index 6e07f00..9e7b171 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: CC0-1.0 -//! BIP324 encrypted transport for exchanging bitcoin P2P *messages*. Much like TLS, a connection begins by exchanging ephemeral +//! BIP-324 encrypted transport for exchanging bitcoin P2P *messages*. Much like TLS, a connection begins by exchanging ephemeral //! elliptic curve public keys and performing a Diffie-Hellman handshake. Thereafter, each participant derives shared session secrets, and may //! freely exchange encrypted packets. //! @@ -360,7 +360,7 @@ impl InboundCipher { ) -> Result<(PacketType, &'a [u8]), Error> { let auth = aad.unwrap_or_default(); // Check minimum size of ciphertext. - if ciphertext.len() < NUM_TAG_BYTES { + if ciphertext.len() < NUM_TAG_BYTES + NUM_HEADER_BYTES { return Err(Error::CiphertextTooSmall); } let (msg, tag) = ciphertext.split_at_mut(ciphertext.len() - NUM_TAG_BYTES); @@ -400,7 +400,7 @@ impl InboundCipher { ) -> Result { let auth = aad.unwrap_or_default(); // Check minimum size of ciphertext. - if ciphertext.len() < NUM_TAG_BYTES { + if ciphertext.len() < NUM_TAG_BYTES + NUM_HEADER_BYTES { return Err(Error::CiphertextTooSmall); } let (msg, tag) = ciphertext.split_at(ciphertext.len() - NUM_TAG_BYTES); @@ -558,7 +558,7 @@ impl OutboundCipher { } } -/// Manages cipher state for a BIP324 encrypted connection. +/// Manages cipher state for a BIP-324 encrypted connection. #[derive(Clone)] pub struct CipherSession { /// A unique identifier for the communication session. @@ -763,6 +763,40 @@ mod tests { assert_eq!(message2, plaintext2[1..].to_vec()); // Skip header byte } + #[test] + fn test_decrypt_min_length() { + // Test that decrypt properly validates minimum ciphertext length. + let alice = + SecretKey::from_str("61062ea5071d800bbfd59e2e8b53d47d194b095ae5a4df04936b49772ef0d4d7") + .unwrap(); + let elliswift_alice = ElligatorSwift::from_str("ec0adff257bbfe500c188c80b4fdd640f6b45a482bbc15fc7cef5931deff0aa186f6eb9bba7b85dc4dcc28b28722de1e3d9108b985e2967045668f66098e475b").unwrap(); + let elliswift_bob = ElligatorSwift::from_str("a4a94dfce69b4a2a0a099313d10f9f7e7d649d60501c9e1d274c300e0d89aafaffffffffffffffffffffffffffffffffffffffffffffffffffffffff8faf88d5").unwrap(); + let session_keys = SessionKeyMaterial::from_ecdh( + elliswift_alice, + elliswift_bob, + alice, + ElligatorSwiftParty::A, + Network::Bitcoin, + ) + .unwrap(); + let mut alice_cipher = CipherSession::new(session_keys, Role::Initiator); + + // Test with ciphertext that is exactly NUM_TAG_BYTES (should fail). + let too_small = vec![0u8; NUM_TAG_BYTES]; + let mut plaintext_buffer = vec![0u8; 100]; + let result = alice_cipher + .inbound() + .decrypt(&too_small, &mut plaintext_buffer, None); + assert_eq!(result, Err(Error::CiphertextTooSmall)); + + // Test decrypt_in_place with same minimum length checks. + let mut too_small = vec![0u8; NUM_TAG_BYTES]; + let result = alice_cipher + .inbound() + .decrypt_in_place(&mut too_small, None); + assert_eq!(result, Err(Error::CiphertextTooSmall)); + } + #[test] fn test_fuzz_packets() { let mut rng = rand::thread_rng(); @@ -877,7 +911,7 @@ mod tests { .unwrap(); } - // The rest are sourced from [the BIP324 test vectors](https://github.com/bitcoin/bips/blob/master/bip-0324/packet_encoding_test_vectors.csv). + // The rest are sourced from [the BIP-324 test vectors](https://github.com/bitcoin/bips/blob/master/bip-0324/packet_encoding_test_vectors.csv). #[test] fn test_vector_1() { diff --git a/protocol/src/serde.rs b/protocol/src/serde.rs index 91ebde2..ee9227a 100644 --- a/protocol/src/serde.rs +++ b/protocol/src/serde.rs @@ -2,7 +2,7 @@ //! Serialize and deserialize V2 messages over the wire. //! -//! A subset of commands are represented with a single byte in V2 instead of the 12-byte ASCII encoding like V1. Message ID mappings are defined in [BIP324](https://github.com/bitcoin/bips/blob/master/bip-0324.mediawiki#user-content-v2_Bitcoin_P2P_message_structure). +//! A subset of commands are represented with a single byte in V2 instead of the 12-byte ASCII encoding like V1. Message ID mappings are defined in [BIP-324](https://github.com/bitcoin/bips/blob/master/bip-0324.mediawiki#user-content-v2_Bitcoin_P2P_message_structure). use core::fmt; diff --git a/protocol/tests/round_trips.rs b/protocol/tests/round_trips.rs index 88b9907..9f9ba95 100644 --- a/protocol/tests/round_trips.rs +++ b/protocol/tests/round_trips.rs @@ -267,7 +267,7 @@ fn regtest_handshake() { receiver: from_and_recv.clone(), sender: from_and_recv, nonce: 1, - user_agent: "BIP324 Client".to_string(), + user_agent: "BIP-324 Client".to_string(), start_height: 0, relay: false, }; @@ -294,6 +294,69 @@ fn regtest_handshake() { assert_eq!(message.cmd(), "version"); } +#[test] +#[cfg(feature = "std")] +fn regtest_handshake_std() { + use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream}, + time::{SystemTime, UNIX_EPOCH}, + }; + + use bip324::{ + io::IntoBip324, + serde::{deserialize, serialize, NetworkMessage}, + }; + use bitcoin::p2p::{message_network::VersionMessage, Address, ServiceFlags}; + + let bitcoind = regtest_process(TransportVersion::V2); + + let stream = TcpStream::connect(bitcoind.params.p2p_socket.unwrap()).unwrap(); + + // Initialize high-level protocol with handshake using the new into_bip324 method + println!("Starting BIP-324 handshake using into_bip324"); + let mut protocol = stream + .into_bip324( + bip324::Network::Regtest, + bip324::Role::Initiator, + None, // no garbage + None, // no decoys + ) + .unwrap(); + + println!("Handshake completed successfully!"); + + // Create version message. + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time went backwards") + .as_secs(); + let ip = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), PORT); + let from_and_recv = Address::new(&ip, ServiceFlags::NONE); + let msg = VersionMessage { + version: 70015, + services: ServiceFlags::NONE, + timestamp: now as i64, + receiver: from_and_recv.clone(), + sender: from_and_recv, + nonce: 1, + user_agent: "BIP-324 Client".to_string(), + start_height: 0, + relay: false, + }; + + let message = serialize(NetworkMessage::Version(msg)); + println!("Sending version message using Protocol::write()"); + protocol.write(&message).unwrap(); + + println!("Reading version response using Protocol::read()"); + let payload = protocol.read().unwrap(); + + let response_message = deserialize(payload.contents()).unwrap(); + assert_eq!(response_message.cmd(), "version"); + + println!("Successfully exchanged version messages using into_bip324 API!"); +} + #[test] #[should_panic] #[cfg(feature = "std")] diff --git a/proxy/Cargo.toml b/proxy/Cargo.toml index ac33450..200df24 100644 --- a/proxy/Cargo.toml +++ b/proxy/Cargo.toml @@ -3,7 +3,7 @@ name = "bip324-proxy" version = "0.4.0" edition = "2021" license = "CC0-1.0" -description = "BIP324 proxy enabling v1-only clients to use the v2 Bitcoin P2P Protocol" +description = "BIP-324 proxy enabling v1-only clients to use the v2 bitcoin p2p protocol" repository = "https://github.com/rust-bitcoin/bip324" readme = "README.md"