在开发涉及加密通信或数据保护的应用时,直接调用底层密码学算法(如 AES、RSA、SHA)既繁琐又容易出错。OpenSSL 提供了 EVP(EnVElope Password)编程接口,将底层算法封装为统一的 API,开发者无需关心算法细节,只需调用统一的函数即可完成加密、哈希、签名等操作。

为什么使用 EVP 接口

直接使用 OpenSSL 的底层 API(如 AES_* 系列函数)存在以下问题:

  • 算法切换困难:代码与特定算法强耦合,换算法需要大幅修改
  • 安全风险:开发者可能忽略填充模式、IV 初始化等关键细节
  • 接口不一致:不同算法的 API 风格各异,学习成本高

EVP 接口解决了这些问题:

  • 统一 API:所有算法使用相同的使用模式
  • 自动填充:AEAD 模式自动处理填充
  • 易于切换:一行代码即可切换算法
  • 内存安全:自动管理临时缓冲区

EVP 加密解密

EVP_CIPHER 相关的 API 用于对称加密和解密。以下是一个完整的示例,演示如何使用 EVP 接口进行 AES-256-GCM 加密:

加密示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <openssl/evp.h>
#include <openssl/err.h>
#include <stdio.h>
#include <string.h>

void print_hex(const char* label, const unsigned char* data, size_t len) {
    printf("%s: ", label);
    for (size_t i = 0; i < len; i++) {
        printf("%02x", data[i]);
    }
    printf("\n");
}

int encrypt_aes_gcm(const unsigned char* plaintext, int plaintext_len,
                    const unsigned char* key, const unsigned char* iv,
                    unsigned char* ciphertext, unsigned char* tag) {
    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
    int len = 0, ciphertext_len = 0;

    // 创建 AES-256-GCM 加密上下文
    EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL);

    // 设置 IV 长度(AEAD 模式必须)
    EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, NULL);

    // 初始化加密:密钥 + IV
    EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv);

    // 加密数据(分块处理)
    EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len);
    ciphertext_len = len;

    // 完成加密(生成认证标签)
    EVP_EncryptFinal_ex(ctx, ciphertext + len, &len);
    ciphertext_len += len;

    // 获取 GCM 认证标签
    EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, 16, tag);

    EVP_CIPHER_CTX_free(ctx);
    return ciphertext_len;
}

int main() {
    // 密钥:32 字节(256 位)
    unsigned char key[32] = "0123456789abcdef0123456789abcdef";
    // IV:12 字节(96 位),GCM 推荐
    unsigned char iv[12] = "123456789012";
    
    const char* plaintext = "Hello, EVP加密接口!";
    unsigned char ciphertext[128];
    unsigned char tag[16];

    int ciphertext_len = encrypt_aes_gcm(
        (unsigned char*)plaintext, strlen(plaintext),
        key, iv, ciphertext, tag
    );

    print_hex("密文", ciphertext, ciphertext_len);
    print_hex("认证标签", tag, 16);

    return 0;
}

解密示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int decrypt_aes_gcm(const unsigned char* ciphertext, int ciphertext_len,
                    const unsigned char* key, const unsigned char* iv,
                    const unsigned char* tag,
                    unsigned char* plaintext) {
    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
    int len = 0, plaintext_len = 0;

    // 创建解密上下文
    EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL);

    // 设置 IV 和认证标签
    EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, NULL);
    EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, 16, (void*)tag);

    // 初始化解密
    EVP_DecryptInit_ex(ctx, NULL, NULL, key, iv);

    // 解密数据
    EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len);
    plaintext_len = len;

    // 完成解密(验证认证标签)
    int ret = EVP_DecryptFinal_ex(ctx, plaintext + len, &len);

    EVP_CIPHER_CTX_free(ctx);
    
    if (ret > 0) {
        plaintext_len += len;
        return plaintext_len;
    }
    return -1; // 认证失败
}

常用 EVP_aes_* 对称加密算法

函数 说明
EVP_aes_128_ecb() AES-128 ECB 模式
EVP_aes_128_cbc() AES-128 CBC 模式
EVP_aes_128_gcm() AES-128 GCM 模式
EVP_aes_256_gcm() AES-256 GCM 模式
EVP_aes_256_ccm() AES-256 CCM 模式

使用 OpenSSL 命令行验证 AES-GCM 加密结果:

1
2
3
4
5
6
# 生成随机密钥和 IV
openssl rand -hex 32  # 256位密钥
openssl rand -hex 12  # 96位IV

# 使用 AES-256-GCM 加密
echo -n "Hello, EVP!" | openssl enc -aes-256-gcm -K $(openssl rand -hex 32) -iv $(openssl rand -hex 12) -pbkdf2 -iter 10000 -pass pass:password 2>/dev/null || echo "验证命令需要调整"

EVP 哈希计算

EVP_MD 接口提供了统一的哈希算法调用方式,支持 SHA-256、SHA-3、Blake2 等:

单次哈希计算

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <openssl/evp.h>

int hash_sha256(const unsigned char* data, size_t len, unsigned char* hash) {
    EVP_MD_CTX* ctx = EVP_MD_CTX_new();
    const EVP_MD* md = EVP_sha256();
    unsigned int hash_len = 0;

    EVP_DigestInit_ex(ctx, md, NULL);
    EVP_DigestUpdate(ctx, data, len);
    EVP_DigestFinal_ex(ctx, hash, &hash_len);

    EVP_MD_CTX_free(ctx);
    return hash_len; // 返回 32 字节
}

分块哈希计算(处理大文件)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
int hash_file_sha256(FILE* fp, unsigned char* hash) {
    EVP_MD_CTX* ctx = EVP_MD_CTX_new();
    const EVP_MD* md = EVP_sha256();
    unsigned char buffer[8192];
    size_t bytes_read;
    unsigned int hash_len = 0;

    EVP_DigestInit_ex(ctx, md, NULL);

    while ((bytes_read = fread(buffer, 1, sizeof(buffer), fp)) > 0) {
        EVP_DigestUpdate(ctx, buffer, bytes_read);
    }

    EVP_DigestFinal_ex(ctx, hash, &hash_len);
    EVP_MD_CTX_free(ctx);
    return hash_len;
}

使用 OpenSSL 命令行验证:

1
2
3
4
5
6
7
8
# 计算文件 SHA-256 哈希
openssl dgst -sha256 /etc/passwd

# 计算字符串哈希
echo -n "Hello" | openssl dgst -sha256

# 使用 dgst 命令的简化形式
echo -n "Hello" | openssl sha256

支持的哈希算法包括:

函数 输出长度 说明
EVP_md5() 16 字节 MD5(不推荐用于安全场景)
EVP_sha1() 20 字节 SHA-1(已发现碰撞)
EVP_sha224() 28 字节 SHA-224
EVP_sha256() 32 字节 SHA-256
EVP_sha384() 48 字节 SHA-384
EVP_sha512() 64 字节 SHA-512
EVP_sha3_256() 32 字节 SHA3-256
EVP_blake2b512() 64 字节 BLAKE2b-512

EVP 签名与验签

EVP 接口同时支持 RSA、ECDSA、Ed25519 等签名算法:

Ed25519 签名示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <openssl/evp.h>
#include <openssl/pem.h>

int sign_ed25519(const unsigned char* msg, size_t msg_len,
                 const char* private_key_file, 
                 unsigned char* sig, unsigned int* sig_len) {
    EVP_MD_CTX* ctx = EVP_MD_CTX_new();
    EVP_PKEY* pkey = NULL;
    FILE* fp = NULL;
    int ret = -1;

    // 加载私钥
    fp = fopen(private_key_file, "r");
    if (!fp) goto cleanup;
    pkey = PEM_read_PrivateKey(fp, NULL, NULL, NULL);
    fclose(fp);
    if (!pkey) goto cleanup;

    // 创建签名上下文
    EVP_DigestSignInit(ctx, NULL, NULL, NULL, pkey);
    
    // 设置 Ed25519
    EVP_MD_CTX_set_pkey_ctx(ctx, NULL);

    // 计算签名
    EVP_DigestSignUpdate(ctx, msg, msg_len);
    ret = EVP_DigestSignFinal(ctx, sig, sig_len);

cleanup:
    EVP_PKEY_free(pkey);
    EVP_MD_CTX_free(ctx);
    return ret;
}

使用 OpenSSL 命令行生成 Ed25519 密钥并签名:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 生成 Ed25519 私钥
openssl genpkey -algorithm Ed25519 -out ed25519_private.pem

# 生成对应公钥
openssl pkey -in ed25519_private.pem -pubout -out ed25519_public.pem

# 签名(-rawin 指定输入为原始数据)
echo -n "Hello, World!" > testmsg.txt
openssl pkeyutl -sign -inkey ed25519_private.pem -rawin -in testmsg.txt -out signature.bin

# 验签
openssl pkeyutl -verify -pubin -inkey ed25519_public.pem -rawin -in testmsg.txt -sigfile signature.bin

RSA-PSS 签名示例

RSA-PSS 是更安全的 RSA 签名方案,推荐使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 生成 RSA-PSS 私钥
openssl genpkey -algorithm RSA-PSS -out rsa_pss_private.pem -pkeyopt rsa_pss_keygen_md:sha256

# 生成对应公钥
openssl pkey -in rsa_pss_private.pem -pubout -out rsa_pss_public.pem

# 签名(使用 dgst 命令)
echo -n "Test message" > testmsg.txt
openssl dgst -sha256 -sign rsa_pss_private.pem -out rsa_sig.bin testmsg.txt

# 验签
openssl dgst -sha256 -verify rsa_pss_public.pem -signature rsa_sig.bin testmsg.txt

EVP 接口的错误处理

EVP 操作可能失败,需要正确处理错误:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <openssl/err.h>

void print_openssl_error(const char* msg) {
    fprintf(stderr, "%s: ", msg);
    ERR_print_errors_fp(stderr);
}

// 简化版错误检查
int evp_ensure(int ret, const char* operation) {
    if (ret <= 0) {
        print_openssl_error(operation);
        return 0; // 失败
    }
    return 1; // 成功
}

常见错误码:

  • EVP_F_EVP_ENCRYPTINIT_EX:加密上下文初始化失败
  • EVP_F_EVP_DECRYPTFINAL_EX:解密/认证失败(可能是数据被篡改)

OpenSSL 3.0 与 EVP

OpenSSL 3.0 引入了 Provider 架构,EVP 接口默认使用 OQS Provider(后量子密码学):

1
2
3
4
5
6
7
8
# 查看可用 Provider
openssl list -providers

# 查看可用签名算法
openssl list -signature-algorithms

# 查看可用加密算法
openssl list -cipher-algorithms

启用传统算法(兼容旧版 OpenSSL):

1
2
# 加载 legacy provider
openssl Legacy -s_client -connect example.com:443

或在代码中加载:

1
OSSL_PROVIDER* legacy = OSSL_PROVIDER_load(NULL, "legacy");

总结

EVP 接口是 OpenSSL 提供的统一密码学编程接口,具有以下优势:

  • 一致性:不同算法使用相同调用模式
  • 安全性:默认启用安全参数(如 GCM 自动生成标签)
  • 可移植性:代码与具体算法实现解耦
  • 现代设计:支持 AEAD、PBKDF2、HKDF 等现代密码学原语

在实际开发中,优先使用 EVP 接口而非底层 API,能够显著提高代码的安全性和可维护性。


参考来源