From aa3f2dbdedc12f7d647083e33a484f7623fc53d0 Mon Sep 17 00:00:00 2001 From: Alexander Simmerl Date: Thu, 10 Jun 2021 14:57:10 +0200 Subject: [PATCH] p2p: break out tests into separate crate In order to explore a path forward where testing utilities as well as the assertions for a single and across crates, this change moves the secret connection tests into the newly introduced test crate. Signed-off-by: Alexander Simmerl --- .../898-change-error-variant.md | 1 + .github/workflows/test.yml | 14 + .gitignore | 2 +- Cargo.toml | 5 +- p2p/Cargo.toml | 33 +- p2p/src/error.rs | 4 +- p2p/src/lib.rs | 17 +- p2p/src/secret_connection.rs | 285 ++++++------------ p2p/src/secret_connection/kdf.rs | 9 +- p2p/src/secret_connection/nonce.rs | 56 +--- p2p/src/secret_connection/protocol.rs | 39 ++- p2p/src/secret_connection/public_key.rs | 47 +-- test/Cargo.toml | 28 ++ test/src/lib.rs | 5 + .../secret_connection => test/src}/pipe.rs | 29 +- test/src/test.rs | 1 + test/src/test/unit.rs | 1 + test/src/test/unit/p2p.rs | 1 + test/src/test/unit/p2p/secret_connection.rs | 113 +++++++ .../test/unit/p2p/secret_connection/nonce.rs | 35 +++ .../unit/p2p/secret_connection/public_key.rs | 17 ++ 21 files changed, 428 insertions(+), 314 deletions(-) create mode 100644 .changelog/unreleased/breaking-changes/898-change-error-variant.md create mode 100644 test/Cargo.toml create mode 100644 test/src/lib.rs rename {p2p/src/secret_connection => test/src}/pipe.rs (92%) create mode 100644 test/src/test.rs create mode 100644 test/src/test/unit.rs create mode 100644 test/src/test/unit/p2p.rs create mode 100644 test/src/test/unit/p2p/secret_connection.rs create mode 100644 test/src/test/unit/p2p/secret_connection/nonce.rs create mode 100644 test/src/test/unit/p2p/secret_connection/public_key.rs diff --git a/.changelog/unreleased/breaking-changes/898-change-error-variant.md b/.changelog/unreleased/breaking-changes/898-change-error-variant.md new file mode 100644 index 000000000..c4d413872 --- /dev/null +++ b/.changelog/unreleased/breaking-changes/898-change-error-variant.md @@ -0,0 +1 @@ + - `[p2p]` Remove superfluous module name suffixes in `p2p::error` diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 79325396f..fd516392a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -78,6 +78,20 @@ jobs: run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - run: wasm-pack test --headless --chrome ./light-client-js/ - run: wasm-pack test --headless --firefox ./light-client-js/ + + tendermint-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - uses: actions-rs/cargo@v1 + with: + command: test-all-features + args: -p tendermint-test + tendermint-testgen: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index f971349c6..a066462b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Generated by Cargo # will have compiled files and executables -/target/ +target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html diff --git a/Cargo.toml b/Cargo.toml index 06612f280..01dfb0a40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,11 +5,12 @@ members = [ "light-client", "light-client-js", "p2p", + "pbt-gen", "proto", "rpc", "tendermint", - "testgen", - "pbt-gen" + "test", + "testgen" ] exclude = [ diff --git a/p2p/Cargo.toml b/p2p/Cargo.toml index f59a2c0a1..fd43c466f 100644 --- a/p2p/Cargo.toml +++ b/p2p/Cargo.toml @@ -1,20 +1,28 @@ [package] -name = "tendermint-p2p" -version = "0.19.0" -edition = "2018" -license = "Apache-2.0" -repository = "https://github.com/informalsystems/tendermint-rs" -readme = "README.md" -keywords = ["p2p", "tendermint", "cosmos"] -authors = [ +name = "tendermint-p2p" +version = "0.19.0" +edition = "2018" +license = "Apache-2.0" +repository = "https://github.com/informalsystems/tendermint-rs" +homepage = "https://tendermint.com" +readme = "README.md" +keywords = ["p2p", "tendermint", "cosmos"] +categories = ["cryptography::cryptocurrencies", "network-programming"] +authors = [ "Tony Arcieri ", "Ismail Khoffi " ] description = """ - The Tendermint P2P stack. + The Tendermint P2P stack in Rust. """ +[lib] +test = false + +[features] +amino = ["prost-amino", "prost-amino-derive"] + [dependencies] chacha20poly1305 = "0.7" ed25519-dalek = "1" @@ -26,7 +34,6 @@ prost = "0.7" rand_core = { version = "0.5", features = ["std"] } sha2 = "0.9" subtle = "2" -subtle-encoding = { version = "0.5" } thiserror = "1" x25519-dalek = "1.1" zeroize = "1" @@ -38,9 +45,3 @@ tendermint-proto = { path = "../proto", version = "0.19.0" } # optional dependencies prost-amino = { version = "0.6", optional = true } prost-amino-derive = { version = "0.6", optional = true } - -[dev-dependencies] -readwrite = "^0.1.1" - -[features] -amino = ["prost-amino", "prost-amino-derive"] diff --git a/p2p/src/error.rs b/p2p/src/error.rs index 08000414b..4f1cc20a7 100644 --- a/p2p/src/error.rs +++ b/p2p/src/error.rs @@ -7,7 +7,7 @@ use thiserror::Error; pub enum Error { /// Cryptographic operation failed #[error("cryptographic error")] - CryptoError, + Crypto, /// Malformatted or otherwise invalid cryptographic key #[error("invalid key")] @@ -15,5 +15,5 @@ pub enum Error { /// Network protocol-related errors #[error("protocol error")] - ProtocolError, + Protocol, } diff --git a/p2p/src/lib.rs b/p2p/src/lib.rs index a9cd97cad..ef95cbccc 100644 --- a/p2p/src/lib.rs +++ b/p2p/src/lib.rs @@ -2,12 +2,23 @@ #![forbid(unsafe_code)] #![deny( + nonstandard_style, + private_in_public, + rust_2018_idioms, trivial_casts, trivial_numeric_casts, unused_import_braces, - unused_qualifications, - rust_2018_idioms, - nonstandard_style + unused_qualifications +)] +#![warn( + clippy::all, + clippy::cargo, + clippy::nursery, + clippy::pedantic, + clippy::unwrap_used, + missing_docs, + unused_import_braces, + unused_qualifications )] #![doc( html_root_url = "https://docs.rs/tendermint-p2p/0.19.0", diff --git a/p2p/src/secret_connection.rs b/p2p/src/secret_connection.rs index 5d9024a01..00011108a 100644 --- a/p2p/src/secret_connection.rs +++ b/p2p/src/secret_connection.rs @@ -17,12 +17,17 @@ use eyre::{eyre, Result, WrapErr}; use merlin::Transcript; use rand_core::OsRng; use subtle::ConstantTimeEq; +use thiserror::Error; use x25519_dalek::{EphemeralSecret, PublicKey as EphemeralPublic}; use tendermint_proto as proto; -pub use self::{kdf::Kdf, nonce::Nonce, protocol::Version, public_key::PublicKey}; -use crate::error::Error; +pub use self::{ + kdf::Kdf, + nonce::{Nonce, SIZE as NONCE_SIZE}, + protocol::Version, + public_key::PublicKey, +}; #[cfg(feature = "amino")] mod amino_types; @@ -32,9 +37,6 @@ mod nonce; mod protocol; mod public_key; -#[cfg(test)] -mod pipe; - /// Size of the MAC tag pub const TAG_SIZE: usize = 16; @@ -45,23 +47,39 @@ pub const DATA_MAX_SIZE: usize = 1024; const DATA_LEN_SIZE: usize = 4; const TOTAL_FRAME_SIZE: usize = DATA_MAX_SIZE + DATA_LEN_SIZE; -/// Handshake is a process of establishing the SecretConnection between two peers. -/// Specification: https://github.com/tendermint/spec/blob/master/spec/p2p/peer.md#authenticated-encryption-handshake -struct Handshake { +/// Kinds of errors +#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)] +pub enum Error { + /// Cryptographic operation failed + #[error("cryptographic error")] + Crypto, + + /// Malformatted or otherwise invalid cryptographic key + #[error("invalid key")] + InvalidKey, + + /// Network protocol-related errors + #[error("protocol error")] + Protocol, +} + +/// Handshake is a process of establishing the `SecretConnection` between two peers. +/// [Specification](https://github.com/tendermint/spec/blob/master/spec/p2p/peer.md#authenticated-encryption-handshake) +pub struct Handshake { protocol_version: Version, state: S, } /// Handshake states -/// AwaitingEphKey means we're waiting for the remote ephemeral pubkey. -struct AwaitingEphKey { +/// `AwaitingEphKey` means we're waiting for the remote ephemeral pubkey. +pub struct AwaitingEphKey { local_privkey: ed25519::Keypair, local_eph_privkey: Option, } -/// AwaitingAuthSig means we're waiting for the remote authenticated signature. -struct AwaitingAuthSig { +/// `AwaitingAuthSig` means we're waiting for the remote authenticated signature. +pub struct AwaitingAuthSig { sc_mac: [u8; 32], kdf: Kdf, recv_cipher: ChaCha20Poly1305, @@ -69,8 +87,10 @@ struct AwaitingAuthSig { local_signature: ed25519::Signature, } +#[allow(clippy::use_self)] impl Handshake { /// Initiate a handshake. + #[must_use] pub fn new( local_privkey: ed25519::Keypair, protocol_version: Version, @@ -80,7 +100,7 @@ impl Handshake { let local_eph_pubkey = EphemeralPublic::from(&local_eph_privkey); ( - Handshake { + Self { protocol_version, state: AwaitingEphKey { local_privkey, @@ -92,7 +112,12 @@ impl Handshake { } /// Performs a Diffie-Hellman key agreement and creates a local signature. - /// Transitions Handshake into AwaitingAuthSig state. + /// Transitions Handshake into `AwaitingAuthSig` state. + /// + /// # Errors + /// + /// * if protocol order was violated, e.g. handshake missing + /// * if challenge signing fails pub fn got_key( &mut self, remote_eph_pubkey: EphemeralPublic, @@ -160,6 +185,10 @@ impl Handshake { impl Handshake { /// Returns a verified pubkey of the remote peer. + /// + /// # Errors + /// + /// * if signature scheme isn't supported pub fn got_signature(&mut self, auth_sig_msg: proto::p2p::AuthSigMessage) -> Result { let remote_pubkey = auth_sig_msg .pub_key @@ -169,19 +198,19 @@ impl Handshake { } proto::crypto::public_key::Sum::Secp256k1(_) => None, }) - .ok_or(Error::CryptoError)?; + .ok_or(Error::Crypto)?; - let remote_sig = ed25519::Signature::try_from(auth_sig_msg.sig.as_slice()) - .map_err(|_| Error::CryptoError)?; + let remote_sig = + ed25519::Signature::try_from(auth_sig_msg.sig.as_slice()).map_err(|_| Error::Crypto)?; if self.protocol_version.has_transcript() { remote_pubkey .verify(&self.state.sc_mac, &remote_sig) - .map_err(|_| Error::CryptoError)?; + .map_err(|_| Error::Crypto)?; } else { remote_pubkey .verify(&self.state.kdf.challenge, &remote_sig) - .map_err(|_| Error::CryptoError)?; + .map_err(|_| Error::Crypto)?; } // We've authorized. @@ -207,12 +236,18 @@ impl SecretConnection { self.remote_pubkey.expect("remote_pubkey uninitialized") } - /// Performs a handshake and returns a new SecretConnection. + /// Performs a handshake and returns a new `SecretConnection`. + /// + /// # Errors + /// + /// * if sharing of the pubkey fails + /// * if sharing of the signature fails + /// * if receiving the signature fails pub fn new( mut io_handler: IoHandler, local_privkey: ed25519::Keypair, protocol_version: Version, - ) -> Result> { + ) -> Result { // Start a handshake process. let local_pubkey = PublicKey::from(&local_privkey); let (mut h, local_eph_pubkey) = Handshake::new(local_privkey, protocol_version); @@ -224,7 +259,7 @@ impl SecretConnection { // Compute a local signature (also recv_cipher & send_cipher) let mut h = h.got_key(remote_eph_pubkey)?; - let mut sc = SecretConnection { + let mut sc = Self { io_handler, protocol_version, recv_buffer: vec![], @@ -252,6 +287,7 @@ impl SecretConnection { } /// Encrypt AEAD authenticated data + #[allow(clippy::cast_possible_truncation)] fn encrypt( &self, chunk: &[u8], @@ -274,7 +310,7 @@ impl SecretConnection { b"", &mut sealed_frame[..TOTAL_FRAME_SIZE], ) - .map_err(|_| Error::CryptoError)?; + .map_err(|_| Error::Crypto)?; sealed_frame[TOTAL_FRAME_SIZE..].copy_from_slice(tag.as_slice()); @@ -284,7 +320,7 @@ impl SecretConnection { /// Decrypt AEAD authenticated data fn decrypt(&self, ciphertext: &[u8], out: &mut [u8]) -> Result { if ciphertext.len() < TAG_SIZE { - return Err(Error::CryptoError).wrap_err_with(|| { + return Err(Error::Crypto).wrap_err_with(|| { format!( "ciphertext must be at least as long as a MAC tag {}", TAG_SIZE @@ -296,7 +332,7 @@ impl SecretConnection { let (ct, tag) = ciphertext.split_at(ciphertext.len() - TAG_SIZE); if out.len() < ct.len() { - return Err(Error::CryptoError).wrap_err("output buffer is too small"); + return Err(Error::Crypto).wrap_err("output buffer is too small"); } let in_out = &mut out[..ct.len()]; @@ -309,7 +345,7 @@ impl SecretConnection { in_out, tag.into(), ) - .map_err(|_| Error::CryptoError)?; + .map_err(|_| Error::Crypto)?; Ok(in_out.len()) } @@ -324,31 +360,34 @@ where if !self.recv_buffer.is_empty() { let n = cmp::min(data.len(), self.recv_buffer.len()); data.copy_from_slice(&self.recv_buffer[..n]); - let mut leftover_portion = vec![0; self.recv_buffer.len().checked_sub(n).unwrap()]; + let mut leftover_portion = vec![ + 0; + self.recv_buffer + .len() + .checked_sub(n) + .expect("leftover calculation failed") + ]; leftover_portion.clone_from_slice(&self.recv_buffer[n..]); self.recv_buffer = leftover_portion; return Ok(n); } - let mut sealed_frame = [0u8; TAG_SIZE + TOTAL_FRAME_SIZE]; + let mut sealed_frame = [0_u8; TAG_SIZE + TOTAL_FRAME_SIZE]; self.io_handler.read_exact(&mut sealed_frame)?; // decrypt the frame - let mut frame = [0u8; TOTAL_FRAME_SIZE]; + let mut frame = [0_u8; TOTAL_FRAME_SIZE]; let res = self.decrypt(&sealed_frame, &mut frame); - if res.is_err() { - return Err(io::Error::new( - io::ErrorKind::Other, - res.err().unwrap().to_string(), - )); + if let Err(err) = res { + return Err(io::Error::new(io::ErrorKind::Other, err.to_string())); } self.recv_nonce.increment(); // end decryption - let chunk_length = u32::from_le_bytes(frame[..4].try_into().unwrap()); + let chunk_length = u32::from_le_bytes(frame[..4].try_into().expect("chunk framing failed")); if chunk_length as usize > DATA_MAX_SIZE { return Err(io::Error::new( @@ -359,7 +398,10 @@ where let mut chunk = vec![0; chunk_length as usize]; chunk.clone_from_slice( - &frame[DATA_LEN_SIZE..(DATA_LEN_SIZE.checked_add(chunk_length as usize).unwrap())], + &frame[DATA_LEN_SIZE + ..(DATA_LEN_SIZE + .checked_add(chunk_length as usize) + .expect("chunk size addition overflow"))], ); let n = cmp::min(data.len(), chunk.len()); @@ -377,7 +419,7 @@ where // Writes encrypted frames of `TAG_SIZE` + `TOTAL_FRAME_SIZE` // CONTRACT: data smaller than DATA_MAX_SIZE is read atomically. fn write(&mut self, data: &[u8]) -> io::Result { - let mut n = 0usize; + let mut n = 0_usize; let mut data_copy = data; while !data_copy.is_empty() { let chunk: &[u8]; @@ -386,21 +428,20 @@ where data_copy = &data_copy[DATA_MAX_SIZE..]; } else { chunk = data_copy; - data_copy = &[0u8; 0]; + data_copy = &[0_u8; 0]; } - let sealed_frame = &mut [0u8; TAG_SIZE + TOTAL_FRAME_SIZE]; + let sealed_frame = &mut [0_u8; TAG_SIZE + TOTAL_FRAME_SIZE]; let res = self.encrypt(chunk, sealed_frame); - if res.is_err() { - return Err(io::Error::new( - io::ErrorKind::Other, - res.err().unwrap().to_string(), - )); + if let Err(err) = res { + return Err(io::Error::new(io::ErrorKind::Other, err.to_string())); } self.send_nonce.increment(); // end encryption self.io_handler.write_all(&sealed_frame[..])?; - n = n.checked_add(chunk.len()).unwrap(); + n = n + .checked_add(chunk.len()) + .expect("overflow when adding chunk lenghts"); } Ok(n) @@ -411,7 +452,7 @@ where } } -/// Returns remote_eph_pubkey +/// Returns `remote_eph_pubkey` fn share_eph_pubkey( handler: &mut IoHandler, local_eph_pubkey: &EphemeralPublic, @@ -421,9 +462,9 @@ fn share_eph_pubkey( // TODO(ismail): on the go side this is done in parallel, here we do send and receive after // each other. thread::spawn would require a static lifetime. // Should still work though. - handler.write_all(&protocol_version.encode_initial_handshake(&local_eph_pubkey))?; + handler.write_all(&protocol_version.encode_initial_handshake(local_eph_pubkey))?; - let mut response_len = 0u8; + let mut response_len = 0_u8; handler.read_exact(slice::from_mut(&mut response_len))?; let mut buf = vec![0; response_len as usize]; @@ -431,15 +472,6 @@ fn share_eph_pubkey( protocol_version.decode_initial_handshake(&buf) } -/// Return is of the form lo, hi -fn sort32(first: [u8; 32], second: [u8; 32]) -> ([u8; 32], [u8; 32]) { - if second > first { - (first, second) - } else { - (second, first) - } -} - /// Sign the challenge with the local private key fn sign_challenge( challenge: &[u8; 32], @@ -447,7 +479,7 @@ fn sign_challenge( ) -> Result { local_privkey .try_sign(challenge) - .map_err(|_| Error::CryptoError.into()) + .map_err(|_| Error::Crypto.into()) } // TODO(ismail): change from DecodeError to something more generic @@ -459,7 +491,7 @@ fn share_auth_signature( ) -> Result { let buf = sc .protocol_version - .encode_auth_signature(pubkey, &local_signature); + .encode_auth_signature(pubkey, local_signature); sc.write_all(&buf)?; @@ -468,133 +500,12 @@ fn share_auth_signature( sc.protocol_version.decode_auth_signature(&buf) } -#[cfg(tests)] -mod tests { - use super::*; - - #[test] - fn test_sort() { - // sanity check - let t1 = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, - ]; - let t2 = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 1, - ]; - let (ref t3, ref t4) = sort32(t1, t2); - assert_eq!(t1, *t3); - assert_eq!(t2, *t4); - } - - #[test] - fn test_dh_compatibility() { - let local_priv = &[ - 15, 54, 189, 54, 63, 255, 158, 244, 56, 168, 155, 63, 246, 79, 208, 192, 35, 194, 39, - 232, 170, 187, 179, 36, 65, 36, 237, 12, 225, 176, 201, 54, - ]; - let remote_pub = &[ - 193, 34, 183, 46, 148, 99, 179, 185, 242, 148, 38, 40, 37, 150, 76, 251, 25, 51, 46, - 143, 189, 201, 169, 218, 37, 136, 51, 144, 88, 196, 10, 20, - ]; - - // generated using computeDHSecret in go - let expected_dh = &[ - 92, 56, 205, 118, 191, 208, 49, 3, 226, 150, 30, 205, 230, 157, 163, 7, 36, 28, 223, - 84, 165, 43, 78, 38, 126, 200, 40, 217, 29, 36, 43, 37, - ]; - let got_dh = diffie_hellman(local_priv, remote_pub); - - assert_eq!(expected_dh, &got_dh); - } -} - -#[cfg(test)] -mod test { - use std::thread; - - use super::*; - - #[test] - fn test_handshake() { - let (pipe1, pipe2) = pipe::async_bipipe_buffered(); - - let peer1 = thread::spawn(|| { - let mut csprng = OsRng {}; - let privkey1: ed25519::Keypair = ed25519::Keypair::generate(&mut csprng); - let conn1 = SecretConnection::new(pipe2, privkey1, Version::V0_34); - assert_eq!(conn1.is_ok(), true); - }); - - let peer2 = thread::spawn(|| { - let mut csprng = OsRng {}; - let privkey2: ed25519::Keypair = ed25519::Keypair::generate(&mut csprng); - let conn2 = SecretConnection::new(pipe1, privkey2, Version::V0_34); - assert_eq!(conn2.is_ok(), true); - }); - - peer1.join().expect("peer1 thread has panicked"); - peer2.join().expect("peer2 thread has panicked"); - } - - #[test] - fn test_read_write_single_message() { - let (pipe1, pipe2) = pipe::async_bipipe_buffered(); - - const MESSAGE: &str = "The Queen's Gambit"; - - let sender = thread::spawn(move || { - let mut csprng = OsRng {}; - let privkey1: ed25519::Keypair = ed25519::Keypair::generate(&mut csprng); - let mut conn1 = SecretConnection::new(pipe2, privkey1, Version::V0_34) - .expect("handshake to succeed"); - - conn1 - .write_all(MESSAGE.as_bytes()) - .expect("expected to write message"); - }); - - let receiver = thread::spawn(move || { - let mut csprng = OsRng {}; - let privkey2: ed25519::Keypair = ed25519::Keypair::generate(&mut csprng); - let mut conn2 = SecretConnection::new(pipe1, privkey2, Version::V0_34) - .expect("handshake to succeed"); - - let mut buf = [0; MESSAGE.len()]; - conn2 - .read_exact(&mut buf) - .expect("expected to read message"); - assert_eq!(MESSAGE.as_bytes(), &buf); - }); - - sender.join().expect("sender thread has panicked"); - receiver.join().expect("receiver thread has panicked"); - } - - #[test] - fn test_evil_peer_shares_invalid_eph_key() { - let mut csprng = OsRng {}; - let local_privkey: ed25519::Keypair = ed25519::Keypair::generate(&mut csprng); - let (mut h, _) = Handshake::new(local_privkey, Version::V0_34); - let bytes: [u8; 32] = [0; 32]; - let res = h.got_key(EphemeralPublic::from(bytes)); - assert_eq!(res.is_err(), true); - } - - #[test] - fn test_evil_peer_shares_invalid_auth_sig() { - let mut csprng = OsRng {}; - let local_privkey: ed25519::Keypair = ed25519::Keypair::generate(&mut csprng); - let (mut h, _) = Handshake::new(local_privkey, Version::V0_34); - let res = h.got_key(EphemeralPublic::from(x25519_dalek::X25519_BASEPOINT_BYTES)); - assert_eq!(res.is_err(), false); - - let mut h = res.unwrap(); - let res = h.got_signature(proto::p2p::AuthSigMessage { - pub_key: None, - sig: vec![], - }); - assert_eq!(res.is_err(), true); +/// Return is of the form lo, hi +#[must_use] +pub fn sort32(first: [u8; 32], second: [u8; 32]) -> ([u8; 32], [u8; 32]) { + if second > first { + (first, second) + } else { + (second, first) } } diff --git a/p2p/src/secret_connection/kdf.rs b/p2p/src/secret_connection/kdf.rs index 3c79ee4a9..b23675e55 100644 --- a/p2p/src/secret_connection/kdf.rs +++ b/p2p/src/secret_connection/kdf.rs @@ -19,14 +19,15 @@ pub struct Kdf { impl Kdf { /// Returns recv secret, send secret, challenge as 32 byte arrays + #[must_use] pub fn derive_secrets_and_challenge(shared_secret: &[u8; 32], loc_is_lo: bool) -> Self { - let mut key_material = [0u8; 96]; + let mut key_material = [0_u8; 96]; Hkdf::::new(None, shared_secret) .expand(HKDF_INFO, &mut key_material) - .unwrap(); + .expect("secret expansion failed"); - let [mut recv_secret, mut send_secret, mut challenge] = [[0u8; 32]; 3]; + let [mut recv_secret, mut send_secret, mut challenge] = [[0_u8; 32]; 3]; if loc_is_lo { recv_secret.copy_from_slice(&key_material[0..32]); @@ -39,7 +40,7 @@ impl Kdf { challenge.copy_from_slice(&key_material[64..96]); key_material.as_mut().zeroize(); - Kdf { + Self { recv_secret, send_secret, challenge, diff --git a/p2p/src/secret_connection/nonce.rs b/p2p/src/secret_connection/nonce.rs index 59c0038a9..77cdadf97 100644 --- a/p2p/src/secret_connection/nonce.rs +++ b/p2p/src/secret_connection/nonce.rs @@ -2,66 +2,34 @@ use std::convert::TryInto; -/// Size of a ChaCha20 (IETF) nonce +/// Size of a `ChaCha20` (IETF) nonce pub const SIZE: usize = 12; -/// SecretConnection nonces (i.e. ChaCha20 nonces) +/// `SecretConnection` nonces (i.e. `ChaCha20` nonces) pub struct Nonce(pub [u8; SIZE]); impl Default for Nonce { - fn default() -> Nonce { - Nonce([0u8; SIZE]) + fn default() -> Self { + Self([0_u8; SIZE]) } } impl Nonce { /// Increment the nonce's counter by 1 pub fn increment(&mut self) { - let counter: u64 = u64::from_le_bytes(self.0[4..].try_into().unwrap()); - self.0[4..].copy_from_slice(&counter.checked_add(1).unwrap().to_le_bytes()); + let counter: u64 = u64::from_le_bytes(self.0[4..].try_into().expect("framing failed")); + self.0[4..].copy_from_slice( + &counter + .checked_add(1) + .expect("overflow in counter addition") + .to_le_bytes(), + ); } /// Serialize nonce as bytes (little endian) #[inline] + #[must_use] pub fn to_bytes(&self) -> &[u8] { &self.0[..] } } - -#[cfg(test)] -mod tests { - use super::*; - use std::collections::HashMap; - - #[test] - fn test_incr_nonce() { - // make sure we match the golang implementation - let mut check_points: HashMap = HashMap::new(); - check_points.insert(0, &[0u8, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]); - check_points.insert(1, &[0u8, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0]); - check_points.insert(510, &[0u8, 0, 0, 0, 255, 1, 0, 0, 0, 0, 0, 0]); - check_points.insert(511, &[0u8, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0]); - check_points.insert(512, &[0u8, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0]); - check_points.insert(1023, &[0u8, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0]); - - let mut nonce = Nonce::default(); - assert_eq!(nonce.to_bytes().len(), SIZE); - - for i in 0..1024 { - nonce.increment(); - if let Some(want) = check_points.get(&i) { - let got = &nonce.to_bytes(); - assert_eq!(got, want); - } - } - } - #[test] - #[should_panic] - fn test_incr_nonce_overflow() { - // other than in the golang implementation we panic if we incremented more than 64 - // bits allow. - // In golang this would reset to an all zeroes nonce. - let mut nonce = Nonce([0u8, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255]); - nonce.increment(); - } -} diff --git a/p2p/src/secret_connection/protocol.rs b/p2p/src/secret_connection/protocol.rs index ce769b798..36c37122c 100644 --- a/p2p/src/secret_connection/protocol.rs +++ b/p2p/src/secret_connection/protocol.rs @@ -37,19 +37,23 @@ pub enum Version { impl Version { /// Does this version of Secret Connection use a transcript hash + #[must_use] pub fn has_transcript(self) -> bool { - self != Version::Legacy + self != Self::Legacy } /// Are messages encoded using Protocol Buffers? - pub fn is_protobuf(self) -> bool { + #[must_use] + pub const fn is_protobuf(self) -> bool { match self { - Version::V0_34 => true, - Version::V0_33 | Version::Legacy => false, + Self::V0_34 => true, + Self::V0_33 | Self::Legacy => false, } } /// Encode the initial handshake message (i.e. first one sent by both peers) + #[allow(clippy::cast_possible_truncation)] + #[must_use] pub fn encode_initial_handshake(self, eph_pubkey: &EphemeralPublic) -> Vec { if self.is_protobuf() { // Equivalent Go implementation: @@ -73,17 +77,21 @@ impl Version { } /// Decode the initial handshake message + /// + /// # Errors + /// + /// * if the message is malformed pub fn decode_initial_handshake(self, bytes: &[u8]) -> Result { let eph_pubkey = if self.is_protobuf() { // Equivalent Go implementation: // https://github.com/tendermint/tendermint/blob/9e98c74/p2p/conn/secret_connection.go#L315-L323 // TODO(tarcieri): proper protobuf framing if bytes.len() != 34 || bytes[..2] != [0x0a, 0x20] { - return Err(Error::ProtocolError) + return Err(Error::Protocol) .wrap_err("malformed handshake message (protocol version mismatch?)"); } - let eph_pubkey_bytes: [u8; 32] = bytes[2..].try_into().unwrap(); + let eph_pubkey_bytes: [u8; 32] = bytes[2..].try_into().expect("framing failed"); EphemeralPublic::from(eph_pubkey_bytes) } else { // Equivalent Go implementation: @@ -91,11 +99,11 @@ impl Version { // // Check that the length matches what we expect and the length prefix is correct if bytes.len() != 33 || bytes[0] != 32 { - return Err(Error::ProtocolError) + return Err(Error::Protocol) .wrap_err("malformed handshake message (protocol version mismatch?)"); } - let eph_pubkey_bytes: [u8; 32] = bytes[1..].try_into().unwrap(); + let eph_pubkey_bytes: [u8; 32] = bytes[1..].try_into().expect("framing failed"); EphemeralPublic::from(eph_pubkey_bytes) }; @@ -108,6 +116,7 @@ impl Version { } /// Encode signature which authenticates the handshake + #[must_use] pub fn encode_auth_signature( self, pub_key: &ed25519::PublicKey, @@ -136,7 +145,8 @@ impl Version { } /// Get the length of the auth message response for this protocol version - pub fn auth_sig_msg_response_len(self) -> usize { + #[must_use] + pub const fn auth_sig_msg_response_len(self) -> usize { if self.is_protobuf() { // 32 + 64 + (proto overhead = 1 prefix + 2 fields + 2 lengths + total length) 103 @@ -147,6 +157,10 @@ impl Version { } /// Decode signature message which authenticates the handshake + /// + /// # Errors + /// + /// * if the decoding of the bytes fails pub fn decode_auth_signature(self, bytes: &[u8]) -> Result { if self.is_protobuf() { // Parse Protobuf-encoded `AuthSigMessage` @@ -155,13 +169,14 @@ impl Version { "malformed handshake message (protocol version mismatch?): {}", e ); - Report::new(Error::ProtocolError).wrap_err(message) + Report::new(Error::Protocol).wrap_err(message) }) } else { self.decode_auth_signature_amino(bytes) } } + #[allow(clippy::unused_self)] #[cfg(feature = "amino")] fn encode_auth_signature_amino( self, @@ -180,6 +195,7 @@ impl Version { buf } + #[allow(clippy::unused_self)] #[cfg(not(feature = "amino"))] fn encode_auth_signature_amino( self, @@ -189,6 +205,7 @@ impl Version { panic!("attempted to encode auth signature using amino, but 'amino' feature is not present") } + #[allow(clippy::unused_self)] #[cfg(feature = "amino")] fn decode_auth_signature_amino(self, bytes: &[u8]) -> Result { // Legacy Amino encoded `AuthSigMessage` @@ -203,6 +220,7 @@ impl Version { }) } + #[allow(clippy::unused_self)] #[cfg(not(feature = "amino"))] fn decode_auth_signature_amino(self, _: &[u8]) -> Result { panic!("attempted to decode auth signature using amino, but 'amino' feature is not present") @@ -216,6 +234,7 @@ impl Version { /// Software Countermeasures (see "Rejecting Known Bad Points" subsection): /// /// +#[allow(clippy::match_same_arms)] fn is_low_order_point(point: &EphemeralPublic) -> bool { // Note: as these are public points and do not interact with secret-key // material in any way, this check does not need to be performed in diff --git a/p2p/src/secret_connection/public_key.rs b/p2p/src/secret_connection/public_key.rs index 1fc1bfb0f..5f83cfb2d 100644 --- a/p2p/src/secret_connection/public_key.rs +++ b/p2p/src/secret_connection/public_key.rs @@ -17,26 +17,32 @@ pub enum PublicKey { impl PublicKey { /// From raw Ed25519 public key bytes - pub fn from_raw_ed25519(bytes: &[u8]) -> Result { + /// + /// # Errors + /// + /// * if the bytes given are invalid + pub fn from_raw_ed25519(bytes: &[u8]) -> Result { ed25519::PublicKey::from_bytes(bytes) - .map(PublicKey::Ed25519) + .map(Self::Ed25519) .map_err(|_| error::Kind::Crypto.into()) } /// Get Ed25519 public key - pub fn ed25519(self) -> Option { + #[must_use] + pub const fn ed25519(self) -> Option { match self { - PublicKey::Ed25519(pk) => Some(pk), + Self::Ed25519(pk) => Some(pk), } } /// Get the remote Peer ID + #[must_use] pub fn peer_id(self) -> node::Id { match self { - PublicKey::Ed25519(pk) => { + Self::Ed25519(pk) => { // TODO(tarcieri): use `tendermint::node::Id::from` let digest = Sha256::digest(pk.as_bytes()); - let mut bytes = [0u8; 20]; + let mut bytes = [0_u8; 20]; bytes.copy_from_slice(&digest[..20]); node::Id::new(bytes) } @@ -51,34 +57,13 @@ impl Display for PublicKey { } impl From<&ed25519::Keypair> for PublicKey { - fn from(sk: &ed25519::Keypair) -> PublicKey { - PublicKey::Ed25519(sk.public) + fn from(sk: &ed25519::Keypair) -> Self { + Self::Ed25519(sk.public) } } impl From for PublicKey { - fn from(pk: ed25519::PublicKey) -> PublicKey { - PublicKey::Ed25519(pk) - } -} - -#[cfg(test)] -mod tests { - use super::PublicKey; - use subtle_encoding::hex; - - const EXAMPLE_SECRET_CONN_KEY: &str = - "F7FEB0B5BA0760B2C58893E329475D1EA81781DD636E37144B6D599AD38AA825"; - - #[test] - fn test_secret_connection_pubkey_serialization() { - let example_key = - PublicKey::from_raw_ed25519(&hex::decode_upper(EXAMPLE_SECRET_CONN_KEY).unwrap()) - .unwrap(); - - assert_eq!( - example_key.to_string(), - "117C95C4FD7E636C38D303493302D2C271A39669" - ); + fn from(pk: ed25519::PublicKey) -> Self { + Self::Ed25519(pk) } } diff --git a/test/Cargo.toml b/test/Cargo.toml new file mode 100644 index 000000000..ea1fbebe4 --- /dev/null +++ b/test/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "tendermint-test" +description = "Tendermint workspace tests and common utilities for testing." +version = "0.19.0" +edition = "2018" +license = "Apache-2.0" +categories = ["development", "test", "tools"] +repository = "https://github.com/informalsystems/tendermint-rs" +keywords = ["blockchain", "tendermint", "testing"] +readme = "README.md" +authors = ["Alexander Simmerl "] + +[lib] +test = true + +[dev-dependencies] +ed25519-dalek = "1" +eyre = "0.6" +flume = "0.10" +rand_core = { version = "0.5", features = ["std"] } +readwrite = "^0.1.1" +subtle-encoding = { version = "0.5" } +thiserror = "1" +x25519-dalek = "1.1" + +tendermint = { path = "../tendermint" } +tendermint-p2p = { path = "../p2p" } +tendermint-proto = { path = "../proto" } diff --git a/test/src/lib.rs b/test/src/lib.rs new file mode 100644 index 000000000..980d67a56 --- /dev/null +++ b/test/src/lib.rs @@ -0,0 +1,5 @@ +#[cfg(test)] +pub mod pipe; + +#[cfg(test)] +mod test; diff --git a/p2p/src/secret_connection/pipe.rs b/test/src/pipe.rs similarity index 92% rename from p2p/src/secret_connection/pipe.rs rename to test/src/pipe.rs index d40dd9529..2f8bd1c17 100644 --- a/p2p/src/secret_connection/pipe.rs +++ b/test/src/pipe.rs @@ -20,16 +20,17 @@ //! Asynchronous in-memory pipe -use flume::{self, Receiver, SendError, Sender, TrySendError}; use std::cmp::min; use std::io::{self, BufRead, Read, Write}; use std::mem::replace; +use flume::{self, Receiver, SendError, Sender, TrySendError}; + // value for libstd const DEFAULT_BUF_SIZE: usize = 8 * 1024; /// The `Read` end of a pipe (see `pipe()`) -pub struct PipeReader { +pub struct Reader { receiver: Receiver>, buffer: Vec, position: usize, @@ -37,23 +38,23 @@ pub struct PipeReader { /// The `Write` end of a pipe (see `pipe()`) that will buffer small writes before sending /// to the reader end. -pub struct PipeBufWriter { +pub struct BufWriter { sender: Option>>, buffer: Vec, size: usize, } /// Creates an asynchronous memory pipe with buffered writer -pub fn async_pipe_buffered() -> (PipeReader, PipeBufWriter) { +pub fn async_pipe_buffered() -> (Reader, BufWriter) { let (tx, rx) = flume::unbounded(); ( - PipeReader { + Reader { receiver: rx, buffer: Vec::new(), position: 0, }, - PipeBufWriter { + BufWriter { sender: Some(tx), buffer: Vec::with_capacity(DEFAULT_BUF_SIZE), size: DEFAULT_BUF_SIZE, @@ -63,8 +64,8 @@ pub fn async_pipe_buffered() -> (PipeReader, PipeBufWriter) { /// Creates a pair of pipes for bidirectional communication using buffered writer, a bit like UNIX's `socketpair(2)`. pub fn async_bipipe_buffered() -> ( - readwrite::ReadWrite, - readwrite::ReadWrite, + readwrite::ReadWrite, + readwrite::ReadWrite, ) { let (r1, w1) = async_pipe_buffered(); let (r2, w2) = async_pipe_buffered(); @@ -75,7 +76,7 @@ fn epipe() -> io::Error { io::Error::new(io::ErrorKind::BrokenPipe, "pipe reader has been dropped") } -impl PipeBufWriter { +impl BufWriter { #[inline] /// Gets a reference to the underlying `Sender` pub fn sender(&self) -> &Sender> { @@ -85,7 +86,7 @@ impl PipeBufWriter { } } -impl BufRead for PipeReader { +impl BufRead for Reader { fn fill_buf(&mut self) -> io::Result<&[u8]> { while self.position >= self.buffer.len() { match self.receiver.recv() { @@ -107,7 +108,7 @@ impl BufRead for PipeReader { } } -impl Read for PipeReader { +impl Read for Reader { fn read(&mut self, buf: &mut [u8]) -> io::Result { if buf.is_empty() { return Ok(0); @@ -124,7 +125,7 @@ impl Read for PipeReader { } } -impl Write for PipeBufWriter { +impl Write for BufWriter { fn write(&mut self, buf: &[u8]) -> io::Result { let buffer_len = self.buffer.len(); let bytes_written = if buf.len() > self.size { @@ -180,11 +181,11 @@ impl Write for PipeBufWriter { /// recommended that `flush()` be used explicitly instead of relying on Drop. /// /// This final flush can be avoided by using `drop(writer.into_inner())`. -impl Drop for PipeBufWriter { +impl Drop for BufWriter { fn drop(&mut self) { if !self.buffer.is_empty() { let data = replace(&mut self.buffer, Vec::new()); - let _ = self.sender().send(data); + self.sender().send(data).ok(); } } } diff --git a/test/src/test.rs b/test/src/test.rs new file mode 100644 index 000000000..d5a9c9412 --- /dev/null +++ b/test/src/test.rs @@ -0,0 +1 @@ +mod unit; diff --git a/test/src/test/unit.rs b/test/src/test/unit.rs new file mode 100644 index 000000000..b4b783557 --- /dev/null +++ b/test/src/test/unit.rs @@ -0,0 +1 @@ +mod p2p; diff --git a/test/src/test/unit/p2p.rs b/test/src/test/unit/p2p.rs new file mode 100644 index 000000000..916b82769 --- /dev/null +++ b/test/src/test/unit/p2p.rs @@ -0,0 +1 @@ +mod secret_connection; diff --git a/test/src/test/unit/p2p/secret_connection.rs b/test/src/test/unit/p2p/secret_connection.rs new file mode 100644 index 000000000..89eec24bc --- /dev/null +++ b/test/src/test/unit/p2p/secret_connection.rs @@ -0,0 +1,113 @@ +use std::io::Read as _; +use std::io::Write as _; +use std::thread; + +use ed25519_dalek::{self as ed25519}; +use rand_core::OsRng; +use x25519_dalek::PublicKey as EphemeralPublic; + +use tendermint_p2p::secret_connection::{sort32, Handshake, SecretConnection, Version}; +use tendermint_proto as proto; + +use crate::pipe; + +mod nonce; +mod public_key; + +#[test] +fn test_handshake() { + let (pipe1, pipe2) = pipe::async_bipipe_buffered(); + + let peer1 = thread::spawn(|| { + let mut csprng = OsRng {}; + let privkey1: ed25519::Keypair = ed25519::Keypair::generate(&mut csprng); + let conn1 = SecretConnection::new(pipe2, privkey1, Version::V0_34); + assert_eq!(conn1.is_ok(), true); + }); + + let peer2 = thread::spawn(|| { + let mut csprng = OsRng {}; + let privkey2: ed25519::Keypair = ed25519::Keypair::generate(&mut csprng); + let conn2 = SecretConnection::new(pipe1, privkey2, Version::V0_34); + assert_eq!(conn2.is_ok(), true); + }); + + peer1.join().expect("peer1 thread has panicked"); + peer2.join().expect("peer2 thread has panicked"); +} + +#[test] +fn test_read_write_single_message() { + const MESSAGE: &str = "The Queen's Gambit"; + + let (pipe1, pipe2) = pipe::async_bipipe_buffered(); + + let sender = thread::spawn(move || { + let mut csprng = OsRng {}; + let privkey1: ed25519::Keypair = ed25519::Keypair::generate(&mut csprng); + let mut conn1 = + SecretConnection::new(pipe2, privkey1, Version::V0_34).expect("handshake to succeed"); + + conn1 + .write_all(MESSAGE.as_bytes()) + .expect("expected to write message"); + }); + + let receiver = thread::spawn(move || { + let mut csprng = OsRng {}; + let privkey2: ed25519::Keypair = ed25519::Keypair::generate(&mut csprng); + let mut conn2 = + SecretConnection::new(pipe1, privkey2, Version::V0_34).expect("handshake to succeed"); + + let mut buf = [0; MESSAGE.len()]; + conn2 + .read_exact(&mut buf) + .expect("expected to read message"); + assert_eq!(MESSAGE.as_bytes(), &buf); + }); + + sender.join().expect("sender thread has panicked"); + receiver.join().expect("receiver thread has panicked"); +} + +#[test] +fn test_evil_peer_shares_invalid_eph_key() { + let mut csprng = OsRng {}; + let local_privkey: ed25519::Keypair = ed25519::Keypair::generate(&mut csprng); + let (mut h, _) = Handshake::new(local_privkey, Version::V0_34); + let bytes: [u8; 32] = [0; 32]; + let res = h.got_key(EphemeralPublic::from(bytes)); + assert_eq!(res.is_err(), true); +} + +#[test] +fn test_evil_peer_shares_invalid_auth_sig() { + let mut csprng = OsRng {}; + let local_privkey: ed25519::Keypair = ed25519::Keypair::generate(&mut csprng); + let (mut h, _) = Handshake::new(local_privkey, Version::V0_34); + let res = h.got_key(EphemeralPublic::from(x25519_dalek::X25519_BASEPOINT_BYTES)); + assert_eq!(res.is_err(), false); + + let mut h = res.unwrap(); + let res = h.got_signature(proto::p2p::AuthSigMessage { + pub_key: None, + sig: vec![], + }); + assert_eq!(res.is_err(), true); +} + +#[test] +fn test_sort() { + // sanity check + let t1 = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, + ]; + let t2 = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, + ]; + let (ref t3, ref t4) = sort32(t1, t2); + assert_eq!(t1, *t3); + assert_eq!(t2, *t4); +} diff --git a/test/src/test/unit/p2p/secret_connection/nonce.rs b/test/src/test/unit/p2p/secret_connection/nonce.rs new file mode 100644 index 000000000..fff7851c8 --- /dev/null +++ b/test/src/test/unit/p2p/secret_connection/nonce.rs @@ -0,0 +1,35 @@ +use std::collections::HashMap; + +use tendermint_p2p::secret_connection::{Nonce, NONCE_SIZE}; + +#[test] +fn test_incr_nonce() { + // make sure we match the golang implementation + let mut check_points: HashMap = HashMap::new(); + check_points.insert(0, &[0_u8, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]); + check_points.insert(1, &[0_u8, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0]); + check_points.insert(510, &[0_u8, 0, 0, 0, 255, 1, 0, 0, 0, 0, 0, 0]); + check_points.insert(511, &[0_u8, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0]); + check_points.insert(512, &[0_u8, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0]); + check_points.insert(1023, &[0_u8, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0]); + + let mut nonce = Nonce::default(); + assert_eq!(nonce.to_bytes().len(), NONCE_SIZE); + + for i in 0..1024 { + nonce.increment(); + if let Some(want) = check_points.get(&i) { + let got = &nonce.to_bytes(); + assert_eq!(got, want); + } + } +} +#[test] +#[should_panic] +fn test_incr_nonce_overflow() { + // other than in the golang implementation we panic if we incremented more than 64 + // bits allow. + // In golang this would reset to an all zeroes nonce. + let mut nonce = Nonce([0_u8, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255]); + nonce.increment(); +} diff --git a/test/src/test/unit/p2p/secret_connection/public_key.rs b/test/src/test/unit/p2p/secret_connection/public_key.rs new file mode 100644 index 000000000..01cad25df --- /dev/null +++ b/test/src/test/unit/p2p/secret_connection/public_key.rs @@ -0,0 +1,17 @@ +use subtle_encoding::hex; + +use tendermint_p2p::secret_connection::PublicKey; + +const EXAMPLE_SECRET_CONN_KEY: &str = + "F7FEB0B5BA0760B2C58893E329475D1EA81781DD636E37144B6D599AD38AA825"; + +#[test] +fn test_secret_connection_pubkey_serialization() { + let example_key = + PublicKey::from_raw_ed25519(&hex::decode_upper(EXAMPLE_SECRET_CONN_KEY).unwrap()).unwrap(); + + assert_eq!( + example_key.to_string(), + "117C95C4FD7E636C38D303493302D2C271A39669" + ); +}