【密码学】一文读懂CCM

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 本文简单介绍了CCM模式下的认证和加密机制,实际上这个是AES-CTR模式和CMAC的一个组合,如果理解了前面这两个,本文应该还是比较好理解的。

一文读懂 CCM (使用分组密码连接-消息认证码的计数器)


(K(%P@7K`J(Q)TKUP96VC7S.jpgCCMXIM`JQ_RXL7T}G28PY1O16E.png加密认证


认证加密

对于消息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

4(~L9N]SBZGK3~ZDK[MF_69.png

image.gifCCM

「分组密码连接-消息认证吗的计数器(CCM)」, 工作模式由NIST作为标准提出,用于保护IEEE802.11 WiFi无线局域网的安全,也可以用在任何需要加密和认证的场合,组成CCM的关键算法是AES算法,对于AES算法,在这里就不过多阐述了,之前的文章也讲过,然后还有CTR工作模式,如果对于这个工作模式不熟悉的读者,同样可以参考我之前的文章,或者自行查阅相关的资料,最后这个方法还用到了CMAC, 同样的,CMAC也在我之前的文章中介绍过,哈哈,可以说,这一篇文章如果之前都能理解前面所介绍的知识,理解这一篇文章还是相对来说比较容易的。

算法输入

对于CCM的处理过程,输入主要包括三个部分

  1. 将要被认证或者加密的数据,即明文消息
  2. 需要被认证,但是不需要被加密的数据,比如说,对于一个IP报文,报文头部是需要用明文传输的,因此不能被加密,但是需要对这些消息进行一个认证
  3. 随机变量N, 这个用于防止重放攻击等其他攻击

算法过程

前文说道过,CCM这个方法是A+E模式的,因此有两部分,首先来看一下对于加密的过程:

_5QC7(LC@X(3W0_$[2HMK_5.pngCCM-认证过程

来具体看一下,认证过程是如何进行操作的,首先要构造, 如下图所示:

$XNLERW}}T{%I)H91%[VV01.png认证B0

简单解释一下,对于对一个分块的第一个字节,是一个Flag, 第一位是保留,第二位是标识有没有额外的不需要加密的数据,后面三个字节是MAC的长度,注意这里是(MAC的长度-2) / 2之后的值,根据RFC里面的描述,这个值是可以取4, 6, 8, 10, 12, 14, or 16字节的,后面我代码实现的时候偷懒了,提前说一下,嘻嘻,不许说我懒,然后最后面是明文长度所占用的字节数,这里明文越长,相应的Nonce也会越短,这就要权衡一下了。

  1. 首先根据上图拼接消息然后对消息进行一个分组,得到, ...,
  2. 对于分组之后的消息计算CMAC, 最终MAC输出的长度根据前面提到过的方法得到 T = CMAC( || ... || )

到这里,认证阶段实际上就完成了,接下来看一下加密的过程:

EU3)X~MUT1Y77XJ[O{[8O79.png

image.gif加密过程

这个加密过程也比较简单,就是简单CBC模式,注意下一CTR0的生成,还要就是第一个产生的序列是不参与明文加密的,或者这么理解,这个加密的是上面输出的MAC。

3$YA288UO[KZ9%_K2HR3%TT.png

image.gifCTR0

好了,到这里整个CCM的认证加密过程就介绍完了。

解密和认证

上文介绍了如何进行加密和认证,后面来简单说一下如何进行解密和认证

  1. 如果CLen < Tlen, 这很显然,是不合理的情况,直接返回认证失败
  2. 执行计数器生成函数,初始计数器和上文保持一致得到, ...,
  3. 然后对消息进行CTR模式的解密,得到明文,到这里解密阶段完成, 这里最后一个分组解密之后即为消息认证码,注意这个分组需要和Ctr0加密之后的结果异或得到T
  4. 按照上面加密过程重新拼接成需要认证的消息,同样得到, ..., , 同样计算CMAC得到
  5. 如果, 则认证失败


代码实现

注意,这个代码仅仅作为学习使用,请勿直接用于生产用途,个人能力有限,代码难免写的不太优雅,还请各位读者大佬海涵。

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的一个组合,如果理解了前面这两个,本文应该还是比较好理解的。

相关文章
|
存储 Rust 并行计算
【密码学】一文读懂XTS模式
这篇文章的灵感来源于我偶然翻到的一个某U盘有关磁盘加密的一个介绍(这一篇不是广告蛤), 然后发现这个模式我之前还真没遇到过,因此呢,就学习了一下,就出来了这一篇文章。
5266 0
【密码学】一文读懂XTS模式
|
Rust 算法 网络安全
【密码学】一文读懂CMAC
介于上一篇文章比较水,然后这个和上一篇也比较相似,CMAC是为了解决DAA当中安全性不足的问题而出现的,这个算法一共有三个密钥,K, K1, K2, 其中K1和K2可以由K导出,接下来就来一起看一下CMAC的具体过程吧,这一篇文章其实也不长。
3786 0
【密码学】一文读懂CMAC
|
Rust 算法 安全
【密码学】一文读懂MurMurHash2
上次我们聊过了一代的MurMurHash算法,是的,我又来水文章了,今天呢,接着来聊一下二代的MurMurHash算法,二代算法的整体结构实际上和一代算法差不太多,只是对于每一轮数据的处理过程当中的运算有一些差异,算法的来源依然是来自于Google官网给提供的源码,对着源码看的结构,对于这个算法呢,有两个版本,一个是32位的,一个是64位的,对于32位的算法和64位的算法,区别在于两个初始的魔数不同,整体运算过程还是十分相似的。
2224 0
【密码学】一文读懂MurMurHash2
|
算法 数据安全/隐私保护
【密码学】一文读懂Whirlpool
首先呢,祝大家今晚节日快乐,Whirlpool是由Vincent Rijmen(高级加密标准的联合创始人)和Paulo S.L.M.Barreto设计的,后者于2000年首次提出了它。
1119 0
【密码学】一文读懂Whirlpool
|
Web App开发 Rust 算法
【密码学】一文读懂ChaCha20
好久没写新的加密算法的原理了, 这次所选取的加密算法结构比较简单, 一起来看一下吧。
7809 0
【密码学】一文读懂ChaCha20
|
Rust 算法 Go
【密码学】一文读懂MurMurHash3
本文应该是MurMurHash算法介绍的最后一篇,来一起看一下最新的MurMurHash算法的具体过程,对于最新的算法来说,整个流程和之前的其实也比较相似,这里从维基百科当中找到了伪代码,也就不贴出来Google官方给出的推荐代码了,先来看一下维基百科给出的伪代码,这里只有32位的伪代码。
2191 0
【密码学】一文读懂MurMurHash3
|
Rust 算法 数据安全/隐私保护
【密码学】一文读懂XTEA加密
本篇文章,我们来看一下上一次讲过的TEA加密算法的一个升级版XTEA, 相比于TEA, XTEA的安全性显然是更高的,其中的过程要比TEA稍微复杂一点点。
1285 0
【密码学】一文读懂XTEA加密
|
算法 安全 Go
【密码学】一文读懂HKDF
我这又来水一篇文章,来聊一下HKDF(基于HMAC的密钥导出函数)。密钥派生函数是密钥管理的组成部分,他的目标是通过一些初始的数据派生出来密码学安全的随机密钥。
3120 1
【密码学】一文读懂HKDF
|
存储 安全 算法
为什么人人都要懂点密码学
人类进入二十一世纪以来,随着计算机和移动设备的普及高速发展,我们的社会已经高度信息化,为了防止信息被窃取、修改,就需要对信息的存储、传递进行加密处理,而加密就需要使用到加密算法,解密需要使用密码才可以看到原文。
236 1
|
Rust NoSQL 算法
【密码学】一文读懂SipHash
在redis的源码当中也有给出siphash(https://github.com/redis/redis/blob/unstable/src/siphash.c)的实现,这里直接给出链接,不贴出来具体的代码了,接下来,我们来看一下这个算法具体的结构。
1662 0
【密码学】一文读懂SipHash