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有:

1. 概念

在密码学中,消息认证码(英语:Message authentication code,缩写为MAC),又译为消息鉴别码文件消息认证码讯息鉴别码信息认证码,是经过特定算法后产生的一小段信息,检查某段消息的完整性,以及作身份验证。它可以用来检查在消息传递过程中,其内容是否被更改过,不管更改的原因是来自意外或是蓄意攻击。同时可以作为消息来源的身份验证,确认消息的来源。

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

1.1 消息认证码的实现

消息认证码的算法中,通常会使用带密钥的散列函数(HMAC),或者块密码

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

Alice认为既然同样持有密钥,对称加密兼顾了消息验证码的功能,只要解密出明文,就可以作为消息验证码使用,Alice想的对吗?

  1. 对称加密明文和密文长度是一致的,如果是一个加解密来确认,对于传输是有负担的,而消息认证码只有几个字节,方便传输;

  2. 如果使用ECB模式,明文块和块之间没有依赖,中间攻击者很可能删除一个完整块或做其他手脚,导致收到信息残缺,此时完整性已经被破坏,这是无法容忍的。

  3. 对于一些随机数或者密钥作为明文(人不可读的信息)的加密,当另一方解密的时候我们很难通过主观断言这就是我们想要的明文,因为对称解密解密错误也是输出一些人类无法识别的数据信息。

因此, Alice考虑的不完全正确。

1.2 认证加密

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

消息认证码我们将在 3.3_Security_对称密钥算法之AEAD 来详细说明,主要包含GCM/GMAC之类的。

1.3 对MAC的攻击

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

1.3.1 重放攻击

重放攻击(英语:replay attack,或称为回放攻击)是一种恶意或欺诈的重复或延迟有效数据的网络攻击形式。 这可以由发起者或由拦截数据并重新传输数据的对手来执行。这是“中间人攻击”的一个较低级别版本。

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

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

  • 使用序号。约定每次发送消息赋予一个递增的序号,在计算MAC的时候将序号值也包含在内。这样Mallory无法计算序号变换的MAC,就没有办法进行攻击了。但这样会增加两人的通信成本,需要共同协商和维护序号。

  • 使用时间戳。约定在发送消息的时候包含当前的时间,如果接到以前的消息,即便是MAC正确也将其作为错误的消息处理,这样就能抵御重放攻击。但这样需要进行时间同步,而且需要考虑到通信的延迟,必须有一定的窗口期,而在窗口期内也是有被重放攻击的风险的。

  • 加扰nonce。约定在通信之前Alice向Bob发送一个随机数,之后Alice会加入这个随机数的MAC计算,Bob也同样做这样的计算。代价就是,增加了通信开销。

1.3.2 推测密钥

和单向散列函数攻击一样,对消息认证码也可以进行暴力破解以及生日攻击。对于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,杂凑讯息鉴别码),是一种通过特别计算方式之后产生的讯息鉴别码(MAC),使用密码杂凑函数,同时结合一个加密金钥。它可以用来保证资料的完整性,同时可以用来作某个讯息的身份验证

2.1 HMAC过程

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

根據RFC 2104,HMAC的數學公式為:

其中:

  • H为密码杂凑函数(如SHA家族)

  • K为密钥(secret key)

  • m是要认证的消息

  • K'是从原始密钥K导出的另一个秘密密钥(如果K短于散列函数的输入块大小,则向右填充(Padding)零;如果比该块大小更长,则对K进行散列)

  • || 代表串接

  • ⊕ 代表异或(XOR)

  • opad 是外部填充(0x5c5c5c…5c5c,一段十六进制常量)

  • ipad 是内部填充(0x363636…3636,一段十六进制常量)

进行处理的过程,我们可以从:

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

2.2 mbedtls示例

我们以HMAC384为例,使用mbedtls来完成这个例子:

https://github.com/carloscn/cryptography/blob/master/modules/digest/src/mbedtls_hmac.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

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的构造方法如下:

CBC-MACCBC操作模式是相似的但还是存在一些重要的区别:

  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:

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

OMAC和AES的关系是:

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

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

最后更新于