PKCS#11 机制与操作:加密、解密、签名、验证

这一章咱们要动真格的了。前面几章我们把 PKCS#11 的 、Session 和对象打好了基础,现在要聊的是这些基础怎么组合起来干活——加密、解密、签名、验证。这四种操作,说白了就是密码学的四大基本动作。我个人习惯把它们分成两组:加密/解密是用来保密的,签名/验证是用来防篡改和确认身份的。你先记住这个分类,后面所有细节都绕不开它。

PKCS#11 加密/解密与签名/验证调用流程 加密 / 解密 C_EncryptInit (session, mechanism, key) C_Encrypt (session, data, cipher) C_DecryptInit (session, mechanism, key) C_Decrypt (session, cipher, data) 签名 / 验证 C_SignInit (session, mechanism, key) C_Sign (session, data, signature) C_VerifyInit (session, mechanism, key) C_Verify (session, data, signature) 核心要点 • 所有操作都遵循:Init(会话, 机制, 密钥) → 执行(会话, 数据, 输出) 的两步模式 • 加密/解密使用相同密钥(对称),签名/验证使用密钥对(非对称) 对称 vs 非对称

机制是什么?为什么它这么重要?

在 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 一把密钥,然后塞数据进去,出来就是密文或明文。我一步步拆开说。

  1. 选机制和密钥:比如 AES-GCM 就用 CKM_AES_GCM,密钥对象必须是 CKO_SECRET_KEY。
  2. 调用 C_EncryptInit:传入会话句柄、机制结构体、密钥句柄。这一步 会准备好内部状态。
  3. 调用 C_Encrypt:传入明文缓冲区,得到密文。注意一次性传完,或者分多次调用 C_EncryptUpdate。
  4. 解密完全反过来: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_SignInitC_SignCKO_PRIVATE_KEYCKM_RSA_PKCS, CKM_ECDSA
验证C_VerifyInitC_VerifyCKO_PUBLIC_KEYCKM_RSA_PKCS, CKM_ECDSA
带哈希签名C_SignInitC_SignCKO_PRIVATE_KEYCKM_SHA256_RSA_PKCS
带哈希验证C_VerifyInitC_VerifyCKO_PUBLIC_KEYCKM_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 拿长度,再正式签。

实战:审计日志签名验证的典型调用序列

假设我们要对一条审计记录做签名。完整流程如下:

  1. 打开 Session,登录(如果需要操作私钥)。
  2. 查找签名私钥(通过 C_FindObjects,标签是审计专用的)。
  3. 调用 C_SignInit,传入 CKM_SHA256_RSA_PKCS 和私钥句柄。
  4. 调用 C_Sign,传入审计日志原文,得到签名值。
  5. 将签名值附加到日志记录后面,或者单独存到签名数据库。
  6. 验证时,查找对应的公钥,调 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)说明硬件或环境有问题,需要重试或告警。