ecksport_core/
state_mach.rs

1use tracing::*;
2
3use crate::frame::{
4    self, ChallengeData, ClientFinishData, ClientHelloData, ClientQueryData, FrameBody,
5    GoodbyeData, ServerDumpData, ServerHelloData,
6};
7use crate::traits::AuthConfig;
8use crate::{auth, peer, topic};
9
10/// External action we should take after processing a message.
11#[derive(Clone, Debug)]
12pub enum Action {
13    /// Sends a frame to the other side.
14    SendFrame(frame::FrameBody),
15
16    /// Completes the handshake.
17    Ready(ReadyData),
18
19    /// Aborts the session, sending a goodbye message.
20    Abort(GoodbyeData),
21
22    /// Exits the handshake, with the received goodbye message.
23    Exit(GoodbyeData),
24
25    /// Sets the agent of the remote peer.
26    SetRemoteAgent(String),
27
28    /// Sets the identity of the remote peer.
29    SetRemoteIdent(peer::Identity),
30
31    /// Connection closed gracefully without event.
32    HappyClose,
33}
34
35/// Generic enum describing the kind of state the handshake is in.
36#[derive(Copy, Clone, Debug, Eq, PartialEq)]
37pub enum StateKind {
38    WaitHello,
39    WaitFinish,
40    Ready,
41    Fail,
42}
43
44/// Client handshake state machine.
45#[derive(Clone)]
46pub enum ClientState<A: AuthConfig> {
47    WaitServerHello {
48        auth: A,
49        protocol: topic::Topic,
50        sent_chal: Option<ChallengeData>,
51    },
52    Ready {
53        auth: A,
54    },
55    Fail,
56}
57
58impl<A: AuthConfig> ClientState<A> {
59    pub fn auth(&self) -> Option<&A> {
60        match self {
61            Self::WaitServerHello { auth, .. } => Some(auth),
62            Self::Ready { auth } => Some(auth),
63            _ => None,
64        }
65    }
66
67    pub fn kind(&self) -> StateKind {
68        match self {
69            Self::WaitServerHello { .. } => StateKind::WaitHello,
70            Self::Ready { .. } => StateKind::Ready,
71            Self::Fail => StateKind::Fail,
72        }
73    }
74}
75
76/// Server handshake state machine.
77#[derive(Clone, Debug)]
78pub enum ServerState<A: AuthConfig> {
79    WaitClientHello {
80        auth: A,
81    },
82    WaitClientFinish {
83        auth: A,
84        protocol: topic::Topic,
85        client_chal: Option<ChallengeData>,
86        server_chal: Option<ChallengeData>,
87    },
88    Ready {
89        auth: A,
90    },
91    Fail,
92}
93
94impl<A: AuthConfig> ServerState<A> {
95    /// Creates a new instance as though a connection has been established and
96    /// we haven't received any frames yet.
97    pub fn init(auth: A) -> Self {
98        Self::WaitClientHello { auth }
99    }
100
101    pub fn auth(&self) -> Option<&A> {
102        match self {
103            Self::WaitClientHello { auth, .. } => Some(auth),
104            Self::WaitClientFinish { auth, .. } => Some(auth),
105            Self::Ready { auth } => Some(auth),
106            _ => None,
107        }
108    }
109
110    pub fn kind(&self) -> StateKind {
111        match self {
112            Self::WaitClientHello { .. } => StateKind::WaitHello,
113            Self::WaitClientFinish { .. } => StateKind::WaitFinish,
114            Self::Ready { .. } => StateKind::Ready,
115            Self::Fail => StateKind::Fail,
116        }
117    }
118}
119
120#[derive(Clone, Debug)]
121pub struct ReadyData {
122    protocol: topic::Topic,
123}
124
125impl ReadyData {
126    pub fn new(protocol: topic::Topic) -> Self {
127        Self { protocol }
128    }
129
130    pub fn protocol(&self) -> topic::Topic {
131        self.protocol
132    }
133}
134
135#[derive(Clone, Debug)]
136pub struct ClientMeta {
137    agent: String,
138}
139
140impl ClientMeta {
141    pub fn new(agent: String) -> Self {
142        Self { agent }
143    }
144
145    pub fn create_query_frame(&self) -> FrameBody {
146        FrameBody::ClientQuery(ClientQueryData::new(self.agent.clone()))
147    }
148}
149
150#[derive(Clone, Debug)]
151pub struct ServerMeta {
152    agent: String,
153    protocols: Vec<topic::Topic>,
154}
155
156impl ServerMeta {
157    pub fn add_protocol(&mut self, proto: topic::Topic) {
158        self.protocols.push(proto);
159    }
160}
161
162impl ServerMeta {
163    pub fn new(agent: String, protocols: Vec<topic::Topic>) -> Self {
164        Self { agent, protocols }
165    }
166}
167
168/// Inits a client session using the parameters we want to use.
169pub fn exec_client_init<A: AuthConfig>(
170    protocol: topic::Topic,
171    auth: A,
172    meta: &ClientMeta,
173) -> (ClientState<A>, Vec<Action>) {
174    // If we should sign in this auth setup, then generate a challenge here.
175    let sent_chal = if auth.get_intent().should_exchange_chals() {
176        let chal = auth::gen_challenge();
177        Some(chal)
178    } else {
179        None
180    };
181
182    let hello = ClientHelloData::new(meta.agent.clone(), protocol, sent_chal);
183    let frame = FrameBody::ClientHello(hello);
184    (
185        ClientState::WaitServerHello {
186            auth,
187            protocol,
188            sent_chal,
189        },
190        vec![Action::SendFrame(frame)],
191    )
192}
193
194/// Performs client handshake state transition on receiving a new frame.
195pub fn exec_client_inp<A: AuthConfig>(
196    state: &ClientState<A>,
197    inp: &FrameBody,
198    _meta: &ClientMeta,
199) -> (ClientState<A>, Vec<Action>) {
200    use ClientState::*;
201    match (state, inp) {
202        (
203            WaitServerHello {
204                auth,
205                protocol,
206                sent_chal,
207            },
208            FrameBody::ServerHello(shello),
209        ) => {
210            let mut actions = Vec::new();
211            actions.push(Action::SetRemoteAgent(shello.agent().to_owned()));
212
213            let intent = auth.get_intent();
214            let should_sign = intent.should_sign(frame::Side::Client);
215            let exp_resp = intent.should_sign(frame::Side::Server);
216            let client_chal = sent_chal.as_ref();
217            let server_chal = shello.challenge();
218            let server_resp = shello.response();
219
220            let mut sent_resp = None;
221
222            // Big complicated logic tree to see what we should do about auth.
223            if intent.should_exchange_chals() {
224                let (Some(client_chal), Some(server_chal)) = (client_chal, server_chal) else {
225                    // Missing expected challenge.
226                    warn!("missing expected challenge");
227                    let goodbye = GoodbyeData::new(-104, "expected chal".to_string());
228                    return (ClientState::Fail, vec![Action::Abort(goodbye)]);
229                };
230
231                if exp_resp {
232                    let Some(server_resp) = server_resp else {
233                        // Missing expected response to challenge.
234                        warn!("missing expected response to challenge");
235                        let goodbye = GoodbyeData::new(-103, "expected chal resp".to_string());
236                        return (ClientState::Fail, vec![Action::Abort(goodbye)]);
237                    };
238
239                    // Ok, let's check it.
240                    match auth.verify_response(
241                        client_chal,
242                        server_chal,
243                        frame::Side::Server,
244                        server_resp,
245                    ) {
246                        Ok(Some(id)) => {
247                            trace!(%id, "verified identity");
248                            actions.push(Action::SetRemoteIdent(id));
249                        }
250                        Ok(None) => { /* do nothing */ }
251                        Err(e) => {
252                            // TODO do something with the error?
253                            warn!(err = %e, "server sent bad challenge response");
254                            let goodbye = GoodbyeData::new(-102, "auth error".to_string());
255                            return (ClientState::Fail, vec![Action::Abort(goodbye)]);
256                        }
257                    }
258                }
259
260                // And we should still sign ourselves.
261                if should_sign {
262                    match auth.sign_challenge(client_chal, server_chal, frame::Side::Client) {
263                        Ok(Some(resp)) => {
264                            sent_resp = Some(resp);
265                        }
266                        Ok(None) => {
267                            warn!("intent said we should sign but generated no response");
268                        }
269                        Err(e) => {
270                            error!(err = %e, "could not generate response");
271                            let goodbye = GoodbyeData::new(-101, "internal error".to_string());
272                            return (ClientState::Fail, vec![Action::Abort(goodbye)]);
273                        }
274                    }
275                }
276            }
277
278            let cfinish = ClientFinishData::new(sent_resp);
279            let frame = FrameBody::ClientFinish(cfinish);
280            let rd = ReadyData::new(*protocol);
281            actions.push(Action::SendFrame(frame));
282            actions.push(Action::Ready(rd));
283            (Ready { auth: auth.clone() }, actions)
284        }
285
286        (WaitServerHello { .. }, FrameBody::Goodbye(gd)) => {
287            // We sent a protocol the server didn't support.
288            (ClientState::Fail, vec![Action::Exit(gd.clone())])
289        }
290
291        _ => {
292            let goodbye = GoodbyeData::new(-101, "idk what you're doing".to_string());
293            (state.clone(), vec![Action::Abort(goodbye)])
294        }
295    }
296}
297
298/// Performs server handshake state transition on receiving a new frame.
299pub fn exec_server_inp<A: AuthConfig>(
300    state: &ServerState<A>,
301    inp: &FrameBody,
302    meta: &ServerMeta,
303) -> (ServerState<A>, Vec<Action>) {
304    use ServerState::*;
305    match (state, inp) {
306        (WaitClientHello { .. }, FrameBody::ClientQuery(_query)) => {
307            let dump = ServerDumpData::new(meta.agent.clone(), meta.protocols.clone());
308            let frame = FrameBody::ServerDump(dump);
309            (state.clone(), vec![Action::SendFrame(frame)])
310        }
311
312        (WaitClientHello { auth }, FrameBody::ClientHello(chello)) => {
313            let mut actions = Vec::new();
314            actions.push(Action::SetRemoteAgent(chello.agent().to_owned()));
315
316            let protocol = chello.protocol();
317
318            // Check to see they picked a good protocol.
319            if !meta.protocols.contains(&protocol) {
320                debug!(%protocol, "connection wanted unsupported protocol");
321                let goodbye = GoodbyeData::new(-100, "unsupported protocol".to_string());
322                return (ServerState::Fail, vec![Action::Abort(goodbye)]);
323            }
324
325            // Check to see if we should send across a challenge.
326            let intent = auth.get_intent();
327            let mut sending_chal = None;
328            let mut sending_resp = None;
329
330            if intent.should_exchange_chals() {
331                let Some(client_chal) = chello.challenge() else {
332                    // Expected the client to send a challenge.
333                    let goodbye = GoodbyeData::new(-104, "expected chal".to_string());
334                    return (ServerState::Fail, vec![Action::Abort(goodbye)]);
335                };
336
337                let server_chal = auth::gen_challenge();
338                sending_chal = Some(server_chal);
339
340                if intent.should_sign(frame::Side::Server) {
341                    match auth.sign_challenge(client_chal, &server_chal, frame::Side::Server) {
342                        Ok(Some(resp)) => {
343                            sending_resp = Some(resp);
344                        }
345                        Ok(None) => {
346                            warn!("intent said we should sign but generated no response");
347                        }
348                        Err(e) => {
349                            error!(err = %e, "could not generate response");
350                            let goodbye = GoodbyeData::new(-101, "internal error".to_string());
351                            return (ServerState::Fail, vec![Action::Abort(goodbye)]);
352                        }
353                    }
354                }
355            }
356
357            let shello = ServerHelloData::new(meta.agent.clone(), sending_resp, sending_chal);
358            let frame = FrameBody::ServerHello(shello);
359            actions.push(Action::SendFrame(frame));
360
361            (
362                ServerState::WaitClientFinish {
363                    auth: auth.clone(),
364                    protocol,
365                    client_chal: chello.challenge().cloned(),
366                    server_chal: sending_chal,
367                },
368                actions,
369            )
370        }
371
372        (
373            WaitClientFinish {
374                auth,
375                protocol,
376                client_chal,
377                server_chal,
378            },
379            FrameBody::ClientFinish(cfinish),
380        ) => {
381            let mut actions = Vec::new();
382
383            // If we expect the client to send a response, go and extract it.
384            let intent = auth.get_intent();
385            if intent.should_exchange_chals() && intent.should_sign(frame::Side::Client) {
386                let (Some(client_chal), Some(server_chal)) = (client_chal, server_chal) else {
387                    error!("in an inconsistent state!  didn't have local expected challenges!");
388                    let goodbye = GoodbyeData::new(-101, "internal error".to_string());
389                    return (ServerState::Fail, vec![Action::Abort(goodbye)]);
390                };
391
392                let Some(client_resp) = cfinish.response() else {
393                    // Missing expected response to challenge.
394                    warn!("missing expected response to challenge");
395                    let goodbye = GoodbyeData::new(-103, "expected chal resp".to_string());
396                    return (ServerState::Fail, vec![Action::Abort(goodbye)]);
397                };
398
399                match auth.verify_response(
400                    client_chal,
401                    server_chal,
402                    frame::Side::Client,
403                    client_resp,
404                ) {
405                    Ok(Some(id)) => {
406                        trace!(%id, "verified identity");
407                        actions.push(Action::SetRemoteIdent(id))
408                    }
409                    Ok(None) => {
410                        // what should we do here?  pass?
411                    }
412                    Err(e) => {
413                        // Invalid credential.
414                        warn!(err = %e, "new client sent bad challenge response");
415                        let goodbye = GoodbyeData::new(-102, "auth error".to_string());
416                        return (ServerState::Fail, vec![Action::Abort(goodbye)]);
417                    }
418                }
419            }
420
421            // TODO check identification
422            let rd = ReadyData::new(*protocol);
423            actions.push(Action::Ready(rd));
424            (ServerState::Ready { auth: auth.clone() }, actions)
425        }
426
427        _ => {
428            let goodbye = GoodbyeData::new(-101, "idk what you're doing".to_string());
429            (ServerState::Fail, vec![Action::Abort(goodbye)])
430        }
431    }
432}