Skip to main content

leodos_protocols/datalink/security/sdls/
ep.rs

1//! SDLS Extended Procedures PDU format (CCSDS 355.1-B-1).
2//!
3//! Extended Procedures are administrative commands carried in
4//! TC frames on a dedicated MAP/VCID. Each PDU uses a TLV header
5//! (8-bit tag + 16-bit length) that identifies the service group
6//! and procedure.
7
8use super::Error;
9
10/// Service groups (2 bits) defined by SDLS-EP (Section 5.3.2.2.3).
11#[derive(Debug, Copy, Clone, Eq, PartialEq)]
12#[repr(u8)]
13pub enum ServiceGroup {
14    /// Key Management (OTAR, activation, verification, etc.).
15    KeyManagement = 0b00,
16    /// SA Management: Initiator → Recipient direction.
17    SaManagementIr = 0b01,
18    /// SA Management: Recipient → Initiator direction.
19    SaManagementRi = 0b10,
20    /// Security Monitoring & Control.
21    MonitoringControl = 0b11,
22}
23
24impl TryFrom<u8> for ServiceGroup {
25    type Error = Error;
26
27    fn try_from(value: u8) -> Result<Self, Self::Error> {
28        match value {
29            0b00 => Ok(Self::KeyManagement),
30            0b01 => Ok(Self::SaManagementIr),
31            0b10 => Ok(Self::SaManagementRi),
32            0b11 => Ok(Self::MonitoringControl),
33            _ => Err(Error::InvalidProcedure),
34        }
35    }
36}
37
38/// Key Management procedures (Table 5-1, Service Group 00).
39#[derive(Debug, Copy, Clone, Eq, PartialEq)]
40#[repr(u8)]
41pub enum KeyProcedure {
42    /// Deliver encrypted session keys via master key.
43    Otar = 0b0001,
44    /// Transition key from PREACTIVE to ACTIVE.
45    Activate = 0b0010,
46    /// Deactivate a key.
47    Deactivate = 0b0011,
48    /// Challenge-response key verification.
49    Verify = 0b0100,
50    /// Cryptographically destroy a key.
51    Destroy = 0b0110,
52    /// Query available keys.
53    Inventory = 0b0111,
54}
55
56impl TryFrom<u8> for KeyProcedure {
57    type Error = Error;
58
59    fn try_from(value: u8) -> Result<Self, Self::Error> {
60        match value {
61            0b0001 => Ok(Self::Otar),
62            0b0010 => Ok(Self::Activate),
63            0b0011 => Ok(Self::Deactivate),
64            0b0100 => Ok(Self::Verify),
65            0b0110 => Ok(Self::Destroy),
66            0b0111 => Ok(Self::Inventory),
67            _ => Err(Error::InvalidProcedure),
68        }
69    }
70}
71
72/// SA Management procedures (Table 5-1, Service Group 01 or 10).
73#[derive(Debug, Copy, Clone, Eq, PartialEq)]
74#[repr(u8)]
75pub enum SaProcedure {
76    /// Create a new Security Association.
77    Create = 0b0001,
78    /// Delete an existing SA.
79    Delete = 0b0100,
80    /// Assign keys to an SA (rekey).
81    Rekey = 0b0110,
82    /// Activate an SA for operational use.
83    Start = 0b1011,
84    /// Deactivate an SA.
85    Stop = 0b1110,
86    /// Mark an SA for retirement.
87    Expire = 0b1001,
88    /// Set the Anti-Replay Sequence Number.
89    SetArsn = 0b1010,
90    /// Set the Anti-Replay Sequence Number Window.
91    SetArsnWindow = 0b0101,
92    /// Read the current ARSN.
93    ReadArsn = 0b0000,
94    /// Read SA status.
95    Status = 0b1111,
96}
97
98impl TryFrom<u8> for SaProcedure {
99    type Error = Error;
100
101    fn try_from(value: u8) -> Result<Self, Self::Error> {
102        match value {
103            0b0001 => Ok(Self::Create),
104            0b0100 => Ok(Self::Delete),
105            0b0110 => Ok(Self::Rekey),
106            0b1011 => Ok(Self::Start),
107            0b1110 => Ok(Self::Stop),
108            0b1001 => Ok(Self::Expire),
109            0b1010 => Ok(Self::SetArsn),
110            0b0101 => Ok(Self::SetArsnWindow),
111            0b0000 => Ok(Self::ReadArsn),
112            0b1111 => Ok(Self::Status),
113            _ => Err(Error::InvalidProcedure),
114        }
115    }
116}
117
118/// Monitoring & Control procedures (Table 5-1, Service Group 11).
119#[derive(Debug, Copy, Clone, Eq, PartialEq)]
120#[repr(u8)]
121pub enum McProcedure {
122    /// Ping the security function.
123    Ping = 0b0001,
124    /// Query log status.
125    LogStatus = 0b0010,
126    /// Dump the security log.
127    DumpLog = 0b0011,
128    /// Erase the security log.
129    EraseLog = 0b0100,
130    /// Trigger a self-test.
131    SelfTest = 0b0101,
132    /// Reset the alarm flag in the FSR.
133    ResetAlarmFlag = 0b0111,
134}
135
136impl TryFrom<u8> for McProcedure {
137    type Error = Error;
138
139    fn try_from(value: u8) -> Result<Self, Self::Error> {
140        match value {
141            0b0001 => Ok(Self::Ping),
142            0b0010 => Ok(Self::LogStatus),
143            0b0011 => Ok(Self::DumpLog),
144            0b0100 => Ok(Self::EraseLog),
145            0b0101 => Ok(Self::SelfTest),
146            0b0111 => Ok(Self::ResetAlarmFlag),
147            _ => Err(Error::InvalidProcedure),
148        }
149    }
150}
151
152/// PDU type: command (Initiator → Recipient) or reply.
153#[derive(Debug, Copy, Clone, Eq, PartialEq)]
154pub enum PduType {
155    /// Command from Initiator to Recipient.
156    Command,
157    /// Reply from Recipient to Initiator.
158    Reply,
159}
160
161use crate::utils::get_bits_u8;
162use crate::utils::set_bits_u8;
163
164#[rustfmt::skip]
165mod bitmask {
166    /// Procedure Type flag (1 bit): 0=command, 1=reply.
167    pub const TYPE_MASK: u8 =          0b_1000_0000;
168    /// User Flag (1 bit): 0=CCSDS, 1=user-defined.
169    pub const USER_FLAG_MASK: u8 =     0b_0100_0000;
170    /// Service Group (2 bits).
171    pub const SERVICE_GROUP_MASK: u8 = 0b_0011_0000;
172    /// Procedure Identification (4 bits).
173    pub const PROCEDURE_ID_MASK: u8 =  0b_0000_1111;
174}
175
176/// PDU header size: 1-byte tag + 2-byte length = 3 bytes.
177pub const EP_HEADER_SIZE: usize = 3;
178
179/// Parsed SDLS-EP PDU header.
180///
181/// Wire format (3 bytes, per Figure 5-2):
182///   byte 0 (Tag): type(1) | user_flag(1) | service_group(2) | procedure_id(4)
183///   byte 1-2: data field length in bits (16-bit BE, octet-aligned)
184#[derive(Debug, Copy, Clone)]
185pub struct EpHeader {
186    /// Command or reply.
187    pub pdu_type: PduType,
188    /// User-defined extension flag.
189    pub user_flag: bool,
190    /// Service group (2 bits).
191    pub service_group: u8,
192    /// Procedure identifier within the service group (4 bits).
193    pub procedure_id: u8,
194}
195
196impl EpHeader {
197    /// Parse an EP header from a byte buffer.
198    ///
199    /// Returns the header and the data field length in bytes.
200    pub fn parse(bytes: &[u8]) -> Result<(Self, u16), Error> {
201        if bytes.len() < EP_HEADER_SIZE {
202            return Err(Error::FrameTooShort);
203        }
204
205        let tag = bytes[0];
206        let pdu_type = match get_bits_u8(tag, bitmask::TYPE_MASK) {
207            0 => PduType::Command,
208            _ => PduType::Reply,
209        };
210        let user_flag = get_bits_u8(tag, bitmask::USER_FLAG_MASK) != 0;
211        let service_group = get_bits_u8(tag, bitmask::SERVICE_GROUP_MASK);
212        let procedure_id = get_bits_u8(tag, bitmask::PROCEDURE_ID_MASK);
213
214        let length_bits = u16::from_be_bytes([bytes[1], bytes[2]]);
215        let length_bytes = length_bits / 8;
216
217        Ok((
218            Self {
219                pdu_type,
220                user_flag,
221                service_group,
222                procedure_id,
223            },
224            length_bytes,
225        ))
226    }
227
228    /// Encode the EP header into a buffer.
229    ///
230    /// `data_len_bytes` is the data field length in bytes; it is
231    /// encoded as bits in the wire format.
232    pub fn encode(&self, data_len_bytes: u16, out: &mut [u8]) -> Result<usize, Error> {
233        if out.len() < EP_HEADER_SIZE {
234            return Err(Error::BufferTooSmall {
235                required: EP_HEADER_SIZE,
236                available: out.len(),
237            });
238        }
239
240        let mut tag = 0u8;
241        let type_val = match self.pdu_type {
242            PduType::Command => 0u8,
243            PduType::Reply => 1u8,
244        };
245        set_bits_u8(&mut tag, bitmask::TYPE_MASK, type_val);
246        set_bits_u8(&mut tag, bitmask::USER_FLAG_MASK, self.user_flag as u8);
247        set_bits_u8(&mut tag, bitmask::SERVICE_GROUP_MASK, self.service_group);
248        set_bits_u8(&mut tag, bitmask::PROCEDURE_ID_MASK, self.procedure_id);
249        out[0] = tag;
250
251        let length_bits = data_len_bytes * 8;
252        let len_bytes = length_bits.to_be_bytes();
253        out[1] = len_bytes[0];
254        out[2] = len_bytes[1];
255
256        Ok(EP_HEADER_SIZE)
257    }
258}
259
260/// Frame Security Report (FSR) — 32-bit OCF carried in TM/AOS/USLP
261/// frames to report security status (Section 4.2.2).
262#[derive(Debug, Copy, Clone)]
263pub struct FrameSecurityReport {
264    /// Last SPI used by the receiving security function.
265    pub last_spi: u16,
266    /// 8 LSBs of the ARSN from the last received frame.
267    pub arsn_lsb: u8,
268    /// Alarm flag: at least one frame was rejected since last reset.
269    pub alarm: bool,
270    /// Bad Sequence Number flag for the last received frame.
271    pub bad_sn: bool,
272    /// Bad MAC flag for the last received frame.
273    pub bad_mac: bool,
274    /// Bad SA flag for the last received frame.
275    pub bad_sa: bool,
276}
277
278impl FrameSecurityReport {
279    /// FSR version number (3 bits, value `100` = version 1).
280    pub const VERSION: u8 = 0b100;
281
282    /// Encode the FSR into a 4-byte buffer.
283    pub fn encode(&self, out: &mut [u8; 4]) {
284        let mut byte0: u8 = 0;
285        set_bits_u8(&mut byte0, 0b_1000_0000, 1);
286        set_bits_u8(&mut byte0, 0b_0111_0000, Self::VERSION);
287        set_bits_u8(&mut byte0, 0b_0000_1000, self.alarm as u8);
288        set_bits_u8(&mut byte0, 0b_0000_0100, self.bad_sn as u8);
289        set_bits_u8(&mut byte0, 0b_0000_0010, self.bad_mac as u8);
290        set_bits_u8(&mut byte0, 0b_0000_0001, self.bad_sa as u8);
291        out[0] = byte0;
292        let spi_bytes = self.last_spi.to_be_bytes();
293        out[1] = spi_bytes[0];
294        out[2] = spi_bytes[1];
295        out[3] = self.arsn_lsb;
296    }
297
298    /// Parse an FSR from a 4-byte buffer.
299    pub fn parse(bytes: &[u8; 4]) -> Self {
300        Self {
301            alarm: get_bits_u8(bytes[0], 0b_0000_1000) != 0,
302            bad_sn: get_bits_u8(bytes[0], 0b_0000_0100) != 0,
303            bad_mac: get_bits_u8(bytes[0], 0b_0000_0010) != 0,
304            bad_sa: get_bits_u8(bytes[0], 0b_0000_0001) != 0,
305            last_spi: u16::from_be_bytes([bytes[1], bytes[2]]),
306            arsn_lsb: bytes[3],
307        }
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn header_roundtrip_key_otar() {
317        let hdr = EpHeader {
318            pdu_type: PduType::Command,
319            user_flag: false,
320            service_group: ServiceGroup::KeyManagement as u8,
321            procedure_id: KeyProcedure::Otar as u8,
322        };
323        let mut buf = [0u8; 8];
324        hdr.encode(10, &mut buf).unwrap();
325
326        let (parsed, length) = EpHeader::parse(&buf).unwrap();
327        assert_eq!(parsed.pdu_type, PduType::Command);
328        assert!(!parsed.user_flag);
329        assert_eq!(parsed.service_group, ServiceGroup::KeyManagement as u8);
330        assert_eq!(parsed.procedure_id, KeyProcedure::Otar as u8);
331        assert_eq!(length, 10);
332    }
333
334    #[test]
335    fn header_roundtrip_sa_start() {
336        let hdr = EpHeader {
337            pdu_type: PduType::Reply,
338            user_flag: true,
339            service_group: ServiceGroup::SaManagementIr as u8,
340            procedure_id: SaProcedure::Start as u8,
341        };
342        let mut buf = [0u8; 8];
343        hdr.encode(32, &mut buf).unwrap();
344
345        let (parsed, length) = EpHeader::parse(&buf).unwrap();
346        assert_eq!(parsed.pdu_type, PduType::Reply);
347        assert!(parsed.user_flag);
348        assert_eq!(parsed.service_group, ServiceGroup::SaManagementIr as u8);
349        assert_eq!(parsed.procedure_id, SaProcedure::Start as u8);
350        assert_eq!(length, 32);
351    }
352
353    #[test]
354    fn header_size_is_3_bytes() {
355        assert_eq!(EP_HEADER_SIZE, 3);
356    }
357
358    #[test]
359    fn service_group_try_from() {
360        assert_eq!(
361            ServiceGroup::try_from(0b00).unwrap(),
362            ServiceGroup::KeyManagement
363        );
364        assert_eq!(
365            ServiceGroup::try_from(0b01).unwrap(),
366            ServiceGroup::SaManagementIr
367        );
368        assert_eq!(
369            ServiceGroup::try_from(0b10).unwrap(),
370            ServiceGroup::SaManagementRi
371        );
372        assert_eq!(
373            ServiceGroup::try_from(0b11).unwrap(),
374            ServiceGroup::MonitoringControl
375        );
376    }
377
378    #[test]
379    fn mc_procedures() {
380        assert_eq!(McProcedure::try_from(0b0001).unwrap(), McProcedure::Ping);
381        assert_eq!(
382            McProcedure::try_from(0b0111).unwrap(),
383            McProcedure::ResetAlarmFlag
384        );
385        assert!(McProcedure::try_from(0b1111).is_err());
386    }
387
388    #[test]
389    fn buffer_too_short() {
390        assert!(EpHeader::parse(&[0u8; 2]).is_err());
391    }
392
393    #[test]
394    fn fsr_roundtrip() {
395        let fsr = FrameSecurityReport {
396            last_spi: 42,
397            arsn_lsb: 0xAB,
398            alarm: true,
399            bad_sn: false,
400            bad_mac: true,
401            bad_sa: false,
402        };
403        let mut buf = [0u8; 4];
404        fsr.encode(&mut buf);
405
406        let parsed = FrameSecurityReport::parse(&buf);
407        assert_eq!(parsed.last_spi, 42);
408        assert_eq!(parsed.arsn_lsb, 0xAB);
409        assert!(parsed.alarm);
410        assert!(!parsed.bad_sn);
411        assert!(parsed.bad_mac);
412        assert!(!parsed.bad_sa);
413    }
414
415    #[test]
416    fn fsr_control_word_type_is_1() {
417        let fsr = FrameSecurityReport {
418            last_spi: 0,
419            arsn_lsb: 0,
420            alarm: false,
421            bad_sn: false,
422            bad_mac: false,
423            bad_sa: false,
424        };
425        let mut buf = [0u8; 4];
426        fsr.encode(&mut buf);
427        assert_eq!(buf[0] & 0x80, 0x80);
428    }
429}