Skip to content

Commit 4b60771

Browse files
committed
feat: challenge-response auth example
docs(hook): remove specific socket type mention
1 parent 6a438ed commit 4b60771

File tree

4 files changed

+123
-2
lines changed

4 files changed

+123
-2
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

msg-socket/src/hooks/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ pub(crate) type ErasedHookResult<T> = HookResult<T, Box<dyn StdError + Send + Sy
9090

9191
/// Connection hook executed during connection establishment.
9292
///
93-
/// For server sockets (Rep, Pub): called when a connection is accepted.
94-
/// For client sockets (Req, Sub): called after connecting.
93+
/// For server sockets: called when a connection is accepted.
94+
/// For client sockets: called after connecting.
9595
///
9696
/// The connection hook receives the raw IO stream and has full control over the handshake protocol.
9797
pub trait ConnectionHook<Io>: Send + Sync + 'static

msg/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ tracing-subscriber = "0.3"
2121
futures.workspace = true
2222
tracing.workspace = true
2323
rand.workspace = true
24+
thiserror.workspace = true
2425
criterion.workspace = true
2526
pprof.workspace = true
2627
openssl.workspace = true

msg/examples/reqrep_challenge.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//! Request-Reply with challenge-response authentication (insecure demo).
2+
//!
3+
//! Protocol:
4+
//! 1. Server sends a random 32-byte challenge
5+
//! 2. Client computes a signature using a shared secret and sends it back
6+
//! 3. Server verifies the signature and accepts or rejects
7+
8+
use bytes::Bytes;
9+
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
10+
use tokio_stream::StreamExt;
11+
12+
use msg::{RepSocket, ReqSocket, hooks::{ConnectionHook, Error, HookResult}, tcp::Tcp};
13+
14+
const SECRET: [u8; 32] = *b"this_is_a_32_byte_secret_key!!!!";
15+
16+
/// Simple XOR-based MAC
17+
fn compute_mac(secret: &[u8; 32], challenge: &[u8; 32]) -> [u8; 32] {
18+
let mut mac = [0u8; 32];
19+
for i in 0..32 {
20+
mac[i] = secret[i] ^ challenge[i] ^ secret[(i + 1) % 32];
21+
}
22+
mac
23+
}
24+
25+
struct ChallengeServerHook {
26+
secret: [u8; 32],
27+
}
28+
29+
impl ChallengeServerHook {
30+
fn new(secret: [u8; 32]) -> Self {
31+
Self { secret }
32+
}
33+
}
34+
35+
#[derive(Debug, thiserror::Error)]
36+
#[error("client authentication failed")]
37+
struct ServerAuthError;
38+
39+
impl<Io> ConnectionHook<Io> for ChallengeServerHook
40+
where
41+
Io: AsyncRead + AsyncWrite + Send + Unpin + 'static,
42+
{
43+
type Error = ServerAuthError;
44+
45+
async fn on_connection(&self, mut io: Io) -> HookResult<Io, Self::Error> {
46+
let challenge: [u8; 32] = rand::random();
47+
io.write_all(&challenge).await?;
48+
49+
let mut response = [0u8; 32];
50+
io.read_exact(&mut response).await?;
51+
52+
let expected = compute_mac(&self.secret, &challenge);
53+
if response != expected {
54+
io.write_all(&[0]).await?;
55+
return Err(Error::hook(ServerAuthError));
56+
}
57+
58+
io.write_all(&[1]).await?;
59+
Ok(io)
60+
}
61+
}
62+
63+
struct ChallengeClientHook {
64+
secret: [u8; 32],
65+
}
66+
67+
impl ChallengeClientHook {
68+
fn new(secret: [u8; 32]) -> Self {
69+
Self { secret }
70+
}
71+
}
72+
73+
#[derive(Debug, thiserror::Error)]
74+
#[error("server rejected authentication")]
75+
struct ClientAuthError;
76+
77+
impl<Io> ConnectionHook<Io> for ChallengeClientHook
78+
where
79+
Io: AsyncRead + AsyncWrite + Send + Unpin + 'static,
80+
{
81+
type Error = ClientAuthError;
82+
83+
async fn on_connection(&self, mut io: Io) -> HookResult<Io, Self::Error> {
84+
let mut challenge = [0u8; 32];
85+
io.read_exact(&mut challenge).await?;
86+
87+
let response = compute_mac(&self.secret, &challenge);
88+
io.write_all(&response).await?;
89+
90+
let mut verdict = [0u8; 1];
91+
io.read_exact(&mut verdict).await?;
92+
93+
if verdict[0] != 1 {
94+
return Err(Error::hook(ClientAuthError));
95+
}
96+
97+
Ok(io)
98+
}
99+
}
100+
101+
#[tokio::main]
102+
async fn main() {
103+
let mut rep =
104+
RepSocket::new(Tcp::default()).with_connection_hook(ChallengeServerHook::new(SECRET));
105+
rep.bind("0.0.0.0:4445").await.unwrap();
106+
107+
let mut req =
108+
ReqSocket::new(Tcp::default()).with_connection_hook(ChallengeClientHook::new(SECRET));
109+
req.connect("0.0.0.0:4445").await.unwrap();
110+
111+
tokio::spawn(async move {
112+
let req = rep.next().await.unwrap();
113+
println!("Server received: {:?}", req.msg());
114+
req.respond(Bytes::from("authenticated response")).unwrap();
115+
});
116+
117+
let res: Bytes = req.request(Bytes::from("hello")).await.unwrap();
118+
println!("Client received: {res:?}");
119+
}

0 commit comments

Comments
 (0)