- 签名:发送方使用自己的私钥对数据的哈希值进行加密,生成签名。
- 验证:接收方使用发送方的公钥解密签名,得到哈希值,然后与自己对原始数据计算出的哈希值进行比较,以验证数据的完整性和来源的真实性。
下面我将分步提供一个完整的、可运行的 C 语言示例,涵盖签名和验证的全过程。

(图片来源网络,侵删)
准备工作:生成密钥对
在编写 C 代码之前,你需要一个 RSA 密钥对(私钥和公钥),你可以使用 OpenSSL 的命令行工具来生成它们。
-
生成私钥 打开终端或命令行,运行以下命令:
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
这会生成一个 2048 位的 RSA 私钥,并保存到
private_key.pem文件中。 -
从私钥提取公钥 运行以下命令,从私钥文件中提取公钥:
(图片来源网络,侵删)openssl rsa -pubout -in private_key.pem -out public_key.pem
这会生成
public_key.pem文件,其中只包含公钥信息。
你有了两个文件:private_key.pem 和 public_key.pem,可以开始编写 C 代码了。
C 语言示例代码
这个示例将演示如何:
- 读取一个待签名的文件(
data.txt)。 - 使用私钥对文件内容进行签名。
- 将签名结果保存到另一个文件(
signature.bin)。 - 使用公钥验证签名是否有效。
创建待签名的数据文件
创建一个名为 data.txt 的文件,并写入一些内容,

(图片来源网络,侵删)
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.txtprivate_key.pempublic_key.pem
然后运行程序:
./sign_verify
预期输出
--- 开始签名过程 ---
签名成功!签名已保存到 signature.bin
--- 开始验证过程(原始数据) ---
验证成功!签名有效。
--- 开始验证过程(篡改数据) ---
验证失败!签名无效或数据被篡改。
程序运行后,会生成一个 signature.bin 文件,其中包含了 data.txt 的数字签名,在第三次验证时,由于我们修改了 data.txt 的内容,验证会失败,这证明了数字签名能够确保数据的完整性。
代码核心要点解析
-
头文件:
openssl/rsa.h,openssl/pem.h: 用于加载 PEM 格式的密钥。openssl/evp.h: 高级加密标准库,是 OpenSSL 中最推荐使用的 API,因为它抽象了底层的加密算法(如 RSA、DSA),使得代码更通用、更简洁。openssl/err.h: 用于打印详细的错误信息。
-
签名流程 (
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_free和EVP_PKEY_free用于释放分配的资源。
- 加载私钥:
-
验证流程 (
verifyFile函数):- 加载公钥:
PEM_read_PUBKEY从文件中读取公钥。 - 读取数据和签名: 将待验证的文件内容和签名文件都读入内存。
- 创建和初始化上下文: 与签名类似,使用
EVP_DigestVerifyInit初始化,但传入的是公钥。 - 更新数据:
EVP_DigestVerifyUpdate将原始数据送入上下文。 - 验证签名:
EVP_DigestVerifyFinal是最后一步,它使用公钥解密签名,得到原始的哈希值,然后与上下文中计算出的哈希值进行比较。- 返回
1表示验证成功。 - 返回
0表示验证失败(签名不匹配或数据被篡改)。 - 返回
< 0表示发生了内部错误。
- 返回
- 加载公钥:
这个例子涵盖了 OpenSSL 签名和验证的核心概念,是理解和实践 C 语言中 OpenSSL 加密功能的一个非常好的起点。
