Skip to main content

arcana/cipher/
ccm.rs

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