> For the complete documentation index, see [llms.txt](https://carloss-organization-4.gitbook.io/tech/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://carloss-organization-4.gitbook.io/tech/tech/security/crypto/3.2security-dui-chen-mi-yao-suan-fa-zhi-maccmachmac.md).

# 3.2\_Security\_对称密钥算法之MAC（CMAC/HMAC）

https\://github.com/carloscn/blog/issues/144

## 3.2\_Security\_对称密钥算法之MAC（CMAC + HMAC）

Digest仅仅能够验证信息的**完整性**（Integrity），而没有办法确认信息的**身份认证**（Authetication）。例如，Alice收到一则需要Bob汇款1000元的消息，同时收到汇款消息的hash值，Alice可以计算hash确认1000元这个消息没有被篡改，但问题是，这则消息不是由Bob发的呢？因此就需要更高明的方法来帮助确认这个消息没有被篡改，还要确认这个消息是Bob发的。

消息认证有着广泛的应用，例如：

* **SWIFT**：环球银行金融电信协会。银行和银行之间通过SWIFT来传递交易消息，而消息的完整性及对消息的验证都是通过消息验证码。
* **IPSec**：互联网基本通信息协议，对通信内容认证和校验都是采用消息验证码。
* **SSL/TLS**：通信内容的认证和完整性校验都是通过消息验证码。

常用的MAC有：

* [CBC-MAC](https://en.wikipedia.org/wiki/CBC-MAC)
* [DAA](https://en.wikipedia.org/wiki/Data_Authentication_Algorithm)
* [GMAC](https://en.wikipedia.org/wiki/Galois_Message_Authentication_Code)
* [HMAC](https://en.wikipedia.org/wiki/HMAC)
* [NMAC](https://en.wikipedia.org/wiki/NMAC)
* [OMAC/CMAC](https://en.wikipedia.org/wiki/One-key_MAC)
* [PMAC](https://en.wikipedia.org/wiki/PMAC_\(cryptography\))
* [Poly1305](https://en.wikipedia.org/wiki/Poly1305)
* [SipHash](https://en.wikipedia.org/wiki/SipHash)
* [UMAC](https://en.wikipedia.org/wiki/UMAC)
* [VMAC](https://en.wikipedia.org/wiki/VMAC)

## 1. 概念

在密码学中，**消息认证码**（英语：Message authentication code，缩写为MAC），又译为**消息鉴别码**、**文件消息认证码**、**讯息鉴别码**、**信息认证码**，是经过特定算法后产生的一小段信息，检查某段消息的[完整性](https://zh.m.wikipedia.org/wiki/%E5%AE%8C%E6%95%B4%E6%80%A7)，以及作[身份验证](https://zh.m.wikipedia.org/wiki/%E8%BA%AB%E4%BB%BD%E9%AA%8C%E8%AF%81)。它可以用来检查在消息传递过程中，其内容是否被更改过，不管更改的原因是来自意外或是蓄意攻击。同时可以作为消息来源的[身份验证](https://zh.m.wikipedia.org/wiki/%E8%BA%AB%E4%BB%BD%E9%AA%8C%E8%AF%81)，确认消息的来源。

这里是一个消息认证码的实例，发送消息的人把**明文消息**和**密钥**送入MAC模块中会得到MAC，接收的人使用相同的密钥和明文消息进行计算，然后对MAC值进行对比，就可以确定这个消息是不是发送方发的。需要注意，**两方确认身份的凭证和对称加密算法一样，都是通过共享密钥来确认身份的**，所以在MAC里面也存在和SYM加解密一样的问题——**密钥配送的问题**（例如，公钥密码、Diffie-Hellman密钥交换、密钥分配中心等）

<div align="center"><img src="https://raw.githubusercontent.com/carloscn/images/main/typora20221027121621.png" alt="" width="85%"></div>

### 1.1 消息认证码的实现

消息认证码的算法中，通常会使用[带密钥的散列函数](https://zh.m.wikipedia.org/wiki/%E9%87%91%E9%91%B0%E9%9B%9C%E6%B9%8A%E8%A8%8A%E6%81%AF%E9%91%91%E5%88%A5%E7%A2%BC)（HMAC），或者[块密码](https://zh.m.wikipedia.org/wiki/%E5%A1%8A%E5%AF%86%E7%A2%BC)

SHA-2之类的单向散列函数可以实现消息认证码，其中一种实现方法称为HMAC。使用AES之类的分组密码也可以实现消息认证码，将密钥作为消息认证码的共享密钥，使用固定的IV值，并用CBC模式将消息全部加密。

> Alice认为既然同样持有密钥，对称加密兼顾了消息验证码的功能，只要解密出明文，就可以作为消息验证码使用，Alice想的对吗？
>
> 1. 对称加密明文和密文长度是一致的，如果是一个加解密来确认，对于传输是有负担的，而消息认证码只有几个字节，方便传输；
> 2. 如果使用ECB模式，明文块和块之间没有依赖，中间攻击者很可能删除一个完整块或做其他手脚，导致收到信息残缺，此时完整性已经被破坏，这是无法容忍的。
> 3. 对于一些随机数或者密钥作为明文（人不可读的信息）的加密，当另一方解密的时候我们很难通过主观断言这就是我们想要的明文，因为对称解密解密错误也是输出一些人类无法识别的数据信息。
>
> 因此， Alice考虑的不完全正确。

### 1.2 认证加密

**信息鉴别码不能提供对信息的保密，若要同时实现保密认证，同时需要对信息进行加密**。2000年后，关于认证加密逐渐展开（AE或者AEAD）。认证加密是一种将对称密码、消息认证码结合的的技术，因此，同时满足了机密性、完整性和认证三大功能。

消息认证码我们将在 [3.3\_Security\_对称密钥算法之AEAD](https://github.com/carloscn/blog/issues/145) 来详细说明，主要包含GCM/GMAC之类的。

### 1.3 对MAC的攻击

利用MAC的一些特性，可以衍生出一些攻击，还需要一些其他手段来抵御这些攻击，提高可靠性和安全性。

#### 1.3.1 重放攻击

**重放攻击**（英语：replay attack，或称为**回放攻击**）是一种恶意或欺诈的重复或延迟有效数据的[网络](https://zh.wikipedia.org/wiki/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C)攻击形式。 这可以由发起者或由拦截数据并重新传输数据的[对手](https://zh.wikipedia.org/w/index.php?title=%E5%AF%B9%E6%89%8B_\(%E5%AF%86%E7%A0%81%E5%AD%A6\)\&action=edit\&redlink=1)来执行。这是“[中间人攻击](https://zh.wikipedia.org/wiki/%E4%B8%AD%E9%97%B4%E4%BA%BA%E6%94%BB%E5%87%BB)”的一个较低级别版本。

<div align="center"><img src="https://raw.githubusercontent.com/carloscn/images/main/typora20221027131531.png" alt="" width="85%"></div>

Bob在接受多个重放攻击之后，导致Bob把自己的钱全部转给了Alice，而Alice就会很迷惑了。

对于抵抗重放攻击，我们通常有以下手段，本质上都是让每次的MAC值发生变化，即便是Mallory拿到了某一次的MAC值，也无法进行重放攻击。

* **使用序号**。约定每次发送消息赋予一个递增的序号，在计算MAC的时候将序号值也包含在内。这样Mallory无法计算序号变换的MAC，就没有办法进行攻击了。但这样会增加两人的通信成本，需要共同协商和维护序号。
* **使用时间戳**。约定在发送消息的时候包含当前的时间，如果接到以前的消息，即便是MAC正确也将其作为错误的消息处理，这样就能抵御重放攻击。但这样需要进行时间同步，而且需要考虑到通信的延迟，必须有一定的窗口期，而在窗口期内也是有被重放攻击的风险的。
* **加扰nonce**。约定在通信之前Alice向Bob发送一个随机数，之后Alice会加入这个随机数的MAC计算，Bob也同样做这样的计算。代价就是，增加了通信开销。

#### 1.3.2 推测密钥

和单向散列函数攻击一样，对消息认证码也可以进行暴力破解以及[生日攻击](https://zh.wikipedia.org/wiki/%E7%94%9F%E6%97%A5%E6%94%BB%E5%87%BB)。对于MAC来说，应保证不能根据MAC值推测出通信双方所使用的密钥。HMAC就是利用sha函数的单向性和抗碰撞性来保证无法根据MAC推测出密钥。

此外，密钥务必是由真随机数生成的，否则会被推测出来。

#### 1.3.3 MAC的局限性

因为双方共同持有相同的密钥，所以第三方证明无法确认谁是发送，谁是接受。也同样没有办法防止否认。

**第三方证明**

Bob再接收到Alice的请求汇款的消息的时候，Bob想向第三方Victor证明这条消息是由Alice发的，Bob是做不到的。因此Victor认为，因为密钥Bob和Alice同样持有，Bob可以自己编写一个假消息，然后做MAC计算。Victor可能会怀疑Bob说谎，而Bob也没有办法来证明。

**否认**

那同样的Alice真的发了消息，但是不承认自己发了，Alice说谎说是Bob自己编的消息然后算的MAC值，同样Bob也没办法来指出Alice在说谎。

因此，**消息验证码没有办法证明出两个持有密钥的人之间的消息传递。而这就要靠数字签名来完成**。

## 2. HMAC原理

**HMAC** (有时扩展为 英语：keyed-hash message authentication code, **金钥杂凑讯息鉴别码**, 或 英语：hash-based message authentication code，**杂凑讯息鉴别码**），是一种通过特别计算方式之后产生的[讯息鉴别码](https://zh.m.wikipedia.org/wiki/訊息鑑別碼)（MAC），使用[密码杂凑函数](https://zh.m.wikipedia.org/wiki/密碼雜湊函數)，同时结合一个加密金钥。它可以用来保证资料的完整性，同时可以用来作某个讯息的[身份验证](https://zh.m.wikipedia.org/wiki/身份验证)。

<div align="center"><img src="https://raw.githubusercontent.com/carloscn/images/main/typora20221105190219.png" alt="" width="60%"></div>

### 2.1 HMAC过程

HMAC有个比较大条的思路就是上面的图片，对消息做MD5或者HASH，求得单向散列，接着对单向散列的数据进行加密。思路是这样的，但是实际上比这个情况要复杂的多。

根據RFC 2104，HMAC的數學公式為：

<div align="center"><img src="https://raw.githubusercontent.com/carloscn/images/main/typora20221105190410.png" alt="" width="40%"></div>

其中：

* H为密码杂凑函数（如SHA家族）
* K为密钥（secret key）
* m是要认证的消息
* K'是从原始密钥K导出的另一个秘密密钥（如果K短于散列函数的输入块大小，则向右填充（Padding）零；如果比该块大小更长，则对K进行散列）
* || 代表串接
* ⊕ 代表异或（XOR）
* opad 是外部填充（0x5c5c5c…5c5c，一段十六进制常量）
* ipad 是内部填充（0x363636…3636，一段十六进制常量）

进行处理的过程，我们可以从：

<div align="center"><img src="https://raw.githubusercontent.com/carloscn/images/main/typora20221105190540.png" alt="" width="80%"></div>

KEY和IPAD和OPAD与message之间是有信息冗余的，然后经过两轮HASH算法之后得到的MAC，我们可以更清晰的从这个角度来看，message和key和i\_pad和o\_pad之间的信息冗余：

<div align="center"><img src="https://raw.githubusercontent.com/carloscn/images/main/typora20221105190732.png" alt="" width="80%"></div>

### 2.2 mbedtls示例

我们以HMAC384为例，使用mbedtls来完成这个例子：

<https://github.com/carloscn/cryptography/blob/master/modules/digest/src/mbedtls\\_hmac.c>

```C
int mbedtls_hmac_sha384(const unsigned char *key,
                        size_t key_byte_size,
                        const unsigned char *input,
                        size_t input_byte_size,
                        unsigned char output[48])
{

    int ret = 0, mret = 0;
    mbedtls_md_info_t *info = NULL;
    mbedtls_md_context_t ctx;

    if (key == NULL ||
        input == NULL ||
        output == NULL) {
        ret = ERROR_COMMON_INPUT_PARAMETERS;
        mbedtls_printf(" * input parameters error, returned %d, line: %d\n",
                       ret, __LINE__);
        goto finish;
    }

    if (key_byte_size == 0 ||
        input_byte_size == 0) {
        ret = ERROR_NONE;
        mbedtls_printf(" * key size or input size == 0, returned %d, line: %d\n",
                       ret, __LINE__);
        goto finish;
    }

    mbedtls_md_init(&ctx);

    info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA384);
    if (NULL == info) {
        ret = ERROR_CRYPTO_INIT_FAILED;
        mbedtls_printf(" * init failed, returned %d, line: %d\n",
                       ret, __LINE__);
        goto ctx_inited;
    }

    mret = mbedtls_md_setup(&ctx, info, 1);
    if (0 != mret) {
        ret = ERROR_CRYPTO_INIT_FAILED;
        mbedtls_printf(" * init failed, returned %d, line: %d\n",
                       ret, __LINE__);
        goto ctx_inited;
    }

    mret = mbedtls_md_hmac_starts(&ctx, key, key_byte_size);
    if (0 != mret) {
        ret = ERROR_CRYPTO_INIT_FAILED;
        mbedtls_printf(" * start failed, returned %d, line: %d\n",
                       ret, __LINE__);
        goto ctx_inited;
    }

    mret = mbedtls_md_hmac_update(&ctx, input, input_byte_size);
    if (0 != mret) {
        ret = ERROR_CRYPTO_ENCRYPT_FAILED;
        mbedtls_printf(" * update failed, returned %d, line: %d\n",
                       ret, __LINE__);
        goto ctx_inited;
    }

    mret = mbedtls_md_hmac_finish(&ctx, output);
    if (0 != mret) {
        ret = ERROR_CRYPTO_ENCRYPT_FAILED;
        mbedtls_printf(" * finish failed, returned %d, line: %d\n",
                       ret, __LINE__);
    }

ctx_inited:
    mbedtls_md_free(&ctx);
finish:
    return ret;
}
```

测试函数为：

<https://github.com/carloscn/cryptography/blob/master/testsuite/src/unit\\_test\\_mbedtls.c#L599>

```C
static void test_mbedtls_hmac_384(void)
{
    int ret = 0, i = 0;
    unsigned char key[] = {1,5,8,8,9,5,6,204,5,4,8,0,0,0,0};
    unsigned char input[] = {1,5,8,8,9,5,6,204,5,4,8,0,0,0,0};
    unsigned char output[48] = {0};

    ret = mbedtls_hmac_sha384(key,
                              sizeof(key)/sizeof(key[0]),
                              input,
                              sizeof(input)/sizeof(input[0]),
                              output);
    for (i = 0; i < 48; i ++) {
        mbedtls_printf("%x", output[i]);
    }
    mbedtls_printf("\n");
    CU_ASSERT_EQUAL(ret, 0);
}
```

## 3. CBC-MAC和CMAC

消息认证码还有另一大类就是CMAC（Cipher-Based Message Authentication Code，CMAC）。提到CMAC就不得不提一下CBC-MAC，CMAC是CBC-MAC的变体，CBC-MAC因为只能对固定长度的消息进行运算，因此存在安全问题，CMAC可以补足这个问题。CMAC最后的输出叫做tag。

### 3.1 CBC-MAC

**CBC-MAC**是在实践中应用非常广泛的standard message MAC。当message的长度固定时，CBC-MAC是安全的。CBC-MAC是最为广泛使用的消息认证算法之一，同时它也是一个ANSI标准（X9.17）。CBC-MAC实际上就是对消息使用CBC模式进行加密，取密文的最后一块作为认证码tag。

CBC的构造方法如下：

<div align="center"><img src="https://raw.githubusercontent.com/carloscn/images/main/typora20221108210554.png" alt="" width="80%"></div>

**CBC-MAC**和**CBC操作模式**是相似的但还是存在一些重要的区别：

1. CBC操作模式使用的初始化向量I V IVIV，这是其安全性的主要保证；但是CBC-MAC却没有用到IV（使用随机向量IV会变得不安全）。
2. 在CBC操作模式中所有的中间值都被输出作为密文的一部分；而CBC-MAC则只输出最终的tag 。如果CBC-MAC输出每一个则就不再安全。

由于有更好的MAC可以供选择，比如HMAC和OMAC，因此，CBC-MAC很难在一些密码库中看到身影。（至少mbedtls没有这个函数）

### 3.2 OMAC

**One-key MAC** (**OMAC**)是一种消息认证码。在定义上，OMAC分为OMAC1和OMAC2，其中OMAC1就是我们说的CMAC，这个定义是2005年NIST进行推荐的。

业界发现了CBC-MAC存在的一些安全问题（在消息不是等长的时候，破坏fixed -length），进而创建了密码型消息身份验证代码（Cipher-Based Message Authentication Code，CMAC）。CMAC提供与CBC-MAC相同类型的数据源身份验证和完整性，但在数学上更为安全。CMAC 是CBC-MAC 的一种变体，它被批准与AES和三重DES一起使用。

CMAC的核心也是借助CBC-MAC：

<div align="center"><img src="https://raw.githubusercontent.com/carloscn/images/main/typora20221109214933.png" alt="" width="100%"></div>

算法一共有三个密钥，K, K1, K2, 其中K1和K2可以由K导出。对于CMAC来说，有两种情况，第一种是数据长度恰好就是分组长度的整数倍，对于这种情况，使用K1和最后一个分组异或之后加密得出结果，另一种情况是数据长度不是分组的整数倍，这就要先padding到分组的整数倍，Padding方法是先添加1bit的1, 其余bit填充0, 直到数据满足分组的整数倍。

OMAC和AES的关系是：

<div align="center"><img src="https://raw.githubusercontent.com/carloscn/images/main/typora20221109215650.png" alt="" width="80%"></div>

mbedtls提供了CMAC的接口，可以使用CMAC接口来计算消息的tag。注，mbedtls需要在编译的时候使能CMAC。`CONFIG_MBEDTLS_MAC_CMAC_ENABLED`。

```C
int mbedtls_cmac_aes_128_ecb(const unsigned char *key, 
							 size_t key_byte_size,
                             const unsigned char *input, 
                             size_t input_byte_size,
                             unsigned char *output, 
                             size_t *output_byte_size)
{
    int rc = 0;     /* mbedtls layer error code */
    int ret = 0;    /* current layer error code */
    mbedtls_cipher_context_t ctx, *p_ctx = NULL;
    mbedtls_cipher_info_t *info = NULL;

    mbedtls_cipher_init(&ctx);
    p_ctx = &ctx;
    info = mbedtls_cipher_info_from_type(MBEDTLS_CIPHER_AES_128_ECB);
    rc = mbedtls_cipher_setup(p_ctx, info);

    rc = mbedtls_cipher_cmac_starts(p_ctx, key, 
								    BYTE_CONV_BITS_NUM(key_byte_size));

    rc = mbedtls_cipher_cmac_update(p_ctx, input, input_byte_size);

    rc = mbedtls_cipher_cmac_finish(p_ctx, output);

    *output_byte_size = mbedtls_cipher_get_key_bitlen(p_ctx)/8;

    if (NULL != p_ctx) {
        mbedtls_cipher_free(p_ctx);
    }
    return ret;
}
```

使用示例： <https://github.com/carloscn/cryptography/blob/master/modules/digest/src/mbedtls\\_cmac\\_exa.c>

## Ref

1. [CONFIG\_MBEDTLS\_MAC\_CMAC\_ENABLED](https://docs.zephyrproject.org/3.0.0/reference/kconfig/CONFIG_MBEDTLS_MAC_CMAC_ENABLED.html)
