2. 生成设备唯一密钥对

任务目标

为每个设备生成唯一的 RSA-2048 非对称密钥对,私钥以 PEM 格式安全存储于设备本地(权限 600),公钥导出用于后续签名验证。本案例聚焦密钥生成与安全落盘,不涉及 HSM 硬件对接。

环境依赖

  • OpenSSL 库(libssl-dev / openssl-devel)
  • C 编译器(gcc)
  • Linux 环境(用于文件权限控制)

关键完整代码


#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/err.h>

#define KEY_DIR    "/opt/device_keys"
#define PRIV_PATH  KEY_DIR "/device_priv.pem"
#define PUB_PATH   KEY_DIR "/device_pub.pem"

/* 生成唯一密钥对:基于设备序列号(此处用时间戳模拟唯一性) */
int generate_device_keypair(const char *serial) {
    // 1. 创建安全目录(0700)
    mkdir(KEY_DIR, 0700);
    chmod(KEY_DIR, 0700);

    // 2. 生成 RSA-2048 密钥对
    EVP_PKEY *pkey = EVP_PKEY_new();
    if (!pkey) return -1;
    EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
    if (!ctx) { EVP_PKEY_free(pkey); return -1; }

    if (EVP_PKEY_keygen_init(ctx) <= 0 ||
        EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048) <= 0 ||
        EVP_PKEY_keygen(ctx, &pkey) <= 0) {
        EVP_PKEY_CTX_free(ctx);
        EVP_PKEY_free(pkey);
        return -1;
    }
    EVP_PKEY_CTX_free(ctx);

    // 3. 私钥加密写盘(AES-256-CBC,密码派生自设备序列号)
    char pass[128];
    snprintf(pass, sizeof(pass), "sn_%s", serial);
    FILE *fp = fopen(PRIV_PATH, "w");
    if (!fp) { EVP_PKEY_free(pkey); return -1; }
    chmod(PRIV_PATH, 0600);  // 仅属主可读写

    if (!PEM_write_PrivateKey(fp, pkey, EVP_aes_256_cbc(),
                              (unsigned char *)pass, strlen(pass),
                              NULL, NULL)) {
        fclose(fp);
        EVP_PKEY_free(pkey);
        return -1;
    }
    fclose(fp);

    // 4. 公钥明文写盘
    fp = fopen(PUB_PATH, "w");
    if (!fp) { EVP_PKEY_free(pkey); return -1; }
    chmod(PUB_PATH, 0644);
    if (!PEM_write_PUBKEY(fp, pkey)) {
        fclose(fp);
        EVP_PKEY_free(pkey);
        return -1;
    }
    fclose(fp);

    EVP_PKEY_free(pkey);
    return 0;
}

/* 验证:用公钥验证私钥签名 */
int verify_keypair(const char *serial) {
    // 读取私钥(需密码)
    char pass[128];
    snprintf(pass, sizeof(pass), "sn_%s", serial);
    FILE *fp = fopen(PRIV_PATH, "r");
    if (!fp) return -1;
    EVP_PKEY *priv = PEM_read_PrivateKey(fp, NULL, NULL, pass);
    fclose(fp);
    if (!priv) return -1;

    // 读取公钥
    fp = fopen(PUB_PATH, "r");
    if (!fp) { EVP_PKEY_free(priv); return -1; }
    EVP_PKEY *pub = PEM_read_PUBKEY(fp, NULL, NULL, NULL);
    fclose(fp);
    if (!pub) { EVP_PKEY_free(priv); return -1; }

    // 签名并验证
    const char *msg = "device_keypair_test";
    unsigned char sig[256];
    size_t sig_len = 0;
    EVP_MD_CTX *mdctx = EVP_MD_CTX_new();

    int ok = 0;
    if (EVP_DigestSignInit(mdctx, NULL, EVP_sha256(), NULL, priv) == 1 &&
        EVP_DigestSignUpdate(mdctx, msg, strlen(msg)) == 1 &&
        EVP_DigestSignFinal(mdctx, sig, &sig_len) == 1) {

        EVP_MD_CTX *mdctx2 = EVP_MD_CTX_new();
        if (EVP_DigestVerifyInit(mdctx2, NULL, EVP_sha256(), NULL, pub) == 1 &&
            EVP_DigestVerifyUpdate(mdctx2, msg, strlen(msg)) == 1 &&
            EVP_DigestVerifyFinal(mdctx2, sig, sig_len) == 1) {
            ok = 1;
        }
        EVP_MD_CTX_free(mdctx2);
    }
    EVP_MD_CTX_free(mdctx);
    EVP_PKEY_free(priv);
    EVP_PKEY_free(pub);
    return ok ? 0 : -1;
}

int main() {
    // 模拟设备序列号(实际应从设备硬件/OTP区读取)
    const char *serial = "DEV-2024-A7F3";
    if (generate_device_keypair(serial) != 0) {
        fprintf(stderr, "密钥生成失败\n");
        return 1;
    }
    if (verify_keypair(serial) != 0) {
        fprintf(stderr, "密钥验证失败\n");
        return 1;
    }
    printf("密钥对生成并验证成功\n");
    return 0;
}

编译与运行


gcc -o keygen keygen.c -lssl -lcrypto
sudo mkdir -p /opt/device_keys   # 确保目录存在
./keygen

验证方法与判定标准

  1. 文件检查:执行后确认 /opt/device_keys/ 下存在 device_priv.pem(权限 600)和 device_pub.pem(权限 644)。
  2. 私钥加密确认:运行 head -1 /opt/device_keys/device_priv.pem,输出应为 -----BEGIN ENCRYPTED PRIVATE KEY-----,证明私钥已加密。
  3. 程序自验证:程序内部完成“私钥签名→公钥验签”闭环,输出 密钥对生成并验证成功 即表示密钥对匹配且可用。
  4. 唯一性验证:修改 serial 值后重新编译运行,对比两次生成的公钥文件(sha256sum device_pub.pem),哈希值应不同,证明每设备密钥唯一。

安全说明

本案例使用设备序列号派生加密密码,实际产品中应从安全硬件(如 OTP/HSM)获取更高级别的密钥派生因子。私钥文件权限已设为 600,目录权限 700,防止其他用户读取。