1. 写在前面
??最近由于工作需要,深入系统的学习了openssl中hmac的实现方式,为了打牢hamc的根基,且能够帮助后来者,在这里记录了自己的一些调试心得。 ??本文分析的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)硬件实现hamc的话,一般使用到该结构体。
struct hmac_ctx_st {
const EVP_MD *md;
EVP_MD_CTX *md_ctx;
EVP_MD_CTX *i_ctx;
EVP_MD_CTX *o_ctx;
};
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;
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;
int ctx_size;
int (*md_ctrl) (EVP_MD_CTX *ctx, int cmd, int p1, void *p2);
} ;
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;
unsigned long flags;
void *md_data;
EVP_PKEY_CTX *pctx;
int (*update) (EVP_MD_CTX *ctx, const void *data, size_t count);
} ;
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 {
const EVP_PKEY_METHOD *pmeth;
ENGINE *engine;
EVP_PKEY *pkey;
EVP_PKEY *peerkey;
int operation;
void *data;
void *app_data;
EVP_PKEY_gen_cb *pkey_gencb;
int *keygen_info;
int keygen_info_count;
} ;
typedef struct evp_pkey_ctx_st EVP_PKEY_CTX;
2.5 EVP_PKEY (文件:ossl_typ.h >>> evp.h)
??pkey算法的结构体,类似pkey算法的句柄。
struct evp_pkey_st {
int type;
int save_type;
CRYPTO_REF_COUNT references;
const EVP_PKEY_ASN1_METHOD *ameth;
ENGINE *engine;
ENGINE *pmeth_engine;
union {
void *ptr;
# ifndef OPENSSL_NO_RSA
struct rsa_st *rsa;
# endif
# ifndef OPENSSL_NO_DSA
struct dsa_st *dsa;
# endif
# ifndef OPENSSL_NO_DH
struct dh_st *dh;
# endif
# ifndef OPENSSL_NO_EC
struct ec_key_st *ec;
ECX_KEY *ecx;
# endif
} pkey;
int save_parameters;
STACK_OF(X509_ATTRIBUTE) *attributes;
CRYPTO_RWLOCK *lock;
} ;
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);
} ;
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中主要就实现了如下几个函数,在实际的应用开发中(引擎的开发),我们也是依葫芦画瓢,实现了这些函数。 ??关键的结构体:
typedef struct {
const EVP_MD *md;
ASN1_OCTET_STRING ktmp;
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,
};
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;
genctx = EVP_PKEY_CTX_new_id(t->nid, NULL);
EVP_PKEY_keygen_init(genctx);
EVP_PKEY_CTX_set_mac_key(genctx, t->key, t->keylen);
EVP_PKEY_keygen(genctx, &pkey);
EVP_PKEY_CTX_free(genctx);
md = EVP_get_digestbyname(t->algname);
mctx = EVP_MD_CTX_new();
EVP_DigestSignInit(mctx, &pctx, md, NULL, pkey);
EVP_DigestSignUpdate(mctx, t->plaintext, t->psize);
EVP_DigestSignFinal(mctx, NULL, &mac_len);
EVP_DigestSignFinal(mctx, mac, &mac_len);
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,实际我们硬件实现时,是由硬件维护的,故硬件实现起来,比软件的流程稍微简单一些。
|