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 密钥的完整描述。我画了一张图,帮你理清它们之间的关系:

TPM2.0 核心数据结构关系图 TPM2.0 核心数据结构关系图 TPM2B 可变长度缓冲区 TPMI_ALG_IDENTIFIER 算法标识符枚举 TPMT_PUBLIC 公钥结构(公开属性) type / nameAlg / attributes / parameters / unique TPMT_SENSITIVE 私钥结构(敏感数据) type / authValue / seedValue / sensitive TPM 密钥对象 TPMT_PUBLIC + TPMT_SENSITIVE + 句柄 buffer 承载 type 字段引用 type 字段引用 说明 TPM2B 和 TPMI 是基础类型 TPMT_PUBLIC + TPMT_SENSITIVE 共同构成完整密钥对象

从图中你可以看到: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 密钥的完整生命周期基础。