spacedls 0.4.0

no_std CCSDS 355.0-B-2 (SDLS) Space Data Link Security implementation
Documentation
use crate::core::{
    AsAuth, AsAuthEnc, AsEnc, AuthEncProvider, AuthEncSpec, AuthProvider, EmptyKey, EncProvider,
    EncSpec, SDLSFrameFormat, SecurityAssociation, ServiceKind, ServiceProviderGeneric,
};
use core::cell::Cell;
use core::fmt::{Debug, Formatter};
use hybrid_array::ArraySize;
use typenum::{NonZero, U0, Zero};

use super::error::SaManagementError;

/// Lifecycle state of a [`ManagedSa`] (CCSDS 355.1-B-1 Section 2.3.3).
///
/// ```text
/// Provisioned --start()--> Operational --stop()--> Retired
/// ```
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SaState {
    Provisioned,
    Operational,
    Retired,
}

/// Security Association with lifecycle management (CCSDS 355.1-B-1 2.3.3).
///
/// Gates cryptographic access behind the `Operational` state. Use
/// [`get_operational`](Self::get_operational) to obtain the inner SA.
pub struct ManagedSa<'a, S: ServiceProviderGeneric, F: SDLSFrameFormat, N: ArraySize> {
    state: Cell<SaState>,
    sa: SecurityAssociation<'a, S, F, N>,
}

impl<S: ServiceProviderGeneric, F: SDLSFrameFormat, N: ArraySize> Debug for ManagedSa<'_, S, F, N> {
    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("ManagedSa")
            .field("state", &self.state.get())
            .field("spi", &self.sa.spi())
            .finish()
    }
}

impl<'a, S: ServiceProviderGeneric, F: SDLSFrameFormat, N: ArraySize> ManagedSa<'a, S, F, N> {
    #[inline]
    #[must_use]
    pub fn state(&self) -> SaState { self.state.get() }
    #[inline]
    #[must_use]
    pub fn spi(&self) -> u16 { self.sa.spi() }
    #[inline]
    #[must_use]
    pub const fn service_type(&self) -> ServiceKind { self.sa.service_type() }
    #[inline]
    #[must_use]
    pub fn is_provisioned(&self) -> bool { matches!(self.state.get(), SaState::Provisioned) }
    #[inline]
    #[must_use]
    pub fn is_operational(&self) -> bool { matches!(self.state.get(), SaState::Operational) }
    #[inline]
    #[must_use]
    pub fn is_retired(&self) -> bool { matches!(self.state.get(), SaState::Retired) }

    #[inline]
    pub fn start(&self) -> Result<(), SaManagementError> {
        if self.is_provisioned() {
            self.state.set(SaState::Operational);
            Ok(())
        } else {
            Err(SaManagementError::InvalidTransition {
                from: self.state.get(),
                to: SaState::Operational,
            })
        }
    }

    #[inline]
    pub fn stop(&self) -> Result<(), SaManagementError> {
        if self.is_operational() {
            self.state.set(SaState::Retired);
            Ok(())
        } else {
            Err(SaManagementError::InvalidTransition {
                from: self.state.get(),
                to: SaState::Retired,
            })
        }
    }

    #[inline]
    pub fn get_operational(&self) -> Result<&SecurityAssociation<'a, S, F, N>, SaManagementError> {
        if self.is_operational() { Ok(&self.sa) } else { Err(SaManagementError::NotOperational) }
    }

    #[inline]
    pub fn get_operational_mut(
        &mut self,
    ) -> Result<&mut SecurityAssociation<'a, S, F, N>, SaManagementError> {
        if self.is_operational() {
            Ok(&mut self.sa)
        } else {
            Err(SaManagementError::NotOperational)
        }
    }

    #[inline]
    #[must_use]
    pub fn inner(&self) -> &SecurityAssociation<'a, S, F, N> { &self.sa }

    #[inline]
    pub fn inner_mut(&mut self) -> &mut SecurityAssociation<'a, S, F, N> { &mut self.sa }
}

impl<'a, S, F> ManagedSa<'a, AsEnc<S>, F, U0>
where
    S: EncProvider,
    F: SDLSFrameFormat<IVLen = <S::Spec as EncSpec>::IvSize>,
    F::MacLen: Zero,
    F::SNLen: Zero,
    F::IVLen: NonZero,
{
    pub fn new_enc(service: S, spi: u16) -> Self {
        Self {
            state: Cell::new(SaState::Provisioned),
            sa: SecurityAssociation::new_enc(service, spi, &EmptyKey),
        }
    }
}

impl<'a, S, F> ManagedSa<'a, AsAuth<S>, F, F::SNLen>
where
    S: AuthProvider,
    F: SDLSFrameFormat,
    F::IVLen: Zero,
    F::PLLen: Zero,
    F::SNLen: NonZero,
    F::MacLen: NonZero,
{
    pub fn new_auth(service: S, spi: u16, sn_window: u16, abm: Option<&'a [u8]>) -> Self {
        Self {
            state: Cell::new(SaState::Provisioned),
            sa: SecurityAssociation::new_auth(service, spi, &EmptyKey, sn_window, abm),
        }
    }
}

impl<'a, S, F> ManagedSa<'a, AsAuthEnc<S>, F, F::SNLen>
where
    S: AuthEncProvider,
    F: SDLSFrameFormat<IVLen = <S::Spec as AuthEncSpec>::IvSize>,
    F::PLLen: Zero,
    F::IVLen: NonZero,
    F::MacLen: NonZero,
    F::SNLen: NonZero,
{
    pub fn new_authenc(service: S, spi: u16, sn_window: u16, abm: Option<&'a [u8]>) -> Self {
        Self {
            state: Cell::new(SaState::Provisioned),
            sa: SecurityAssociation::new_authenc(service, spi, &EmptyKey, sn_window, abm),
        }
    }
}

impl<'a, S, F> ManagedSa<'a, AsAuthEnc<S>, F, F::IVLen>
where
    S: AuthEncProvider,
    F: SDLSFrameFormat<IVLen = <S::Spec as AuthEncSpec>::IvSize>,
    F::PLLen: Zero,
    F::IVLen: NonZero,
    F::MacLen: NonZero,
    F::SNLen: Zero,
{
    pub fn new_authenc_ctr(service: S, spi: u16, sn_window: u16, abm: Option<&'a [u8]>) -> Self {
        Self {
            state: Cell::new(SaState::Provisioned),
            sa: SecurityAssociation::new_authenc_ctr(service, spi, &EmptyKey, sn_window, abm),
        }
    }
}

#[cfg(all(test, feature = "softcrypto"))]
mod tests {
    use super::*;
    use crate::core::{BIT128, Key};

    #[test]
    fn empty_key_returns_none() {
        let k = EmptyKey;
        assert_eq!(Key::<BIT128>::size(&k), 0);
        assert_eq!(Key::<BIT128>::get(&k), None);
    }

    #[test]
    fn new_sa_is_provisioned() {
        let msa = make_test_enc_sa();
        assert_eq!(msa.state(), SaState::Provisioned);
        assert!(msa.is_provisioned());
        assert!(!msa.is_operational());
        assert!(!msa.is_retired());
    }

    #[test]
    fn start_from_provisioned() {
        let msa = make_test_enc_sa();
        assert!(msa.start().is_ok());
        assert_eq!(msa.state(), SaState::Operational);
    }

    #[test]
    fn start_from_operational_fails() {
        let msa = make_test_enc_sa();
        msa.start().unwrap();
        assert_eq!(
            msa.start(),
            Err(SaManagementError::InvalidTransition {
                from: SaState::Operational,
                to: SaState::Operational,
            })
        );
    }

    #[test]
    fn start_from_retired_fails() {
        let msa = make_test_enc_sa();
        msa.start().unwrap();
        msa.stop().unwrap();
        assert_eq!(
            msa.start(),
            Err(SaManagementError::InvalidTransition {
                from: SaState::Retired,
                to: SaState::Operational
            })
        );
    }

    #[test]
    fn stop_from_operational() {
        let msa = make_test_enc_sa();
        msa.start().unwrap();
        assert!(msa.stop().is_ok());
        assert_eq!(msa.state(), SaState::Retired);
    }

    #[test]
    fn stop_from_provisioned_fails() {
        let msa = make_test_enc_sa();
        assert_eq!(
            msa.stop(),
            Err(SaManagementError::InvalidTransition {
                from: SaState::Provisioned,
                to: SaState::Retired
            })
        );
    }

    #[test]
    fn stop_from_retired_fails() {
        let msa = make_test_enc_sa();
        msa.start().unwrap();
        msa.stop().unwrap();
        assert_eq!(
            msa.stop(),
            Err(SaManagementError::InvalidTransition {
                from: SaState::Retired,
                to: SaState::Retired
            })
        );
    }

    #[test]
    fn get_operational_succeeds() {
        let msa = make_test_enc_sa();
        msa.start().unwrap();
        assert!(msa.get_operational().is_ok());
        assert_eq!(msa.get_operational().unwrap().spi(), 0x1001);
    }

    #[test]
    fn get_operational_fails_provisioned() {
        let msa = make_test_enc_sa();
        assert_eq!(msa.get_operational().err(), Some(SaManagementError::NotOperational));
    }

    #[test]
    fn get_operational_fails_retired() {
        let msa = make_test_enc_sa();
        msa.start().unwrap();
        msa.stop().unwrap();
        assert_eq!(msa.get_operational().err(), Some(SaManagementError::NotOperational));
    }

    #[test]
    fn get_operational_mut_succeeds() {
        let mut msa = make_test_enc_sa();
        msa.start().unwrap();
        assert!(msa.get_operational_mut().is_ok());
    }

    #[test]
    fn inner_access_always_works() {
        let msa = make_test_enc_sa();
        assert_eq!(msa.inner().spi(), 0x1001);

        msa.start().unwrap();
        assert_eq!(msa.inner().spi(), 0x1001);

        msa.stop().unwrap();
        assert_eq!(msa.inner().spi(), 0x1001);
    }

    #[test]
    fn spi_delegation() {
        let msa = make_test_enc_sa();
        assert_eq!(msa.spi(), 0x1001);
    }

    use crate::softcrypto::AesCbc;
    use crate::testfmt::EncFmt;
    use typenum::U16;

    fn make_test_enc_sa()
    -> ManagedSa<'static, AsEnc<AesCbc<aes::Aes128, cipher::block_padding::Pkcs7>>, EncFmt, U0>
    {
        ManagedSa::new_enc(AesCbc::default(), 0x1001)
    }

    #[test]
    fn set_key_then_operate() {
        use crate::core::ConstKey;
        use hybrid_array::Array;
        use std::boxed::Box;

        let key: &'static ConstKey<BIT128> = Box::leak(Box::new(ConstKey::new([
            0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf,
            0x4f, 0x3c,
        ])));

        let mut msa = make_test_enc_sa();
        assert!(msa.get_operational().is_err());

        msa.inner_mut().set_key(key);
        msa.start().unwrap();
        assert!(msa.is_operational());

        let iv: Array<u8, U16> = [0u8; 16].into();
        let plain = b"managed-sa-test!";
        let mut cipher = std::vec![0u8; 32];
        let (_, hdr) = msa.get_operational().unwrap().encrypt(iv, plain, &mut cipher).unwrap();

        let written = plain.len() + usize::from(hdr.pad_len);
        let mut buf = cipher[..written].to_vec();
        let recovered = msa.get_operational().unwrap().decrypt(hdr, &mut buf).unwrap();
        assert_eq!(&buf[..recovered], plain);

        msa.stop().unwrap();
        assert!(msa.get_operational().is_err());
    }
}