OpenSSL之EVP用法

OpenSSL EVP(high-level cryptographic functions)提供了丰富的密码学中的各种函数。OpenSSL中实现了各种对称算法、摘要算法以及签名/验签算法。EVP函数将这些具体的算法进行了封装。
EVP主要封装了如下功能函数:
1)实现了BASE64编解码BIO;
2)实现了加解密BIO;
3)实现了摘要BIO;
4)实现了reliable BIO;
5)封装了摘要算法;
6)封装了对称加解密算法;
7)封装了非对称密钥的加密(公钥)、解密(私钥)、签名与验证以及辅助函数;
8)基于口令的加密(PBE);
9)对称密钥处理;
10)数字信封:数字信封用对方的公钥加密对称密钥,数据则用此对称密钥加密。发送给对方时,同时发送对称密钥密文和数据密文。接收方首先用自己的私钥解密密钥密文,得到对称密钥,然后用它解密数据。
11)其他辅助函数。

本文假设你已经安装好了OpenSSL,并且持有一份1.1.1的源码。
EVP相关的头文件在evp.h中、源文件在crypto/evp目录中。
由于EVP的功能过于强大,再加上我的精力和水平有限,暂时只对部分功能进行摘录和说明。

摘要相关的主要数据结构和函数:

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;               /* 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;

这个结构定义了摘要算法的抽象方法。主要字段含义:
type —— 摘要算法的NID。
pkey_type —— 与摘要算法相关的密钥NID。
md_size —— 摘要值的输出大小。
flags —— 内部标志。
init —— 初使化函数。
update —— 输入计算函数。
final —— 输出计算函数。
copy —— 摘要运算上下文复制函数。
cleanup —— 摘要运算上下文清理函数。
block_size —— 摘要运算分组大小。
ctx_size —— 摘要运算分组缓冲区大小。
md_ctrl —— 摘要运算指令控制函数。

支持的摘要算法包括:
const EVP_MD *EVP_md5(void);
const EVP_MD *EVP_sha1(void);
const EVP_MD *EVP_sha256(void);
const EVP_MD *EVP_sha512(void);

拿EVP_md5()来说,其返回值为:

static const EVP_MD md5_md = {
    NID_md5,
    NID_md5WithRSAEncryption,
    MD5_DIGEST_LENGTH,
    0,
    init,
    update,
    final,
    NULL,
    NULL,
    MD5_CBLOCK,
    sizeof(EVP_MD *) + sizeof(MD5_CTX),
};

下面这几个函数查询md的属性信息:

int EVP_MD_type(const EVP_MD *md);
# define EVP_MD_nid(e)  EVP_MD_type(e)
# define EVP_MD_name(e) OBJ_nid2sn(EVP_MD_nid(e))
int EVP_MD_pkey_type(const EVP_MD *md);
int EVP_MD_size(const EVP_MD *md);
int EVP_MD_block_size(const EVP_MD *md);

有时我们对使用的摘要算法不熟悉,这几个函数很有帮助。

EVP_MD_CTX *EVP_MD_CTX_new(void);
void EVP_MD_CTX_free(EVP_MD_CTX *ctx);
这两个函数用于创建和释放对称摘要上下文对象。

int EVP_DigestInit(EVP_MD_CTX *ctx, const EVP_MD *type);
初使化摘要上下文,type为摘要算法抽象集合。
成功返回1,失败返回0。

int EVP_DigestUpdate(EVP_MD_CTX *ctx, const void *d, size_t cnt);
向摘要计算的海棉结构输入一段数据。
成功返回1,失败返回0。

int EVP_DigestFinal(EVP_MD_CTX *ctx, unsigned char *md, unsigned int *s);
生成最终摘要,输出摘要值和长度。
成功返回1,失败返回0。

int EVP_Digest(const void *data, size_t count, unsigned char *md, unsigned int *size, const EVP_MD *type, ENGINE *impl);
使用包装的一次性方法计算一段小数据的摘要。
成功返回1,失败返回0。

对称加密相关的主要数据结构和函数:

struct evp_cipher_st {
int nid;
int block_size;
/* Default value for variable length ciphers /
int key_len;
int iv_len;
/
Various flags /
unsigned long flags;
/
init key /
int (
init) (EVP_CIPHER_CTX *ctx, const unsigned char *key,
const unsigned char iv, int enc);
/
encrypt/decrypt data /
int (
do_cipher) (EVP_CIPHER_CTX *ctx, unsigned char *out,
const unsigned char in, size_t inl);
/
cleanup ctx /
int (
cleanup) (EVP_CIPHER_CTX );
/
how big ctx->cipher_data needs to be /
int ctx_size;
/
Populate a ASN1_TYPE with parameters /
int (
set_asn1_parameters) (EVP_CIPHER_CTX *, ASN1_TYPE );
/
Get parameters from a ASN1_TYPE /
int (
get_asn1_parameters) (EVP_CIPHER_CTX *, ASN1_TYPE );
/
Miscellaneous operations /
int (
ctrl) (EVP_CIPHER_CTX *, int type, int arg, void ptr);
/
Application data */
void app_data;
} /
EVP_CIPHER */ ;
typedef struct evp_cipher_st EVP_CIPHER;
这个结构定义了对称加密算法的抽象方法。主要字段含义:
nid —— 加密算法的NID。
block_size —— 分组大小。
key_len —— 密钥长度。
iv_len —— 初使向量长度。
flags —— 内部标志。
init —— 初使化函数。
do_cipher —— 中间运算函数。
cleanup —— 最终运算函数。
ctx_size —— 上下文大小。
ctrl —— 控制函数。
app_data —— 应用程序数据。

支持的CIPHER抽象加解密算法包括:
const EVP_CIPHER *EVP_des_ecb(void);
const EVP_CIPHER *EVP_des_ede3(void);
const EVP_CIPHER *EVP_aes_128_ecb(void);
const EVP_CIPHER *EVP_aes_128_cbc(void);

下面这几个函数查询cipher的属性信息:
int EVP_CIPHER_nid(const EVP_CIPHER *cipher);
int EVP_CIPHER_type(const EVP_CIPHER *ctx);
# define EVP_CIPHER_name(e) OBJ_nid2sn(EVP_CIPHER_nid(e))
int EVP_CIPHER_block_size(const EVP_CIPHER *cipher);
int EVP_CIPHER_key_length(const EVP_CIPHER *cipher);
int EVP_CIPHER_iv_length(const EVP_CIPHER *cipher);
有时我们对使用的加密算法不熟悉,这几个函数很有帮助。

EVP_CIPHER_CTX *EVP_CIPHER_CTX_new(void);
void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *c);
这两个函数用于创建和释放对称加解密上下文对象。

int EVP_CIPHER_CTX_set_key_length(EVP_CIPHER_CTX *x, int keylen);
当对称算法密钥长度为可变长时,设置对称算法的密钥长度。
成功返回1,失败返回0。

int EVP_CIPHER_CTX_set_padding(EVP_CIPHER_CTX *c, int pad);
设置对称算法的填充,对称算法有时候会涉及填充。
pad取值0和1,当pad为1时表示使用填充。默认的填充策略采用PKCS5规范,即最后一个分组被填充n个字节时,其填充值均为n。
成功返回1,失败返回0。

int EVP_EncryptInit(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher, const unsigned char *key, const unsigned char *iv);
初使化对称加密上下文。
成功返加1,失败返回0。

int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl, const unsigned char *in, int inl);
加密一段明文。
成功返加1,失败返回0。成功时,outl输出密文长度。

int EVP_EncryptFinal(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl);
加密余下的明文。
成功返加1,失败返回0。成功时,outl输出密文长度。

int EVP_DecryptInit(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher, const unsigned char *key, const unsigned char *iv);
初使化对称解密上下文。
成功返加1,失败返回0。

int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl, const unsigned char *in, int inl);
解密一段密文。
成功返加1,失败返回0。成功时,outl输出明文长度。

int EVP_DecryptFinal(EVP_CIPHER_CTX *ctx, unsigned char *outm, int *outl);
解密余下的密文。
成功返加1,失败返回0。成功时,outl输出明文长度。

int EVP_BytesToKey(const EVP_CIPHER *type, const EVP_MD *md,
const unsigned char *salt,
const unsigned char *data, int datal, int count,
unsigned char *key, unsigned char *iv);
计算密钥函数,它根据算法类型、摘要算法、salt以及输入数据计算出一个对称密钥和初始化向量iv。返加密钥的长度。
在PEM_do_header()函数中根据口令生成密钥时,有使用到这个函数。

非对称加密相关的主要数据结构和函数:

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 */ ;
struct evp_pkey_st EVP_PKEY;

这个结构定义了非对称密钥信息的存储容器。主要字段含义:
type —— 非对称加密算法的NID。
save_type —— 保存的PKEY类型。
pkey —— 保存的PKEY指针,如RSA结构指针。

EVP_PKEY *EVP_PKEY_new(void);
void EVP_PKEY_free(EVP_PKEY *pkey);
这两个函数用于创建和释放PKEY上下文对象。

int EVP_PKEY_assign(EVP_PKEY *pkey, int type, void *key);
为PKEY关联指定算法类型的上下文结构,如为RSA关联的宏定义如下:

#  define EVP_PKEY_assign_RSA(pkey,rsa) EVP_PKEY_assign((pkey),EVP_PKEY_RSA,\
                                        (char *)(rsa))

# define EVP_SignInit(a,b) EVP_DigestInit(a,b)
# define EVP_SignUpdate(a,b,c) EVP_DigestUpdate(a,b,c)
int EVP_SignFinal(EVP_MD_CTX *ctx, unsigned char *md, unsigned int *s,
EVP_PKEY *pkey);
签名计算。从宏定义可以看出实际上就是先计算摘要,再用RSA私钥加密。
成功返加1,失败返回0。

# define EVP_VerifyInit(a,b) EVP_DigestInit(a,b)
# define EVP_VerifyUpdate(a,b,c) EVP_DigestUpdate(a,b,c)
int EVP_VerifyFinal(EVP_MD_CTX *ctx, const unsigned char *sigbuf,
unsigned int siglen, EVP_PKEY *pkey);
验签计算。从宏定义可以看出实际上就是先计算摘要,再用RSA公钥解密签名,再与摘要进行比对。
成功返加1,失败返回0。

使用举例1:

下面这个例子演示了使用MD5的两种方法进行摘要计算的过程。

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

#include <openssl/evp.h>

namespace dakuang {}

void printHex(const unsigned char* pBuf, int nLen)
{
    for (int i = 0; i < nLen; ++i)
    {
        printf("%02x", pBuf[i]);
    }
    printf("\n");
}

int main(int argc, char* argv[])
{
    EVP_MD_CTX* ctx = EVP_MD_CTX_new();

    char sText[] = "abdefg1234567890";

    const EVP_MD* md = EVP_md5();
    int ret = EVP_DigestInit(ctx, md);
    printf("EVP_DigestInit() ret:[%d] \n", ret);
    ret = EVP_DigestUpdate(ctx, (const void*)sText, strlen(sText));
    printf("EVP_DigestUpdate() ret:[%d] \n", ret);
    unsigned char sMD5[16] = {0};
    unsigned int nMD5Len = 0;
    ret = EVP_DigestFinal(ctx, sMD5, &nMD5Len);
    printf("EVP_DigestFinal() ret:[%d] \n", ret);

    if (ret == 1)
    {
       printHex((const unsigned char*)sMD5, nMD5Len);
    }

    EVP_MD_CTX_free(ctx);


    ret = EVP_Digest((const void*)sText, strlen(sText), sMD5, &nMD5Len, EVP_md5(), NULL);
    printf("EVP_Digest() ret:%d \n", ret);

    if (ret == 1)
    {
       printHex((const unsigned char*)sMD5, nMD5Len);
    }

    return 0;
}

输出:
EVP_DigestInit() ret:[1]
EVP_DigestUpdate() ret:[1]
EVP_DigestFinal() ret:[1]
e380e88e8d09ebf8d8659a15b0ea70b5
EVP_Digest() ret:1
e380e88e8d09ebf8d8659a15b0ea70b5

使用举例2:

下面这个例子演示了使用DES进行加解密的过程。为了方便程序实现,破例使用了std::string。

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

#include <openssl/evp.h>
#include <string>

namespace dakuang {}

int main(int argc, char* argv[])
{
    unsigned char sKey[] = "12345678";
    unsigned char sIV[] = "12345678";

    char sText[] = "abcdefghijklmnopqrstuvwxyz";
    std::string strCipher;

    {
        EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();

        const EVP_CIPHER* cipher = EVP_des_ecb();
        int ret = EVP_EncryptInit(ctx, cipher, sKey, sIV);
        printf("EVP_EncryptInit() ret:[%d] \n", ret);

        char sCipher[128] = {0};
        int nCipherLen = 128;
        ret = EVP_EncryptUpdate(ctx, (unsigned char*)sCipher, &nCipherLen, (const unsigned char*)sText, strlen(sText));
        printf("EVP_EncryptUpdate() ret:[%d] \n", ret);
        printf("nCipherLen:[%d] \n", nCipherLen);

        if (nCipherLen > 0)
        {
            strCipher.append(sCipher, nCipherLen);
        }

        ret = EVP_EncryptFinal(ctx, (unsigned char*)sCipher, &nCipherLen);
        printf("EVP_EncryptFinal() ret:[%d] \n", ret);
        printf("nCipherLen:[%d] \n", nCipherLen);

        if (nCipherLen > 0)
        {
            strCipher.append(sCipher, nCipherLen);
        }

        printf("cipher size:[%d] \n", strCipher.size());

        EVP_CIPHER_CTX_free(ctx);
    }

    {
        EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();

        const EVP_CIPHER* cipher = EVP_des_ecb();
        int ret = EVP_DecryptInit(ctx, cipher, sKey, sIV);
        printf("EVP_DecryptInit() ret:[%d] \n", ret);

        char* pText = (char*)malloc(strCipher.size());;
        int nTextLen = strCipher.size();
        ret = EVP_DecryptUpdate(ctx, (unsigned char*)pText, &nTextLen, (const unsigned char*)strCipher.data(), strCipher.size());
        printf("EVP_DecryptUpdate() ret:[%d] \n", ret);
        printf("nTextLen:[%d] \n", nTextLen);

        std::string strText;
        if (nTextLen > 0)
        {
            strText.append(pText, nTextLen);
        }

        ret = EVP_DecryptFinal(ctx, (unsigned char*)pText, &nTextLen);
        printf("EVP_DecryptFinal() ret:[%d] \n", ret);
        printf("nTextLen:[%d] \n", nTextLen);

        if (nTextLen > 0)
        {
            strText.append(pText, nTextLen);
        }

        printf("text size:[%d] body:[%s] \n", strText.size(), strText.data());

        free(pText);
        EVP_CIPHER_CTX_free(ctx);
    }

    return 0;
}

输出:
EVP_EncryptInit() ret:[1]
EVP_EncryptUpdate() ret:[1]
nCipherLen:[24]
EVP_EncryptFinal() ret:[1]
nCipherLen:[8]
cipher size:[32]
EVP_DecryptInit() ret:[1]
EVP_DecryptUpdate() ret:[1]
nTextLen:[24]
EVP_DecryptFinal() ret:[1]
nTextLen:[2]
text size:[26] body:[abcdefghijklmnopqrstuvwxyz]

使用举例3:

下面这个例子演示了使用SHA1进行RSA签名和验签计算的过程。

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

#include <openssl/evp.h>
#include <openssl/rsa.h>
#include <openssl/err.h>

namespace dakuang {}

int main(int argc, char* argv[])
{
    ERR_load_RSA_strings();

    RSA* rsa = RSA_new();
    BIGNUM* bne = BN_new();
    BN_set_word(bne, RSA_3);
    int ret = RSA_generate_key_ex(rsa, 512, bne, NULL);
    printf("RSA_generate_key_ex() ret:[%d] \n", ret);
    BN_free(bne);

    EVP_PKEY* pkey = EVP_PKEY_new();
    ret = EVP_PKEY_assign_RSA(pkey, rsa);
    printf("EVP_PKEY_assign_RSA() ret:[%d] \n", ret);


    EVP_MD_CTX* md_ctx = EVP_MD_CTX_new();

    char sData[] = "1234567890";

    ret = EVP_SignInit(md_ctx, EVP_sha1());
    printf("EVP_SignInit() ret:[%d] \n", ret);
    ret = EVP_SignUpdate(md_ctx, sData, strlen(sData));
    printf("EVP_SignUpdate() ret:[%d] \n", ret);
    unsigned char sSha1[64] = {0};
    unsigned int nSha1Len = 0;
    ret = EVP_SignFinal(md_ctx, sSha1, &nSha1Len, pkey);
    printf("EVP_SignFinal() ret:[%d] \n", ret);
    printf("sha1 len:[%d] \n", nSha1Len);


    EVP_MD_CTX* md_ctx2 = EVP_MD_CTX_new();

    ret = EVP_VerifyInit(md_ctx2, EVP_sha1());
    printf("EVP_VerifyInit() ret:[%d] \n", ret);
    ret = EVP_VerifyUpdate(md_ctx2, sData, strlen(sData));
    printf("EVP_VerifyUpdate() ret:[%d] \n", ret);
    ret = EVP_VerifyFinal(md_ctx2, sSha1, nSha1Len, pkey);
    printf("EVP_VerifyFinal() ret:[%d] \n", ret);

    EVP_MD_CTX_free(md_ctx);
    EVP_MD_CTX_free(md_ctx2);
    EVP_PKEY_free(pkey);

    return 0;
}

输出:
RSA_generate_key_ex() ret:[1]
EVP_PKEY_assign_RSA() ret:[1]
EVP_SignInit() ret:[1]
EVP_SignUpdate() ret:[1]
EVP_SignFinal() ret:[1]
sha1 len:[64]
EVP_VerifyInit() ret:[1]
EVP_VerifyUpdate() ret:[1]
EVP_VerifyFinal() ret:[1]

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容