Skip to main content

osdp/secure/
session.rs

1//! Type-state secure-channel state machine.
2//!
3//! # Spec: Annex D.4
4//!
5//! States are phantom-typed so that calling out-of-order is a *compile* error
6//! rather than a runtime fault. The intended sequence is:
7//!
8//! ```text
9//! Disconnected --challenge(RND.A)----> Challenged
10//! Challenged   --recv_ccrypt(...)----> Cryptogrammed
11//! Cryptogrammed--recv_rmac_i(...)----> Secure
12//! ```
13//!
14//! Calls from any state may transition back to [`Disconnected`] on error.
15
16use crate::error::SecureSessionError;
17use crate::reply::CCrypt;
18use crate::secure::crypto::{SessionKeys, client_cryptogram, initial_rmac, server_cryptogram};
19use crate::secure::mac::cbc_mac;
20use core::marker::PhantomData;
21use subtle::ConstantTimeEq;
22
23/// Phantom: session not started.
24#[derive(Debug, Clone, Copy)]
25pub struct Disconnected;
26/// Phantom: ACU has sent CHLNG and is awaiting CCRYPT.
27#[derive(Debug, Clone, Copy)]
28pub struct Challenged;
29/// Phantom: ACU has verified CCRYPT and is awaiting RMAC_I.
30#[derive(Debug, Clone, Copy)]
31pub struct Cryptogrammed;
32/// Phantom: SC fully established.
33#[derive(Debug, Clone, Copy)]
34pub struct Secure;
35
36/// Secure-channel session state.
37#[derive(Debug, Clone)]
38pub struct Session<S> {
39    scbk: [u8; 16],
40    rnd_a: [u8; 8],
41    rnd_b: [u8; 8],
42    cuid: [u8; 8],
43    keys: SessionKeys,
44    /// Last MAC received from the *other* device — used as ICV for the next
45    /// outgoing MAC, per Annex D.5.
46    last_their_mac: [u8; 16],
47    _state: PhantomData<S>,
48}
49
50impl Session<Disconnected> {
51    /// Begin a new session, holding the SCBK we will use.
52    pub fn new(scbk: [u8; 16]) -> Self {
53        Self {
54            scbk,
55            rnd_a: [0; 8],
56            rnd_b: [0; 8],
57            cuid: [0; 8],
58            keys: SessionKeys {
59                s_enc: [0; 16],
60                s_mac1: [0; 16],
61                s_mac2: [0; 16],
62            },
63            last_their_mac: [0; 16],
64            _state: PhantomData,
65        }
66    }
67
68    /// Move to [`Challenged`] by capturing the `RND.A` we are about to send.
69    pub fn challenge(mut self, rnd_a: [u8; 8]) -> Session<Challenged> {
70        self.rnd_a = rnd_a;
71        Session {
72            scbk: self.scbk,
73            rnd_a: self.rnd_a,
74            rnd_b: self.rnd_b,
75            cuid: self.cuid,
76            keys: self.keys,
77            last_their_mac: self.last_their_mac,
78            _state: PhantomData,
79        }
80    }
81}
82
83impl Session<Challenged> {
84    /// Verify the PD's [`CCrypt`] and move to [`Cryptogrammed`].
85    pub fn receive_ccrypt(
86        mut self,
87        ccrypt: &CCrypt,
88    ) -> Result<Session<Cryptogrammed>, (Session<Disconnected>, SecureSessionError)> {
89        self.cuid = ccrypt.cuid;
90        self.rnd_b = ccrypt.rnd_b;
91        self.keys = SessionKeys::derive(&self.scbk, &self.rnd_a);
92        let expected = client_cryptogram(&self.keys.s_enc, &self.rnd_a, &self.rnd_b);
93        if expected.ct_eq(&ccrypt.client_cryptogram).unwrap_u8() == 0 {
94            return Err((self.into_disconnected(), SecureSessionError::BadCryptogram));
95        }
96        Ok(Session {
97            scbk: self.scbk,
98            rnd_a: self.rnd_a,
99            rnd_b: self.rnd_b,
100            cuid: self.cuid,
101            keys: self.keys,
102            last_their_mac: self.last_their_mac,
103            _state: PhantomData,
104        })
105    }
106
107    fn into_disconnected(self) -> Session<Disconnected> {
108        Session {
109            scbk: self.scbk,
110            rnd_a: [0; 8],
111            rnd_b: [0; 8],
112            cuid: [0; 8],
113            keys: SessionKeys {
114                s_enc: [0; 16],
115                s_mac1: [0; 16],
116                s_mac2: [0; 16],
117            },
118            last_their_mac: [0; 16],
119            _state: PhantomData,
120        }
121    }
122}
123
124impl Session<Cryptogrammed> {
125    /// Compute the server cryptogram to ship in `osdp_SCRYPT`.
126    pub fn server_cryptogram(&self) -> [u8; 16] {
127        server_cryptogram(&self.keys.s_enc, &self.rnd_a, &self.rnd_b)
128    }
129
130    /// Initial R-MAC, computed from `S-MAC1`/`S-MAC2` over the server cryptogram.
131    pub fn initial_rmac(&self) -> [u8; 16] {
132        initial_rmac(
133            &self.keys.s_mac1,
134            &self.keys.s_mac2,
135            &self.server_cryptogram(),
136        )
137    }
138
139    /// PD echoed back our initial R-MAC; advance to fully [`Secure`].
140    pub fn confirm_rmac_i(
141        self,
142        their_rmac_i: &[u8; 16],
143    ) -> Result<Session<Secure>, (Session<Disconnected>, SecureSessionError)> {
144        let mine = self.initial_rmac();
145        if mine.ct_eq(their_rmac_i).unwrap_u8() == 0 {
146            let mut me = self;
147            me.last_their_mac = [0; 16];
148            return Err((
149                Session {
150                    scbk: me.scbk,
151                    rnd_a: [0; 8],
152                    rnd_b: [0; 8],
153                    cuid: [0; 8],
154                    keys: SessionKeys {
155                        s_enc: [0; 16],
156                        s_mac1: [0; 16],
157                        s_mac2: [0; 16],
158                    },
159                    last_their_mac: [0; 16],
160                    _state: PhantomData,
161                },
162                SecureSessionError::BadCryptogram,
163            ));
164        }
165        Ok(Session {
166            scbk: self.scbk,
167            rnd_a: self.rnd_a,
168            rnd_b: self.rnd_b,
169            cuid: self.cuid,
170            keys: self.keys,
171            last_their_mac: mine,
172            _state: PhantomData,
173        })
174    }
175}
176
177impl Session<Secure> {
178    /// Compute the MAC trailer for an outgoing packet body.
179    ///
180    /// `bytes` is the SOM..end-of-DATA region (as per [`super::mac::cbc_mac`]).
181    pub fn mac(&mut self, bytes: &[u8]) -> [u8; 16] {
182        let mac = cbc_mac(
183            bytes,
184            &self.last_their_mac,
185            &self.keys.s_mac1,
186            &self.keys.s_mac2,
187        );
188        self.last_their_mac = mac;
189        mac
190    }
191
192    /// Verify a received MAC against our locally-computed value.
193    pub fn verify(&mut self, bytes: &[u8], wire_mac: &[u8; 4]) -> Result<(), SecureSessionError> {
194        let computed = cbc_mac(
195            bytes,
196            &self.last_their_mac,
197            &self.keys.s_mac1,
198            &self.keys.s_mac2,
199        );
200        if computed[..4].ct_eq(wire_mac).unwrap_u8() == 0 {
201            return Err(SecureSessionError::BadCryptogram);
202        }
203        self.last_their_mac = computed;
204        Ok(())
205    }
206
207    /// Borrow the derived session keys (for AES-CBC DATA encryption).
208    pub fn keys(&self) -> &SessionKeys {
209        &self.keys
210    }
211
212    /// Last MAC the other end produced (used as encryption ICV per Annex D).
213    pub fn last_other_mac(&self) -> &[u8; 16] {
214        &self.last_their_mac
215    }
216
217    /// Encrypt `data` for an `SCS_17`/`SCS_18` payload using the current ICV
218    /// (`!last_other_mac`). Returns the ciphertext (always padded to a 16-byte
219    /// multiple).
220    ///
221    /// # Spec: Annex D.5
222    pub fn seal_data(&self, data: &[u8]) -> alloc::vec::Vec<u8> {
223        let iv = crate::secure::cipher::complement_icv(&self.last_their_mac);
224        crate::secure::cipher::encrypt_data(&self.keys.s_enc, &iv, data)
225    }
226
227    /// Decrypt a payload received in an `SCS_17`/`SCS_18` packet, validating
228    /// the 0x80 padding.
229    ///
230    /// # Spec: Annex D.5
231    pub fn open_data(&self, ct: &[u8]) -> Result<alloc::vec::Vec<u8>, SecureSessionError> {
232        let iv = crate::secure::cipher::complement_icv(&self.last_their_mac);
233        crate::secure::cipher::decrypt_data(&self.keys.s_enc, &iv, ct)
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use crate::reply::CCrypt;
241
242    #[test]
243    fn handshake_happy_path() {
244        let scbk = crate::secure::SCBK_D;
245        let session = Session::<Disconnected>::new(scbk);
246
247        let rnd_a = [1u8; 8];
248        let session = session.challenge(rnd_a);
249
250        let rnd_b = [2u8; 8];
251        let cuid = [3u8; 8];
252        let keys = crate::secure::crypto::SessionKeys::derive(&scbk, &rnd_a);
253        let cc = crate::secure::crypto::client_cryptogram(&keys.s_enc, &rnd_a, &rnd_b);
254        let ccrypt = CCrypt {
255            cuid,
256            rnd_b,
257            client_cryptogram: cc,
258        };
259        let session = session.receive_ccrypt(&ccrypt).unwrap();
260
261        let rmac = session.initial_rmac();
262        let session = session.confirm_rmac_i(&rmac).unwrap();
263        let _ = session.last_other_mac();
264    }
265
266    #[test]
267    fn bad_ccrypt_rejected() {
268        let scbk = crate::secure::SCBK_D;
269        let session = Session::<Disconnected>::new(scbk).challenge([0u8; 8]);
270        let ccrypt = CCrypt {
271            cuid: [0; 8],
272            rnd_b: [0; 8],
273            client_cryptogram: [0; 16],
274        };
275        let err = session.receive_ccrypt(&ccrypt).unwrap_err();
276        assert_eq!(err.1, SecureSessionError::BadCryptogram);
277    }
278}