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}