4. HSM密钥导入与导出封装

任务目标

在硬件安全模块(HSM)中生成基于NIST P-256曲线的ECC密钥对,并使用私钥对消息摘要执行ECDSA签名,最后通过公钥验证签名有效性。本案例聚焦于HSM内部密钥生成与签名操作的标准流程,不涉及密钥导出或明文私钥访问。

环境与依赖

本案例基于OpenSSL 3.x的PKCS#11接口实现,需要以下环境:

  • OpenSSL 3.0+(支持PKCS#11 provider)
  • SoftHSMv2作为HSM模拟器(或真实HSM设备)
  • 操作系统:Linux(示例命令基于Ubuntu/Debian)

安装依赖:

sudo apt install softhsm2 openssl libengine-pkcs11-openssl
# 初始化HSM (若首次使用)
sudo softhsm2-util --init- --slot 0 --label "demo" --pin 1234 --so-pin 4321

实现代码

以下C程序使用PKCS#11标准接口完成密钥生成与ECDSA签名验证全流程:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "pkcs11.h"

#define PIN "1234"
#define _LABEL "demo"

static CK_FUNCTION_LIST_PTR p11 = NULL;
static CK_SLOT_ID slot_id = 0;
static CK_SESSION_HANDLE session = 0;
static CK_OBJECT_HANDLE priv_key = 0, pub_key = 0;

static CK_RV init_pkcs11(void) {
    CK_RV rv;
    CK_C_GetFunctionList get_list = NULL;
    
    void *handle = dlopen("libsofthsm2.so", RTLD_NOW);
    if (!handle) return CKR_GENERAL_ERROR;
    
    get_list = (CK_C_GetFunctionList)dlsym(handle, "C_GetFunctionList");
    if (!get_list) return CKR_GENERAL_ERROR;
    
    rv = get_list(&p11);
    if (rv != CKR_OK) return rv;
    
    rv = p11->C_Initialize(NULL);
    if (rv != CKR_OK && rv != CKR_CRYPTOKI_ALREADY_INITIALIZED) return rv;
    
    CK_ULONG count;
    rv = p11->C_GetSlotList(CK_TRUE, NULL, &count);
    if (rv != CKR_OK) return rv;
    
    CK_SLOT_ID *slots = malloc(sizeof(CK_SLOT_ID) * count);
    rv = p11->C_GetSlotList(CK_TRUE, slots, &count);
    if (rv != CKR_OK) { free(slots); return rv; }
    
    // 查找目标
    for (CK_ULONG i = 0; i < count; i++) {
        CK__INFO info;
        if (p11->C_GetTokenInfo(slots[i], &info) == CKR_OK) {
            if (strncmp((char*)info.label, _LABEL, strlen(_LABEL)) == 0) {
                slot_id = slots[i];
                break;
            }
        }
    }
    free(slots);
    return (slot_id != 0) ? CKR_OK : CKR__NOT_RECOGNIZED;
}

static CK_RV generate_keypair(void) {
    CK_RV rv;
    CK_MECHANISM mech = {CKM_EC_KEY_PAIR_GEN, NULL, 0};
    
    // 私钥模板
    CK_ATTRIBUTE priv_tmpl[] = {
        {CKA_, &(CK_BBOOL){CK_TRUE}, sizeof(CK_BBOOL)},
        {CKA_PRIVATE, &(CK_BBOOL){CK_TRUE}, sizeof(CK_BBOOL)},
        {CKA_SIGN, &(CK_BBOOL){CK_TRUE}, sizeof(CK_BBOOL)},
        {CKA_EC_PARAMS, (CK_VOID_PTR)"\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07", 10} // P-256 OID
    };
    
    // 公钥模板
    CK_ATTRIBUTE pub_tmpl[] = {
        {CKA_, &(CK_BBOOL){CK_TRUE}, sizeof(CK_BBOOL)},
        {CKA_PRIVATE, &(CK_BBOOL){CK_FALSE}, sizeof(CK_BBOOL)},
        {CKA_VERIFY, &(CK_BBOOL){CK_TRUE}, sizeof(CK_BBOOL)},
        {CKA_EC_PARAMS, (CK_VOID_PTR)"\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07", 10}
    };
    
    rv = p11->C_GenerateKeyPair(session, &mech,
                                pub_tmpl, sizeof(pub_tmpl)/sizeof(CK_ATTRIBUTE),
                                priv_tmpl, sizeof(priv_tmpl)/sizeof(CK_ATTRIBUTE),
                                &pub_key, &priv_key);
    return rv;
}

static CK_RV sign_and_verify(void) {
    CK_RV rv;
    CK_MECHANISM sign_mech = {CKM_ECDSA, NULL, 0};
    CK_MECHANISM verify_mech = {CKM_ECDSA, NULL, 0};
    
    // 待签名数据(实际应用中为消息摘要)
    CK_BYTE msg[] = "HSM ECDSA test message";
    CK_BYTE signature[72];
    CK_ULONG sig_len = sizeof(signature);
    
    // 签名
    rv = p11->C_SignInit(session, &sign_mech, priv_key);
    if (rv != CKR_OK) return rv;
    rv = p11->C_Sign(session, msg, sizeof(msg)-1, signature, &sig_len);
    if (rv != CKR_OK) return rv;
    
    printf("签名长度: %lu 字节\n", sig_len);
    
    // 验证
    rv = p11->C_VerifyInit(session, &verify_mech, pub_key);
    if (rv != CKR_OK) return rv;
    rv = p11->C_Verify(session, msg, sizeof(msg)-1, signature, sig_len);
    
    return rv;
}

int main(void) {
    CK_RV rv;
    
    rv = init_pkcs11();
    if (rv != CKR_OK) { printf("PKCS#11初始化失败: 0x%lx\n", rv); return 1; }
    
    rv = p11->C_OpenSession(slot_id, CKF_SERIAL_SESSION | CKF_RW_SESSION,
                            NULL, NULL, &session);
    if (rv != CKR_OK) { printf("会话打开失败: 0x%lx\n", rv); return 1; }
    
    rv = p11->C_Login(session, CKU_USER, (CK_UTF8CHAR_PTR)PIN, strlen(PIN));
    if (rv != CKR_OK) { printf("登录失败: 0x%lx\n", rv); return 1; }
    
    rv = generate_keypair();
    if (rv != CKR_OK) { printf("密钥生成失败: 0x%lx\n", rv); return 1; }
    printf("ECC P-256密钥对生成成功\n");
    
    rv = sign_and_verify();
    if (rv == CKR_OK) {
        printf("ECDSA签名与验证成功\n");
    } else {
        printf("签名/验证失败: 0x%lx\n", rv);
    }
    
    p11->C_Logout(session);
    p11->C_CloseSession(session);
    p11->C_Finalize(NULL);
    
    return (rv == CKR_OK) ? 0 : 1;
}

编译与运行

# 编译(需安装libsofthsm2-dev或相应PKCS#11头文件)
gcc -o ecdsa_demo ecdsa_demo.c -I/usr/include/softhsm2 -ldl -lcrypto

# 运行
./ecdsa_demo

验证方法

程序执行后,观察输出是否依次出现以下结果:

  1. ECC P-256密钥对生成成功——表示C_GenerateKeyPair调用成功,密钥对已安全存储在HSM中
  2. 签名长度: 70~72 字节——ECDSA签名长度在70-72字节之间(P-256曲线标准范围)
  3. ECDSA签名与验证成功——表示C_Verify返回CKR_OK,签名验证通过

若最后一行输出签名/验证失败,则说明签名验证未通过,需检查密钥模板参数或数据输入是否一致。

常见问题

  • 找不到:确认SoftHSM初始化时使用的label与代码中_LABEL一致
  • C_GenerateKeyPair返回CKR_ATTRIBUTE_VALUE_INVALID:检查EC_PARAMS的OID编码是否正确(P-256对应DER编码的OID 1.2.840.10045.3.1.7)
  • 签名长度异常:ECDSA签名长度取决于r和s值,P-256曲线下通常在70-72字节之间,若超出72字节说明实现有误