【OpenSSL 之五】:HMAC算法分析

您所在的位置:网站首页 openssl实现aes图像加密 【OpenSSL 之五】:HMAC算法分析

【OpenSSL 之五】:HMAC算法分析

2024-07-12 13:53| 来源: 网络整理| 查看: 265

1. 写在前面

  最近由于工作需要,深入系统的学习了OpenSSL中HMAC的实现方式,为了打牢HMAC的根基,且能够帮助后来者,在这里记录了自己的一些调试心得。   本文分析的OpenSSL的代码版本为:openssl-1.1.1h   hamc的路径:crypto/hmac,主要包含3个文件hmac.c \ hm_pmeth.c \ hmeth.c。   详细的HMAC原理分析详见:加密算法 之二 HMAC

2. 主要结构 typedef struct hmac_ctx_st HMAC_CTX;typedef struct evp_md_st EVP_MD;typedef struct evp_md_ctx_st EVP_MD_CTX;typedef struct evp_pkey_ctx_st EVP_PKEY_CTX;typedef struct evp_pkey_st EVP_PKEYtypedef struct evp_pkey_method_st EVP_PKEY_METHOD; 2.1 HAMC_CTX(文件:ossl_typ.h >>> evp_local.h)

  该结构属于OpenSSL软算法自定义的一个结构体,若使用OpenSSL的软算法的话,会用到该结构体,但是若调用引擎(Engine)硬件实现HMAC的话,一般使用到该结构体。

struct hmac_ctx_st { const EVP_MD *md; /* 摘要算法的结构体,每种算法都有这么一个结构体,类似算法的句柄 */ EVP_MD_CTX *md_ctx; /* 摘要算法的上下文 */ EVP_MD_CTX *i_ctx; /* i代表ipad(内部秘钥),是ipad散列运算的上下文 */ EVP_MD_CTX *o_ctx; /* o代表opad(外部秘钥),是opad散列运算的上下文 */ }; typedef struct hmac_ctx_st HMAC_CTX; 2.2 EVP_MD(文件:ossl_typ.h >>> evp.h)

  摘要算法的结构体,类似摘要算法的句柄。该结构体中定义了通用的摘要计算的抽象方法的集合,可以将其理解为EVP_MD_CTX的子类。

struct evp_md_st { int type; int pkey_type; int md_size; /* digest的长度(这个是与算法有关的,比如sha256,摘要值的长度为32字节) */ unsigned long flags; int (*init) (EVP_MD_CTX *ctx); /* 初始化函数 */ int (*update) (EVP_MD_CTX *ctx, const void *data, size_t count); /* 中间过程运算,更新函数 */ int (*final) (EVP_MD_CTX *ctx, unsigned char *md); /* 最后一笔运算,用于获取摘要值,不在进行数据的摘要运算 */ int (*copy) (EVP_MD_CTX *to, const EVP_MD_CTX *from); /* 复制函数 */ int (*cleanup) (EVP_MD_CTX *ctx); /* 复位函数 */ int block_size; /* md的块大小 */ int ctx_size; /* how big does the ctx->md_data need to be */ /* control function */ int (*md_ctrl) (EVP_MD_CTX *ctx, int cmd, int p1, void *p2); } /* EVP_MD */ ; typedef struct evp_md_st EVP_MD; 2.3 EVP_MD_CTX(文件:ossl_typ.h >>> evp_loacl.h)

   摘要算法的上下文。    既然是上下文,肯定包含“摘要”和“数据”。*md_data即为摘要的数据指针,空间一般需要自己申请。    对于本文来说,该结构体中的变量为“*pctx”,它指向了pkey的上下文(hmac在openssl中被划分为pkey类),EVP_PKEY_CTX 的定义如2.4所示 。

struct evp_md_ctx_st { const EVP_MD *digest; /* 摘要 */ ENGINE *engine; /* functional reference if 'digest' is ENGINE-provided */ unsigned long flags; void *md_data; /* 指向摘要的具体上下文,这个一般有用户自己定义(在openssl的软算法中指向HMAC_CTX所声明的结构体) */ /* Public key context for sign/verify */ EVP_PKEY_CTX *pctx; /* 签名(auth)的上下文,openssl将auth归为了pkey类,但是它的运算过程与md运算的过程类似 */ /* Update function: usually copied from EVP_MD */ int (*update) (EVP_MD_CTX *ctx, const void *data, size_t count); } /* EVP_MD_CTX */ ; typedef struct evp_md_ctx_st EVP_MD_CTX; 2.4 EVP_PKEY_CTX (文件:ossl_typ.h >>> evp.h)

  pkey的上下文结构体如下:

struct evp_pkey_ctx_st { /* Method associated with this operation */ const EVP_PKEY_METHOD *pmeth; /* Engine that implements this method or NULL if builtin */ ENGINE *engine; /* Key: may be NULL */ EVP_PKEY *pkey; /* Peer key for key agreement, may be NULL */ EVP_PKEY *peerkey; /* Actual operation */ int operation; /* Algorithm specific data */ void *data; /* Application specific data */ void *app_data; /* Keygen callback */ EVP_PKEY_gen_cb *pkey_gencb; /* implementation specific keygen data */ int *keygen_info; int keygen_info_count; } /* EVP_PKEY_CTX */ ; typedef struct evp_pkey_ctx_st EVP_PKEY_CTX; 2.5 EVP_PKEY (文件:ossl_typ.h >>> evp.h)

  pkey算法的结构体,类似pkey算法的句柄。

/* * Type needs to be a bit field Sub-type needs to be for variations on the * method, as in, can it do arbitrary encryption.... */ struct evp_pkey_st { int type; int save_type; CRYPTO_REF_COUNT references; const EVP_PKEY_ASN1_METHOD *ameth; ENGINE *engine; ENGINE *pmeth_engine; /* If not NULL public key ENGINE to use */ union { void *ptr; # ifndef OPENSSL_NO_RSA struct rsa_st *rsa; /* RSA */ # endif # ifndef OPENSSL_NO_DSA struct dsa_st *dsa; /* DSA */ # endif # ifndef OPENSSL_NO_DH struct dh_st *dh; /* DH */ # endif # ifndef OPENSSL_NO_EC struct ec_key_st *ec; /* ECC */ ECX_KEY *ecx; /* X25519, X448, Ed25519, Ed448 */ # endif } pkey; int save_parameters; STACK_OF(X509_ATTRIBUTE) *attributes; /* [ 0 ] */ CRYPTO_RWLOCK *lock; } /* EVP_PKEY */ ; typedef struct evp_pkey_st EVP_PKEY; 2.6 EVP_PKEY_METHOD(文件:ossl_typ.h >>> evp.h)

  该结构体中定义了通用的mac计算的抽象方法的集合。

struct evp_pkey_method_st { int pkey_id; int flags; int (*init) (EVP_PKEY_CTX *ctx); int (*copy) (EVP_PKEY_CTX *dst, EVP_PKEY_CTX *src); void (*cleanup) (EVP_PKEY_CTX *ctx); int (*paramgen_init) (EVP_PKEY_CTX *ctx); int (*paramgen) (EVP_PKEY_CTX *ctx, EVP_PKEY *pkey); int (*keygen_init) (EVP_PKEY_CTX *ctx); int (*keygen) (EVP_PKEY_CTX *ctx, EVP_PKEY *pkey); int (*sign_init) (EVP_PKEY_CTX *ctx); int (*sign) (EVP_PKEY_CTX *ctx, unsigned char *sig, size_t *siglen, const unsigned char *tbs, size_t tbslen); int (*verify_init) (EVP_PKEY_CTX *ctx); int (*verify) (EVP_PKEY_CTX *ctx, const unsigned char *sig, size_t siglen, const unsigned char *tbs, size_t tbslen); int (*verify_recover_init) (EVP_PKEY_CTX *ctx); int (*verify_recover) (EVP_PKEY_CTX *ctx, unsigned char *rout, size_t *routlen, const unsigned char *sig, size_t siglen); int (*signctx_init) (EVP_PKEY_CTX *ctx, EVP_MD_CTX *mctx); int (*signctx) (EVP_PKEY_CTX *ctx, unsigned char *sig, size_t *siglen, EVP_MD_CTX *mctx); int (*verifyctx_init) (EVP_PKEY_CTX *ctx, EVP_MD_CTX *mctx); int (*verifyctx) (EVP_PKEY_CTX *ctx, const unsigned char *sig, int siglen, EVP_MD_CTX *mctx); int (*encrypt_init) (EVP_PKEY_CTX *ctx); int (*encrypt) (EVP_PKEY_CTX *ctx, unsigned char *out, size_t *outlen, const unsigned char *in, size_t inlen); int (*decrypt_init) (EVP_PKEY_CTX *ctx); int (*decrypt) (EVP_PKEY_CTX *ctx, unsigned char *out, size_t *outlen, const unsigned char *in, size_t inlen); int (*derive_init) (EVP_PKEY_CTX *ctx); int (*derive) (EVP_PKEY_CTX *ctx, unsigned char *key, size_t *keylen); int (*ctrl) (EVP_PKEY_CTX *ctx, int type, int p1, void *p2); int (*ctrl_str) (EVP_PKEY_CTX *ctx, const char *type, const char *value); int (*digestsign) (EVP_MD_CTX *ctx, unsigned char *sig, size_t *siglen, const unsigned char *tbs, size_t tbslen); int (*digestverify) (EVP_MD_CTX *ctx, const unsigned char *sig, size_t siglen, const unsigned char *tbs, size_t tbslen); int (*check) (EVP_PKEY *pkey); int (*public_check) (EVP_PKEY *pkey); int (*param_check) (EVP_PKEY *pkey); int (*digest_custom) (EVP_PKEY_CTX *ctx, EVP_MD_CTX *mctx); } /* EVP_PKEY_METHOD */ ; typedef struct evp_pkey_method_st EVP_PKEY_METHOD; 3. 主要函数

  由于工作的需要,本次只研究了hm_pmeth.c和hamc.c的相关函数,就逐个分析hm_pmeth.c和hmac.c中的函数。   在1.1.1中,大多数的数据结构已经不再向使用者开放,从封装的角度来看,这是更合理的。如果你在头文件中找不到结构定义,不妨去源码中搜一搜。

3.1 hmac.c中的主要函数 HMAC_CTX HMAC_CTX_new(void) (1)创建HAMC_CTX上下文结构(即为上下文结构分配一块内存空间)。int HMAC_Init_ex(HMAC_CTX *ctx, const void *key, int len, const EVP_MD *md, ENGINE *impl) (1)初始化HAMC_CTX上下文结构,key为秘钥,len为秘钥长度,md为计算hash的函数集合(digest的句柄) (2)若key的长度大于“block size”,则需要先对key做一次hash运算,若key的长度小于“block size”,后边以“0”补齐,直到key的长度等于“block size”为止。 (3)计算ipad,并计算ipad的hash值,存放在i_ctx上下文中; (4)计算opad,并计算opad的hash值,存放在o_ctx上下文中; (5)将ctx->i_ctx复制到ctx->md_ctx中,此举的目的是根据hamc算法的定义,ipad首先参与hash计算。int HMAC_Init(HMAC_CTX *ctx, const void *key, int len, const EVP_MD *md) (1)此函数用于兼容按照早期版本开发的工程,直接调用HMAC_Init_ex实现。int HMAC_Update(HMAC_CTX *ctx, const unsigned char *data, size_t len) (1)调用EVP_DigestUpdate实现hash运算(充分看出计算mac和计算hash有很多相似之处)。int HMAC_Final(HMAC_CTX *ctx, unsigned char *md, unsigned int *len) (1)调用函数 EVP_DigestFinal_ex() 获取update运算的hash值,放到buf中; (2)调用函数 EVP_MD_CTX_copy_ex() 将i_ctx上下文复制到md_ctx中; (3)调用函数 EVP_DigestUpdate() 计算hash值; (4)再次调用函数 EVP_DigestFinal_ex() 获取最终的hash(digest)值。 通过以上的这4步运算,按照算法的要求将opadkey拼接到buf的最前方,实现计算hash的最终结果。void HMAC_CTX_free(HMAC_CTX *ctx) (1)释放HAMC_CTX上下文结构(这里特别注意需要逐层释放,先释放最内层的,在释放最外层的)。unsigned char *HMAC(const EVP_MD *evp_md, const void *key, int key_len, const unsigned char *d, size_t n, unsigned char *md, unsigned int *md_len) (1)该函数实现单笔hash值的计算,为上面函数的组合体。 3.2 hm_pmeth.c中的主要函数

  结构体EVP_PKEY_METHOD中定义的pkey操作的函数很多,但可能多数都用不到,在hm_pmeth.c中主要就实现了如下几个函数,在实际的应用开发中(引擎的开发),我们也是依葫芦画瓢,实现了这些函数。   关键的结构体:

/* HMAC pkey context structure */ typedef struct { const EVP_MD *md; /* MD for HMAC use */ ASN1_OCTET_STRING ktmp; /* Temp storage for key */ HMAC_CTX *ctx; } HMAC_PKEY_CTX; static int pkey_hmac_init(EVP_PKEY_CTX *ctx) (1)初始化HMAC_PKEY_CTX上下文结构,并赋值给ctx->data; (2)这个函数的本质作用是为ctx的data变量分配空间。static int pkey_hmac_copy(EVP_PKEY_CTX *dst, EVP_PKEY_CTX *src) (1)赋值上下文。static void pkey_hmac_cleanup(EVP_PKEY_CTX *ctx) (1)清空,复位。static int pkey_hmac_keygen(EVP_PKEY_CTX *ctx, EVP_PKEY *pkey) (1)此函数重要实现的是,将ctx->data中的秘钥复制到pkey中,该函数实现的是秘钥的搬移,而非重新生成。static int hmac_signctx_init(EVP_PKEY_CTX *ctx, EVP_MD_CTX *mctx) (1)此函数主要是为mctx上下文指定进行摘要运算的update函数。static int hmac_signctx(EVP_PKEY_CTX *ctx, unsigned char *sig, size_t *siglen, EVP_MD_CTX *mctx) (1)若mctx为空,则仅返回摘要值的长度。 (2)若mctx非空,则调用HMAC_Final()获取最终的摘要值。static int pkey_hmac_ctrl(EVP_PKEY_CTX *ctx, int type, int p1, void *p2) (1)EVP_PKEY_CTRL_SET_MAC_KEY:将key写入ctx->data中,这样key就保存在ctx上下文中。 (2)EVP_PKEY_CTRL_MD:为hamc运算关联相关的md操作(因为hamc运算本质上digest运算,所以必须指定digest的函数集合)。 (3)EVP_PKEY_CTRL_DIGESTINIT:从ctx->pkey->pkey.ptr中获取key,并进行hamc的初始化操作。(这里换做engine操作的话,会将key保存到自定义的上下文中,供硬件调用,不需要软件维护,openssl是软件维护了key)static int pkey_hmac_ctrl_str(EVP_PKEY_CTX *ctx, const char *type, const char *value) 4. 软件实现

  分析完以上的函数之后,我们相同的模式实现了engine(引擎)的驱动,并写了sample代码,下面重点通过分析例子代码,梳理一下代码的执行流程。   首先贴出我已经写好并验证的代码,如下:

/* 明文 */ static const unsigned char P[] = { 0x1A, 0x1E, 0x1F, 0x2F, 0x3F, 0x4F, 0xFA, 0xBD, 0xED, 0xCD, 0xFA, 0xFC, 0xCA, 0xDA, 0xDB, 0x12, 0x34, 0x56, 0x78, 0x90, 0x9A, 0x1D, 0x11, 0x1E, 0x12, 0x6C, 0x36, 0xDD, 0xFF, 0x12, 0x9A, 0x0F, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0xFF, 0xDD, 0x12, 0x33, 0x44, 0x01, 0x12, 0x4A, 0x3F, 0x1A, 0x2B, 0xC8, 0x59, 0x6A, 0x05, 0x85, 0xE0, }; /* 秘钥 */ static const unsigned char K[] = { 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44, 0x44, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44, 0x44, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44, 0x44, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44, 0x44, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44, 0x44, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44, 0x44, }; /* 通过sha1计算的hamc值 */ static const unsigned char E_hamc_sha1[] = { 0xFC, 0xE9, 0xFD, 0xB7, 0x95, 0x75, 0x3B, 0xFA, 0x5D, 0xC1, 0xF5, 0x8B, 0x4B, 0x25, 0x17, 0x33, 0xE5, 0x29, 0xD4, 0x04, }; /* 自定义的结构体 */ struct test_sign { const char *name; unsigned int nid; const char *algname; const unsigned char *plaintext; const unsigned char *key; const unsigned char *mac; int psize; int keylen; }; static struct test_sign test_signs[] = { { .name = "HMAC(md5)", .nid = EVP_PKEY_HMAC, .algname = "MD5", .plaintext = P, .key = K, .mac = E_hamc_md5, .psize = sizeof(P), .keylen = sizeof(K) }, {0}; } static int test_hmac(struct test_sign *t) { int ret = SUCCESS, test; EVP_MD_CTX *mctx = NULL; EVP_PKEY_CTX *pctx = NULL, *genctx = NULL; EVP_PKEY *pkey = NULL; const EVP_MD *md = NULL; unsigned char mac[EVP_MAX_MD_SIZE]; size_t mac_len = 0; /* key的生成过程 */ genctx = EVP_PKEY_CTX_new_id(t->nid, NULL); /* 通过nid获取 EVP_PKEY_CTX 上下文 */ EVP_PKEY_keygen_init(genctx); /* 对新申请的上下文进行初始化,主要用户内存的申请、数据的填充等 */ EVP_PKEY_CTX_set_mac_key(genctx, t->key, t->keylen); /* 将key设置到 genctx 上下文中 */ EVP_PKEY_keygen(genctx, &pkey); /* 将key复制到pkey中 */ EVP_PKEY_CTX_free(genctx); /* 释放 EVP_PKEY_CTX 上下文 */ /* 通过sha1计算mac值 */ md = EVP_get_digestbyname(t->algname); /* 通过算法名称获取md */ mctx = EVP_MD_CTX_new(); /* 创建一个全新的 EVP_MD_CTX 上下文*/ EVP_DigestSignInit(mctx, &pctx, md, NULL, pkey); /* 将md、pkey与mctx进行绑定 */ EVP_DigestSignUpdate(mctx, t->plaintext, t->psize); /* 计算摘要值 */ EVP_DigestSignFinal(mctx, NULL, &mac_len); /* 获取mac值的长度 */ EVP_DigestSignFinal(mctx, mac, &mac_len); /* 获取mac值 */ /* check */ TEST_ASSERT(((mac_len == sizeof(t->mac)) && (!memcmp(mac, t->mac, mac_len))), t->name, "digest"); ret |= test; /* 释放内存 */ EVP_PKEY_CTX_free(pctx); EVP_MD_CTX_free(mctx); EVP_PKEY_free(pkey); return ret; }

  计算mac值,主要分为两步走:第1步 生成秘钥,第2步:计算mac值(通过计算hash的方式计算mac值)。

4.1 秘钥生成 EVP_PKEY_CTX_new_id():通过nid获取 EVP_PKEY_CTX 类型的上下文 genctx;EVP_PKEY_keygen_init():对新申请的上下文进行初始化,主要用户内存的申请、数据的填充等(若向openssl注册了engine且该engine支持hamc运算,则该函数会调用engine的init函数,否则直接调用openssl的init函数);EVP_PKEY_CTX_set_mac_key():将秘钥设置到 genctx 上下文中。EVP_PKEY_keygen():/* 将key复制到pkey中 */EVP_PKEY_CTX_free():/* 释放genctx上下文,到此为止genctx就寿终正寝了 */   以上一系列操作的目的就是将key放到 EVP_PKEY *pkey 中,目前我所能实现的方式就是这种,不知道是否可以直接将key放到pkey中。 4.2 mac计算 EVP_get_digestbyname():通过算法名称获取EVP_MD md(摘要操作的函数集合);EVP_MD_CTX_new() :创建一个摘要上下文EVP_DigestSignInit():该函数主要做了以下几件事情 (1)若mctx->pctx为空,则新窗口 pctx 上下文; (2)为mctx->update指定hash运算的update函数,同时将mctx->pctx->operation = EVP_PKEY_OP_SIGNCTX; (3)将md与pctx进行关联; (4)进行mctx的初始化操作。EVP_DigestSignUpdate():计算摘要值。EVP_DigestSignFinal():获取mac长度或mac值。 5. 总结 在openssl中,hamc的计算被归为pkey类的计算,但是它和digest的计算有很多的显示之处,主要区别在于digest的计算需要秘钥,而hamc的计算需要秘钥,且秘钥还有两个ipad_key、opad_key,而且这两个可以都是通过我们的秘钥key生成的。hamc的计算有一个秘钥生成的过程,与其说是秘钥生成,不如说是秘钥的复制,其实就是将秘钥放到EVP_PKEY 结构体中的ptr位置处(内存需要自己申请)。在EVP_MD_CTX 下文中包含 EVP_PKEY_CTX上下文 ,因为本质上hamc运算也是用digest的那一套函数接口进行计算。openssl的hamc软算法自己维护了ipad_key、opad_key,实际我们硬件实现时,是由硬件维护的,故硬件实现起来,比软件的流程稍微简单一些。


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3