Skip to main content

leodos_protocols/datalink/security/
mod.rs

1//! Frame-level encryption and authentication for the datalink layer.
2//!
3//! Provides the [`SecurityProcessor`] trait, [`SdlsProcessor`] for
4//! CCSDS 355.0-B-2 frame-level crypto, and [`NoSecurity`] as a
5//! passthrough.
6
7use core::convert::Infallible;
8
9use sdls::{CryptoProvider, SecurityAssociation};
10
11/// Space Data Link Security (CCSDS 355.0-B-2).
12pub mod sdls;
13
14/// Applies or removes security (encryption/authentication) on frames.
15pub trait SecurityProcessor {
16    /// Error type for security operations.
17    type Error;
18    /// Applies security (encrypt/authenticate) to a frame in-place.
19    fn apply(
20        &mut self,
21        frame: &mut [u8],
22    ) -> Result<usize, Self::Error>;
23    /// Removes security (decrypt/verify) from a frame in-place.
24    fn process(
25        &mut self,
26        frame: &mut [u8],
27    ) -> Result<usize, Self::Error>;
28}
29
30/// SDLS-based security processor (CCSDS 355.0-B-2).
31///
32/// Wraps a [`SecurityAssociation`] and [`CryptoProvider`] to
33/// implement in-place frame encryption and authentication.
34///
35/// The `header_end` offset tells the processor where the transfer
36/// frame header ends (5 for TC, 6 for TM). The security header
37/// is inserted at that offset, followed by the encrypted data
38/// field and optional MAC trailer.
39pub struct SdlsProcessor<C> {
40    sa: SecurityAssociation,
41    crypto: C,
42    header_end: usize,
43}
44
45impl<C: CryptoProvider> SdlsProcessor<C> {
46    /// Creates a new SDLS processor.
47    ///
48    /// `header_end` is the byte offset where the transfer frame
49    /// header ends (5 for TC, 6 for TM).
50    pub fn new(
51        sa: SecurityAssociation,
52        crypto: C,
53        header_end: usize,
54    ) -> Self {
55        Self {
56            sa,
57            crypto,
58            header_end,
59        }
60    }
61
62    /// Returns a reference to the security association.
63    pub fn sa(&self) -> &SecurityAssociation {
64        &self.sa
65    }
66
67    /// Returns a mutable reference to the security association.
68    pub fn sa_mut(&mut self) -> &mut SecurityAssociation {
69        &mut self.sa
70    }
71}
72
73impl<C: CryptoProvider> SecurityProcessor for SdlsProcessor<C> {
74    type Error = sdls::Error;
75
76    fn apply(
77        &mut self,
78        frame: &mut [u8],
79    ) -> Result<usize, Self::Error> {
80        let data_start = self.header_end + self.sa.header_size();
81        let data_end = frame.len() - self.sa.trailer_size();
82        sdls::apply_security(
83            &mut self.sa,
84            &self.crypto,
85            frame,
86            self.header_end,
87            data_start,
88            data_end,
89        )?;
90        Ok(frame.len())
91    }
92
93    fn process(
94        &mut self,
95        frame: &mut [u8],
96    ) -> Result<usize, Self::Error> {
97        let data_start = self.header_end + self.sa.header_size();
98        let data_end = frame.len() - self.sa.trailer_size();
99        sdls::process_security(
100            &mut self.sa,
101            &self.crypto,
102            frame,
103            self.header_end,
104            data_start,
105            data_end,
106        )?;
107        Ok(frame.len())
108    }
109}
110
111/// No-op security processor (passthrough).
112///
113/// Leaves frames untouched. Use when SDLS is not configured.
114pub struct NoSecurity;
115
116impl SecurityProcessor for NoSecurity {
117    type Error = Infallible;
118
119    fn apply(
120        &mut self,
121        frame: &mut [u8],
122    ) -> Result<usize, Self::Error> {
123        Ok(frame.len())
124    }
125
126    fn process(
127        &mut self,
128        frame: &mut [u8],
129    ) -> Result<usize, Self::Error> {
130        Ok(frame.len())
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use sdls::{
138        AesGcmCrypto, ClearModeCrypto, SecurityAssociation,
139        ServiceType,
140    };
141
142    fn auth_sa() -> SecurityAssociation {
143        SecurityAssociation {
144            spi: 1,
145            service_type: ServiceType::Authentication,
146            iv_len: 4,
147            sn_len: 4,
148            pl_len: 0,
149            mac_len: 16,
150            sequence_number: 0,
151            sequence_window: 100,
152            auth_mask: heapless::Vec::new(),
153        }
154    }
155
156    fn aead_sa() -> SecurityAssociation {
157        SecurityAssociation {
158            spi: 5,
159            service_type: ServiceType::AuthenticatedEncryption,
160            iv_len: 12,
161            sn_len: 0,
162            pl_len: 0,
163            mac_len: 16,
164            sequence_number: 1,
165            sequence_window: 100,
166            auth_mask: heapless::Vec::new(),
167        }
168    }
169
170    #[test]
171    fn no_security_passthrough() {
172        let mut processor = NoSecurity;
173        let mut frame = [0xAA; 32];
174        let len = processor.apply(&mut frame).unwrap();
175        assert_eq!(len, 32);
176        assert!(frame.iter().all(|&b| b == 0xAA));
177    }
178
179    #[test]
180    fn sdls_clear_mode_roundtrip() {
181        let sa = auth_sa();
182        let header_end = 5;
183        let data_start = header_end + sa.header_size();
184        let data_end = data_start + 10;
185        let total = data_end + sa.trailer_size();
186
187        let mut send =
188            SdlsProcessor::new(sa.clone(), ClearModeCrypto, header_end);
189        let mut recv =
190            SdlsProcessor::new(sa, ClearModeCrypto, header_end);
191
192        let mut frame = [0u8; 64];
193        frame[0..5].copy_from_slice(&[0xC0, 0x00, 0x2A, 0x00, 0x28]);
194        frame[data_start..data_end]
195            .copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
196
197        let len = send.apply(&mut frame[..total]).unwrap();
198        assert_eq!(len, total);
199
200        let len = recv.process(&mut frame[..total]).unwrap();
201        assert_eq!(len, total);
202        assert_eq!(
203            &frame[data_start..data_end],
204            &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
205        );
206    }
207
208    #[test]
209    fn sdls_aes_gcm_roundtrip() {
210        let sa = aead_sa();
211        let header_end = 5;
212        let data_start = header_end + sa.header_size();
213        let data_end = data_start + 8;
214        let total = data_end + sa.trailer_size();
215
216        let key = [0xABu8; 16];
217
218        let mut send = SdlsProcessor::new(
219            sa.clone(),
220            AesGcmCrypto::new_128(&key),
221            header_end,
222        );
223        let mut recv = SdlsProcessor::new(
224            sa,
225            AesGcmCrypto::new_128(&key),
226            header_end,
227        );
228
229        let mut frame = [0u8; 64];
230        frame[0..5].copy_from_slice(&[0xC0, 0x00, 0x2A, 0x00, 0x28]);
231        let original = [1u8, 2, 3, 4, 5, 6, 7, 8];
232        frame[data_start..data_end].copy_from_slice(&original);
233
234        send.apply(&mut frame[..total]).unwrap();
235        assert_ne!(&frame[data_start..data_end], &original);
236
237        recv.process(&mut frame[..total]).unwrap();
238        assert_eq!(&frame[data_start..data_end], &original);
239    }
240}