CloudHSM实现不导出私钥明文场景下的PKCS#12密钥封装

本文介绍如何基于 AWS CloudHSM 实现不导出私钥明文场景下的 PKCS#12 密钥封装。对比了可导出密钥(EXTRACTABLE=true)与不可导出密钥(EXTRACTABLE=false)两种方案的实现差异,并提供完整的 Java 代码样例,涵盖 CA 证书生成、客户端证书签发、PKCS#12 组装及验证的全流程。

一、背景

1、PKCS#12是什么

PKCS#12(.p12 / .pfx)是一种将私钥、证书和证书链打包在一起的二进制格式,通常用密码保护。它是行业标准,所有主流 TLS 库和操作系统都原生支持。本文介绍如何基于CloudHSM实现不导出私钥明文场景下的PKCS#12封装。

典型使用场景:

  • 双向 TLS(mTLS)客户端认证:支付系统调用银行 API,银行要求客户端出示证书证明身份
  • Web 服务器 SSL/TLS 部署:IIS、Tomcat 等原生支持导入 .p12 文件
  • 代码签名:Windows 应用或 Android APK 的数字签名
  • S/MIME 邮件加密:金融机构间加密邮件传输
  • IoT 设备身份认证:每台设备烧录唯一 .p12 文件用于连接云端
  • VPN 客户端证书认证:员工远程接入使用证书代替密码

相比分散文件(.key + .crt + ca.crt),PKCS#12 将所有内容打包为单一加密文件,避免私钥和证书不匹配、传输过程中私钥泄露等问题。

二、方案设计

1、如何封装一个PKCS#12密钥

在没有 CloudHSM 保护私钥的情况下,封装 PKCS#12 的主要步骤:

┌──────────────────────────────────────────────────────────────────┐
│                        证书签发流程                                │
│                                                                  │
│  ┌─────────────┐    ┌─────────────┐    ┌──────────────────────┐  │
│  │ 步骤1        │    │ 步骤2        │    │ 步骤3                 │  │
│  │ 创建 CA 私钥 │    │ 创建 Client  │    │ 封装 PKCS#12          │  │
│  │ 签署 CA 证书 │    │ 私钥         │    │                      │  │
│  │ (自签名)    │    │ 用 CA 私钥   │    │  Client 私钥          │  │
│  │             │    │ 签署 Client  │    │  + Client 证书        │  │
│  │  输出:       │    │ 证书         │    │  + CA 证书链           │  │
│  │  ca.key     │    │             │    │  + 密码保护            │  │
│  │  ca.crt     │    │  输出:       │    │                      │  │
│  │             │    │  client.key  │    │  输出:                │  │
│  │             │    │  client.crt  │    │  client.p12(二进制)   │  │
│  │             │    │             │    │  client.pem(Base64)  │  │
│  └─────────────┘    └─────────────┘    └──────────────────────┘  │
└──────────────────────────────────────────────────────────────────┘

PKCS#12 密钥的标准格式为二进制(.p12),但也可以用 Base64 编码保存为 PEM 格式(-----BEGIN PKCS12-----)。

2、在CloudHSM实现对可导出密钥(EXTRACTABLE=true)的PKCS#12封装

在上文的三个步骤中,步骤1和步骤2的私钥由CloudHSM负责生成并保护。步骤3中,Client私钥需要从CloudHSM导出,然后封装。这种密钥类型是可导出密钥(EXTRACTABLE=true)。

封装 PKCS12 参考 AWS 官方文档的标准流程,用 setKeyEntry 实现:

KeyStore ks = KeyStore.getInstance("CloudHSM");
ks.load(null, null);

// 私钥 getEncoded() 返回真实字节,JDK PKCS12KeyStore 可以编码加密
ks.setKeyEntry(alias, privateKey, password, certChain);

// .p12 文件中包含:加密后的私钥字节 + 证书链
ks.store(outputStream, password);

// 重新加载后,getKey 从 .p12 文件中解密出私钥
Key key = ks.getKey(alias, password);

PKCS#12 文件内容:私钥字节(加密存储)+ 证书链。文件可以脱离 HSM 独立使用。

3、在CloudHSM实现对不可导出密钥(EXTRACTABLE=false)的PKCS#12封装

为了提升安全性,CloudHSM最佳实践是私钥不离开加密机,那么就意味着上述流程的步骤3的私钥导出无法实现。这里可以用CloudHSM加密机内的密钥ID作为reference。

在此场景下,setKeyEntry是不能使用的,因为 JDK 底层调用链:

setKeyEntry / setEntry
   PKCS12KeyStore.engineSetKeyEntry()
     key.getEncoded()          // 返回 null,私钥不可导出
     encryptPrivateKey(null)   // 失败
     抛出 KeyStoreException: "Certificate chain is not valid"

由此PKCS将只保存证书,私钥通过 label 在 HSM 中关联:

KeyStore ks = KeyStore.getInstance("CloudHSM");
ks.load(null, null);

// 只存证书,不触发私钥编码
ks.setCertificateEntry(alias + "_cert", clientCert);
ks.setCertificateEntry("ca_cert", caCert);

// .p12 文件中只有证书(公开信息),没有私钥字节
ks.store(outputStream, password);

// 重新加载后,getKey 直接查 HSM(按 label 匹配),不依赖 .p12 中的私钥
Key key = ks.getKey(label, null);

PKCS#12 文件内容:仅证书。私钥永远在 HSM 中,应用程序必须连接到 CloudHSM 集群才能使用。

4、二者对比

可导出密钥 不可导出密钥
封装 API setKeyEntry setCertificateEntry
.p12 中有私钥 有(加密存储)
getEncoded() 返回私钥字节 返回 null
离线使用 可以 不可以,必须连 CloudHSM
getKey 来源 从 .p12 文件解密 从 HSM 按 label 查找
安全性 私钥可被提取 私钥永不离开 HSM

三、代码样例

本项目演示**不可导出密钥(EXTRACTABLE=false)**的 PKCS#12 封装,即上述方案 3 的完整实现。所有私钥均为 ECC P-256,签名算法为 SHA256withECDSA。

1、获取代码并构建

安装 CloudHSM JCE Provider 到本地 Maven 仓库

CloudHSM JCE Provider JAR 不在 Maven Central 上,需要手动安装到本地仓库:

# CloudHSM JCE Provider JAR 随 Client SDK 5 安装,默认路径:
ls /opt/cloudhsm/java/cloudhsm-jce-5.17.0.jar

# 安装到本地 Maven 仓库
mvn install:install-file \
  -Dfile=/opt/cloudhsm/java/cloudhsm-jce-5.17.0.jar \
  -DgroupId=com.amazonaws \
  -DartifactId=cloudhsm-jce \
  -Dversion=5.17.0 \
  -Dpackaging=jar

设置环境变量

export HSM_USER=<your_cu_username>
export HSM_PASSWORD=<your_cu_password>

编译打包

git clone <repo-url>
cd cloudhsm-pkcs12-demo
mvn clean package -q

生成三个 fat-jar:

JAR 说明
target/ca-keygen.jar 生成 CA 密钥对和根证书
target/client-cert-p12.jar 生成客户端密钥、签发证书、组装 .p12
target/p12-reader.jar 读取并解析 .p12 文件内容

2、生成CA证书

java -jar target/ca-keygen.jar

返回结果如下:

使用用户: user01 连接到CloudHSM...

--- 步骤1:在 HSM 中生成 CA ECC P-256 密钥对 ---
✅ CA ECC P-256 密钥对生成成功(不可导出、持久化)
私钥 getEncoded(): null (不可导出,符合预期)

--- 步骤2:生成自签名 CA 根证书 ---
✅ 自签名 CA 根证书生成并验证成功
CA 根证书已保存到: output/ca_cert.pem
Subject: C=CN, O=BITIPCMAN, OU=Security, CN=CloudHSM PKCS12 Demo CA
Issuer: C=CN, O=BITIPCMAN, OU=Security, CN=CloudHSM PKCS12 Demo CA
有效期: Tue Mar 17 13:09:30 UTC 2026 ~ Mon Mar 17 13:09:30 UTC 2036
签名算法: SHA256withECDSA

✅ CA 密钥对和根证书生成完成

输出:

  • HSM 中持久化 CA 密钥对(label: ca_key_private / ca_key_public,不可导出)
  • output/ca_cert.pem — 自签名 CA 根证书(有效期 10 年)

此步骤为一次性操作,后续签发客户端证书时复用同一个CA。

3、生成设备密钥、签署证书、并封装为PKCS#12格式

java -jar target/client-cert-p12.jar

程序自动完成以下 5 个步骤:

  1. 加载 CA 根证书(output/ca_cert.pem)和 HSM 中的 CA 私钥
  2. 在 HSM 中生成客户端设备 ECC P-256 密钥对(不可导出、持久化)
  3. 用 CA 私钥签发客户端设备证书(有效期 3 年)
  4. 组装 CloudHSM KeyStore .p12(二进制 + PEM 两种格式)
  5. 验证:重新加载 .p12,从 HSM 获取私钥并执行测试签名

密钥 label 使用 client_ + 8 位随机字符串(如 client_7hu30lvs),每次运行生成不同的 label,避免 HSM 中冲突。

返回结果如下:

使用用户: user01 连接到CloudHSM...
密钥 label: client_552bka7l (公钥: client_552bka7l:public)

--- 步骤1:加载 CA 根证书和私钥 ---
CA 证书: C=CN, O=BITIPCMAN, OU=Security, CN=CloudHSM PKCS12 Demo CA
已找到 CA 私钥: ca_key_private

--- 步骤2:在 HSM 中生成客户端设备 ECC P-256 密钥对 ---
✅ 客户端设备 ECC P-256 密钥对生成成功(不可导出、持久化)
   私钥 getEncoded(): null (不可导出,符合预期)

--- 步骤3:用 CA 私钥签发客户端设备证书 ---
✅ 客户端设备证书签发成功,CA 签名验证通过
   Subject: EMAILADDRESS=device_admin@bitipcman.com, CN=device.bitipcman.com, OU=DEVICE, O=BITIPCMAN, L=BEIJING, ST=BJ, C=CN
   Issuer: CN=CloudHSM PKCS12 Demo CA, OU=Security, O=BITIPCMAN, C=CN
   有效期: Tue Mar 17 13:10:16 UTC 2026 ~ Sat Mar 17 13:10:16 UTC 2029
客户端设备证书已保存到: output/client_552bka7l_cert.pem

--- 步骤4:组装 CloudHSM KeyStore .p12 ---
✅ CloudHSM KeyStore .p12 组装完成
   二进制文件: output/client_552bka7l_keystore.p12
   PEM 文件: output/client_552bka7l_keystore.pem
   密码: changeit
   私钥: client_552bka7l (HSM key reference,通过 label 关联)
   公钥: client_552bka7l:public
   证书: client_552bka7l_cert → 客户端设备证书
   证书: ca_cert → CA 根证书

--- 步骤5:验证 .p12 ---
   私钥 getEncoded(): null (key reference,符合预期)
   签名测试: 成功 (72 bytes)
   证书加载: 成功

✅ 全部完成

输出文件(均保存在 output/ 目录下):

文件 说明
output/client_<label>_cert.pem 客户端设备证书(PEM 格式)
output/client_<label>_keystore.p12 CloudHSM KeyStore(二进制 PKCS12 格式)
output/client_<label>_keystore.pem CloudHSM KeyStore(Base64 PEM 格式,-----BEGIN PKCS12-----

CloudHSM 中的对象和 .p12 文件内的 alias 对应关系:

位置 名称 示例值
CloudHSM 内 私钥 label client_7hu30lvs
CloudHSM 内 公钥 label client_7hu30lvs:public
.p12 文件内 客户端证书 alias client_7hu30lvs_cert
.p12 文件内 CA 证书 alias ca_cert

注意:客户端证书 alias 加了 _cert 后缀,是为了避免和 HSM 中已有的同名私钥 label 冲突(CloudHSM KeyStore 不允许 alias 与已有密钥 label 重名)。

4、使用OpenSSL校验PKCS#12文件

运行如下命令解析:

openssl pkcs12 -in output/client_552bka7l_keystore.p12 -out output/client_552bka7l_keystore_private_key.pem -noenc

输入密码 changeit 后,解析结果如下:

Bag Attributes
    friendlyName: 636c69656e745f353532626b61376c5f63657274
    2.16.840.1.113894.746875.1.1: <Unsupported tag 6>
subject=C = CN, ST = BJ, L = BEIJING, O = BITIPCMAN, OU = DEVICE, CN = device.bitipcman.com, emailAddress = device_admin@bitipcman.com
issuer=C = CN, O = BITIPCMAN, OU = Security, CN = CloudHSM PKCS12 Demo CA
-----BEGIN CERTIFICATE-----
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-----END CERTIFICATE-----
Bag Attributes
    friendlyName: 63615f63657274
    2.16.840.1.113894.746875.1.1: <Unsupported tag 6>
subject=CN = CloudHSM PKCS12 Demo CA, OU = Security, O = BITIPCMAN, C = CN
issuer=CN = CloudHSM PKCS12 Demo CA, OU = Security, O = BITIPCMAN, C = CN
-----BEGIN CERTIFICATE-----
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-----END CERTIFICATE-----

在以上代码中,friendlyName是十六进制编码的,解码后可获得:

  • 636c69656e745f353532626b61376c5f63657274client_552bka7l_cert(客户端设备证书)
  • 63615f63657274ca_cert(CA 根证书)

在解码结果中,客户端设备证书的 alias 为 client_552bka7l_cert,去掉 _cert 后缀即可获得 CloudHSM 中的私钥 label client_552bka7l,公钥 label 为 client_552bka7l:public

注意:由于使用的是不可导出密钥的方案,.p12 文件中没有私钥数据,openssl 导出的文件中只包含两张证书,没有 -----BEGIN PRIVATE KEY----- 块。这符合预期——私钥永远在 CloudHSM 中。

5、使用Java代码解析PKCS#12

本项目提供 P12PemReader 工具,可在不连接 CloudHSM 的情况下解析 PEM 格式的 .p12 文件:

java -jar target/p12-reader.jar output/client_552bka7l_keystore.pem

返回结果如下:

读取 PEM 文件: output/client_552bka7l_keystore.pem
PKCS12 二进制大小: 1542 bytes

=== Alias: client_552bka7l_cert ===
[CloudHSM 私钥 label] client_552bka7l
[CloudHSM 公钥 label] client_552bka7l:public
[类型] 客户端设备证书
  Subject: EMAILADDRESS=device_admin@bitipcman.com, CN=device.bitipcman.com, OU=DEVICE, O=BITIPCMAN, L=BEIJING, ST=BJ, C=CN
  Issuer:  CN=CloudHSM PKCS12 Demo CA, OU=Security, O=BITIPCMAN, C=CN
  Serial:  1773753016751
  有效期:  Tue Mar 17 13:10:16 UTC 2026 ~ Sat Mar 17 13:10:16 UTC 2029
  算法:    SHA256withECDSA
  --- PEM ---
  -----BEGIN CERTIFICATE-----
  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  -----END CERTIFICATE-----

=== Alias: ca_cert ===
[类型] CA 根证书
  Subject: C=CN, O=BITIPCMAN, OU=Security, CN=CloudHSM PKCS12 Demo CA
  Issuer:  C=CN, O=BITIPCMAN, OU=Security, CN=CloudHSM PKCS12 Demo CA
  Serial:  1773752970786
  有效期:  Tue Mar 17 13:09:30 UTC 2026 ~ Mon Mar 17 13:09:30 UTC 2036
  算法:    SHA256withECDSA
  --- PEM ---
  -----BEGIN CERTIFICATE-----
  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  -----END CERTIFICATE-----

输出内容包括:

  • CloudHSM 内的私钥 label 和公钥 label(从证书 alias 推导)
  • 客户端设备证书详情(Subject、Issuer、序列号、有效期、PEM)
  • CA 根证书详情

四、参考文档


最后修改于 2026-03-17