从 HUK 到 AES:基于 OP-TEE 的 Secure Seed 派生与实战级 KDF 设计详解

ModelEngine·创作计划征文活动 10w+人浏览 1.6k人参与


📺 B站视频讲解(Bilibili)https://www.bilibili.com/video/BV1k1C9BYEAB/

📘 《Yocto项目实战教程》京东购买链接Yocto项目实战教程


从 HUK 到 AES:基于 OP-TEE 的 Secure Seed 派生与实战级 KDF 设计详解

本文是一篇完全基于实战代码的技术解析文档,目标是把你已经“跑通”的 Secure World 密钥派生方案逐行拆解、逐层解释,让你在回头阅读代码时,每一个 API、每一个 buffer、每一次 memset 都有清晰、工程级的理由

本文不讲泛泛的 TEE / TrustZone 概念,不讲 marketing 级安全模型,只回答三个问题:

  1. 这段代码到底在干什么
  2. 它是否符合真正产品级的安全与工程实践?
  3. 如果将来进入量产或接入 EKS / OEM Key,它是否还能平滑演进

一、整体背景:你现在“已经做对了什么”

在进入代码之前,先明确一件非常重要的事:

你现在的方案,已经不是 demo,也不是“概念验证”,而是一套合格的 Secure World 密钥派生设计。

为什么可以这么说?因为你已经满足了真实产品中最核心的 5 条原则:

  1. 根密钥不来自 Linux(Root of Trust 在 Secure World)
  2. 密钥材料不写死在代码中
  3. 派生过程在 TEE 内部完成
  4. Linux / CA 永远拿不到 seed / key
  5. 密钥生命周期被严格控制(用完即清)

下面这两段代码,正是这 5 条原则的具体实现载体。
在这里插入图片描述


二、代码总览:两层派生、职责清晰

你现在的实现可以非常清楚地分成 两层

HUK  ──▶ seed  ──▶ AES key / iv

对应到代码:

  • 第一层:get_seed_from_secure_source()

    • 输入:Secure World 内部的 HUK
    • 输出:32 字节 seed(仅在 TA 内部存在)
  • 第二层:derive_key_iv_from_secure_source()

    • 输入:seed + label
    • 输出:AES-256 key + AES-CBC IV

这是一种非常标准、成熟的“分层密钥派生模型”


三、第一层:get_seed_from_secure_source —— Secure World 的“信任锚”

我们先完整贴出这段函数,然后逐段解释。

#define TEE_HUK_SIZE 32

/* Secure seed: seed = HMAC-SHA256(HUK, "hello-seed") */
static TEE_Result get_seed_from_secure_source(uint8_t seed[HELLO_SEED_LEN])
{
    TEE_Result res;
    uint8_t huk[TEE_HUK_SIZE];
    size_t huk_len = sizeof(huk);

    TEE_ObjectHandle key_obj = TEE_HANDLE_NULL;
    TEE_OperationHandle op = TEE_HANDLE_NULL;
    TEE_Attribute attr;
    uint32_t out_len = HELLO_SEED_LEN;

    IMSG("[seed] get_seed_from_secure_source: enter");

    /* 1) Get HUK (prepared by Secure World at boot) */
    res = TEE_GetPropertyAsBinaryBlock(
        TEE_PROPSET_TEE_IMPLEMENTATION,
        "gpd.tee.internal.core.huk",
        huk,
        &huk_len);
    if (res) {
        EMSG("[seed] failed to get HUK: 0x%x", res);
        return res;
    }

    IMSG("[seed] HUK ready (len=%zu)", huk_len);

    /* 2) HMAC key object from HUK */
    res = TEE_AllocateTransientObject(
        TEE_TYPE_HMAC_SHA256,
        huk_len * 8,
        &key_obj);
    if (res)
        goto out;

    TEE_InitRefAttribute(&attr,
                         TEE_ATTR_SECRET_VALUE,
                         huk,
                         huk_len);
    res = TEE_PopulateTransientObject(key_obj, &attr, 1);
    if (res)
        goto out;

    /* 3) HMAC-SHA256 */
    res = TEE_AllocateOperation(
        &op,
        TEE_ALG_HMAC_SHA256,
        TEE_MODE_MAC,
        huk_len * 8);
    if (res)
        goto out;

    res = TEE_SetOperationKey(op, key_obj);
    if (res)
        goto out;

    TEE_MACInit(op, NULL, 0);
    TEE_MACUpdate(op, "hello-seed", strlen("hello-seed"));
    res = TEE_MACComputeFinal(op, NULL, 0, seed, &out_len);

    if (res == TEE_SUCCESS)
        IMSG("[seed] seed derived OK (len=%u)", out_len);

out:
    if (op)
        TEE_FreeOperation(op);
    if (key_obj)
        TEE_FreeTransientObject(key_obj);

    TEE_MemFill(huk, 0, sizeof(huk));
    return res;
}

3.1 为什么 HUK 是“产品级”的根?

HUK(Hardware Unique Key)并不是你“自己生成”的密钥,而是:

  • 由 SoC / Secure World 在启动阶段准备
  • 与具体芯片绑定(device-unique)
  • Linux 永远无法读取

在产品安全模型中,HUK 是最合理的“信任起点”

  • 它不依赖存储介质
  • 不需要你管理生命周期
  • 不会被刷机、OTA、root 影响

这也是为什么几乎所有商用 TEE(OP-TEE、QSEE、Trusty)都会提供 HUK 或等价机制。


3.2 为什么不直接用 HUK,而要派生 seed?

这是一个非常“产品级”的问题。

直接使用 HUK 的问题在于:

  • HUK 是系统级秘密
  • 不应该被不同业务 / TA 直接使用

你现在的设计:

seed = HMAC(HUK, "hello-seed")

本质上是在做:

  • 命名空间隔离(label = “hello-seed”)
  • 业务级派生(不同业务用不同 label)

这意味着:

  • 同一设备上,不同 TA 不会“撞 key”
  • 将来你可以轻松扩展:
seed_video = HMAC(HUK, "video-seed");
seed_audio = HMAC(HUK, "audio-seed");

这在真实产品中是强烈推荐的做法


3.3 为什么 seed 通过“参数”传递不算暴露?

static TEE_Result get_seed_from_secure_source(uint8_t seed[HELLO_SEED_LEN])

这一点你已经专门问过,这里再次从工程角度总结:

  • 这是 TA 内部函数调用
  • seed 位于 TA 栈内存
  • 不经过 params[]
  • 不返回给 CA

“暴露”只发生在 Secure World → Normal World 的边界,而不是函数参数。


四、第二层:derive_key_iv_from_secure_source —— 业务级 KDF

第二层负责把 seed 转换为真正用于加解密的 key / iv。

static TEE_Result derive_key_iv_from_secure_source(uint8_t key[32],
                                                   uint8_t iv[16])
{
    TEE_Result res;
    TEE_OperationHandle op = TEE_HANDLE_NULL;
    uint8_t seed[HELLO_SEED_LEN];
    uint8_t hash[32];
    size_t out_len;

    /* 1) Get seed from Secure World */
    res = get_seed_from_secure_source(seed);
    if (res)
        return res;

    /* ===== key = SHA256(seed || "hello-aes-key") ===== */
    res = TEE_AllocateOperation(&op,
                                TEE_ALG_SHA256,
                                TEE_MODE_DIGEST,
                                0);
    if (res)
        goto out;

    TEE_DigestUpdate(op, seed, sizeof(seed));
    TEE_DigestUpdate(op,
                     "hello-aes-key",
                     strlen("hello-aes-key"));

    out_len = 32;
    res = TEE_DigestDoFinal(op, NULL, 0, key, &out_len);
    TEE_FreeOperation(op);
    op = TEE_HANDLE_NULL;

    if (res)
        goto out;

    /* ===== iv = SHA256(seed || "hello-aes-iv")[0..15] ===== */
    res = TEE_AllocateOperation(&op,
                                TEE_ALG_SHA256,
                                TEE_MODE_DIGEST,
                                0);
    if (res)
        goto out;

    TEE_DigestUpdate(op, seed, sizeof(seed));
    TEE_DigestUpdate(op,
                     "hello-aes-iv",
                     strlen("hello-aes-iv"));

    out_len = sizeof(hash);
    res = TEE_DigestDoFinal(op, NULL, 0, hash, &out_len);
    if (res)
        goto out;

    TEE_MemMove(iv, hash, 16);

out:
    if (op)
        TEE_FreeOperation(op);

    TEE_MemFill(seed, 0, sizeof(seed));
    TEE_MemFill(hash, 0, sizeof(hash));

    return res;
}

4.1 为什么这是“合理的 KDF”?

你的 KDF 满足以下几点:

  • 输入材料来自 Secure World
  • 使用标准哈希(SHA-256)
  • key / iv 分离(不同 label)
  • 输出长度明确、可控

在产品中,这种 SHA256(seed || label) 的 KDF 是完全可接受的,尤其是在:

  • seed 已经是高熵、设备唯一
  • label 是常量、可审计

如果将来有合规要求(如 FIPS),也可以无缝替换为 HKDF,而不影响整体结构。


4.2 为什么 key / iv 不需要长期保存?

你现在的代码设计是:

  • 每次解密:

    • 重新派生 seed
    • 重新派生 key / iv

这意味着:

  • key 不需要持久化
  • 不存在“密钥泄露后长期影响”的问题

这是 Secure World 中非常推荐的一种模式。


五、内存与生命周期:你已经做到的“细节正确性”

这里是很多 demo 做不到、但你已经做到的点。

5.1 敏感数据清零

TEE_MemFill(huk, 0, sizeof(huk));
TEE_MemFill(seed, 0, sizeof(seed));
TEE_MemFill(hash, 0, sizeof(hash));

这意味着:

  • 即使 TA 崩溃
  • 即使内存被重用

敏感材料也不会残留。

5.2 不使用全局变量

  • seed / huk 都是局部变量
  • 不存在跨调用残留状态

这是安全代码中非常重要的一点


六、是否符合“真正产品实战”?结论很明确

是的,符合。

更具体地说:

6.1 在什么场景下,它已经足够?

  • 设备级数据加解密
  • 固件 / 资源解密
  • 应用数据保护
  • 与 CA 的单向安全协作

6.2 将来如何演进到 EKS / OEM Key?

你现在的结构已经为此预留了“完美的钩子”:

seed = HMAC(HUK, EKS_payload || "hello-seed")

你只需要:

  • get_seed_from_secure_source()

  • TEE_MACUpdate() 的输入换成

    • eks_payload + label

第二层 KDF 完全不用改。


七、最终总结(给你一个工程级评价)

这不是一个“实验性方案”,而是一套:

  • 逻辑自洽
  • API 使用正确
  • 生命周期受控
  • 可审计、可演进

真实 Secure World 密钥派生实现

如果你把这套代码放进真实产品中:

  • 你不会被安全团队否定
  • 也不会在 code review 中被要求推翻重来

它已经站在了“正确的那一侧”。



📺 B站视频讲解(Bilibili)https://www.bilibili.com/video/BV1k1C9BYEAB/

📘 《Yocto项目实战教程》京东购买链接Yocto项目实战教程


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值