C语言如何用OpenSSL实现数字签名?

99ANYc3cd6
预计阅读时长 30 分钟
位置: 首页 C语言 正文
  1. 签名:发送方使用自己的私钥对数据的哈希值进行加密,生成签名。
  2. 验证:接收方使用发送方的公钥解密签名,得到哈希值,然后与自己对原始数据计算出的哈希值进行比较,以验证数据的完整性和来源的真实性。

下面我将分步提供一个完整的、可运行的 C 语言示例,涵盖签名和验证的全过程。

c语言 openssl sign
(图片来源网络,侵删)

准备工作:生成密钥对

在编写 C 代码之前,你需要一个 RSA 密钥对(私钥和公钥),你可以使用 OpenSSL 的命令行工具来生成它们。

  1. 生成私钥 打开终端或命令行,运行以下命令:

    openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048

    这会生成一个 2048 位的 RSA 私钥,并保存到 private_key.pem 文件中。

  2. 从私钥提取公钥 运行以下命令,从私钥文件中提取公钥:

    c语言 openssl sign
    (图片来源网络,侵删)
    openssl rsa -pubout -in private_key.pem -out public_key.pem

    这会生成 public_key.pem 文件,其中只包含公钥信息。

你有了两个文件:private_key.pempublic_key.pem,可以开始编写 C 代码了。


C 语言示例代码

这个示例将演示如何:

  1. 读取一个待签名的文件(data.txt)。
  2. 使用私钥对文件内容进行签名。
  3. 将签名结果保存到另一个文件(signature.bin)。
  4. 使用公钥验证签名是否有效。

创建待签名的数据文件

创建一个名为 data.txt 的文件,并写入一些内容,

c语言 openssl sign
(图片来源网络,侵删)
Hello, this is a message to be signed.
你好,这是一条待签名的消息。

C 语言源代码 (sign_verify.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include <openssl/evp.h>
// 定义一个函数来打印 OpenSSL 错误信息
void handleErrors(const char *msg) {
    fprintf(stderr, "Error: %s\n", msg);
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
}
// 函数:使用私钥对文件进行签名
int signFile(const char *filePath, const char *privateKeyPath, const char *signaturePath) {
    FILE *fp, *sigFp;
    EVP_PKEY *pkey = NULL;
    EVP_MD_CTX *mdctx = NULL;
    unsigned char *sig = NULL;
    unsigned char *fileData = NULL;
    size_t fileLen, sigLen;
    // 1. 读取私钥
    fp = fopen(privateKeyPath, "rb");
    if (!fp) handleErrors("无法打开私钥文件");
    pkey = PEM_read_PrivateKey(fp, NULL, NULL, NULL);
    fclose(fp);
    if (!pkey) handleErrors("无法解析私钥");
    // 2. 读取待签名的文件
    fp = fopen(filePath, "rb");
    if (!fp) handleErrors("无法打开待签名文件");
    fseek(fp, 0, SEEK_END);
    fileLen = ftell(fp);
    fseek(fp, 0, SEEK_SET);
    fileData = malloc(fileLen + 1);
    if (!fileData) handleErrors("内存分配失败");
    fread(fileData, 1, fileLen, fp);
    fclose(fp);
    // 3. 初始化签名上下文
    mdctx = EVP_MD_CTX_new();
    if (!mdctx) handleErrors("无法创建 EVP_MD_CTX");
    if (EVP_DigestSignInit(mdctx, NULL, EVP_sha256(), NULL, pkey) != 1) {
        handleErrors("EVP_DigestSignInit 失败");
    }
    // 4. 更新上下文(提供数据)
    if (EVP_DigestSignUpdate(mdctx, fileData, fileLen) != 1) {
        handleErrors("EVP_DigestSignUpdate 失败");
    }
    // 5. 确定签名的长度
    if (EVP_DigestSignFinal(mdctx, NULL, &sigLen) != 1) {
        handleErrors("EVP_DigestSignFinal (第一次) 失败");
    }
    sig = malloc(sigLen);
    if (!sig) handleErrors("为签名分配内存失败");
    // 6. 生成签名
    if (EVP_DigestSignFinal(mdctx, sig, &sigLen) != 1) {
        handleErrors("EVP_DigestSignFinal (第二次) 失败");
    }
    // 7. 将签名写入文件
    sigFp = fopen(signaturePath, "wb");
    if (!sigFp) handleErrors("无法打开签名文件");
    fwrite(sig, 1, sigLen, sigFp);
    fclose(sigFp);
    // 8. 清理资源
    free(fileData);
    free(sig);
    EVP_MD_CTX_free(mdctx);
    EVP_PKEY_free(pkey);
    printf("签名成功!签名已保存到 %s\n", signaturePath);
    return 1;
}
// 函数:使用公钥验证文件签名
int verifyFile(const char *filePath, const char *publicKeyPath, const char *signaturePath) {
    FILE *fp, *sigFp;
    EVP_PKEY *pkey = NULL;
    EVP_MD_CTX *mdctx = NULL;
    unsigned char *sig = NULL;
    unsigned char *fileData = NULL;
    size_t fileLen, sigLen;
    // 1. 读取公钥
    fp = fopen(publicKeyPath, "rb");
    if (!fp) handleErrors("无法打开公钥文件");
    pkey = PEM_read_PUBKEY(fp, NULL, NULL, NULL);
    fclose(fp);
    if (!pkey) handleErrors("无法解析公钥");
    // 2. 读取待验证的文件
    fp = fopen(filePath, "rb");
    if (!fp) handleErrors("无法打开待验证文件");
    fseek(fp, 0, SEEK_END);
    fileLen = ftell(fp);
    fseek(fp, 0, SEEK_SET);
    fileData = malloc(fileLen + 1);
    if (!fileData) handleErrors("内存分配失败");
    fread(fileData, 1, fileLen, fp);
    fclose(fp);
    // 3. 读取签名文件
    sigFp = fopen(signaturePath, "rb");
    if (!sigFp) handleErrors("无法打开签名文件");
    fseek(sigFp, 0, SEEK_END);
    sigLen = ftell(sigFp);
    fseek(sigFp, 0, SEEK_SET);
    sig = malloc(sigLen);
    if (!sig) handleErrors("为签名分配内存失败");
    fread(sig, 1, sigLen, sigFp);
    fclose(sigFp);
    // 4. 初始化验证上下文
    mdctx = EVP_MD_CTX_new();
    if (!mdctx) handleErrors("无法创建 EVP_MD_CTX");
    if (EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, pkey) != 1) {
        handleErrors("EVP_DigestVerifyInit 失败");
    }
    // 5. 更新上下文(提供数据)
    if (EVP_DigestVerifyUpdate(mdctx, fileData, fileLen) != 1) {
        handleErrors("EVP_DigestVerifyUpdate 失败");
    }
    // 6. 验证签名
    int verificationResult = EVP_DigestVerifyFinal(mdctx, sig, sigLen);
    // 7. 清理资源
    free(fileData);
    free(sig);
    EVP_MD_CTX_free(mdctx);
    EVP_PKEY_free(pkey);
    // 7. 输出验证结果
    if (verificationResult == 1) {
        printf("验证成功!签名有效,\n");
        return 1;
    } else if (verificationResult == 0) {
        printf("验证失败!签名无效或数据被篡改,\n");
        return 0;
    } else {
        handleErrors("EVP_DigestVerifyFinal 发生错误");
    }
}
int main(int argc, char *argv[]) {
    // 初始化 OpenSSL
    OpenSSL_add_all_algorithms();
    ERR_load_crypto_strings();
    const char *dataFile = "data.txt";
    const char *privateKeyFile = "private_key.pem";
    const char *publicKeyFile = "public_key.pem";
    const char *signatureFile = "signature.bin";
    // --- 步骤 1: 签名 ---
    printf("--- 开始签名过程 ---\n");
    signFile(dataFile, privateKeyFile, signatureFile);
    // --- 步骤 2: 验证(原始数据) ---
    printf("\n--- 开始验证过程(原始数据) ---\n");
    verifyFile(dataFile, publicKeyFile, signatureFile);
    // --- 步骤 3: 验证(篡改后的数据) ---
    printf("\n--- 开始验证过程(篡改数据) ---\n");
    // 修改 data.txt 文件以模拟篡改
    FILE *fp = fopen(dataFile, "w");
    if (fp) {
        fprintf(fp, "This message has been tampered with!");
        fclose(fp);
    }
    // 尝试验证篡改后的文件
    verifyFile(dataFile, publicKeyFile, signatureFile);
    // 清理 OpenSSL
    EVP_cleanup();
    ERR_free_strings();
    return 0;
}

编译代码

你需要安装 OpenSSL 开发库,在 Linux (Debian/Ubuntu) 上,可以使用 libssl-dev

# 安装 OpenSSL 开发库 (如果尚未安装)
sudo apt-get update
sudo apt-get install libssl-dev
# 编译 C 代码
# -lcrypt 是链接 OpenSSL 库所必需的
gcc sign_verify.c -o sign_verify -lcrypto

运行程序

确保你的目录中有以下文件:

  • sign_verify (编译后的可执行文件)
  • data.txt
  • private_key.pem
  • public_key.pem

然后运行程序:

./sign_verify

预期输出

--- 开始签名过程 ---
签名成功!签名已保存到 signature.bin
--- 开始验证过程(原始数据) ---
验证成功!签名有效。
--- 开始验证过程(篡改数据) ---
验证失败!签名无效或数据被篡改。

程序运行后,会生成一个 signature.bin 文件,其中包含了 data.txt 的数字签名,在第三次验证时,由于我们修改了 data.txt 的内容,验证会失败,这证明了数字签名能够确保数据的完整性。


代码核心要点解析

  1. 头文件:

    • openssl/rsa.h, openssl/pem.h: 用于加载 PEM 格式的密钥。
    • openssl/evp.h: 高级加密标准库,是 OpenSSL 中最推荐使用的 API,因为它抽象了底层的加密算法(如 RSA、DSA),使得代码更通用、更简洁。
    • openssl/err.h: 用于打印详细的错误信息。
  2. 签名流程 (signFile 函数):

    • 加载私钥: PEM_read_PrivateKey 从文件中读取私钥,返回一个 EVP_PKEY 结构体指针,这是 OpenSSL 中表示密钥的通用方式。
    • 读取数据: 将待签名的文件内容读入内存。
    • 创建上下文: EVP_MD_CTX_new() 创建一个消息摘要上下文,它管理着哈希计算和签名操作的状态。
    • 初始化: EVP_DigestSignInit 使用指定的哈希算法(这里是 EVP_sha256())和私钥来初始化上下文。
    • 更新数据: EVP_DigestSignUpdate 将数据块送入上下文进行哈希计算,可以多次调用,处理大文件时非常有用。
    • 计算签名长度: EVP_DigestSignFinal 第一次调用时,sig 参数传 NULL,它会返回签名所需的长度 sigLen
    • 生成签名: 分配足够大的内存后,再次调用 EVP_DigestSignFinal,它会将实际的签名写入 sig 缓冲区。
    • 清理: EVP_MD_CTX_freeEVP_PKEY_free 用于释放分配的资源。
  3. 验证流程 (verifyFile 函数):

    • 加载公钥: PEM_read_PUBKEY 从文件中读取公钥。
    • 读取数据和签名: 将待验证的文件内容和签名文件都读入内存。
    • 创建和初始化上下文: 与签名类似,使用 EVP_DigestVerifyInit 初始化,但传入的是公钥。
    • 更新数据: EVP_DigestVerifyUpdate 将原始数据送入上下文。
    • 验证签名: EVP_DigestVerifyFinal 是最后一步,它使用公钥解密签名,得到原始的哈希值,然后与上下文中计算出的哈希值进行比较。
      • 返回 1 表示验证成功。
      • 返回 0 表示验证失败(签名不匹配或数据被篡改)。
      • 返回 < 0 表示发生了内部错误。

这个例子涵盖了 OpenSSL 签名和验证的核心概念,是理解和实践 C 语言中 OpenSSL 加密功能的一个非常好的起点。

-- 展开阅读全文 --
头像
dede html2text乱码如何解决?
« 上一篇 2025-12-16
织梦format=html5是什么?
下一篇 » 2025-12-16

相关文章

取消
微信二维码
支付宝二维码

目录[+]