rustls-ccm 0.1.0

CCM and CCM-8 cipher suites for rustls (TLS 1.2 and TLS 1.3)
Documentation
#![warn(missing_docs)]
//! AES-CCM cipher suites for [rustls](https://github.com/rustls/rustls).
//!
//! Neither [aws-lc-rs](https://github.com/aws/aws-lc-rs) nor
//! [ring](https://github.com/briansmith/ring) expose AES-CCM, so rustls's
//! built-in providers cannot offer these suites. This crate fills the gap
//! using the [RustCrypto](https://github.com/RustCrypto) `aes` + `ccm` crates,
//! plugged in via rustls's [`CryptoProvider`]
//! extension point.
//!
//! CCM cipher suites are required or recommended by several IoT and energy
//! protocols, including IEEE 2030.5 (Smart Energy), Matter, Thread, and
//! constrained-device TLS profiles (RFC 7925).
//!
//! # Cipher suites
//!
//! ## TLS 1.2 ([RFC 7251](https://www.rfc-editor.org/rfc/rfc7251))
//!
//! | Suite | Tag | Key |
//! |---|---|---|
//! | [`TLS_ECDHE_ECDSA_WITH_AES_128_CCM`] | 16 B | 128-bit |
//! | [`TLS_ECDHE_ECDSA_WITH_AES_256_CCM`] | 16 B | 256-bit |
//! | [`TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8`] | 8 B | 128-bit |
//! | [`TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8`] | 8 B | 256-bit |
//!
//! ## TLS 1.3 ([RFC 8446](https://www.rfc-editor.org/rfc/rfc8446))
//!
//! | Suite | Tag | Key |
//! |---|---|---|
//! | [`TLS13_AES_128_CCM_SHA256`] | 16 B | 128-bit |
//! | [`TLS13_AES_128_CCM_8_SHA256`] | 8 B | 128-bit |
//!
//! # Usage
//!
//! Use [`crypto_provider()`] for an aws-lc-rs provider with all CCM suites
//! prepended, or pick individual suites and build your own provider.
//!
//! ```
//! let provider = rustls_ccm::crypto_provider();
//! let config = rustls::ClientConfig::builder_with_provider(provider.into())
//!     .with_safe_default_protocol_versions()
//!     .unwrap();
//! ```

use std::sync::LazyLock;

use aes::{Aes128, Aes256};
use ccm::Ccm;
use ccm::aead::generic_array::typenum::{U8, U12, U16};
use ccm::aead::{AeadCore, AeadInPlace, KeyInit};
use rustls::crypto::CryptoProvider;
use rustls::{
    CipherSuite, CipherSuiteCommon, SupportedCipherSuite, Tls12CipherSuite, Tls13CipherSuite,
};

mod tls12;
mod tls13;

// ---------------------------------------------------------------------------
// Cipher variant abstraction
// ---------------------------------------------------------------------------

/// Trait abstracting over the four AES-CCM cipher configurations.
pub(crate) trait CcmVariant: Send + Sync + 'static {
    type Cipher: AeadInPlace + AeadCore<NonceSize = U12> + KeyInit + Send + Sync;
    const KEY_LEN: usize;
    const TAG_LEN: usize;
}

pub(crate) enum Aes128Ccm8V {}
impl CcmVariant for Aes128Ccm8V {
    type Cipher = Ccm<Aes128, U8, U12>;
    const KEY_LEN: usize = 16;
    const TAG_LEN: usize = 8;
}

pub(crate) enum Aes128Ccm16V {}
impl CcmVariant for Aes128Ccm16V {
    type Cipher = Ccm<Aes128, U16, U12>;
    const KEY_LEN: usize = 16;
    const TAG_LEN: usize = 16;
}

pub(crate) enum Aes256Ccm8V {}
impl CcmVariant for Aes256Ccm8V {
    type Cipher = Ccm<Aes256, U8, U12>;
    const KEY_LEN: usize = 32;
    const TAG_LEN: usize = 8;
}

pub(crate) enum Aes256Ccm16V {}
impl CcmVariant for Aes256Ccm16V {
    type Cipher = Ccm<Aes256, U16, U12>;
    const KEY_LEN: usize = 32;
    const TAG_LEN: usize = 16;
}

// ---------------------------------------------------------------------------
// TLS 1.2 suite definitions (RFC 7251) — all use SHA-256
// ---------------------------------------------------------------------------

fn tls12_base() -> &'static Tls12CipherSuite {
    let base = rustls::crypto::aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256;
    let SupportedCipherSuite::Tls12(s) = base else {
        unreachable!()
    };
    s
}

static SUITE_TLS12_128_CCM: LazyLock<Tls12CipherSuite> = LazyLock::new(|| {
    let base = tls12_base();
    Tls12CipherSuite {
        common: CipherSuiteCommon {
            suite: CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_CCM,
            hash_provider: base.common.hash_provider,
            confidentiality_limit: 1 << 24,
        },
        prf_provider: base.prf_provider,
        kx: base.kx,
        sign: base.sign,
        aead_alg: &tls12::Tls12CcmAead::<Aes128Ccm16V>::NEW,
    }
});

static SUITE_TLS12_256_CCM: LazyLock<Tls12CipherSuite> = LazyLock::new(|| {
    let base = tls12_base();
    Tls12CipherSuite {
        common: CipherSuiteCommon {
            suite: CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_CCM,
            hash_provider: base.common.hash_provider,
            confidentiality_limit: 1 << 24,
        },
        prf_provider: base.prf_provider,
        kx: base.kx,
        sign: base.sign,
        aead_alg: &tls12::Tls12CcmAead::<Aes256Ccm16V>::NEW,
    }
});

static SUITE_TLS12_128_CCM8: LazyLock<Tls12CipherSuite> = LazyLock::new(|| {
    let base = tls12_base();
    Tls12CipherSuite {
        common: CipherSuiteCommon {
            suite: CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8,
            hash_provider: base.common.hash_provider,
            confidentiality_limit: 1 << 24,
        },
        prf_provider: base.prf_provider,
        kx: base.kx,
        sign: base.sign,
        aead_alg: &tls12::Tls12CcmAead::<Aes128Ccm8V>::NEW,
    }
});

static SUITE_TLS12_256_CCM8: LazyLock<Tls12CipherSuite> = LazyLock::new(|| {
    let base = tls12_base();
    Tls12CipherSuite {
        common: CipherSuiteCommon {
            suite: CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8,
            hash_provider: base.common.hash_provider,
            confidentiality_limit: 1 << 24,
        },
        prf_provider: base.prf_provider,
        kx: base.kx,
        sign: base.sign,
        aead_alg: &tls12::Tls12CcmAead::<Aes256Ccm8V>::NEW,
    }
});

// ---------------------------------------------------------------------------
// TLS 1.3 suite definitions (RFC 8446) — both use SHA-256
// ---------------------------------------------------------------------------

fn tls13_base() -> &'static Tls13CipherSuite {
    let base = rustls::crypto::aws_lc_rs::cipher_suite::TLS13_AES_128_GCM_SHA256;
    let SupportedCipherSuite::Tls13(s) = base else {
        unreachable!()
    };
    s
}

static SUITE_TLS13_128_CCM: LazyLock<Tls13CipherSuite> = LazyLock::new(|| {
    let base = tls13_base();
    Tls13CipherSuite {
        common: CipherSuiteCommon {
            suite: CipherSuite::TLS13_AES_128_CCM_SHA256,
            hash_provider: base.common.hash_provider,
            confidentiality_limit: 1 << 24,
        },
        hkdf_provider: base.hkdf_provider,
        aead_alg: &tls13::Tls13CcmAead::<Aes128Ccm16V>::NEW,
        quic: None,
    }
});

static SUITE_TLS13_128_CCM8: LazyLock<Tls13CipherSuite> = LazyLock::new(|| {
    let base = tls13_base();
    Tls13CipherSuite {
        common: CipherSuiteCommon {
            suite: CipherSuite::TLS13_AES_128_CCM_8_SHA256,
            hash_provider: base.common.hash_provider,
            confidentiality_limit: 1 << 24,
        },
        hkdf_provider: base.hkdf_provider,
        aead_alg: &tls13::Tls13CcmAead::<Aes128Ccm8V>::NEW,
        quic: None,
    }
});

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/// `TLS_ECDHE_ECDSA_WITH_AES_128_CCM` (0xC0AC, RFC 7251).
pub static TLS_ECDHE_ECDSA_WITH_AES_128_CCM: LazyLock<SupportedCipherSuite> =
    LazyLock::new(|| SupportedCipherSuite::Tls12(&SUITE_TLS12_128_CCM));

/// `TLS_ECDHE_ECDSA_WITH_AES_256_CCM` (0xC0AD, RFC 7251).
pub static TLS_ECDHE_ECDSA_WITH_AES_256_CCM: LazyLock<SupportedCipherSuite> =
    LazyLock::new(|| SupportedCipherSuite::Tls12(&SUITE_TLS12_256_CCM));

/// `TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8` (0xC0AE, RFC 7251).
pub static TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8: LazyLock<SupportedCipherSuite> =
    LazyLock::new(|| SupportedCipherSuite::Tls12(&SUITE_TLS12_128_CCM8));

/// `TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8` (0xC0AF, RFC 7251).
pub static TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8: LazyLock<SupportedCipherSuite> =
    LazyLock::new(|| SupportedCipherSuite::Tls12(&SUITE_TLS12_256_CCM8));

/// `TLS_AES_128_CCM_SHA256` (0x1304, RFC 8446). Recommended=Y.
///
/// Standard TLS 1.3 cipher suite for constrained environments (Matter, Thread, CoAP).
pub static TLS13_AES_128_CCM_SHA256: LazyLock<SupportedCipherSuite> =
    LazyLock::new(|| SupportedCipherSuite::Tls13(&SUITE_TLS13_128_CCM));

/// `TLS_AES_128_CCM_8_SHA256` (0x1305, RFC 8446).
///
/// TLS 1.3 cipher suite with truncated 8-byte tag for bandwidth-constrained devices.
pub static TLS13_AES_128_CCM_8_SHA256: LazyLock<SupportedCipherSuite> =
    LazyLock::new(|| SupportedCipherSuite::Tls13(&SUITE_TLS13_128_CCM8));

/// All CCM cipher suites provided by this crate (TLS 1.2 + TLS 1.3).
pub fn all_suites() -> [SupportedCipherSuite; 6] {
    [
        *TLS_ECDHE_ECDSA_WITH_AES_128_CCM,
        *TLS_ECDHE_ECDSA_WITH_AES_256_CCM,
        *TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8,
        *TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8,
        *TLS13_AES_128_CCM_SHA256,
        *TLS13_AES_128_CCM_8_SHA256,
    ]
}

/// Returns an aws-lc-rs [`CryptoProvider`] with all CCM suites prepended.
pub fn crypto_provider() -> CryptoProvider {
    let mut provider = rustls::crypto::aws_lc_rs::default_provider();
    for suite in all_suites().into_iter().rev() {
        provider.cipher_suites.insert(0, suite);
    }
    provider
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn all_suites_accessible() {
        let suites = all_suites();
        assert_eq!(
            suites[0].suite(),
            CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_CCM
        );
        assert_eq!(
            suites[1].suite(),
            CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_CCM
        );
        assert_eq!(
            suites[2].suite(),
            CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8
        );
        assert_eq!(
            suites[3].suite(),
            CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8
        );
        assert_eq!(suites[4].suite(), CipherSuite::TLS13_AES_128_CCM_SHA256);
        assert_eq!(suites[5].suite(), CipherSuite::TLS13_AES_128_CCM_8_SHA256);
    }

    #[test]
    fn crypto_provider_includes_all_ccm() {
        let provider = crypto_provider();
        for suite in all_suites() {
            assert!(
                provider
                    .cipher_suites
                    .iter()
                    .any(|s| s.suite() == suite.suite()),
                "missing {:?}",
                suite.suite()
            );
        }
    }

    #[test]
    fn ccm_round_trip() {
        let key = [0x42u8; 16];
        let nonce = ccm::aead::generic_array::GenericArray::from([1u8; 12]);
        let aad = b"additional data";
        let plaintext = b"hello CCM";

        // Full tag (16-byte)
        let cipher = <Ccm<Aes128, U16, U12> as KeyInit>::new_from_slice(&key).unwrap();
        let mut buf = plaintext.to_vec();
        let tag = cipher
            .encrypt_in_place_detached(&nonce, aad.as_slice(), &mut buf)
            .unwrap();
        assert_eq!(tag.len(), 16);
        cipher
            .decrypt_in_place_detached(&nonce, aad.as_slice(), &mut buf, &tag)
            .unwrap();
        assert_eq!(&buf, plaintext);

        // 8-byte tag
        let cipher8 = <Ccm<Aes128, U8, U12> as KeyInit>::new_from_slice(&key).unwrap();
        let mut buf = plaintext.to_vec();
        let tag = cipher8
            .encrypt_in_place_detached(&nonce, aad.as_slice(), &mut buf)
            .unwrap();
        assert_eq!(tag.len(), 8);
        cipher8
            .decrypt_in_place_detached(&nonce, aad.as_slice(), &mut buf, &tag)
            .unwrap();
        assert_eq!(&buf, plaintext);
    }

    #[test]
    fn ccm_tampered_fails() {
        let key = [0x42u8; 16];
        let nonce = ccm::aead::generic_array::GenericArray::from([2u8; 12]);
        let cipher = <Ccm<Aes128, U16, U12> as KeyInit>::new_from_slice(&key).unwrap();
        let mut buf = b"secret".to_vec();
        let tag = cipher
            .encrypt_in_place_detached(&nonce, b"", &mut buf)
            .unwrap();
        buf[0] ^= 0xff;
        assert!(
            cipher
                .decrypt_in_place_detached(&nonce, b"", &mut buf, &tag)
                .is_err()
        );
    }

    #[test]
    fn ccm256_round_trip() {
        let key = [0x42u8; 32];
        let nonce = ccm::aead::generic_array::GenericArray::from([3u8; 12]);
        let cipher = <Ccm<Aes256, U16, U12> as KeyInit>::new_from_slice(&key).unwrap();
        let mut buf = b"aes-256-ccm".to_vec();
        let tag = cipher
            .encrypt_in_place_detached(&nonce, b"", &mut buf)
            .unwrap();
        cipher
            .decrypt_in_place_detached(&nonce, b"", &mut buf, &tag)
            .unwrap();
        assert_eq!(&buf, b"aes-256-ccm");
    }

    #[test]
    fn tls12_key_block_shapes() {
        use rustls::crypto::cipher::Tls12AeadAlgorithm;

        let aead128 = tls12::Tls12CcmAead::<Aes128Ccm16V>::NEW;
        assert_eq!(aead128.key_block_shape().enc_key_len, 16);

        let aead256 = tls12::Tls12CcmAead::<Aes256Ccm16V>::NEW;
        assert_eq!(aead256.key_block_shape().enc_key_len, 32);

        let aead128_8 = tls12::Tls12CcmAead::<Aes128Ccm8V>::NEW;
        assert_eq!(aead128_8.key_block_shape().enc_key_len, 16);
        assert_eq!(aead128_8.key_block_shape().fixed_iv_len, 4);
        assert_eq!(aead128_8.key_block_shape().explicit_nonce_len, 8);
    }

    #[test]
    fn tls13_key_lens() {
        use rustls::crypto::cipher::Tls13AeadAlgorithm;

        assert_eq!(tls13::Tls13CcmAead::<Aes128Ccm16V>::NEW.key_len(), 16);
        assert_eq!(tls13::Tls13CcmAead::<Aes128Ccm8V>::NEW.key_len(), 16);
    }
}