
【OpenSSL 之五】:HMAC算法分析

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_PKEY
- typedef 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,实际我们硬件实现时,是由硬件维护的,故硬件实现起来,比软件的流程稍微简单一些。




更多推荐








所有评论(0)