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}