最近由于工作需要,深入系统的学习了OpenSSL中HMAC的实现方式,为了打牢HMAC的根基,且能够帮助后来者,在这里记录了自己的一些调试心得。
本文分析的OpenSSL的代码版本为:openssl-1.1.1h
hamc的路径:crypto/hmac
,主要包含3个文件hmac.c \ hm_pmeth.c \ hmeth.c。
详细的HMAC原理分析详见:加密算法 之二 HMAC
HMAC_CTX
;EVP_MD
;EVP_MD_CTX
;EVP_PKEY_CTX
;EVP_PKEY
EVP_PKEY_METHOD
;该结构属于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;
摘要算法的结构体,类似摘要算法的句柄。该结构体中定义了通用的摘要计算的抽象方法的集合,可以将其理解为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;
摘要算法的上下文。
既然是上下文,肯定包含“摘要”和“数据”。*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;
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;
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;
该结构体中定义了通用的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;
由于工作的需要,本次只研究了hm_pmeth.c和hamc.c的相关函数,就逐个分析hm_pmeth.c和hmac.c中的函数。
在1.1.1中,大多数的数据结构已经不再向使用者开放,从封装的角度来看,这是更合理的。如果你在头文件中找不到结构定义,不妨去源码中搜一搜。
结构体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;
分析完以上的函数之后,我们相同的模式实现了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值)。
EVP_MD_CTX
下文中包含 EVP_PKEY_CTX
上下文 ,因为本质上hamc运算也是用digest的那一套函数接口进行计算。