spacedls 0.4.0

no_std CCSDS 355.0-B-2 (SDLS) Space Data Link Security implementation
Documentation
use crate::core::{
    AuthEncParams, AuthEncProvider, AuthEncSpec, GenericEncryptError, Mac, VerifyMacResult,
};
use cipher::{BlockCipherEncrypt, BlockSizeUser, KeyInit, KeyIvInit, KeySizeUser, StreamCipher};
use core::convert::Infallible;
use core::marker::PhantomData;
use ctr::Ctr32BE;
use ctutils::CtEq;
use ghash::{GHash, universal_hash::UniversalHash};
use hybrid_array::Array;
use hybrid_array::typenum::{U12, U16};

type Block = Array<u8, U16>;

/// Type-level specification for AES-GCM: 12-byte IV, 16-byte tag.
#[derive(Debug, Clone, Copy, Default)]
pub struct AesGcmSpec<Aes>(PhantomData<Aes>);

impl<Aes> AuthEncSpec for AesGcmSpec<Aes>
where Aes: BlockSizeUser<BlockSize = U16> + BlockCipherEncrypt + KeyInit
{
    type KeySize = Aes::KeySize;
    type IvSize = U12;
    type MacSize = U16;
}

/// Software AES-GCM authenticated encryption provider (NIST SP 800-38D).
#[derive(Debug, Clone, Copy)]
pub struct AesGcm<Aes>(PhantomData<Aes>);

impl<Aes> Default for AesGcm<Aes> {
    fn default() -> Self { Self(PhantomData) }
}

impl<Aes> AesGcm<Aes>
where Aes: BlockSizeUser<BlockSize = U16> + BlockCipherEncrypt + KeyInit
{
    fn derive_h(cipher: &Aes) -> Block {
        let mut h = Block::default();
        cipher.encrypt_block(&mut h);
        h
    }

    fn build_j0(iv: &Array<u8, U12>) -> Block {
        let mut j0 = Block::default();
        j0[..12].copy_from_slice(iv);
        j0[15] = 1;
        j0
    }

    fn feed_slices(ghash: &mut GHash, slices: &[&[u8]]) -> usize {
        let mut buf = Block::default();
        let mut buf_pos = 0;
        let mut total = 0;

        for slice in slices {
            let mut remaining = *slice;
            total += remaining.len();

            if buf_pos > 0 {
                let fill = (16 - buf_pos).min(remaining.len());
                buf[buf_pos..buf_pos + fill].copy_from_slice(&remaining[..fill]);
                buf_pos += fill;
                remaining = &remaining[fill..];
                if buf_pos == 16 {
                    ghash.update(&[buf]);
                    buf = Block::default();
                    buf_pos = 0;
                }
            }

            let full_blocks = remaining.len() / 16;
            for block_bytes in remaining[..full_blocks * 16].chunks_exact(16) {
                let mut block = Block::default();
                block.copy_from_slice(block_bytes);
                ghash.update(&[block]);
            }

            let tail = remaining.len() % 16;
            if tail > 0 {
                buf[..tail].copy_from_slice(&remaining[full_blocks * 16..]);
                buf_pos = tail;
            }
        }

        if buf_pos > 0 {
            ghash.update(&[buf]);
        }

        total
    }

    fn finalize_tag(mut ghash: GHash, tag_mask: &Block, aad_len: usize, ct_len: usize) -> Block {
        let mut len_block = Block::default();
        len_block[..8].copy_from_slice(&((aad_len as u64 * 8).to_be_bytes()));
        len_block[8..].copy_from_slice(&((ct_len as u64 * 8).to_be_bytes()));
        ghash.update(&[len_block]);

        let mut tag = ghash.finalize();
        for (a, b) in tag.iter_mut().zip(tag_mask.iter()) {
            *a ^= *b;
        }
        tag
    }
}

impl<Aes> AuthEncProvider for AesGcm<Aes>
where Aes: BlockSizeUser<BlockSize = U16> + BlockCipherEncrypt + KeyInit + KeySizeUser
{
    type Spec = AesGcmSpec<Aes>;
    type SealError = GenericEncryptError;
    type OpenError = Infallible;

    fn seal(
        &self,
        p: &AuthEncParams<Self::Spec>,
        aad: &[&[u8]],
        plain: &[u8],
        cipher: &mut [u8],
    ) -> Result<(usize, Mac<<Self::Spec as AuthEncSpec>::MacSize>), Self::SealError> {
        if cipher.len() < plain.len() {
            return Err(GenericEncryptError::OutputTooSmall);
        }

        let aes = Aes::new(&p.key);
        let h = Self::derive_h(&aes);
        let mut ghash = GHash::new(&h);
        let j0 = Self::build_j0(&p.iv);

        let mut ctr = <Ctr32BE<Aes> as KeyIvInit>::new(&p.key, &j0);
        let mut tag_mask = Block::default();
        ctr.apply_keystream(&mut tag_mask);

        let aad_len = Self::feed_slices(&mut ghash, aad);

        cipher[..plain.len()].copy_from_slice(plain);
        ctr.apply_keystream(&mut cipher[..plain.len()]);
        Self::feed_slices(&mut ghash, &[&cipher[..plain.len()]]);

        let tag = Self::finalize_tag(ghash, &tag_mask, aad_len, plain.len());
        Ok((plain.len(), tag.into()))
    }

    fn open(
        &self,
        p: &AuthEncParams<Self::Spec>,
        aad: &[&[u8]],
        cipher: &mut [u8],
        mac: &[u8],
    ) -> Result<(usize, VerifyMacResult), Self::OpenError> {
        let n = mac.len();
        if n == 0 || n > 16 {
            return Ok((0, VerifyMacResult::InvalidMac));
        }

        let aes = Aes::new(&p.key);
        let h = Self::derive_h(&aes);
        let mut ghash = GHash::new(&h);
        let j0 = Self::build_j0(&p.iv);

        let mut ctr = <Ctr32BE<Aes> as KeyIvInit>::new(&p.key, &j0);
        let mut tag_mask = Block::default();
        ctr.apply_keystream(&mut tag_mask);

        let aad_len = Self::feed_slices(&mut ghash, aad);
        Self::feed_slices(&mut ghash, &[cipher]);

        let expected_tag = Self::finalize_tag(ghash, &tag_mask, aad_len, cipher.len());

        if !bool::from(mac.ct_eq(&expected_tag[..n])) {
            return Ok((0, VerifyMacResult::InvalidMac));
        }

        ctr.apply_keystream(cipher);

        let verify = if n < 16 { VerifyMacResult::OkButTruncated } else { VerifyMacResult::Ok };
        Ok((cipher.len(), verify))
    }

    fn pad_len(_: usize) -> u16 { 0 }
}

#[cfg(test)]
mod tests {
    use aes::Aes128;
    use std::vec;

    use super::AesGcm;
    use crate::core::{AuthEncParams, AuthEncProvider, GenericEncryptError, VerifyMacResult};
    use assert_matches::assert_matches;

    type Provider = AesGcm<Aes128>;

    #[test]
    fn round_trip() {
        let provider = Provider::default();
        let params = AuthEncParams { key: [0x77_u8; 16].into(), iv: [0x33_u8; 12].into() };
        let aad: &[&[u8]] = &[b"security-header"];
        let plain = b"protected-space-pdu";

        let mut cipher = vec![0_u8; plain.len()];

        let (cipher_len, mac) = provider.seal(&params, aad, plain, &mut cipher).unwrap();
        assert_eq!(cipher_len, plain.len());
        assert_eq!(mac.len(), 16);

        let (recovered_len, mac_result) =
            provider.open(&params, aad, &mut cipher[..cipher_len], mac.as_slice()).unwrap();
        assert_matches!(mac_result, VerifyMacResult::Ok);
        assert_eq!(&cipher[..recovered_len], plain);
    }

    #[test]
    fn streaming_aad() {
        let provider = Provider::default();
        let params = AuthEncParams { key: [0x77_u8; 16].into(), iv: [0x33_u8; 12].into() };
        let plain = b"protected-space-pdu";

        let mut cipher_single = vec![0_u8; plain.len()];
        let (_, mac_single) = provider
            .seal(&params, &[b"header-part-a", b"header-part-b"], plain, &mut cipher_single)
            .unwrap();

        let mut cipher_concat = vec![0_u8; plain.len()];
        let (_, mac_concat) = provider
            .seal(&params, &[b"header-part-aheader-part-b"], plain, &mut cipher_concat)
            .unwrap();

        assert_eq!(cipher_single, cipher_concat);
        assert_eq!(*mac_single, *mac_concat);
    }

    #[test]
    fn reject_wrong_aad() {
        let provider = Provider::default();
        let params = AuthEncParams { key: [0x12_u8; 16].into(), iv: [0x34_u8; 12].into() };
        let aad: &[&[u8]] = &[b"aad-ok"];
        let wrong_aad: &[&[u8]] = &[b"aad-wrong"];
        let plain = b"payload";

        let mut cipher = vec![0_u8; plain.len()];
        let (cipher_len, tag) = provider.seal(&params, aad, plain, &mut cipher).unwrap();

        let result =
            provider.open(&params, wrong_aad, &mut cipher[..cipher_len], tag.as_slice()).unwrap();

        assert!(matches!(result.1, VerifyMacResult::InvalidMac));
    }

    #[test]
    fn reject_output_too_small() {
        let provider = Provider::default();
        let params = AuthEncParams { key: [0x44_u8; 16].into(), iv: [0x55_u8; 12].into() };
        let aad: &[&[u8]] = &[b"security-header"];
        let plain = b"protected-space-pdu";
        let mut cipher = vec![0_u8; plain.len() - 1];

        let result = provider.seal(&params, aad, plain, &mut cipher);
        assert_matches!(result, Err(GenericEncryptError::OutputTooSmall));
    }

    #[test]
    fn truncated_mac_accepted() {
        let provider = Provider::default();
        let params = AuthEncParams { key: [0x44_u8; 16].into(), iv: [0x55_u8; 12].into() };
        let aad: &[&[u8]] = &[b"security-header"];
        let plain = b"protected-space-pdu";

        let mut cipher = vec![0_u8; plain.len()];
        let (cipher_len, tag) = provider.seal(&params, aad, plain, &mut cipher).unwrap();

        let mut decrypt_buf = cipher[..cipher_len].to_vec();
        let (recovered, result) =
            provider.open(&params, aad, &mut decrypt_buf, &tag.as_slice()[..12]).unwrap();
        assert_matches!(result, VerifyMacResult::OkButTruncated);
        assert_eq!(&decrypt_buf[..recovered], plain);
    }

    #[test]
    fn truncated_mac_rejected_if_tampered() {
        let provider = Provider::default();
        let params = AuthEncParams { key: [0x44_u8; 16].into(), iv: [0x55_u8; 12].into() };
        let aad: &[&[u8]] = &[b"security-header"];
        let plain = b"protected-space-pdu";

        let mut cipher = vec![0_u8; plain.len()];
        let (cipher_len, tag) = provider.seal(&params, aad, plain, &mut cipher).unwrap();

        let mut bad_tag = tag.as_slice()[..12].to_vec();
        bad_tag[0] ^= 0x01;

        let mut decrypt_buf = cipher[..cipher_len].to_vec();
        let (_, result) = provider.open(&params, aad, &mut decrypt_buf, &bad_tag).unwrap();
        assert_matches!(result, VerifyMacResult::InvalidMac);
    }
}