Skip to main content

leodos_protocols/datalink/security/sdls/
sa_mgmt.rs

1//! Security Association lifecycle management (CCSDS 355.1-B-1).
2//!
3//! SA state machine:
4//!   UNKEYED → (rekey) → KEYED → (start) → OPERATIONAL
5//!   OPERATIONAL → (stop) → KEYED
6//!   KEYED → (expire) → UNKEYED
7//!   Any → (delete) → removed
8
9use super::Error;
10use super::SecurityAssociation;
11use super::ServiceType;
12
13/// SA lifecycle state.
14#[derive(Debug, Copy, Clone, Eq, PartialEq)]
15pub enum SaState {
16    /// Created but no keys assigned.
17    Unkeyed,
18    /// Keys assigned, ready to start.
19    Keyed,
20    /// Active in frame security processing.
21    Operational,
22}
23
24/// A managed Security Association with lifecycle state.
25pub struct ManagedSa {
26    /// The underlying SA parameters.
27    pub sa: SecurityAssociation,
28    /// Current lifecycle state.
29    pub state: SaState,
30    /// Assigned encryption key ID (if keyed).
31    pub key_id: Option<u16>,
32}
33
34/// Maximum number of managed SAs.
35pub const MAX_SAS: usize = 64;
36
37impl ManagedSa {
38    /// Creates a new SA in UNKEYED state.
39    pub fn new(spi: u16, service_type: ServiceType) -> Result<Self, Error> {
40        if spi == 0 || spi == 0xFFFF {
41            return Err(Error::ReservedSpi(spi));
42        }
43        Ok(Self {
44            sa: SecurityAssociation {
45                spi,
46                service_type,
47                iv_len: 0,
48                sn_len: 0,
49                pl_len: 0,
50                mac_len: 0,
51                sequence_number: 0,
52                sequence_window: 0,
53                auth_mask: heapless::Vec::new(),
54            },
55            state: SaState::Unkeyed,
56            key_id: None,
57        })
58    }
59
60    /// Assign a key to this SA (UNKEYED → KEYED).
61    ///
62    /// Per Section 3.3.3.3.4: SA must be in the Unkeyed state.
63    pub fn rekey(&mut self, key_id: u16) -> Result<(), Error> {
64        (self.state == SaState::Unkeyed)
65            .then(|| {
66                self.key_id = Some(key_id);
67                self.state = SaState::Keyed;
68            })
69            .ok_or(Error::InvalidSaState)
70    }
71
72    /// Activate the SA for operations (KEYED → OPERATIONAL).
73    pub fn start(&mut self) -> Result<(), Error> {
74        (self.state == SaState::Keyed)
75            .then(|| self.state = SaState::Operational)
76            .ok_or(Error::InvalidSaState)
77    }
78
79    /// Deactivate the SA (OPERATIONAL → KEYED).
80    pub fn stop(&mut self) -> Result<(), Error> {
81        (self.state == SaState::Operational)
82            .then(|| self.state = SaState::Keyed)
83            .ok_or(Error::InvalidSaState)
84    }
85
86    /// Expire the SA, removing its key association (KEYED → UNKEYED).
87    ///
88    /// Per Section 3.3.3.4.2: SA must be in the Keyed state.
89    pub fn expire(&mut self) -> Result<(), Error> {
90        (self.state == SaState::Keyed)
91            .then(|| {
92                self.key_id = None;
93                self.state = SaState::Unkeyed;
94            })
95            .ok_or(Error::InvalidSaState)
96    }
97
98    /// Set the Anti-Replay Sequence Number.
99    pub fn set_arsn(&mut self, arsn: u64) {
100        self.sa.sequence_number = arsn;
101    }
102
103    /// Read the current ARSN.
104    pub fn read_arsn(&self) -> u64 {
105        self.sa.sequence_number
106    }
107}
108
109/// A table of managed Security Associations.
110pub struct SaTable {
111    entries: heapless::Vec<ManagedSa, MAX_SAS>,
112}
113
114impl SaTable {
115    /// Creates an empty SA table.
116    pub fn new() -> Self {
117        Self {
118            entries: heapless::Vec::new(),
119        }
120    }
121
122    /// Creates a new SA and adds it to the table.
123    pub fn create(&mut self, spi: u16, service_type: ServiceType) -> Result<(), Error> {
124        if self.find(spi).is_some() {
125            return Err(Error::DuplicateSpi(spi));
126        }
127        let sa = ManagedSa::new(spi, service_type)?;
128        self.entries.push(sa).map_err(|_| Error::SaTableFull)
129    }
130
131    /// Deletes an SA from the table.
132    ///
133    /// Per Section 3.3.3.6.2: SA must be in the Unkeyed state.
134    pub fn delete(&mut self, spi: u16) -> Result<(), Error> {
135        let pos = self
136            .entries
137            .iter()
138            .position(|e| e.sa.spi == spi)
139            .ok_or(Error::UnknownSpi(spi))?;
140        if self.entries[pos].state != SaState::Unkeyed {
141            return Err(Error::InvalidSaState);
142        }
143        self.entries.swap_remove(pos);
144        Ok(())
145    }
146
147    /// Finds an SA by SPI.
148    pub fn find(&self, spi: u16) -> Option<&ManagedSa> {
149        self.entries.iter().find(|e| e.sa.spi == spi)
150    }
151
152    /// Finds an SA by SPI (mutable).
153    pub fn find_mut(&mut self, spi: u16) -> Option<&mut ManagedSa> {
154        self.entries.iter_mut().find(|e| e.sa.spi == spi)
155    }
156
157    /// Rekeys an SA.
158    pub fn rekey(&mut self, spi: u16, key_id: u16) -> Result<(), Error> {
159        self.find_mut(spi)
160            .ok_or(Error::UnknownSpi(spi))?
161            .rekey(key_id)
162    }
163
164    /// Starts an SA.
165    pub fn start(&mut self, spi: u16) -> Result<(), Error> {
166        self.find_mut(spi)
167            .ok_or(Error::UnknownSpi(spi))?
168            .start()
169    }
170
171    /// Stops an SA.
172    pub fn stop(&mut self, spi: u16) -> Result<(), Error> {
173        self.find_mut(spi)
174            .ok_or(Error::UnknownSpi(spi))?
175            .stop()
176    }
177
178    /// Expires an SA.
179    pub fn expire(&mut self, spi: u16) -> Result<(), Error> {
180        self.find_mut(spi)
181            .ok_or(Error::UnknownSpi(spi))?
182            .expire()
183    }
184
185    /// Sets the ARSN for an SA.
186    pub fn set_arsn(&mut self, spi: u16, arsn: u64) -> Result<(), Error> {
187        self.find_mut(spi)
188            .ok_or(Error::UnknownSpi(spi))?
189            .set_arsn(arsn);
190        Ok(())
191    }
192
193    /// Reads the ARSN for an SA.
194    pub fn read_arsn(&self, spi: u16) -> Result<u64, Error> {
195        Ok(self.find(spi).ok_or(Error::UnknownSpi(spi))?.read_arsn())
196    }
197
198    /// Returns an iterator over all SAs with their states.
199    pub fn iter(&self) -> impl Iterator<Item = &ManagedSa> {
200        self.entries.iter()
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn sa_lifecycle() {
210        let mut sa = ManagedSa::new(1, ServiceType::AuthenticatedEncryption).unwrap();
211        assert_eq!(sa.state, SaState::Unkeyed);
212
213        assert!(sa.start().is_err());
214        assert!(sa.stop().is_err());
215        assert!(sa.expire().is_err());
216
217        sa.rekey(10).unwrap();
218        assert_eq!(sa.state, SaState::Keyed);
219        assert_eq!(sa.key_id, Some(10));
220
221        // Can't rekey from Keyed — must be Unkeyed
222        assert!(sa.rekey(20).is_err());
223
224        sa.start().unwrap();
225        assert_eq!(sa.state, SaState::Operational);
226        assert!(sa.start().is_err());
227
228        // Can't expire from Operational — must stop first
229        assert!(sa.expire().is_err());
230
231        sa.stop().unwrap();
232        assert_eq!(sa.state, SaState::Keyed);
233
234        sa.expire().unwrap();
235        assert_eq!(sa.state, SaState::Unkeyed);
236        assert_eq!(sa.key_id, None);
237    }
238
239    #[test]
240    fn sa_table_crud() {
241        let mut table = SaTable::new();
242        table
243            .create(1, ServiceType::Authentication)
244            .unwrap();
245        table
246            .create(2, ServiceType::Encryption)
247            .unwrap();
248
249        assert!(table
250            .create(1, ServiceType::Authentication)
251            .is_err());
252
253        table.rekey(1, 100).unwrap();
254        table.start(1).unwrap();
255        assert_eq!(
256            table.find(1).unwrap().state,
257            SaState::Operational
258        );
259
260        // Can't delete from Operational
261        assert!(table.delete(1).is_err());
262
263        table.stop(1).unwrap();
264        // Can't delete from Keyed either
265        assert!(table.delete(1).is_err());
266
267        table.expire(1).unwrap();
268        // Now in Unkeyed — can delete
269        table.delete(1).unwrap();
270        assert!(table.find(1).is_none());
271    }
272
273    #[test]
274    fn sa_table_arsn() {
275        let mut table = SaTable::new();
276        table
277            .create(5, ServiceType::AuthenticatedEncryption)
278            .unwrap();
279
280        table.set_arsn(5, 42).unwrap();
281        assert_eq!(table.read_arsn(5).unwrap(), 42);
282
283        assert!(table.set_arsn(99, 0).is_err());
284    }
285
286    #[test]
287    fn reserved_spi_rejected() {
288        assert!(ManagedSa::new(0, ServiceType::Authentication).is_err());
289        assert!(ManagedSa::new(0xFFFF, ServiceType::Authentication).is_err());
290    }
291}