Skip to main content

ferritls_core/
ccm.rs

1//! AES-CCM(RFC 3610 / SP 800-38C),认证加密。
2//!
3//! FIPS 批准(SP 800-38C 对 M ∈ {4,6,8,10,12,14,16} 均批准;但 SP
4//! 800-52r2 的 TLS 批准套件面只含 M=16 的 [`Aes128CcmTls`],见适配层
5//! 套件表)。标签长度 M 与 nonce 长度(↔ 长度域 L = 15 − nonce 长度)
6//! 为**运行时参数**,两条入口:
7//!
8//! - 固定参数集类型(宏生成,委托引擎,nonce 以 `&[u8; N]` 类型化):
9//!   - [`Aes128Ccm`]:M=16、13 字节 nonce、L=2;
10//!   - [`Aes128CcmTls`]:M=16、12 字节 nonce、L=3(RFC 8446 §B.5 的
11//!     AEAD_AES_128_CCM,TLS 1.3 记录层使用);
12//!   - [`Aes128Ccm8Tls`]:M=8、12 字节 nonce、L=3(RFC 8446 §B.5 的
13//!     AEAD_AES_128_CCM_8;非批准 TLS 套件,仅默认模式装配)。
14//! - 全参数空间 [`Aes128CcmAny`]:构造时选 M,调用时传任意合法长度
15//!   nonce(7..=13 字节)。M=4/6 标签强度低于 RFC 3610 作者建议
16//!   (≥ 8),仅为兼容既有协议而提供。
17//!
18//! AES-192/256-CCM 未提供:无 TLS 消费者(TLS 1.3 套件表只有
19//! AES-128 两档),属显式省略。
20//!
21//! 外部锚定:RFC 3610 §8 全部 24 个官方分组向量(M=8/L=2 与
22//! M=10/L=2,含 AAD 路径)经程序化提取(tools 流程同 RFC 8448:
23//! python 解析官方原文 + python-cryptography 双向复算)后直跑;
24//! M=16 与全 M 矩阵的 TLS 形态期望值由 python-cryptography(OpenSSL
25//! 后端,先对官方原文校验通过)生成——见 `tests/ccm.rs` 与
26//! docs/VECTOR-PROVENANCE.md。上电自检覆盖(M5)。
27
28use crate::aes::Aes128;
29
30// ---------------------------------------------------------------------------
31// 运行时引擎:标签长/nonce 长均为协议协商的**公开量**,分支与长度域
32// 计算不引入任何常数时间顾虑;秘密仅经 Aes128(ZeroizeOnDrop)持有。
33// ---------------------------------------------------------------------------
34
35/// 参数校验:nonce ∈ 7..=13 字节(L = 15 − nonce ∈ [2,8],RFC 3610
36/// §2,L=1 为规范保留值)、M ∈ {4,6,8,10,12,14,16}(B0 的 3 位 M'
37/// 字段编码域,偶数)。返回 L。
38fn validate_params(nonce_len: usize, tag_len: usize) -> Result<usize, crate::Error> {
39    if !(7..=13).contains(&nonce_len) || !(4..=16).contains(&tag_len) || !tag_len.is_multiple_of(2)
40    {
41        return Err(crate::Error::InvalidInput);
42    }
43    Ok(15 - nonce_len)
44}
45
46/// B0 首字节 flags:64·Adata + 8·M' + L'(RFC 3610 §2.2;
47/// M' = (M−2)/2,L' = L−1)。
48fn b0_flags(tag_len: usize, has_aad: bool, l: usize) -> u8 {
49    ((((tag_len - 2) / 2) << 3) | (u8::from(has_aad) << 6) as usize | (l - 1)) as u8
50}
51
52/// 长度域检查(RFC 3610 §2.2):明文 < 2^(8L)(L=8 时上限为 2^64,
53/// usize 无法越过,免检);AAD < 2^16 − 2^8——两字节短形编码上限
54/// (RFC 3610 §2.2 的长形 0xfffe 转义无 TLS 消费者,不支持)。
55fn check_length_domain(l: usize, pt_len: usize, aad_len: usize) -> Result<(), crate::Error> {
56    if l < 8 && pt_len >= 1usize << (8 * l) {
57        return Err(crate::Error::InvalidInput);
58    }
59    if aad_len >= 0xff00 {
60        return Err(crate::Error::InvalidInput);
61    }
62    Ok(())
63}
64
65/// 原始 CTR 块(未加密):A_i = (L−1) || nonce || counter(L 字节 BE)。
66/// counter 为 u32,但 L 可达 8(移位量至多 56)——经 u64 展开字节。
67fn ctr_raw(nonce: &[u8], l: usize, counter: u32) -> [u8; 16] {
68    debug_assert!(1 + nonce.len() + l == 16);
69    let mut block = [0u8; 16];
70    block[0] = l as u8 - 1;
71    block[1..1 + nonce.len()].copy_from_slice(nonce);
72    let ctr = counter as u64;
73    for i in 0..l {
74        block[16 - l + i] = (ctr >> (8 * (l - 1 - i))) as u8;
75    }
76    block
77}
78
79fn ctr_block(aes: &Aes128, nonce: &[u8], l: usize, counter: u32) -> [u8; 16] {
80    let mut block = ctr_raw(nonce, l, counter);
81    aes.encrypt_block(&mut block);
82    block
83}
84
85/// CTR 密钥流异或(P1 位切片批量路径,同 gcm.rs)。L ≤ 8 时最大块号
86/// ceil(2^(8L)/16) 在 u32 内不可能回绕。
87fn ctr_xor(aes: &Aes128, nonce: &[u8], l: usize, start: u32, data: &[u8]) -> Vec<u8> {
88    let mut out = vec![0u8; data.len()];
89    let mut counter = start;
90    let mut ks = [0u8; crate::aes::CTR_BATCH_BLOCKS * 16];
91    for (in_chunk, out_chunk) in data
92        .chunks(crate::aes::CTR_BATCH_BLOCKS * 16)
93        .zip(out.chunks_mut(crate::aes::CTR_BATCH_BLOCKS * 16))
94    {
95        let n = in_chunk.len().div_ceil(16);
96        aes.encrypt_ctr_batch(ctr_raw(nonce, l, counter), n, &mut ks);
97        let ks_slice = &ks[..out_chunk.len()];
98        for (o, (b, k)) in out_chunk.iter_mut().zip(in_chunk.iter().zip(ks_slice)) {
99            *o = b ^ k;
100        }
101        counter = counter.wrapping_add(n as u32);
102    }
103    out
104}
105
106/// CBC-MAC over B0 || 格式化 AAD || 明文(均补齐到 16 字节块)。
107fn cbc_mac(
108    aes: &Aes128,
109    nonce: &[u8],
110    l: usize,
111    tag_len: usize,
112    aad: &[u8],
113    pt: &[u8],
114) -> [u8; 16] {
115    let mut buf: Vec<u8> = Vec::with_capacity(16 + aad.len() + pt.len() + 32);
116    // B0: flags || nonce || 明文长度(L 字节 BE);Flags 第 6 位
117    // 为 Adata(RFC 3610 §2.2:64·Adata + 8·M' + L')——带 AAD
118    // 时必须置位,否则与规范实现不互操作。
119    buf.push(b0_flags(tag_len, !aad.is_empty(), l));
120    buf.extend_from_slice(nonce);
121    // 明文长度用 L 字节 BE;seal/open 已拒绝超长度域的输入
122    //(L 可达 8,移位量至多 56——经 u64 展开以保 32 位目标可移植)
123    let plen = pt.len() as u64;
124    for i in (0..l).rev() {
125        buf.push((plen >> (8 * i)) as u8);
126    }
127
128    // AAD 编码:2 字节长度 + 数据 + 补零(RFC 3610 §2.2,
129    // len < 2^16-2^8)。**AAD 段在此独立补齐到 16 字节边界**
130    //(add-auth-data 与 payload 各自补齐)——若与 payload 连续
131    // 排列仅在末尾补一次,MAC 与规范实现(OpenSSL 等)不一致。
132    if !aad.is_empty() {
133        buf.extend_from_slice(&(aad.len() as u16).to_be_bytes());
134        buf.extend_from_slice(aad);
135        let rem = buf.len() % 16;
136        if rem != 0 {
137            buf.resize(buf.len() + 16 - rem, 0);
138        }
139    }
140
141    buf.extend_from_slice(pt);
142
143    let mut t = [0u8; 16];
144    for chunk in buf.chunks(16) {
145        let mut block = [0u8; 16];
146        block[..chunk.len()].copy_from_slice(chunk);
147        for i in 0..16 {
148            block[i] ^= t[i];
149        }
150        aes.encrypt_block(&mut block);
151        t = block;
152    }
153    t
154}
155
156/// 先截断 CBC-MAC 到 M 字节、再与前 M 字节 S0 异或(RFC 3610 §2.4/
157/// §2.5 的顺序,**不可**先全宽异或再截断——两者对 M<16 结果不同)。
158fn tag_finish(t: &mut [u8; 16], s0: &[u8; 16], tag_len: usize) {
159    for i in 0..tag_len {
160        t[i] ^= s0[i];
161    }
162}
163
164fn seal_core(
165    aes: &Aes128,
166    nonce: &[u8],
167    tag_len: usize,
168    aad: &[u8],
169    plaintext: &[u8],
170) -> Result<Vec<u8>, crate::Error> {
171    let l = validate_params(nonce.len(), tag_len)?;
172    check_length_domain(l, plaintext.len(), aad.len())?;
173    let mut t = cbc_mac(aes, nonce, l, tag_len, aad, plaintext);
174    let ct = ctr_xor(aes, nonce, l, 1, plaintext);
175    let s0 = ctr_block(aes, nonce, l, 0);
176    tag_finish(&mut t, &s0, tag_len);
177    let mut out = ct;
178    out.extend_from_slice(&t[..tag_len]);
179    Ok(out)
180}
181
182fn open_core(
183    aes: &Aes128,
184    nonce: &[u8],
185    tag_len: usize,
186    aad: &[u8],
187    ct_and_tag: &[u8],
188) -> Result<Vec<u8>, crate::Error> {
189    let l = validate_params(nonce.len(), tag_len)?;
190    if ct_and_tag.len() < tag_len {
191        return Err(crate::Error::VerificationFailed);
192    }
193    // 密文长度受同一长度域约束(超出必非本模块产物);校验先于任何
194    // AES 运算。
195    if l < 8 && ct_and_tag.len() - tag_len >= 1usize << (8 * l) {
196        return Err(crate::Error::VerificationFailed);
197    }
198    let split = ct_and_tag.len() - tag_len;
199    let (ct, tag) = ct_and_tag.split_at(split);
200
201    let pt = ctr_xor(aes, nonce, l, 1, ct);
202    let mut t = cbc_mac(aes, nonce, l, tag_len, aad, &pt);
203    let s0 = ctr_block(aes, nonce, l, 0);
204    tag_finish(&mut t, &s0, tag_len);
205    crate::ct::verify_tag(&t[..tag_len], tag)?;
206    Ok(pt)
207}
208
209// ---------------------------------------------------------------------------
210// 全参数公开类型
211// ---------------------------------------------------------------------------
212
213/// AES-128-CCM,标签长度运行时可配的全参数实例(RFC 3610 完整参数
214/// 空间:M ∈ {4,6,8,10,12,14,16},nonce 7..=13 字节)。
215///
216/// M=4/6 的完整性标签弱于 RFC 3610 作者建议(≥ 8),仅为兼容既有
217/// 协议提供;TLS 批准套件面(SP 800-52r2)只使用 M=16(见模块头)。
218#[derive(Clone)]
219pub struct Aes128CcmAny {
220    aes: Aes128,
221    tag_len: usize,
222}
223
224impl Aes128CcmAny {
225    /// 密钥字节数。
226    pub const KEY_LEN: usize = 16;
227
228    /// 本算法在 FIPS 140-3 下的批准状态:SP 800-38C 对全部受支持
229    /// M 值批准(TLS 批准套件面不含 M=8,见模块头)。
230    pub const APPROVAL: crate::Approval = crate::Approval::Approved;
231
232    /// 构造:`tag_len` 必须 ∈ {4,6,8,10,12,14,16},否则返回
233    /// [`Error::InvalidInput`](crate::Error)。
234    pub fn new(key: &[u8; 16], tag_len: usize) -> Result<Self, crate::Error> {
235        if !(4..=16).contains(&tag_len) || !tag_len.is_multiple_of(2) {
236            return Err(crate::Error::InvalidInput);
237        }
238        Ok(Self {
239            aes: Aes128::new(key),
240            tag_len,
241        })
242    }
243
244    /// 加密:返回 `密文 || 标签`(标签 `tag_len` 字节)。
245    ///
246    /// `nonce` 长度必须为 7..=13 字节;长度域限制见内部 `check_length_domain`。
247    pub fn seal(
248        &self,
249        nonce: &[u8],
250        aad: &[u8],
251        plaintext: &[u8],
252    ) -> Result<Vec<u8>, crate::Error> {
253        seal_core(&self.aes, nonce, self.tag_len, aad, plaintext)
254    }
255
256    /// 解密并验证;失败统一返回
257    /// [`Error::VerificationFailed`](crate::Error::VerificationFailed)。
258    pub fn open(
259        &self,
260        nonce: &[u8],
261        aad: &[u8],
262        ct_and_tag: &[u8],
263    ) -> Result<Vec<u8>, crate::Error> {
264        open_core(&self.aes, nonce, self.tag_len, aad, ct_and_tag)
265    }
266}
267
268impl std::fmt::Debug for Aes128CcmAny {
269    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270        f.debug_struct("Aes128CcmAny")
271            .field("tag_len", &self.tag_len)
272            .finish()
273    }
274}
275
276// ---------------------------------------------------------------------------
277// 固定参数集类型(nonce 长度类型化;委托运行时引擎)
278// ---------------------------------------------------------------------------
279
280/// 生成一个固定 nonce/标签长度的 CCM 实例类型(薄包装,委托
281/// [`Aes128CcmAny`] 的引擎函数)。
282macro_rules! ccm_impl {
283    ($name:ident, $nonce_len:expr, $tag_len:expr, $doc:expr) => {
284        #[doc = $doc]
285        #[derive(Clone)]
286        pub struct $name {
287            any: Aes128CcmAny,
288        }
289
290        impl $name {
291            /// 密钥字节数。
292            pub const KEY_LEN: usize = 16;
293            /// nonce 长度(L = 15 − nonce_len)。
294            pub const NONCE_LEN: usize = $nonce_len;
295            /// 标签字节数。
296            pub const TAG_LEN: usize = $tag_len;
297
298            /// 本算法在 FIPS 140-3 下的批准状态。
299            pub const APPROVAL: crate::Approval = crate::Approval::Approved;
300
301            /// 展开密钥。
302            pub fn new(key: &[u8; 16]) -> Self {
303                Self {
304                    // 参数为编译期常量,构造必不失败
305                    any: Aes128CcmAny::new(key, $tag_len).expect("fixed parameter set"),
306                }
307            }
308
309            /// 加密:返回 `密文 || 标签`。
310            ///
311            /// 长度域限制(RFC 3610 §2.2):明文长度必须 < 2^(8L)、AAD
312            /// 长度必须 < 2^16 − 2^8(两字节长度编码上限),超限返回
313            /// [`Error::InvalidInput`](crate::Error)(规范要求的显式拒绝,
314            /// 不得按位截断后继续)。
315            pub fn seal(
316                &self,
317                nonce: &[u8; $nonce_len],
318                aad: &[u8],
319                plaintext: &[u8],
320            ) -> Result<Vec<u8>, crate::Error> {
321                self.any.seal(nonce, aad, plaintext)
322            }
323
324            /// 解密并验证;失败统一返回
325            /// [`Error::VerificationFailed`](crate::Error::VerificationFailed)。
326            pub fn open(
327                &self,
328                nonce: &[u8; $nonce_len],
329                aad: &[u8],
330                ct_and_tag: &[u8],
331            ) -> Result<Vec<u8>, crate::Error> {
332                self.any.open(nonce, aad, ct_and_tag)
333            }
334        }
335
336        impl std::fmt::Debug for $name {
337            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
338                f.write_str(stringify!($name))
339            }
340        }
341    };
342}
343
344ccm_impl!(
345    Aes128Ccm,
346    13,
347    16,
348    "AES-128-CCM 实例(M=16,13 字节 nonce,L=2)。"
349);
350ccm_impl!(
351    Aes128CcmTls,
352    12,
353    16,
354    "AES-128-CCM 实例(M=16,12 字节 nonce,L=3)——RFC 8446 §B.5 TLS 1.3 参数集。"
355);
356ccm_impl!(
357    Aes128Ccm8Tls,
358    12,
359    8,
360    "AES-128-CCM 实例(M=8,12 字节 nonce,L=3)——RFC 8446 §B.5 的 \
361     AEAD_AES_128_CCM_8;8 字节标签不在 SP 800-52r2 TLS 批准套件面。"
362);
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn round_trip_and_tamper() {
370        let key = [0x07u8; 16];
371        let nonce13 = [
372            0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
373        ];
374        let aead = Aes128Ccm::new(&key);
375
376        for len in [0usize, 1, 15, 16, 17, 33, 64] {
377            let pt: Vec<u8> = (0..len).map(|i| i as u8).collect();
378            let sealed = aead.seal(&nonce13, b"aad", &pt).unwrap();
379            assert_eq!(sealed.len(), len + 16);
380            let opened = aead.open(&nonce13, b"aad", &sealed).expect("round trip");
381            assert_eq!(opened, pt, "len {len}");
382        }
383
384        let sealed = aead.seal(&nonce13, b"aad", b"hello ccm").unwrap();
385        let mut bad = sealed.clone();
386        let last = bad.len() - 1;
387        bad[last] ^= 1;
388        assert_eq!(
389            aead.open(&nonce13, b"aad", &bad),
390            Err(crate::Error::VerificationFailed)
391        );
392        assert_eq!(
393            aead.open(&nonce13, b"bad", &sealed),
394            Err(crate::Error::VerificationFailed)
395        );
396
397        // TLS 参数集(nonce 12 / L=3):同往返回归
398        let nonce12 = [
399            0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
400        ];
401        let tls = Aes128CcmTls::new(&key);
402        for len in [0usize, 1, 15, 16, 17, 33, 64] {
403            let pt: Vec<u8> = (0..len).map(|i| i as u8).collect();
404            let sealed = tls.seal(&nonce12, b"aad", &pt).unwrap();
405            assert_eq!(sealed.len(), len + 16);
406            let opened = tls.open(&nonce12, b"aad", &sealed).expect("round trip");
407            assert_eq!(opened, pt, "tls len {len}");
408        }
409        // 两参数集对同一 (key, 数据) 输出必须不同(nonce/L 均不同)
410        assert_ne!(
411            aead.seal(&nonce13, b"aad", b"x").unwrap(),
412            tls.seal(&nonce12, b"aad", b"x").unwrap()
413        );
414
415        // CCM_8 TLS 参数集:标签 8 字节,往返 + 与 Any(M=8) 输出一致
416        let ccm8 = Aes128Ccm8Tls::new(&key);
417        let any8 = Aes128CcmAny::new(&key, 8).unwrap();
418        for len in [0usize, 1, 15, 16, 17, 33, 64] {
419            let pt: Vec<u8> = (0..len).map(|i| i as u8).collect();
420            let sealed = ccm8.seal(&nonce12, b"aad", &pt).unwrap();
421            assert_eq!(sealed.len(), len + 8);
422            let opened = ccm8.open(&nonce12, b"aad", &sealed).expect("round trip");
423            assert_eq!(opened, pt, "ccm8 len {len}");
424            assert_eq!(
425                sealed,
426                any8.seal(nonce12.as_slice(), b"aad", &pt).unwrap(),
427                "fixed Ccm8Tls must match Any(M=8), len {len}"
428            );
429        }
430
431        // 长度域拒绝(RFC 3610 §2.2):L=2 明文上限 65535 字节;
432        // AAD 两字节编码上限 0xff00。L=3 上限为 2^24 字节,端到端构造
433        // 过慢,经由 open 的长度域检查覆盖(校验先于任何 AES 运算)。
434        let big = vec![0u8; 1 << 16];
435        assert_eq!(
436            aead.seal(&nonce13, b"", &big),
437            Err(crate::Error::InvalidInput)
438        );
439        assert_eq!(
440            aead.seal(&nonce13, &[0u8; 0xff00], &[0u8; 16]),
441            Err(crate::Error::InvalidInput)
442        );
443        assert_eq!(
444            tls.open(&nonce12, b"", &vec![0u8; 16 + (1 << 24)]),
445            Err(crate::Error::VerificationFailed)
446        );
447    }
448
449    /// 全参数 API 的参数校验面(RFC 3610 §2 的合法/非法边界)。
450    #[test]
451    fn any_param_validation() {
452        let key = [0x11u8; 16];
453        // 非法 tag_len:奇数、<4、>16
454        for bad in [2usize, 18, 9, 5, 0] {
455            assert!(
456                matches!(
457                    Aes128CcmAny::new(&key, bad),
458                    Err(crate::Error::InvalidInput)
459                ),
460                "tag_len {bad} must be rejected"
461            );
462        }
463        // 全部 7 个合法 M 可构造
464        for m in [4usize, 6, 8, 10, 12, 14, 16] {
465            assert!(Aes128CcmAny::new(&key, m).is_ok(), "tag_len {m}");
466        }
467
468        let any = Aes128CcmAny::new(&key, 8).unwrap();
469        // 非法 nonce 长度:6 / 14(L=1 为规范保留)
470        for bad_nonce in [vec![0u8; 6], vec![0u8; 14]] {
471            assert_eq!(
472                any.seal(&bad_nonce, b"", b"pt"),
473                Err(crate::Error::InvalidInput),
474                "nonce len {} must be rejected",
475                bad_nonce.len()
476            );
477            assert_eq!(
478                any.open(&bad_nonce, b"", &[0u8; 24]),
479                Err(crate::Error::InvalidInput),
480                "nonce len {} must be rejected on open",
481                bad_nonce.len()
482            );
483        }
484        // 全 nonce 长度面 7..=13 往返一致
485        for n in 7usize..=13 {
486            let nonce = vec![0xa0u8; n];
487            let sealed = any.seal(&nonce, b"aad", b"payload").unwrap();
488            assert_eq!(sealed.len(), 7 + 8);
489            assert_eq!(any.open(&nonce, b"aad", &sealed).unwrap(), b"payload");
490        }
491        // 过短 ct+tag(< M)在 open 上统一 VerificationFailed
492        let nonce12 = [0u8; 12];
493        assert_eq!(
494            any.open(&nonce12, b"", &[0u8; 7]),
495            Err(crate::Error::VerificationFailed)
496        );
497    }
498}