Skip to main content

rustls_ccm/
lib.rs

1#![warn(missing_docs)]
2//! AES-CCM cipher suites for [rustls](https://github.com/rustls/rustls).
3//!
4//! Neither [aws-lc-rs](https://github.com/aws/aws-lc-rs) nor
5//! [ring](https://github.com/briansmith/ring) expose AES-CCM, so rustls's
6//! built-in providers cannot offer these suites. This crate fills the gap
7//! using the [RustCrypto](https://github.com/RustCrypto) `aes` + `ccm` crates,
8//! plugged in via rustls's [`CryptoProvider`]
9//! extension point.
10//!
11//! CCM cipher suites are required or recommended by several IoT and energy
12//! protocols, including IEEE 2030.5 (Smart Energy), Matter, Thread, and
13//! constrained-device TLS profiles (RFC 7925).
14//!
15//! # Cipher suites
16//!
17//! ## TLS 1.2 ([RFC 7251](https://www.rfc-editor.org/rfc/rfc7251))
18//!
19//! | Suite | Tag | Key |
20//! |---|---|---|
21//! | [`TLS_ECDHE_ECDSA_WITH_AES_128_CCM`] | 16 B | 128-bit |
22//! | [`TLS_ECDHE_ECDSA_WITH_AES_256_CCM`] | 16 B | 256-bit |
23//! | [`TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8`] | 8 B | 128-bit |
24//! | [`TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8`] | 8 B | 256-bit |
25//!
26//! ## TLS 1.3 ([RFC 8446](https://www.rfc-editor.org/rfc/rfc8446))
27//!
28//! | Suite | Tag | Key |
29//! |---|---|---|
30//! | [`TLS13_AES_128_CCM_SHA256`] | 16 B | 128-bit |
31//! | [`TLS13_AES_128_CCM_8_SHA256`] | 8 B | 128-bit |
32//!
33//! # Limitations
34//!
35//! Raw traffic-secret extraction for kTLS/hardware offload
36//! (`dangerous_extract_secrets()`) is not supported:
37//! [`ConnectionTrafficSecrets`](rustls::ConnectionTrafficSecrets) has no CCM
38//! variant, so `extract_keys` returns `UnsupportedOperationError` for all CCM
39//! suites. `SSLKEYLOGFILE`-style key logging ([`rustls::KeyLog`]) works
40//! normally — it is fed from the key schedule and does not involve
41//! `extract_keys`.
42//!
43//! # Usage
44//!
45//! Use [`crypto_provider()`] for an aws-lc-rs provider with all CCM suites
46//! appended after the defaults (CCM is negotiated only when the peer offers
47//! nothing stronger), or pick individual suites and build your own provider.
48//!
49//! ```
50//! let provider = rustls_ccm::crypto_provider();
51//! let config = rustls::ClientConfig::builder_with_provider(provider.into())
52//!     .with_safe_default_protocol_versions()
53//!     .unwrap();
54//! ```
55
56use std::sync::LazyLock;
57
58use aes::{Aes128, Aes256};
59use ccm::Ccm;
60use ccm::aead::{AeadCore, AeadInOut, KeyInit};
61use ccm::consts::{U8, U12, U16};
62use rustls::crypto::CryptoProvider;
63use rustls::{
64    CipherSuite, CipherSuiteCommon, SupportedCipherSuite, Tls12CipherSuite, Tls13CipherSuite,
65};
66
67mod tls12;
68mod tls13;
69
70// ---------------------------------------------------------------------------
71// Cipher variant abstraction
72// ---------------------------------------------------------------------------
73
74/// Trait abstracting over the four AES-CCM cipher configurations.
75pub(crate) trait CcmVariant: Send + Sync + 'static {
76    type Cipher: AeadInOut + AeadCore<NonceSize = U12> + KeyInit + Send + Sync;
77    const KEY_LEN: usize;
78    const TAG_LEN: usize;
79}
80
81pub(crate) enum Aes128Ccm8V {}
82impl CcmVariant for Aes128Ccm8V {
83    type Cipher = Ccm<Aes128, U8, U12>;
84    const KEY_LEN: usize = 16;
85    const TAG_LEN: usize = 8;
86}
87
88pub(crate) enum Aes128Ccm16V {}
89impl CcmVariant for Aes128Ccm16V {
90    type Cipher = Ccm<Aes128, U16, U12>;
91    const KEY_LEN: usize = 16;
92    const TAG_LEN: usize = 16;
93}
94
95pub(crate) enum Aes256Ccm8V {}
96impl CcmVariant for Aes256Ccm8V {
97    type Cipher = Ccm<Aes256, U8, U12>;
98    const KEY_LEN: usize = 32;
99    const TAG_LEN: usize = 8;
100}
101
102pub(crate) enum Aes256Ccm16V {}
103impl CcmVariant for Aes256Ccm16V {
104    type Cipher = Ccm<Aes256, U16, U12>;
105    const KEY_LEN: usize = 32;
106    const TAG_LEN: usize = 16;
107}
108
109// CCM makes two block-cipher calls per 16-byte block (CBC-MAC + CTR), so per
110// the CFRG AEAD limits analysis its confidentiality margin at a given data
111// volume is roughly half of GCM's. rustls uses 1 << 24 records for AES-GCM;
112// halve it for CCM.
113const CONFIDENTIALITY_LIMIT: u64 = 1 << 23;
114
115// ---------------------------------------------------------------------------
116// TLS 1.2 suite definitions (RFC 7251) — all use SHA-256
117// ---------------------------------------------------------------------------
118
119fn tls12_base() -> &'static Tls12CipherSuite {
120    let base = rustls::crypto::aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256;
121    let SupportedCipherSuite::Tls12(s) = base else {
122        unreachable!()
123    };
124    s
125}
126
127static SUITE_TLS12_128_CCM: LazyLock<Tls12CipherSuite> = LazyLock::new(|| {
128    let base = tls12_base();
129    Tls12CipherSuite {
130        common: CipherSuiteCommon {
131            suite: CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_CCM,
132            hash_provider: base.common.hash_provider,
133            confidentiality_limit: CONFIDENTIALITY_LIMIT,
134        },
135        prf_provider: base.prf_provider,
136        kx: base.kx,
137        sign: base.sign,
138        aead_alg: &tls12::Tls12CcmAead::<Aes128Ccm16V>::NEW,
139    }
140});
141
142static SUITE_TLS12_256_CCM: LazyLock<Tls12CipherSuite> = LazyLock::new(|| {
143    let base = tls12_base();
144    Tls12CipherSuite {
145        common: CipherSuiteCommon {
146            suite: CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_CCM,
147            hash_provider: base.common.hash_provider,
148            confidentiality_limit: CONFIDENTIALITY_LIMIT,
149        },
150        prf_provider: base.prf_provider,
151        kx: base.kx,
152        sign: base.sign,
153        aead_alg: &tls12::Tls12CcmAead::<Aes256Ccm16V>::NEW,
154    }
155});
156
157static SUITE_TLS12_128_CCM8: LazyLock<Tls12CipherSuite> = LazyLock::new(|| {
158    let base = tls12_base();
159    Tls12CipherSuite {
160        common: CipherSuiteCommon {
161            suite: CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8,
162            hash_provider: base.common.hash_provider,
163            confidentiality_limit: CONFIDENTIALITY_LIMIT,
164        },
165        prf_provider: base.prf_provider,
166        kx: base.kx,
167        sign: base.sign,
168        aead_alg: &tls12::Tls12CcmAead::<Aes128Ccm8V>::NEW,
169    }
170});
171
172static SUITE_TLS12_256_CCM8: LazyLock<Tls12CipherSuite> = LazyLock::new(|| {
173    let base = tls12_base();
174    Tls12CipherSuite {
175        common: CipherSuiteCommon {
176            suite: CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8,
177            hash_provider: base.common.hash_provider,
178            confidentiality_limit: CONFIDENTIALITY_LIMIT,
179        },
180        prf_provider: base.prf_provider,
181        kx: base.kx,
182        sign: base.sign,
183        aead_alg: &tls12::Tls12CcmAead::<Aes256Ccm8V>::NEW,
184    }
185});
186
187// ---------------------------------------------------------------------------
188// TLS 1.3 suite definitions (RFC 8446) — both use SHA-256
189// ---------------------------------------------------------------------------
190
191fn tls13_base() -> &'static Tls13CipherSuite {
192    let base = rustls::crypto::aws_lc_rs::cipher_suite::TLS13_AES_128_GCM_SHA256;
193    let SupportedCipherSuite::Tls13(s) = base else {
194        unreachable!()
195    };
196    s
197}
198
199static SUITE_TLS13_128_CCM: LazyLock<Tls13CipherSuite> = LazyLock::new(|| {
200    let base = tls13_base();
201    Tls13CipherSuite {
202        common: CipherSuiteCommon {
203            suite: CipherSuite::TLS13_AES_128_CCM_SHA256,
204            hash_provider: base.common.hash_provider,
205            confidentiality_limit: CONFIDENTIALITY_LIMIT,
206        },
207        hkdf_provider: base.hkdf_provider,
208        aead_alg: &tls13::Tls13CcmAead::<Aes128Ccm16V>::NEW,
209        quic: None,
210    }
211});
212
213static SUITE_TLS13_128_CCM8: LazyLock<Tls13CipherSuite> = LazyLock::new(|| {
214    let base = tls13_base();
215    Tls13CipherSuite {
216        common: CipherSuiteCommon {
217            suite: CipherSuite::TLS13_AES_128_CCM_8_SHA256,
218            hash_provider: base.common.hash_provider,
219            confidentiality_limit: CONFIDENTIALITY_LIMIT,
220        },
221        hkdf_provider: base.hkdf_provider,
222        aead_alg: &tls13::Tls13CcmAead::<Aes128Ccm8V>::NEW,
223        quic: None,
224    }
225});
226
227// ---------------------------------------------------------------------------
228// Public API
229// ---------------------------------------------------------------------------
230
231/// `TLS_ECDHE_ECDSA_WITH_AES_128_CCM` (0xC0AC, RFC 7251).
232pub static TLS_ECDHE_ECDSA_WITH_AES_128_CCM: LazyLock<SupportedCipherSuite> =
233    LazyLock::new(|| SupportedCipherSuite::Tls12(&SUITE_TLS12_128_CCM));
234
235/// `TLS_ECDHE_ECDSA_WITH_AES_256_CCM` (0xC0AD, RFC 7251).
236pub static TLS_ECDHE_ECDSA_WITH_AES_256_CCM: LazyLock<SupportedCipherSuite> =
237    LazyLock::new(|| SupportedCipherSuite::Tls12(&SUITE_TLS12_256_CCM));
238
239/// `TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8` (0xC0AE, RFC 7251).
240pub static TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8: LazyLock<SupportedCipherSuite> =
241    LazyLock::new(|| SupportedCipherSuite::Tls12(&SUITE_TLS12_128_CCM8));
242
243/// `TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8` (0xC0AF, RFC 7251).
244pub static TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8: LazyLock<SupportedCipherSuite> =
245    LazyLock::new(|| SupportedCipherSuite::Tls12(&SUITE_TLS12_256_CCM8));
246
247/// `TLS_AES_128_CCM_SHA256` (0x1304, RFC 8446). Recommended=Y.
248///
249/// Standard TLS 1.3 cipher suite for constrained environments (Matter, Thread, CoAP).
250pub static TLS13_AES_128_CCM_SHA256: LazyLock<SupportedCipherSuite> =
251    LazyLock::new(|| SupportedCipherSuite::Tls13(&SUITE_TLS13_128_CCM));
252
253/// `TLS_AES_128_CCM_8_SHA256` (0x1305, RFC 8446).
254///
255/// TLS 1.3 cipher suite with truncated 8-byte tag for bandwidth-constrained devices.
256pub static TLS13_AES_128_CCM_8_SHA256: LazyLock<SupportedCipherSuite> =
257    LazyLock::new(|| SupportedCipherSuite::Tls13(&SUITE_TLS13_128_CCM8));
258
259/// All CCM cipher suites provided by this crate (TLS 1.2 + TLS 1.3).
260pub fn all_suites() -> [SupportedCipherSuite; 6] {
261    [
262        *TLS_ECDHE_ECDSA_WITH_AES_128_CCM,
263        *TLS_ECDHE_ECDSA_WITH_AES_256_CCM,
264        *TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8,
265        *TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8,
266        *TLS13_AES_128_CCM_SHA256,
267        *TLS13_AES_128_CCM_8_SHA256,
268    ]
269}
270
271/// Returns an aws-lc-rs [`CryptoProvider`] with all CCM suites appended.
272///
273/// The default suites keep priority: a rustls server built from this provider
274/// still prefers AES-GCM / ChaCha20-Poly1305 and falls back to CCM only for
275/// peers that offer nothing stronger. To *prefer* CCM (e.g. a profile that
276/// mandates it, like IEEE 2030.5), insert the suites you want at the front
277/// instead:
278///
279/// ```
280/// let mut provider = rustls::crypto::aws_lc_rs::default_provider();
281/// provider
282///     .cipher_suites
283///     .insert(0, *rustls_ccm::TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8);
284/// ```
285pub fn crypto_provider() -> CryptoProvider {
286    let mut provider = rustls::crypto::aws_lc_rs::default_provider();
287    provider.cipher_suites.extend(all_suites());
288    provider
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn all_suites_accessible() {
297        let suites = all_suites();
298        assert_eq!(
299            suites[0].suite(),
300            CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_CCM
301        );
302        assert_eq!(
303            suites[1].suite(),
304            CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_CCM
305        );
306        assert_eq!(
307            suites[2].suite(),
308            CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8
309        );
310        assert_eq!(
311            suites[3].suite(),
312            CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8
313        );
314        assert_eq!(suites[4].suite(), CipherSuite::TLS13_AES_128_CCM_SHA256);
315        assert_eq!(suites[5].suite(), CipherSuite::TLS13_AES_128_CCM_8_SHA256);
316    }
317
318    #[test]
319    fn crypto_provider_includes_all_ccm() {
320        let provider = crypto_provider();
321        for suite in all_suites() {
322            assert!(
323                provider
324                    .cipher_suites
325                    .iter()
326                    .any(|s| s.suite() == suite.suite()),
327                "missing {:?}",
328                suite.suite()
329            );
330        }
331    }
332
333    #[test]
334    fn ccm_round_trip() {
335        let key = [0x42u8; 16];
336        let nonce = ccm::aead::array::Array::from([1u8; 12]);
337        let aad = b"additional data";
338        let plaintext = b"hello CCM";
339
340        // Full tag (16-byte)
341        let cipher = <Ccm<Aes128, U16, U12> as KeyInit>::new_from_slice(&key).unwrap();
342        let mut buf = plaintext.to_vec();
343        let tag = cipher
344            .encrypt_inout_detached(&nonce, aad.as_slice(), buf.as_mut_slice().into())
345            .unwrap();
346        assert_eq!(tag.len(), 16);
347        cipher
348            .decrypt_inout_detached(&nonce, aad.as_slice(), buf.as_mut_slice().into(), &tag)
349            .unwrap();
350        assert_eq!(&buf, plaintext);
351
352        // 8-byte tag
353        let cipher8 = <Ccm<Aes128, U8, U12> as KeyInit>::new_from_slice(&key).unwrap();
354        let mut buf = plaintext.to_vec();
355        let tag = cipher8
356            .encrypt_inout_detached(&nonce, aad.as_slice(), buf.as_mut_slice().into())
357            .unwrap();
358        assert_eq!(tag.len(), 8);
359        cipher8
360            .decrypt_inout_detached(&nonce, aad.as_slice(), buf.as_mut_slice().into(), &tag)
361            .unwrap();
362        assert_eq!(&buf, plaintext);
363    }
364
365    #[test]
366    fn ccm_tampered_fails() {
367        let key = [0x42u8; 16];
368        let nonce = ccm::aead::array::Array::from([2u8; 12]);
369        let cipher = <Ccm<Aes128, U16, U12> as KeyInit>::new_from_slice(&key).unwrap();
370        let mut buf = b"secret".to_vec();
371        let tag = cipher
372            .encrypt_inout_detached(&nonce, b"", buf.as_mut_slice().into())
373            .unwrap();
374        buf[0] ^= 0xff;
375        assert!(
376            cipher
377                .decrypt_inout_detached(&nonce, b"", buf.as_mut_slice().into(), &tag)
378                .is_err()
379        );
380    }
381
382    #[test]
383    fn ccm256_round_trip() {
384        let key = [0x42u8; 32];
385        let nonce = ccm::aead::array::Array::from([3u8; 12]);
386        let cipher = <Ccm<Aes256, U16, U12> as KeyInit>::new_from_slice(&key).unwrap();
387        let mut buf = b"aes-256-ccm".to_vec();
388        let tag = cipher
389            .encrypt_inout_detached(&nonce, b"", buf.as_mut_slice().into())
390            .unwrap();
391        cipher
392            .decrypt_inout_detached(&nonce, b"", buf.as_mut_slice().into(), &tag)
393            .unwrap();
394        assert_eq!(&buf, b"aes-256-ccm");
395    }
396
397    #[test]
398    fn tls12_key_block_shapes() {
399        use rustls::crypto::cipher::Tls12AeadAlgorithm;
400
401        let aead128 = tls12::Tls12CcmAead::<Aes128Ccm16V>::NEW;
402        assert_eq!(aead128.key_block_shape().enc_key_len, 16);
403
404        let aead256 = tls12::Tls12CcmAead::<Aes256Ccm16V>::NEW;
405        assert_eq!(aead256.key_block_shape().enc_key_len, 32);
406
407        let aead128_8 = tls12::Tls12CcmAead::<Aes128Ccm8V>::NEW;
408        assert_eq!(aead128_8.key_block_shape().enc_key_len, 16);
409        assert_eq!(aead128_8.key_block_shape().fixed_iv_len, 4);
410        assert_eq!(aead128_8.key_block_shape().explicit_nonce_len, 8);
411    }
412
413    #[test]
414    fn tls13_key_lens() {
415        use rustls::crypto::cipher::Tls13AeadAlgorithm;
416
417        assert_eq!(tls13::Tls13CcmAead::<Aes128Ccm16V>::NEW.key_len(), 16);
418        assert_eq!(tls13::Tls13CcmAead::<Aes128Ccm8V>::NEW.key_len(), 16);
419    }
420}