PKCS#11 机制与操作:加密、解密、签名、验证
这一章咱们要动真格的了。前面几章我们把 PKCS#11 的 、Session 和对象打好了基础,现在要聊的是这些基础怎么组合起来干活——加密、解密、签名、验证。这四种操作,说白了就是密码学的四大基本动作。我个人习惯把它们分成两组:加密/解密是用来保密的,签名/验证是用来防篡改和确认身份的。你先记住这个分类,后面所有细节都绕不开它。
机制是什么?为什么它这么重要?
在 PKCS#11 里,机制(Mechanism) 决定了你要用哪种算法、哪种模式去干活。你想想看,同样是用 AES 加密,CBC 模式和 GCM 模式差别大得很。机制就是告诉 :「我用的是 CKM_AES_CBC_PAD,不是 CKM_AES_ECB。」
我个人经验是,新手最容易犯的错就是随便选一个机制。我在项目中遇到过有人直接用了 CKM_RSA_PKCS 去签审计日志,结果发现对方 只支持 CKM_SHA256_RSA_PKCS。嗯,那真是「万事俱备,只欠机制不支持」的尴尬场景。
注意:调用任何 Init 函数之前,一定要先查 支持哪些机制。用 C_GetMechanismInfo 去看,别硬来。我曾经见过生产环境因为机制不匹配,HSC 直接返回 CKR_MECHANISM_INVALID,整批日志签名全部失败。
加密和解密的核心流程
加密和解密的流程非常对称。说白了,就是 Init 一把密钥,然后塞数据进去,出来就是密文或明文。我一步步拆开说。
- 选机制和密钥:比如 AES-GCM 就用
CKM_AES_GCM,密钥对象必须是CKO_SECRET_KEY。 - 调用 C_EncryptInit:传入会话句柄、机制结构体、密钥句柄。这一步 会准备好内部状态。
- 调用 C_Encrypt:传入明文缓冲区,得到密文。注意一次性传完,或者分多次调用
C_EncryptUpdate。 - 解密完全反过来:
C_DecryptInit+C_Decrypt,用同一个密钥。
// 伪代码示例:AES-GCM 加密
CK_SESSION_HANDLE hSession;
CK_OBJECT_HANDLE hKey;
CK_BYTE plaintext[] = "审计日志内容";
CK_BYTE ciphertext[256];
CK_ULONG cipherLen = sizeof(ciphertext);
CK_GCM_PARAMS gcmParams;
gcmParams.pIv = myIv;
gcmParams.ulIvLen = 12;
gcmParams.ulTagBits = 128;
CK_MECHANISM mech = {CKM_AES_GCM, &gcmParams, sizeof(gcmParams)};
// 第一步:初始化加密
rv = C_EncryptInit(hSession, &mech, hKey);
if (rv != CKR_OK) {
// 处理错误,我曾经卡在这里是因为没给 IV
}
// 第二步:执行加密
rv = C_Encrypt(hSession, plaintext, sizeof(plaintext), ciphertext, &cipherLen);
if (rv == CKR_OK) {
// ciphertext 里就是加密结果
}
小提示:对于分块加密(比如文件很大),用 C_EncryptUpdate 和 C_EncryptFinal 组合。我习惯先调 C_EncryptInit,然后循环调 Update,最后 Final 收尾。这样内存占用小,也符合审计日志流式处理的需求。
签名和验证:审计日志的灵魂
审计日志签名,核心就是用私钥对日志内容做签名,别人用公钥验证。PKCS#11 里,C_Sign 和 C_Verify 就是干这个的。注意,签名用的是私钥,验证用的是公钥,千万别搞反了。
| 操作 | Init 函数 | 执行函数 | 密钥类型 | 典型机制 |
|---|---|---|---|---|
| 签名 | C_SignInit | C_Sign | CKO_PRIVATE_KEY | CKM_RSA_PKCS, CKM_ECDSA |
| 验证 | C_VerifyInit | C_Verify | CKO_PUBLIC_KEY | CKM_RSA_PKCS, CKM_ECDSA |
| 带哈希签名 | C_SignInit | C_Sign | CKO_PRIVATE_KEY | CKM_SHA256_RSA_PKCS |
| 带哈希验证 | C_VerifyInit | C_Verify | CKO_PUBLIC_KEY | CKM_SHA256_RSA_PKCS |
你可能会问:「为什么要有带哈希的机制?」说白了,就是 内部帮你算哈希,你只需要传原始数据就行。省事。但有个坑——有些旧 不支持带哈希的机制,你得自己先 SHA256,然后传摘要。这取决于你的部署环境。我个人建议优先用带哈希的,省得自己再造轮子。
避坑指南:我踩过的那些雷
- 密钥未登录:签名私钥通常是
CKA_PRIVATE为 True,不登录 Session 直接就返回CKR_USER_NOT_LOGGED_IN。我刚开始做审计日志项目时,连这个都忘了,排查了半天。 - 机制结构体初始化不全:比如 RSA-PSS 需要额外参数
CK_RSA_PKCS_PSS_PARAMS,忘了填充 hashAlg 就等着报错吧。 - 数据太大一次性调用:有些 对单次输入长度有限制。审计日志可能很长,最好用 Update/Final 模式分批处理。
- 签名结果缓冲区太小:RSA 2048 的签名长度固定 256 字节,别申请 128 字节的缓冲区。先调一次
C_Sign传 NULL 拿长度,再正式签。
实战:审计日志签名验证的典型调用序列
假设我们要对一条审计记录做签名。完整流程如下:
- 打开 Session,登录(如果需要操作私钥)。
- 查找签名私钥(通过
C_FindObjects,标签是审计专用的)。 - 调用
C_SignInit,传入CKM_SHA256_RSA_PKCS和私钥句柄。 - 调用
C_Sign,传入审计日志原文,得到签名值。 - 将签名值附加到日志记录后面,或者单独存到签名数据库。
- 验证时,查找对应的公钥,调
C_VerifyInit+C_Verify,传入原文和签名值。
// 签名验证的完整伪代码
CK_BYTE logData[] = "2024-01-15 10:30:00 | admin | DELETE /api/users";
CK_BYTE signature[256];
CK_ULONG sigLen = sizeof(signature);
// 签名侧
rv = C_SignInit(hSession, &mech, hPrivateKey);
rv = C_Sign(hSession, logData, sizeof(logData), signature, &sigLen);
// 验证侧(另一个 Session 或另一台机器)
rv = C_VerifyInit(hSession, &mech, hPublicKey);
rv = C_Verify(hSession, logData, sizeof(logData), signature, sigLen);
if (rv == CKR_OK) {
// 日志未被篡改
printf("签名验证通过\n");
} else if (rv == CKR_SIGNATURE_INVALID) {
// 日志被篡改或密钥不匹配
printf("签名验证失败!\n");
}
关键点:务必将 C_Verify 的返回值区分为 CKR_OK 和 CKR_SIGNATURE_INVALID。前者表示验证通过,后者表示签名不一致。其他错误码(如 CKR_DEVICE_ERROR)说明硬件或环境有问题,需要重试或告警。