一文读懂 CCM
(使用分组密码连接-消息认证码的计数器)
CCM加密认证
认证加密
对于消息M, 要同时提供认证和加密的话,有四种通用的办法。
- 「先哈希后加密(H->E)」: 首先对于原始的消息M进行哈希处理得到
h = H(M)
, 然后将原始的消息和哈希值一起加密E(K, M || h)
- 「先认证后加密(A->E)」: 这个方案有两个密钥,首先用到之前讲过的消息验证码MAC, 计算得到T=MAC(, M)对明文进行认证,然后将消息和MAC一起加密: E(, M || T)
- 「先加密后认证(E->A)」: 这个方案同样采用了两个密钥,首先加密消息得到密文C=E(, M), 然后对密文计算MAC值,T = MAC(, C), 最后通过(T, C)来进行认证
- 「独立进行加密认证(A + E)」: 这个方案也采用两个密钥,加密消息得到C = E(, M), 然后对明文计算MAC值,T = MAC(, M), 最后通过(T, C)来进行认证,对于这种方案,显然这两个操作的顺序是无关的
CCM
CCM
「分组密码连接-消息认证吗的计数器(CCM)」, 工作模式由NIST作为标准提出,用于保护IEEE802.11 WiFi无线局域网的安全,也可以用在任何需要加密和认证的场合,组成CCM的关键算法是AES算法,对于AES算法,在这里就不过多阐述了,之前的文章也讲过,然后还有CTR工作模式,如果对于这个工作模式不熟悉的读者,同样可以参考我之前的文章,或者自行查阅相关的资料,最后这个方法还用到了CMAC, 同样的,CMAC也在我之前的文章中介绍过,哈哈,可以说,这一篇文章如果之前都能理解前面所介绍的知识,理解这一篇文章还是相对来说比较容易的。
算法输入
对于CCM的处理过程,输入主要包括三个部分
- 将要被认证或者加密的数据,即明文消息
- 需要被认证,但是不需要被加密的数据,比如说,对于一个IP报文,报文头部是需要用明文传输的,因此不能被加密,但是需要对这些消息进行一个认证
- 随机变量N, 这个用于防止重放攻击等其他攻击
算法过程
前文说道过,CCM这个方法是A+E模式的,因此有两部分,首先来看一下对于加密的过程:
CCM-认证过程
来具体看一下,认证过程是如何进行操作的,首先要构造, 如下图所示:
认证B0
简单解释一下,对于对一个分块的第一个字节,是一个Flag, 第一位是保留,第二位是标识有没有额外的不需要加密的数据,后面三个字节是MAC的长度,注意这里是(MAC的长度-2) / 2之后的值,根据RFC里面的描述,这个值是可以取4, 6, 8, 10, 12, 14, or 16字节的,后面我代码实现的时候偷懒了,提前说一下,嘻嘻,不许说我懒,然后最后面是明文长度所占用的字节数,这里明文越长,相应的Nonce也会越短,这就要权衡一下了。
- 首先根据上图拼接消息然后对消息进行一个分组,得到, ...,
- 对于分组之后的消息计算CMAC, 最终MAC输出的长度根据前面提到过的方法得到 T = CMAC( || ... || )
到这里,认证阶段实际上就完成了,接下来看一下加密的过程:
加密过程
这个加密过程也比较简单,就是简单CBC模式,注意下一CTR0的生成,还要就是第一个产生的序列是不参与明文加密的,或者这么理解,这个加密的是上面输出的MAC。
CTR0
好了,到这里整个CCM的认证加密过程就介绍完了。
解密和认证
上文介绍了如何进行加密和认证,后面来简单说一下如何进行解密和认证
- 如果CLen < Tlen, 这很显然,是不合理的情况,直接返回认证失败
- 执行计数器生成函数,初始计数器和上文保持一致得到, ...,
- 然后对消息进行CTR模式的解密,得到明文,到这里解密阶段完成, 这里最后一个分组解密之后即为消息认证码,注意这个分组需要和Ctr0加密之后的结果异或得到T
- 按照上面加密过程重新拼接成需要认证的消息,同样得到, ..., , 同样计算CMAC得到
- 如果, 则认证失败
代码实现
注意,这个代码仅仅作为学习使用,请勿直接用于生产用途,个人能力有限,代码难免写的不太优雅,还请各位读者大佬海涵。
use aes::AES; use std::convert::TryInto; pub struct AES128CCM { key: [u8; 16], } fn u16to8(p: u16) -> [u8; 2] { return [(p >> 8) as u8, (p >> 0) as u8]; } fn block_xor(a: &[u8], b: &[u8]) -> [u8; 16] { let mut out = [0u8; 16]; for i in 0..16 { out[i] = a[i] ^ b[i]; } out } fn block_xor_8(a: &[u8], b: &[u8]) -> [u8; 8] { let mut out = [0u8; 8]; for i in 0..8 { out[i] = a[i] ^ b[i]; } out } fn block_add_one(a: &mut [u8]) { let mut carry = 1; for i in 0..16 { let (t, c) = a[15 - i].overflowing_add(carry); a[15 - i] = t; if !c { return; } carry = c as u8; } } impl AES128CCM { pub fn new(key: [u8; 16]) -> Self { Self { key, } } pub fn cbc_mac(&self, iv: [u8; 13], add: &[u8], message: &[u8]) -> Vec<u8> { let mut result = vec![]; result.push(0x0); let add_len = add.len(); let tag_len = 8usize; let iv_len = iv.len(); result[0] = 0; result[0] |= ((add_len as u8 > 0) as u8) << 6; result[0] |= ((tag_len as u8 - 2) / 2) << 3; result[0] |= (16 - 1 - iv_len as u8) - 1; // set iv for i in 0..13 { result.push(iv[i]); } let message_len = u16to8(message.len() as u16); for i in 0..2 { result.push(message_len[i]) } let adata_len = u16to8(add.len() as u16); for i in 0..2 { result.push(adata_len[i]); } for i in 0..add.len() { result.push(add[i]); } let remain = (add.len() - 2) % 16; for _ in 0..remain { result.push(0x0); } for i in 0..message.len() { result.push(message[i]); } let remain = 16 - (result.len()) % 16; for _ in 0..remain { result.push(0x0); } let mut aes = AES::new(&self.key); let mut output = aes.encrypt_block(&result[0..16].try_into().expect("")); for chunk in result[16..].chunks(16) { output = aes.encrypt_block(&block_xor(&output, chunk)); } let cbc_mac = output; let mut ctr = [0u8; 16]; ctr[0] = 0x01; for i in 1..14 { ctr[i] = iv[i - 1]; } ctr[14] = 0x00; ctr[15] = 0x00; let mut out: Vec<u8> = Vec::new(); let ctr_mac = aes.encrypt_block(&ctr); block_add_one(&mut ctr[..]); for chunk in message.chunks(16) { let enc = aes.encrypt_block(&ctr[..].try_into().expect("")); if chunk.len() == 16 { out.extend_from_slice(&block_xor(&enc, chunk)); block_add_one(&mut ctr[..]); } else { for (i, item) in chunk.iter().enumerate() { out.push(*item ^ enc[i]) } } } let digest = block_xor(&cbc_mac, &ctr_mac); let mut output = vec![]; for i in 0..add.len() { output.push(add[i]); } for i in 0..out.len() { output.push(out[i]); } for i in 0..8 { output.push(digest[i]); } output } pub fn verify(&self, ciphertext: &[u8], nonce: &[u8], adata: &[u8]) -> bool { let mut ctr = [0u8; 16]; ctr[0] = 0x01; for i in 1..14 { ctr[i] = nonce[i - 1]; } ctr[14] = 0x00; ctr[15] = 0x00; let mut out: Vec<u8> = Vec::new(); let mut aes = AES::new(&self.key); let ctr_mac = aes.encrypt_block(&ctr); block_add_one(&mut ctr[..]); for chunk in ciphertext.chunks(16) { let enc = aes.encrypt_block(&ctr[..].try_into().expect("")); // println!("enc = {:02x?}", enc); if chunk.len() == 16 { out.extend_from_slice(&block_xor(&enc, chunk)); block_add_one(&mut ctr[..]); } else { for (i, item) in chunk.iter().enumerate() { out.push(*item ^ enc[i]) } } } let t = block_xor_8(&ctr_mac, &ciphertext[ciphertext.len() - 8..]); let message = &out[..out.len() - 8]; let mut result = vec![]; result.push(0x0); let add_len = adata.len(); let tag_len = 8usize; let nonce_len = nonce.len(); result[0] = 0; result[0] |= ((add_len as u8 > 0) as u8) << 6; result[0] |= ((tag_len as u8 - 2) / 2) << 3; result[0] |= (16 - 1 - nonce_len as u8) - 1; // set nonce for i in 0..13 { result.push(nonce[i]); } let message_len = u16to8(message.len() as u16); for i in 0..2 { result.push(message_len[i]) } let adata_len = u16to8(adata.len() as u16); for i in 0..2 { result.push(adata_len[i]); } for i in 0..adata.len() { result.push(adata[i]); } let remain = (adata.len() - 2) % 16; for _ in 0..remain { result.push(0x0); } for i in 0..message.len() { result.push(message[i]); } let remain = 16 - (result.len()) % 16; for _ in 0..remain { result.push(0x0); } let mut output = aes.encrypt_block(&result[0..16].try_into().expect("")); for chunk in result[16..].chunks(16) { output = aes.encrypt_block(&block_xor(&output, chunk)); } let cbc_mac = output; let mut flag = true; for i in 0..8 { if cbc_mac[i] ^ t[i] != 0 { flag = false; } } flag } } #[cfg(test)] mod tests { use hex_literal::hex; use crate::{AES128CCM}; #[test] fn it_works() { // 例子来源于RFC3610 let key = hex!("C0C1C2C3C4C5C6C7C8C9CACBCCCDCECF"); let message: [u8; 23] = hex!("08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E"); let ccm = AES128CCM::new(key); let nonce: [u8; 13] = hex!("00 00 00 03 02 01 00 A0 A1 A2 A3 A4 A5"); let adata: [u8; 8] = hex!("00 01 02 03 04 05 06 07"); let output = ccm.cbc_mac(nonce, &adata, &message); println!("{:02x?}", output); // let output = hex!("00 01 02 03 04 05 06 07 58 8C 97 9A 61 C6 63 D2 // F0 66 D0 C2 C0 F9 89 80 6D 5F 6B 61 DA C3 84 17 // E8 D1 2C FD F9 26 E0"); // println!("{:?}", output); } #[test] fn test_verify_v1() { let key = hex!("C0C1C2C3C4C5C6C7C8C9CACBCCCDCECF"); let nonce: [u8; 13] = hex!("00 00 00 03 02 01 00 A0 A1 A2 A3 A4 A5"); let adata: [u8; 8] = hex!("00 01 02 03 04 05 06 07"); let output = hex!("58 8C 97 9A 61 C6 63 D2 F0 66 D0 C2 C0 F9 89 80 6D 5F 6B 61 DA C3 84 17 E8 D1 2C FD F9 26 E0"); let ccm = AES128CCM::new(key); let verify = ccm.verify(&output, &nonce, &adata); println!("{:?}", verify); } #[test] fn test_v2() { let key = hex!("C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF"); let message: [u8; 24] = hex!("08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F"); let ccm = AES128CCM::new(key); let nonce: [u8; 13] = hex!("00 00 00 04 03 02 01 A0 A1 A2 A3 A4 A5"); let adata: [u8; 8] = hex!("00 01 02 03 04 05 06 07"); let output = ccm.cbc_mac(nonce, &adata, &message); println!("{:02x?}", output); } }
小结
本文简单介绍了CCM模式下的认证和加密机制,实际上这个是AES-CTR模式和CMAC的一个组合,如果理解了前面这两个,本文应该还是比较好理解的。