3. TPM2.0 核心数据结构:从结构体到密钥封存
聊 TPM 密钥封存,绕不开几个核心数据结构。说白了,TPM 就是靠这些结构体来组织密钥的。我刚开始接触 TPM2.0 时,对着规范看了半天,满眼都是 TPM2B、TPMI、TPMT 这些前缀,说实话有点懵。后来在项目里踩过几次坑,才真正理解它们的设计意图。
这一节,我们就把四个最核心的结构掰开揉碎讲清楚。你理解透它们,密钥封存的底层逻辑就通了八成。
3.1 TPM2B:可变长度缓冲区
TPM2B 是 TPM2.0 里最基础的结构。它用来承载可变长度的数据,比如密钥字节、哈希值、证书等。为什么叫 2B?因为它包含两个基本字段:size 和 buffer。
核心定义(C语言风格):
typedef struct {
UINT16 size; // buffer 中有效数据的字节数
BYTE buffer[sizeof(TPM2B_MAX)]; // 实际数据,最大长度由具体类型决定
} TPM2B;
嗯,这里要注意:size 字段是 2 字节(UINT16),所以最大能表示 65535 字节。但具体到不同用途,TPM 规范会限制更小的最大值。比如 TPM2B_PUBLIC_KEY_RSA 的 buffer 最大就是 256 字节(2048 位 RSA)。
我个人习惯把 TPM2B 理解为「带长度前缀的字节数组」。这种设计在 TPM 命令交互中非常关键——TPM 设备需要知道每次传输的数据有多长,否则无法正确解析。
避坑指南: 我曾经在解析 TPM2B 时直接读取 buffer 的全部 512 字节,结果把未初始化的内存也传给了上层应用。记住:永远用 size 字段决定有效数据长度,不要假设 buffer 有多大。
3.2 TPMI_ALG_IDENTIFIER:算法标识符
TPMI_ALG_IDENTIFIER 本质上是一个 UINT16 枚举值。它告诉 TPM:「我要用哪种算法」。RSA、ECC、AES、SHA256……每种算法都有唯一 ID。
你想想看,TPM 怎么知道你要生成的是 RSA 密钥还是 ECC 密钥?就是靠这个标识符。它出现在密钥模板、命令参数、响应数据中,几乎无处不在。
| 算法标识符 | 值(十六进制) | 说明 |
|---|---|---|
| TPM_ALG_RSA | 0x0001 | RSA 算法 |
| TPM_ALG_ECC | 0x0023 | 椭圆曲线算法 |
| TPM_ALG_AES | 0x0006 | AES 对称加密 |
| TPM_ALG_SHA256 | 0x000B | SHA-256 哈希 |
| TPM_ALG_HMAC | 0x0005 | HMAC 消息认证码 |
为什么单独拎出来讲?因为很多初学者在构造密钥模板时,把算法标识符填错了位置,导致 TPM 返回错误码。我建议你每次使用前,都对照规范确认一下枚举值。
3.3 TPMT_PUBLIC:公钥结构
这是密钥封存的核心结构。TPMT_PUBLIC 描述了密钥的公开属性,包括算法类型、密钥用途、认证策略等。TPM 用它来验证密钥的合法性,并决定密钥能做什么操作。
结构体骨架:
typedef struct {
TPMI_ALG_PUBLIC type; // 算法类型(RSA/ECC/Symmetric)
TPMI_ALG_HASH nameAlg; // 用于计算密钥名称的哈希算法
TPMA_OBJECT objectAttributes; // 密钥属性位掩码
TPM2B_DIGEST authPolicy; // 认证策略(可选)
TPMU_PUBLIC_PARMS parameters; // 算法参数(如 RSA 密钥长度、ECC 曲线)
TPMU_PUBLIC_ID unique; // 公钥唯一标识(实际公钥数据)
} TPMT_PUBLIC;
这里我重点讲两个字段:
- objectAttributes:这是一个 32 位的位掩码。比如
fixedTPM位表示该密钥是否绑定到当前 TPM 芯片;sign位表示能否用于签名。我见过有人把decrypt和sign同时置位,结果密钥既不能加密也不能签名——因为 TPM 要求某些属性互斥。 - authPolicy:这是策略会话的哈希值。如果设置了策略,那么使用密钥前必须通过策略验证。这是实现高级访问控制的基础。
重要提醒: TPMT_PUBLIC 是公开信息,可以被任何人读取。但一旦密钥被加载到 TPM 中,修改 public 部分会导致密钥名称变化,进而使密钥无法使用。所以,密钥模板必须在创建时确定,不可事后篡改。
3.4 TPMT_SENSITIVE:私钥结构
私钥结构是 TPM 中最敏感的部分。它包含密钥的实际私密数据,比如 RSA 私钥指数、ECC 私钥标量。TPM 保证这部分数据永远不会以明文形式离开芯片(除非你明确要求导出)。
结构体骨架:
typedef struct {
TPMI_ALG_PUBLIC type; // 算法类型,必须与 TPMT_PUBLIC.type 一致
TPM2B_AUTH authValue; // 密钥的授权值(密码或 HMAC 密钥)
TPM2B_DIGEST seedValue; // 用于派生密钥的种子(可选)
TPMU_SENSITIVE_COMP sensitive; // 实际私钥数据(RSA 私钥、ECC 私钥等)
} TPMT_SENSITIVE;
这里有个关键点:authValue。它并不是密钥的「密码」,而是用于授权使用密钥的认证数据。在密钥封存场景中,authValue 通常为空(全零),因为我们会用父密钥的层次结构来保护子密钥。
我记得有一次,项目里需要把外部生成的私钥导入 TPM。当时我直接填充了 TPMT_SENSITIVE 的 sensitive 字段,却忘了设置正确的 authValue。结果 TPM 拒绝加载,返回错误码 TPM_RC_AUTH_FAIL。后来查规范才发现,即使 authValue 为空,也必须显式设置长度为 0 的 TPM2B_AUTH。
3.5 结构关系总览
这四个结构不是孤立的。它们共同构成了 TPM 密钥的完整描述。我画了一张图,帮你理清它们之间的关系:
从图中你可以看到:TPM2B 和 TPMI_ALG_IDENTIFIER 是基础构建块,它们被组合进 TPMT_PUBLIC 和 TPMT_SENSITIVE 中。而这两个结构,再加上一个运行时句柄,就构成了 TPM 中完整的密钥对象。
3.6 实战:构造一个封存密钥模板
光说不练假把式。我们来看一个实际例子:如何构造一个用于封存的 RSA-2048 密钥模板。
// 1. 定义算法标识符
TPMI_ALG_PUBLIC type = TPM_ALG_RSA;
TPMI_ALG_HASH nameAlg = TPM_ALG_SHA256;
// 2. 设置密钥属性
TPMA_OBJECT attributes = 0;
attributes |= TPMA_OBJECT_fixedTPM; // 绑定到本 TPM
attributes |= TPMA_OBJECT_fixedParent; // 绑定到父密钥
attributes |= TPMA_OBJECT_sensitiveDataOrigin; // 私钥在 TPM 内部生成
attributes |= TPMA_OBJECT_userWithAuth; // 需要用户授权
attributes |= TPMA_OBJECT_decrypt; // 可用于解密
attributes |= TPMA_OBJECT_noDA; // 不启用字典攻击保护
// 3. 设置 RSA 参数
TPMT_PUBLIC public = {0};
public.type = type;
public.nameAlg = nameAlg;
public.objectAttributes = attributes;
public.parameters.rsaDetail.keyBits = 2048;
public.parameters.rsaDetail.exponent = 0; // 使用默认指数 65537
public.parameters.rsaDetail.scheme.scheme = TPM_ALG_NULL; // 不限制填充方案
// 4. unique 字段留空(由 TPM 生成时填充)
public.unique.rsa.size = 0;
// 5. 私钥部分:authValue 为空
TPMT_SENSITIVE sensitive = {0};
sensitive.type = type;
sensitive.authValue.size = 0;
// sensitive.sensitive 由 TPM 内部生成
个人经验: 我建议你在构造模板时,先把 objectAttributes 的每一位含义列出来,逐位确认。我曾经因为漏设 fixedParent 位,导致密钥在迁移父密钥后无法加载——TPM 认为密钥被篡改了。这种问题排查起来非常痛苦。
3.7 小结
这一节我们拆解了 TPM2.0 的四个核心数据结构。TPM2B 是数据容器,TPMI 是算法标签,TPMT_PUBLIC 是公开描述,TPMT_SENSITIVE 是私密核心。它们组合在一起,就构成了 TPM 密钥的完整生命周期基础。