Skip to main content

arcana/cipher/
ccm.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Cédric Mesnil <cslashm@pm.me>
3
4//! AES-CCM AEAD (NIST SP 800-38C, RFC 3610).
5//!
6//! CCM = Counter with CBC-MAC. It is the second AES AEAD shipped by
7//! `arcana` alongside AES-GCM, and the older of the two.
8//! CCM is the AEAD used by Bluetooth Low Energy, ZigBee, IPsec
9//! ESP-CCM, TLS 1.2 ciphersuites of the form `*_CCM*`, 802.15.4
10//! mesh networks, and IETF protocols that need a small / portable
11//! AEAD with no GHASH dependency.
12//!
13//! Compared to GCM:
14//!
15//! * **Pro**: only needs `encrypt_block`. Decryption never invokes
16//!   AES decrypt -- it XORs the same CTR keystream and recomputes
17//!   the MAC over the plaintext. Smaller code, smaller embedded
18//!   footprint, no GHASH state.
19//! * **Pro**: no soft-failure modes -- the construction is a
20//!   straightforward MAC-then-encrypt with explicit length fields.
21//! * **Con**: two-pass over the data (one for MAC, one for CTR), so
22//!   it's slower than GCM on hardware that has carry-less multiply.
23//! * **Con**: tag length and the unusual `(N, L)` parameter trade
24//!   are easy to misconfigure -- see the parameter validation below.
25//!
26//! # Parameters
27//!
28//! CCM is parameterised by `(M, L)` where:
29//!
30//! * **`M`** is the **tag length in bytes**. Allowed: `{4, 6, 8, 10, 12, 14, 16}`.
31//!   Smaller M = faster but lower forgery resistance. Most protocols
32//!   use M = 8 (Bluetooth, ZigBee) or M = 16 (TLS, ESP).
33//!
34//! * **`L`** is the **length-of-length field in bytes**. Allowed:
35//!   `{2, 3, 4, 5, 6, 7, 8}`. The maximum payload length is
36//!   `2^(8*L) - 1` bytes; the nonce length is `15 - L` bytes.
37//!   `L = 2` gives a 13-byte nonce and a max payload of ~64 KB,
38//!   which is the choice in **RFC 3610** and almost every modern
39//!   protocol that uses CCM. The convenience constructors below
40//!   pin `L = 2`.
41//!
42//! # Construction (NIST SP 800-38C / RFC 3610)
43//!
44//! 1. **Format the first MAC block** `B0`:
45//!
46//!    ```text
47//!     +-----+----------+----------+
48//!     | flg | nonce N  | length Q |
49//!     +-----+----------+----------+
50//!       1B    15 - L      L
51//!    ```
52//!
53//!    where `flg = (Adata << 6) | ((M-2)/2 << 3) | (L-1)`,
54//!    `Adata = 1` iff AAD is non-empty, and `Q` is the big-endian
55//!    payload length encoded in `L` bytes.
56//!
57//! 2. **Format the AAD blocks**: prepend `length(AAD)` encoded as
58//!    2 bytes (BE) for `0 < len < 2^16 - 2^8`, or 6 bytes for the
59//!    larger ranges (we only support the 2-byte form: caller AAD
60//!    must fit in the canonical IETF range), then the AAD bytes,
61//!    then zero-pad to a multiple of 16.
62//!
63//! 3. **Format the payload blocks**: just the plaintext bytes, zero-
64//!    padded to a multiple of 16.
65//!
66//! 4. **CBC-MAC** all of the above (B0 || formatted_aad || formatted_payload)
67//!    with AES under the key. The CBC-MAC output is the unencrypted
68//!    tag T (taking the first M bytes of the final 16-byte CBC state).
69//!
70//! 5. **Counter blocks** A_i: format
71//!
72//!    ```text
73//!     +-----+----------+--------+
74//!     | flg | nonce N  | i (BE) |
75//!     +-----+----------+--------+
76//!       1B    15 - L      L
77//!    ```
78//!
79//!    where `flg = L - 1`. `S_i = AES_K(A_i)` is the keystream block.
80//!
81//! 6. **Encrypt T** by XOR with the first M bytes of `S_0`. The result
82//!    is the on-the-wire tag.
83//!
84//! 7. **Encrypt the payload** by XOR with `S_1, S_2, ...` (counter
85//!    starts at 1).
86//!
87//! Decryption reverses steps 6-7 to recover the plaintext, then
88//! recomputes T via steps 1-4 and compares in constant time.
89
90use super::aes::Aes;
91use crate::BlockCipher;
92
93// ============================================================================
94// Parameter validation
95// ============================================================================
96
97/// Validate CCM `(M, L)` parameters per NIST SP 800-38C / RFC 3610.
98///
99/// Returns an error string for diagnostic use; the public API
100/// surfaces this as `None` from `encrypt`/`decrypt`. We use a string
101/// here only as a debug aid for the test suite.
102fn check_params(m: usize, l: usize) -> Result<(), &'static str> {
103    if !matches!(m, 4 | 6 | 8 | 10 | 12 | 14 | 16) {
104        return Err("CCM: tag length M must be in {4,6,8,10,12,14,16}");
105    }
106    if !(2..=8).contains(&l) {
107        return Err("CCM: length-of-length L must be in {2..=8}");
108    }
109    Ok(())
110}
111
112// ============================================================================
113// Generic AES-CCM core (any L, any M, any AES variant via Aes)
114// ============================================================================
115
116/// AES-CCM encrypt with the generic `(M, L)` parameters.
117///
118/// `nonce.len()` must be exactly `15 - L`. `plaintext.len()` must
119/// fit in `2^(8*L)` bytes (no overflow check is enforced for `L >=
120/// 8` since usize is bounded). AAD must currently be < `2^16 - 2^8`
121/// bytes (the canonical 2-byte length encoding).
122///
123/// Returns `(ciphertext, tag)` where `ciphertext.len() ==
124/// plaintext.len()` and `tag.len() == M`.
125pub fn ccm_encrypt(
126    aes: &Aes,
127    m: usize,
128    l: usize,
129    nonce: &[u8],
130    aad: &[u8],
131    plaintext: &[u8],
132) -> Option<(Vec<u8>, Vec<u8>)> {
133    check_params(m, l).ok()?;
134    if nonce.len() != 15 - l {
135        return None;
136    }
137    if l < 8 {
138        // Plaintext length must fit in L bytes.
139        let max_pt: u128 = 1u128 << (8 * l);
140        if (plaintext.len() as u128) >= max_pt {
141            return None;
142        }
143    }
144    if aad.len() >= (1usize << 16) - (1usize << 8) {
145        // AAD too long for the 2-byte length encoding we support.
146        return None;
147    }
148
149    // Step 1-4: compute CBC-MAC over (B0 || formatted_aad ||
150    // formatted_payload). The result is the unencrypted tag T.
151    let t = cbc_mac(aes, m, l, nonce, aad, plaintext);
152
153    // Step 5-6: compute the keystream block A_0 = AES_K(format(0))
154    // and XOR its first M bytes with T to produce the encrypted tag.
155    let mut a0 = ctr_block(l, nonce, 0);
156    aes.encrypt_block(&mut a0);
157    let mut tag = vec![0u8; m];
158    for i in 0..m {
159        tag[i] = t[i] ^ a0[i];
160    }
161
162    // Step 7: CTR encrypt the payload starting at i = 1.
163    let mut ct = plaintext.to_vec();
164    let mut counter: u64 = 1;
165    let mut pos = 0;
166    while pos < ct.len() {
167        let mut block = ctr_block(l, nonce, counter);
168        aes.encrypt_block(&mut block);
169        let take = (16).min(ct.len() - pos);
170        for i in 0..take {
171            ct[pos + i] ^= block[i];
172        }
173        pos += 16;
174        counter += 1;
175    }
176
177    Some((ct, tag))
178}
179
180/// AES-CCM decrypt with the generic `(M, L)` parameters.
181///
182/// Returns `Some(plaintext)` only if the recomputed tag matches
183/// (constant-time compare). Returns `None` for any malformed input
184/// (wrong nonce length, wrong tag length, parameter out of range,
185/// AAD too long, payload too long) **and** for tag mismatch.
186///
187/// Callers MUST treat `None` as a hard authentication failure and
188/// MUST NOT use the (intermediate) decrypted bytes for any purpose
189/// even if they were exposed by an aggressive optimiser -- the
190/// function does not leak them.
191pub fn ccm_decrypt(
192    aes: &Aes,
193    m: usize,
194    l: usize,
195    nonce: &[u8],
196    aad: &[u8],
197    ciphertext: &[u8],
198    tag: &[u8],
199) -> Option<Vec<u8>> {
200    check_params(m, l).ok()?;
201    if nonce.len() != 15 - l {
202        return None;
203    }
204    if tag.len() != m {
205        return None;
206    }
207    if l < 8 {
208        let max_pt: u128 = 1u128 << (8 * l);
209        if (ciphertext.len() as u128) >= max_pt {
210            return None;
211        }
212    }
213    if aad.len() >= (1usize << 16) - (1usize << 8) {
214        return None;
215    }
216
217    // CTR-decrypt the payload (= same XOR as encrypt).
218    let mut pt = ciphertext.to_vec();
219    let mut counter: u64 = 1;
220    let mut pos = 0;
221    while pos < pt.len() {
222        let mut block = ctr_block(l, nonce, counter);
223        aes.encrypt_block(&mut block);
224        let take = (16).min(pt.len() - pos);
225        for i in 0..take {
226            pt[pos + i] ^= block[i];
227        }
228        pos += 16;
229        counter += 1;
230    }
231
232    // Recompute the tag over the recovered plaintext.
233    let t = cbc_mac(aes, m, l, nonce, aad, &pt);
234    let mut a0 = ctr_block(l, nonce, 0);
235    aes.encrypt_block(&mut a0);
236    let mut expected = vec![0u8; m];
237    for i in 0..m {
238        expected[i] = t[i] ^ a0[i];
239    }
240
241    // Constant-time compare the tag.
242    let mut diff = 0u8;
243    for i in 0..m {
244        diff |= expected[i] ^ tag[i];
245    }
246    if diff != 0 {
247        return None;
248    }
249
250    Some(pt)
251}
252
253// ============================================================================
254// Internal helpers
255// ============================================================================
256
257/// Build the CCM CTR-block format (NIST SP 800-38C §6.2):
258///
259/// ```text
260///   flg = L - 1
261///   block = flg || nonce || counter (BE on L bytes)
262/// ```
263fn ctr_block(l: usize, nonce: &[u8], counter: u64) -> [u8; 16] {
264    debug_assert_eq!(nonce.len(), 15 - l);
265    let mut block = [0u8; 16];
266    block[0] = (l - 1) as u8;
267    block[1..1 + nonce.len()].copy_from_slice(nonce);
268    // BE counter in the last L bytes.
269    let ctr_be = counter.to_be_bytes();
270    // Pick the trailing L bytes of ctr_be (a u64 has 8 bytes, so
271    // for L <= 8 we always have enough).
272    let l_used = l.min(8);
273    block[16 - l_used..].copy_from_slice(&ctr_be[8 - l_used..]);
274    block
275}
276
277/// Build the CCM B0 first MAC block (NIST SP 800-38C §A.2.1):
278///
279/// ```text
280///   flg = (Adata << 6) | (((M - 2) / 2) << 3) | (L - 1)
281///   B0  = flg || nonce || Q (BE on L bytes)
282/// ```
283fn b0_block(m: usize, l: usize, nonce: &[u8], aad_len: usize, payload_len: usize) -> [u8; 16] {
284    debug_assert_eq!(nonce.len(), 15 - l);
285    let adata: u8 = if aad_len > 0 { 1 } else { 0 };
286    let m_field: u8 = (((m as u8) - 2) / 2) << 3;
287    let l_field: u8 = (l as u8) - 1;
288    let flg: u8 = (adata << 6) | m_field | l_field;
289
290    let mut b0 = [0u8; 16];
291    b0[0] = flg;
292    b0[1..1 + nonce.len()].copy_from_slice(nonce);
293
294    // Q = payload_len encoded BE in L bytes.
295    let q_be = (payload_len as u64).to_be_bytes();
296    let l_used = l.min(8);
297    b0[16 - l_used..].copy_from_slice(&q_be[8 - l_used..]);
298    b0
299}
300
301/// Run CBC-MAC over (B0 || formatted_aad || formatted_payload) and
302/// return the final 16-byte block. The caller will truncate to M
303/// bytes.
304fn cbc_mac(aes: &Aes, m: usize, l: usize, nonce: &[u8], aad: &[u8], payload: &[u8]) -> [u8; 16] {
305    let mut state = b0_block(m, l, nonce, aad.len(), payload.len());
306    aes.encrypt_block(&mut state);
307
308    // Format AAD: 2-byte BE length || aad || zero pad to 16-byte multiple.
309    if !aad.is_empty() {
310        // Build the prefix block: 2 bytes of BE length followed by
311        // up to 14 bytes of AAD (the rest spills into subsequent
312        // blocks).
313        let len_bytes = (aad.len() as u16).to_be_bytes();
314        let mut prefix = [0u8; 16];
315        prefix[0] = len_bytes[0];
316        prefix[1] = len_bytes[1];
317        let take = (14).min(aad.len());
318        prefix[2..2 + take].copy_from_slice(&aad[..take]);
319        for i in 0..16 {
320            state[i] ^= prefix[i];
321        }
322        aes.encrypt_block(&mut state);
323
324        let mut pos = take;
325        while pos < aad.len() {
326            let mut block = [0u8; 16];
327            let chunk = (16).min(aad.len() - pos);
328            block[..chunk].copy_from_slice(&aad[pos..pos + chunk]);
329            for i in 0..16 {
330                state[i] ^= block[i];
331            }
332            aes.encrypt_block(&mut state);
333            pos += chunk;
334        }
335    }
336
337    // Format payload: just the bytes, zero-padded to a 16-byte multiple.
338    let mut pos = 0;
339    while pos < payload.len() {
340        let mut block = [0u8; 16];
341        let chunk = (16).min(payload.len() - pos);
342        block[..chunk].copy_from_slice(&payload[pos..pos + chunk]);
343        for i in 0..16 {
344            state[i] ^= block[i];
345        }
346        aes.encrypt_block(&mut state);
347        pos += chunk;
348    }
349
350    state
351}
352
353// ============================================================================
354// Public convenience: AES-128-CCM with L = 2 (the RFC 3610 / IETF default)
355// ============================================================================
356
357/// AES-CCM with the **RFC 3610 default parameters**: `L = 2`, so
358/// the nonce is 13 bytes and the maximum payload is `2^16 - 1`
359/// bytes. The tag length `M` is configurable per call.
360///
361/// This is the wrapper most callers want. The generic `(M, L)`
362/// form is exposed via [`ccm_encrypt`] / [`ccm_decrypt`] for
363/// protocols that need a different `L`.
364pub struct AesCcm {
365    aes: Aes,
366    m: usize,
367}
368
369impl AesCcm {
370    /// Initialise AES-CCM with `M` ∈ `{4, 6, 8, 10, 12, 14, 16}`.
371    /// Returns `None` for invalid `M` or invalid AES key length.
372    pub fn new(key: &[u8], m: usize) -> Option<Self> {
373        if !matches!(m, 4 | 6 | 8 | 10 | 12 | 14 | 16) {
374            return None;
375        }
376        if !matches!(key.len(), 16 | 24 | 32) {
377            return None;
378        }
379        Some(Self {
380            aes: <Aes as BlockCipher>::new(key),
381            m,
382        })
383    }
384
385    /// Encrypt and authenticate. `nonce.len() == 13`.
386    /// Returns `(ciphertext, tag)` where `tag.len() == M`.
387    pub fn encrypt(&self, nonce: &[u8; 13], aad: &[u8], plaintext: &[u8]) -> Option<(Vec<u8>, Vec<u8>)> {
388        ccm_encrypt(&self.aes, self.m, 2, nonce, aad, plaintext)
389    }
390
391    /// Decrypt and verify. `nonce.len() == 13`, `tag.len() == M`.
392    /// Returns `None` on tag mismatch or any malformed input.
393    pub fn decrypt(&self, nonce: &[u8; 13], aad: &[u8], ciphertext: &[u8], tag: &[u8]) -> Option<Vec<u8>> {
394        ccm_decrypt(&self.aes, self.m, 2, nonce, aad, ciphertext, tag)
395    }
396}
397
398// ============================================================================
399// Tests
400// ============================================================================
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405
406    fn hex(s: &str) -> Vec<u8> {
407        let s: String = s.chars().filter(|c| !c.is_whitespace()).collect();
408        assert!(s.len() % 2 == 0);
409        (0..s.len())
410            .step_by(2)
411            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
412            .collect()
413    }
414
415    /// **RFC 3610 §8 Packet Vector #1** -- the canonical AES-CCM
416    /// test vector. Pinned byte-exact against:
417    ///
418    /// ```text
419    /// Key:    C0 C1 C2 C3 C4 C5 C6 C7  C8 C9 CA CB CC CD CE CF
420    /// Nonce:  00 00 00 03 02 01 00 A0  A1 A2 A3 A4 A5
421    /// AAD:    00 01 02 03 04 05 06 07
422    /// Plain:  08 09 0A 0B 0C 0D 0E 0F  10 11 12 13 14 15 16 17
423    ///         18 19 1A 1B 1C 1D 1E
424    /// CCM:    58 8C 97 9A 61 C6 63 D2  F0 66 D0 C2 C0 F9 89 80
425    ///         6D 5F 6B 61 DA C3 84
426    /// Tag:    17 E8 D1 2C FD F9 26 E0   <-- last 8 bytes "17e8d12cfdf926e0"
427    /// ```
428    ///
429    /// Note: in the RFC layout, the "CCM" output is the concatenation
430    /// of (ciphertext || tag). We verify both halves separately.
431    #[test]
432    fn rfc3610_packet_vector_1() {
433        let key = hex("c0c1c2c3c4c5c6c7c8c9cacbcccdcecf");
434        let nonce: [u8; 13] = {
435            let v = hex("00000003020100a0a1a2a3a4a5");
436            v.try_into().unwrap()
437        };
438        let aad = hex("0001020304050607");
439        let plaintext = hex("08090a0b0c0d0e0f101112131415161718191a1b1c1d1e");
440
441        // RFC 3610 packet vector #1 uses M = 8 (8-byte tag).
442        let ccm = AesCcm::new(&key, 8).unwrap();
443        let (ct, tag) = ccm.encrypt(&nonce, &aad, &plaintext).unwrap();
444
445        let expected_ct = hex("588c979a61c663d2f066d0c2c0f989806d5f6b61dac384");
446        let expected_tag = hex("17e8d12cfdf926e0");
447        assert_eq!(ct, expected_ct);
448        assert_eq!(tag, expected_tag);
449
450        // Round-trip: decrypt with the produced (ct, tag).
451        let pt = ccm.decrypt(&nonce, &aad, &ct, &tag).unwrap();
452        assert_eq!(pt, plaintext);
453    }
454
455    /// **RFC 3610 §8 Packet Vector #2** -- a second pinned vector
456    /// for cross-checking. Same key, different nonce + payload.
457    #[test]
458    fn rfc3610_packet_vector_2() {
459        let key = hex("c0c1c2c3c4c5c6c7c8c9cacbcccdcecf");
460        let nonce: [u8; 13] = hex("00000004030201a0a1a2a3a4a5").try_into().unwrap();
461        let aad = hex("0001020304050607");
462        let plaintext = hex("08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
463
464        let ccm = AesCcm::new(&key, 8).unwrap();
465        let (ct, tag) = ccm.encrypt(&nonce, &aad, &plaintext).unwrap();
466
467        let expected_ct = hex("72c91a36e135f8cf291ca894085c87e3cc15c439c9e43a3b");
468        let expected_tag = hex("a091d56e10400916");
469        assert_eq!(ct, expected_ct);
470        assert_eq!(tag, expected_tag);
471
472        // Round-trip.
473        let pt = ccm.decrypt(&nonce, &aad, &ct, &tag).unwrap();
474        assert_eq!(pt, plaintext);
475    }
476
477    /// Round-trip on an arbitrary message with M = 16 (TLS / ESP profile).
478    #[test]
479    fn ccm_aes128_m16_roundtrip() {
480        let key = [0x42u8; 16];
481        let nonce = [0xa5u8; 13];
482        let aad = b"some context";
483        let pt = b"hello world; this is a test of moderate length to span more than one AES block.";
484
485        let ccm = AesCcm::new(&key, 16).unwrap();
486        let (ct, tag) = ccm.encrypt(&nonce, aad, pt).unwrap();
487        assert_eq!(tag.len(), 16);
488        assert_ne!(ct.as_slice(), pt.as_slice());
489
490        let back = ccm.decrypt(&nonce, aad, &ct, &tag).unwrap();
491        assert_eq!(back.as_slice(), pt.as_slice());
492    }
493
494    /// AES-256-CCM round-trip (longer key, same construction).
495    #[test]
496    fn ccm_aes256_m16_roundtrip() {
497        let key = [0x77u8; 32];
498        let nonce = [0x11u8; 13];
499        let aad = b"";
500        let pt = b"AES-256-CCM message";
501
502        let ccm = AesCcm::new(&key, 16).unwrap();
503        let (ct, tag) = ccm.encrypt(&nonce, aad, pt).unwrap();
504        let back = ccm.decrypt(&nonce, aad, &ct, &tag).unwrap();
505        assert_eq!(back.as_slice(), pt.as_slice());
506    }
507
508    /// Decrypt rejects a tampered ciphertext byte.
509    #[test]
510    fn ccm_rejects_tampered_ciphertext() {
511        let key = [0x01u8; 16];
512        let nonce = [0x02u8; 13];
513        let pt = b"do not modify";
514
515        let ccm = AesCcm::new(&key, 8).unwrap();
516        let (mut ct, tag) = ccm.encrypt(&nonce, b"", pt).unwrap();
517        ct[0] ^= 0x01;
518        assert!(ccm.decrypt(&nonce, b"", &ct, &tag).is_none());
519    }
520
521    /// Decrypt rejects a tampered tag byte.
522    #[test]
523    fn ccm_rejects_tampered_tag() {
524        let key = [0x01u8; 16];
525        let nonce = [0x02u8; 13];
526        let pt = b"do not modify";
527
528        let ccm = AesCcm::new(&key, 8).unwrap();
529        let (ct, mut tag) = ccm.encrypt(&nonce, b"", pt).unwrap();
530        tag[0] ^= 0x01;
531        assert!(ccm.decrypt(&nonce, b"", &ct, &tag).is_none());
532    }
533
534    /// Decrypt rejects modified AAD (proves AAD is in the MAC input).
535    #[test]
536    fn ccm_rejects_modified_aad() {
537        let key = [0xffu8; 16];
538        let nonce = [0x10u8; 13];
539        let aad = b"context-A";
540        let pt = b"shared payload";
541
542        let ccm = AesCcm::new(&key, 8).unwrap();
543        let (ct, tag) = ccm.encrypt(&nonce, aad, pt).unwrap();
544        assert!(ccm.decrypt(&nonce, b"context-B", &ct, &tag).is_none());
545    }
546
547    /// Decrypt rejects when the wrong key is used.
548    #[test]
549    fn ccm_rejects_wrong_key() {
550        let key1 = [0x33u8; 16];
551        let mut key2 = key1;
552        key2[0] ^= 0x01;
553        let nonce = [0x44u8; 13];
554        let pt = b"sensitive";
555
556        let ccm1 = AesCcm::new(&key1, 8).unwrap();
557        let (ct, tag) = ccm1.encrypt(&nonce, b"", pt).unwrap();
558        let ccm2 = AesCcm::new(&key2, 8).unwrap();
559        assert!(ccm2.decrypt(&nonce, b"", &ct, &tag).is_none());
560    }
561
562    /// Empty plaintext is allowed: ct.len() == 0 but the tag still
563    /// authenticates the AAD and the length fields.
564    #[test]
565    fn ccm_empty_plaintext() {
566        let key = [0x55u8; 16];
567        let nonce = [0x66u8; 13];
568        let aad = b"only-context";
569
570        let ccm = AesCcm::new(&key, 8).unwrap();
571        let (ct, tag) = ccm.encrypt(&nonce, aad, b"").unwrap();
572        assert!(ct.is_empty());
573        let back = ccm.decrypt(&nonce, aad, &ct, &tag).unwrap();
574        assert!(back.is_empty());
575
576        // Modifying the AAD must still be detected on empty plaintext.
577        assert!(ccm.decrypt(&nonce, b"other", &ct, &tag).is_none());
578    }
579
580    /// Parameter validation: invalid M values are rejected.
581    #[test]
582    fn ccm_rejects_invalid_m() {
583        let key = [0u8; 16];
584        for bad_m in [0, 1, 2, 3, 5, 7, 9, 11, 13, 15, 17, 18, 32] {
585            assert!(AesCcm::new(&key, bad_m).is_none(), "M={} should be rejected", bad_m);
586        }
587        for good_m in [4, 6, 8, 10, 12, 14, 16] {
588            assert!(AesCcm::new(&key, good_m).is_some(), "M={} should be accepted", good_m);
589        }
590    }
591}