OpenSSL 文件验签与字符串验签原理及 C 语言实现详解

OpenSSL 文件验签与字符串验签原理及 C 语言实现详解

摘要

本文深入探讨了基于 OpenSSL 的文件验签与字符串验签原理及 C 语言实现方法。先阐述数字签名验证原理,包括基于非对称加密算法和哈希算法的签名生成与验证流程,着重介绍 OpenSSL 中利用 EVP 高级接口实现数字签名验证的关键函数及其作用。随后分别提供文件验签和字符串验签的完整 C 语言代码示例,详细解析各步骤要点,如公钥加载、数据读取、验证上下文初始化、更新验证数据及完成验证等,并给出代码步骤详解。此外,还涵盖错误处理与调试技巧,通过 OpenSSL 错误报告机制排查验证失败原因,列举常见错误类型及处理方式。同时对比文件验签与字符串验签特性,从数据来源、加载方式、大数据处理、适用场景及性能角度分析差异。最后总结最佳实践,涵盖算法选择、密钥管理、错误处理、资源管理和性能优化等方面,助力开发者在 OpenSSL 中高效实现文件和字符串签名验证功能,保障数据完整性和真实性。

数字签名验证原理

签名生成原理

  • 哈希计算 :对原始数据使用哈希算法(如 SHA - 256)生成固定长度的消息摘要。
  • 私钥加密 :使用签名者的私钥对消息摘要进行加密,生成数字签名。
  • 签名存储 :将数字签名与原始数据一起存储或传输。

签名验证原理

  • 哈希计算 :对接收到的原始数据使用相同的哈希算法生成消息摘要。
  • 公钥解密 :使用签名者的公钥对数字签名进行解密,得到原始消息摘要。
  • 摘要比对 :比较计算得到的消息摘要和解密得到的消息摘要,如果一致则验证通过。

在 OpenSSL 中,借助 EVP(Envelope)高级接口实现上述流程,主要涉及以下函数:

  • EVP_DigestVerifyInit() :初始化验证上下文。
  • EVP_DigestVerifyUpdate() :输入待验证数据。
  • EVP_DigestVerifyFinal() :完成验证并返回结果。

文件验签 C 语言实现

以下是使用 OpenSSL EVP 接口实现文件验签的完整 C 代码示例:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/err.h>

// 验证文件签名
int verify_file_signature(const char* pubkey_file, 
                         const char* data_file,
                         const char* sig_file) {
    EVP_MD_CTX* mdctx = NULL;
    EVP_PKEY* pubkey = NULL;
    FILE* pubkey_fp = NULL;
    FILE* data_fp = NULL;
    FILE* sig_fp = NULL;
    unsigned char* data = NULL;
    unsigned char* sig = NULL;
    size_t data_len = 0;
    size_t sig_len = 0;
    int result = -1;

    // 1. 读取公钥文件
    pubkey_fp = fopen(pubkey_file, "r");
    if (!pubkey_fp) {
        fprintf(stderr, "Error opening public key file\n");
        goto cleanup;
    }
    
    pubkey = PEM_read_PUBKEY(pubkey_fp, NULL, NULL, NULL);
    if (!pubkey) {
        fprintf(stderr, "Error reading public key\n");
        goto cleanup;
    }

    // 2. 读取数据文件
    data_fp = fopen(data_file, "rb");
    if (!data_fp) {
        fprintf(stderr, "Error opening data file\n");
        goto cleanup;
    }
    
    fseek(data_fp, 0, SEEK_END);
    data_len = ftell(data_fp);
    fseek(data_fp, 0, SEEK_SET);
    
    data = (unsigned char*)malloc(data_len);
    if (!data) {
        fprintf(stderr, "Memory allocation error\n");
        goto cleanup;
    }
    
    if (fread(data, 1, data_len, data_fp) != data_len) {
        fprintf(stderr, "Error reading data file\n");
        goto cleanup;
    }

    // 3. 读取签名文件
    sig_fp = fopen(sig_file, "rb");
    if (!sig_fp) {
        fprintf(stderr, "Error opening signature file\n");
        goto cleanup;
    }
    
    fseek(sig_fp, 0, SEEK_END);
    sig_len = ftell(sig_fp);
    fseek(sig_fp, 0, SEEK_SET);
    
    sig = (unsigned char*)malloc(sig_len);
    if (!sig) {
        fprintf(stderr, "Memory allocation error\n");
        goto cleanup;
    }
    
    if (fread(sig, 1, sig_len, sig_fp) != sig_len) {
        fprintf(stderr, "Error reading signature file\n");
        goto cleanup;
    }

    // 4. 初始化验证上下文
    mdctx = EVP_MD_CTX_new();
    if (!mdctx) {
        fprintf(stderr, "Error creating EVP_MD_CTX\n");
        goto cleanup;
    }
    
    if (EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, pubkey) != 1) {
        fprintf(stderr, "Error initializing verification\n");
        goto cleanup;
    }

    // 5. 更新验证数据
    if (EVP_DigestVerifyUpdate(mdctx, data, data_len) != 1) {
        fprintf(stderr, "Error updating verification data\n");
        goto cleanup;
    }

    // 6. 完成验证
    result = EVP_DigestVerifyFinal(mdctx, sig, sig_len);
    if (result == 1) {
        printf("Signature verification successful\n");
    } else if (result == 0) {
        printf("Signature verification failed\n");
    } else {
        fprintf(stderr, "Error during verification\n");
        result = -1;
    }

cleanup:
    // 7. 清理资源
    if (mdctx) EVP_MD_CTX_free(mdctx);
    if (pubkey) EVP_PKEY_free(pubkey);
    if (pubkey_fp) fclose(pubkey_fp);
    if (data_fp) fclose(data_fp);
    if (sig_fp) fclose(sig_fp);
    if (data) free(data);
    if (sig) free(sig);
    
    return result;
}

int main(int argc, char* argv[]) {
    if (argc != 4) {
        printf("Usage: %s <public_key.pem> <data_file> <signature_file>\n", argv[0]);
        return 1;
    }
    
    return verify_file_signature(argv[1], argv[2], argv[3]) != 1;
}

代码步骤详解

  1. 加载公钥 :利用 PEM_read_PUBKEY 从 PEM 格式的公钥文件中读取公钥。
  2. 读取数据文件 :将待验证的文件内容完整读入内存缓冲区。
  3. 读取签名文件 :把签名数据读入内存缓冲区。
  4. 初始化验证上下文 :创建 EVP_MD_CTX 结构,并借助 EVP_DigestVerifyInit 进行初始化。
  5. 更新验证数据 :调用 EVP_DigestVerifyUpdate 将文件数据输入验证过程。
  6. 完成验证 :通过 EVP_DigestVerifyFinal 进行最终验证。
  7. 资源清理 :释放所有分配的内存和文件资源。

字符串验签 C 语言实现

字符串验签与文件验签主要区别在于数据来源不同,以下是处理内存中字符串数据的验签实现:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/err.h>

// 验证字符串签名
int verify_string_signature(const char* pubkey_file, 
                          const char* message,
                          const unsigned char* sig,
                          size_t sig_len) {
    EVP_MD_CTX* mdctx = NULL;
    EVP_PKEY* pubkey = NULL;
    FILE* pubkey_fp = NULL;
    int result = -1;

    // 1. 读取公钥文件
    pubkey_fp = fopen(pubkey_file, "r");
    if (!pubkey_fp) {
        fprintf(stderr, "Error opening public key file\n");
        goto cleanup;
    }
    
    pubkey = PEM_read_PUBKEY(pubkey_fp, NULL, NULL, NULL);
    if (!pubkey) {
        fprintf(stderr, "Error reading public key\n");
        goto cleanup;
    }

    // 2. 初始化验证上下文
    mdctx = EVP_MD_CTX_new();
    if (!mdctx) {
        fprintf(stderr, "Error creating EVP_MD_CTX\n");
        goto cleanup;
    }
    
    if (EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, pubkey) != 1) {
        fprintf(stderr, "Error initializing verification\n");
        goto cleanup;
    }

    // 3. 更新验证数据(直接使用字符串)
    if (EVP_DigestVerifyUpdate(mdctx, message, strlen(message)) != 1) {
        fprintf(stderr, "Error updating verification data\n");
        goto cleanup;
    }

    // 4. 完成验证
    result = EVP_DigestVerifyFinal(mdctx, sig, sig_len);
    if (result == 1) {
        printf("Signature verification successful\n");
    } else if (result == 0) {
        printf("Signature verification failed\n");
    } else {
        fprintf(stderr, "Error during verification\n");
        result = -1;
    }

cleanup:
    // 5. 清理资源
    if (mdctx) EVP_MD_CTX_free(mdctx);
    if (pubkey) EVP_PKEY_free(pubkey);
    if (pubkey_fp) fclose(pubkey_fp);
    
    return result;
}

// Base64解码函数
int base64_decode(const char* encoded, unsigned char** decoded, size_t* decoded_len) {
    BIO* bio = NULL;
    BIO* b64 = NULL;
    int len = strlen(encoded);
    *decoded = (unsigned char*)malloc(len);
    
    if (!*decoded) {
        return -1;
    }
    
    b64 = BIO_new(BIO_f_base64());
    bio = BIO_new_mem_buf((void*)encoded, len);
    bio = BIO_push(b64, bio);
    
    BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
    *decoded_len = BIO_read(bio, *decoded, len);
    
    BIO_free_all(bio);
    return (*decoded_len > 0) ? 0 : -1;
}

int main() {
    const char* pubkey_file = "public_key.pem";
    const char* message = "This is a test message to verify";
    const char* base64_sig = "MEUCIQD..."; // Base64编码的签名
    
    unsigned char* sig = NULL;
    size_t sig_len = 0;
    
    // 解码Base64签名
    if (base64_decode(base64_sig, &sig, &sig_len) != 0) {
        fprintf(stderr, "Error decoding base64 signature\n");
        return 1;
    }
    
    int ret = verify_string_signature(pubkey_file, message, sig, sig_len);
    free(sig);
    
    return ret != 1;
}

代码步骤详解

  1. 加载公钥 :与文件验签相同,运用 PEM_read_PUBKEY 加载公钥。
  2. 初始化验证上下文 :创建并初始化 EVP_MD_CTX 结构。
  3. 更新验证数据 :直接使用字符串数据调用 EVP_DigestVerifyUpdate
  4. Base64 解码 :提供辅助函数处理 Base64 编码的签名数据。
  5. 完成验证 :调用 EVP_DigestVerifyFinal 进行最终验证。

错误处理与调试

OpenSSL 提供详细的错误报告机制,可在验证失败时获取更多信息:

c 复制代码
void print_openssl_error() {
    unsigned long err;
    const char* file = NULL;
    const char* data = NULL;
    int line, flags;
    
    while ((err = ERR_get_error_line_data(&file, &line, &data, &flags)) != 0) {
        char buf[256];
        ERR_error_string_n(err, buf, sizeof(buf));
        fprintf(stderr, "OpenSSL error: %s\n", buf);
        fprintf(stderr, "  at %s:%d\n", file, line);
        if (data && (flags & ERR_TXT_STRING)) {
            fprintf(stderr, "  data: %s\n", data);
        }
    }
}

// 在验证函数中使用示例
if (EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, pubkey) != 1) {
    fprintf(stderr, "Error initializing verification\n");
    print_openssl_error();
    goto cleanup;
}

常见错误处理

  • 初始化失败 :检查公钥格式是否正确,算法是否支持。
  • 更新数据失败 :检查数据缓冲区是否有效。
  • 验证失败 :检查签名数据是否正确,公钥是否匹配私钥。
  • 内存错误 :确保所有分配的资源都被正确释放。

文件验签与字符串验签对比

特性 文件验签 字符串验签
数据来源 文件系统 内存中的字符串
数据加载 需要文件 I/O 操作 直接使用内存数据
大数据处理 可分块读取处理 通常一次性处理
适用场景 验证文件完整性 验证 API 消息、配置数据等
性能考虑 受磁盘 I/O 影响 纯内存操作,速度更快

总结与最佳实践

  • 算法选择 :推荐使用 SHA - 256 或更强的哈希算法,避免 MD5 或 SHA1 等不安全算法。
  • 密钥管理 :妥善保管私钥,公钥可通过证书链验证真实性。
  • 错误处理 :始终检查 OpenSSL 函数返回值,实现全面错误处理。
  • 资源管理 :确保分配资源正确释放,防止内存泄漏。
  • 性能优化 :大文件可分块调用 EVP_DigestVerifyUpdate,避免一次性加载。
相关推荐
Bioinfo Guy3 分钟前
R包安装报错解决案例系列|R包使用及ARM架构解决data.table安装错误问题
开发语言·arm开发·r语言
1白天的黑夜134 分钟前
数据结构之堆(topk问题、堆排序)
c语言·数据结构·算法
加什么瓦1 小时前
Java—多线程
java·开发语言
zdy12635746881 小时前
python第35天打卡
开发语言·python
安卓机器1 小时前
如何撰写一篇优质 Python 相关的技术文档 进阶指南
开发语言·python
waterHBO1 小时前
python 小工具,获取 github 仓库信息
开发语言·python·github
放逐者-保持本心,方可放逐1 小时前
浅谈 JavaScript 性能优化
开发语言·javascript·性能优化·vue3·v-memo·vue3性能优化·v-once
kaiyuanheshang2 小时前
python项目和依赖管理工具uv简介
开发语言·python·uv
金串串2 小时前
js-day4
开发语言·前端·javascript
啊啊啊~~2 小时前
js 手写promise
开发语言·前端·javascript