夜雨聆风学习资料网

ARTICLE · 1058987

《我为 ESP32 设计了一种“小应用安装包”》

《我为 ESP32 设计了一种“小应用安装包”》
ESP32 MINI APP PLATFORMSERIES 04

我为 ESP32 设计了一种
“小应用安装包”

从一个 WASM 文件,到可识别、可验证、可更新的 .app。

.APP

EMAP · MANIFEST · WASM

应用包,是程序进入平台前的可信边界

EMAP V1SIGNED

EDITOR'S NOTE

SERIES 04

一个 .wasm 文件只能告诉设备“要执行什么代码”。要让它成为可以安装、识别、升级和校验的小应用,还需要一个真正的应用包。

上一篇文章里,我介绍了系统 API:小应用不直接操作屏幕、触摸芯片和存储,而是通过平台提供的统一入口申请能力。

到这里,一个小应用已经有了两块重要拼图:

用 WebAssembly 承载可以执行的程序;

用系统 API 连接屏幕、触摸和存储。

但如果我现在把一个 snake.wasm 文件直接丢给 ESP32,设备仍然会遇到一连串问题:

它是谁?版本是多少?需要哪些权限?适不适合当前平台?文件在下载时有没有损坏?又是谁发布了它?

这些信息,都不在单独的 WASM 文件里。

所以,我又给这个平台补上了一层:把应用代码、应用说明和可信校验封装成一个 .app 文件。

这篇不讲复杂的密码学推导,先从一个最直观的问题开始:一个能被 ESP32 安装的小应用包,里面到底应该装什么?

01

PART

为什么不能直接安装 `.wasm`?

WHY WASM IS NOT ENOUGH

WASM 文件很像一台机器能执行的“程序本体”。它包含函数、内存、指令和导入导出信息,却不负责描述完整的应用身份。

如果设备只拿到一段 WASM,它最多能判断这是不是一个 WebAssembly 模块,却很难回答下面这些问题:

应用在系统里的唯一 ID 是什么?

桌面上应该显示什么名字?

当前安装的是哪个版本?

它需要屏幕、触摸还是持久化存储?

它要求的平台最低版本是多少?

下载到的代码是否完整?

这个包是不是由我信任的人签发的?

电脑和手机上的安装包也不只有可执行代码。它们通常还会带上名称、版本、权限、图标、资源和签名等信息。

ESP32 的资源更有限,安装包可以做得更小,但这些基本问题同样绕不开。

因此,我没有把“能加载 WASM”直接等同于“能安装应用”。在两者之间,还需要一个应用包格式。

02

PART

我的 `.app` 文件并不是 ZIP

EMAP V1 PACKAGE

INSIDE AN .APP PACKAGE

EMAP

16 B

Manifest

身份 · 版本 · 权限

SIGN

P-256

WASM

main.wasm

固定顺序、明确长度、无压缩:先把每个字节的含义说清楚。

很多人看到“应用包”,第一反应可能是压缩包:把配置文件、WASM 和资源一起压缩,再改一个扩展名。

这当然能做,但当前版本的 ESP32 平台没有采用这种方式。

我设计的第一版格式叫 EMAP v1。它是一个结构非常固定的二进制文件,从前到后只有四部分:

TEXTESP MINI APP
┌──────────────┬──────────────┬──────────────┬──────────────┐│ 16 字节头部   │ Manifest     │ ECDSA 签名    │ WASM 程序     ││ EMAP + 长度   │ 应用说明      │ 可信校验       │ main.wasm    │└──────────────┴──────────────┴──────────────┴──────────────┘

当前 v1 没有压缩,也没有单独的图片和音频资源区。一个包里只承载一个 WASM 主程序。

这看起来不够“豪华”,却很适合第一版:

不需要在 ESP32 上实现复杂的解压和目录处理;

每一部分的起点和长度都能直接计算;

打包工具和设备端可以使用同一套明确规则;

出错时更容易知道问题发生在哪一层。

先让格式简单、确定、可验证,再考虑资源文件和压缩,是我更愿意采用的顺序。

03

PART

前 16 个字节,是整个包的“目录”

THE 16-BYTE HEADER

.app 文件最前面是一个 16 字节头部,结构可以写成:

TEXTESP MINI APP
<4sIII

这不是写进 .app 文件的一行文字,而是 Python struct 模块用来描述二进制布局的格式表达式。拆开来看:

<

后面的整数采用小端序保存

4s

连续 4 个字节,内容固定为 EMAP

I

一个 4 字节无符号整数;三个 I 依次记录 Manifest、WASM 和签名长度

EMAP | Manifest 长度 | WASM 长度 | 签名长度
4 B  | 4 B     | 4 B   | 4 B

这里采用小端序,依次包含:

字段
作用
EMAP
4 字节魔数,用来识别包格式
Manifest 长度
告诉设备应用说明占多少字节
WASM 长度
告诉设备程序主体占多少字节
签名长度
告诉设备签名占多少字节

设备先读取这 16 个字节,就能算出后面每一段应该从哪里开始、在哪里结束。

它还会检查:

Manifest 不能超过 2048 字节;

WASM 主体不能超过 256 KiB;

签名长度必须落在允许范围内;

四部分相加必须刚好等于文件总长度。

最后一条很重要。如果声明的内容已经结束,文件尾部却还有多余字节,设备会直接拒绝它。

在资源有限的 MCU 上,越早排除长度异常,后面越不容易在解析和内存使用上出问题。

04

PART

Manifest 是应用的“身份证”

MANIFEST AS IDENTITY

头部只负责描述结构,真正说明“这是什么应用”的,是 Manifest。

它采用 JSON。以目前平台里的 Snake 为例,核心内容大致如下:

JSONESP MINI APP
{  "manifest_version": 1,  "api_version": 1,  "id": "com.example.snake",  "name": "Snake",  "version": "0.1.0",  "platform_min": "0.1.0",  "entry": "main.wasm",  "permissions": ["display", "touch", "storage"],  "hardware": ["display", "touch"],  "wasm_size": 12345,  "sha256": "……"}

其中几组字段承担了不同职责。

1. 身份与版本

id 是应用在系统里的唯一身份。安装新版本时,平台也是通过这个 ID 判断“这是一个新应用”还是“已有应用的更新”。

name 用于展示,version 则记录应用自己的版本号。

我对 ID 也做了限制:它至少由三段小写字母或数字组成,每段都必须以字母开头,例如 com.example.snake。这样可以减少随意命名带来的冲突和解析歧义。

2. 平台兼容性

api_version 表示应用针对哪一版系统 API 构建。

platform_min 表示它要求的平台最低版本。如果某个应用依赖 0.4.0 才提供的新能力,而设备还停留在 0.3.0,安装阶段就会拒绝,而不是等到启动后才崩溃。

3. 权限与硬件需求

permissions 和 hardware 看起来相近,含义并不一样。

permissions 表示应用申请使用哪些系统能力,例如显示、触摸、存储;

hardware 表示应用运行所必需的设备条件,例如必须有屏幕和触摸面板。

前者回答“允许它做什么”,后者回答“设备要具备什么”。

当前平台只接受已经定义的能力,重复项和未知项都会被拒绝。

4. 程序入口与完整性

entry 当前固定为 main.wasm

wasm_size 和 sha256 则分别记录 WASM 主体的长度与 SHA-256 摘要。设备可以用它们确认:包里实际收到的程序,与打包时描述的程序完全一致。

Manifest 因此不只是“给人看的配置”。它是设备作出安装决定时使用的一份机器可读合同。

05

PART

SHA-256 能发现损坏,却不能确定发布者

INTEGRITY

当打包工具生成 .app 时,会先计算 WASM 主体的 SHA-256,并把结果写进 Manifest。

设备安装时再计算一次。如果两个结果不同,就说明程序内容已经发生变化。

它可以发现:

下载不完整;

传输过程中某些字节被破坏;

文件被替换或修改;

Manifest 声明的大小与实际内容不一致。

但哈希只能回答“内容有没有变”,不能单独回答“是谁发布的”。

如果有人同时替换 WASM,并重新计算一个新的 SHA-256 写回 Manifest,哈希本身依旧能够对上。

所以,完整性校验之后,还需要可信来源校验。

06

PART

P-256 签名,给 Manifest 盖一个数字印章

TRUSTED SIGNATURE

当前格式使用 ECDSA P-256 + SHA-256 做签名。

可以把它理解成一对配套的钥匙:

私钥只保存在打包端,用来签发应用;

公钥内置在设备固件里,用来验证签名。

打包时,工具会对 Manifest 的原始字节计算摘要,再用私钥生成签名。设备收到包以后,用固件中的公钥验证。

如果 Manifest 中任何一个字符发生变化,签名都会失效。

又因为 Manifest 里保存了 WASM 的长度和 SHA-256,所以这枚签名也间接绑定了后面的程序主体。想替换 WASM,就必须修改哈希;一旦修改哈希,原来的签名又无法通过。

这里容易混淆的一点是:签名不等于加密。

.app 里的 Manifest 和 WASM 并没有被隐藏。签名解决的是“内容是否由受信任的私钥签发、签发后是否被改动”,不是“别人能不能看到内容”。

07

PART

设备安装前,会按顺序做一轮验证

VERIFY BEFORE INSTALL

VERIFY BEFORE COMMIT

识别包头验证签名检查 Manifest核对 WASM回读复验提交注册表

一个包进入设备后,并不会立刻被写进应用列表。

当前验证流程可以概括为:

TEXTESP MINI APP
识别 EMAP 与长度        ↓验证 Manifest 签名        ↓解析并检查 Manifest        ↓检查 API / 平台版本 / 权限与硬件声明        ↓核对 WASM 大小、SHA-256 与魔数        ↓写入存储并回读复验        ↓最后更新应用注册表

其中还有一些不太显眼但很重要的检查:

JSON 顶层字段不能重名;

应用版本必须是合法的 X.Y.Z

权限和硬件列表不能出现重复或未知值;

WASM 必须以标准的 \0asm 头开始;

包的尾部不能夹带未声明内容。

08

PART

安装不是一次简单的“复制文件”

TRANSACTIONAL INSTALL

TWO-SLOT UPDATE

slot 0

当前可用版本

slot 1

写入并复验新包

Registry

最后切换引用

通过 USB 或网络收到完整包以后,平台会先在内存中完成验证,再写入应用存储。

写入之后,设备还会把文件重新读回来,比较字节并再次验证包。只有这些步骤全部成功,才会更新保存在 NVS 里的应用注册表。

同一个应用 ID 更新时,平台会在两个文件槽位之间切换:

TEXTESP MINI APP
com.example.snake.0.appcom.example.snake.1.app

新的包先写到另一个槽位,验证通过后,再把注册表指向它。这样,旧版本不会在新文件尚未可靠落盘时就被覆盖。

USB 上传也采用 开始 → 分块 → 提交 的流程。没有收齐全部数据,就不能进入安装步骤;取消或超时只会丢弃本次上传,不会修改已经安装的应用。

这已经具备事务式安装的基本结构。不过针对突然断电的完整故障注入测试,我还在继续补充,所以这一部分暂时不展开宣称“任何情况下都绝对安全”。等验证完整后,我会单独写一篇安装与恢复机制。

09

PART

从源码到 `.app`,经历了什么?

BUILD TO PACKAGE

以 Snake 为例,完整过程可以简化成三步:

TEXTESP MINI APP
C 源码  ↓ 编译snake.wasm  ↓ 加入 Manifest、哈希与签名snake.app  ↓ USB / HTTPSESP32 验证并安装

项目里的打包命令类似这样:

BASHESP MINI APP
python tools/app_package.py pack \  apps/snake/manifest.json \  dist/snake.wasm \  dist/snake.app \  --key local/dev_private.pem

生成以后,还可以先在电脑上用同一套规则检查:

BASHESP MINI APP
python tools/app_package.py verify \  dist/snake.app \  --key local/dev_public.pem

打包工具会检查 WASM 魔数、字段格式和大小限制,自动写入 wasm_size 与 sha256,再对最终 Manifest 签名。

这一步把“编译得到的程序”变成了“平台能够管理的应用”。

10

PART

为什么第一版要刻意保持简单?

KEEP V1 SIMPLE

现在的 .app 还没有图标、图片资源、压缩算法、多模块依赖和增量更新。

这些都可以继续加,但它们会同时增加打包工具、设备解析、存储管理和兼容策略的复杂度。

第一版我更关心四件事:

1

设备能明确识别包的边界;

2

应用有稳定的身份、版本和能力声明;

3

下载损坏和未受信任的包会被拒绝;

4

安装失败时,不轻易破坏已有应用。

这四件事成立以后,图标和资源才有一个可靠的容器可以依附。

对 MCU 平台来说,简单并不代表随便。恰恰相反,格式越简单,每一个字节的含义越应该明确。

END

NOTE

写在最后

CLOSING

做完这一步以后,我对“ESP32 上的小应用”有了一个更完整的定义:

它不是一段能够被 WAMR 执行的 WASM,也不只是几组系统 API 调用。

它还应该拥有:

可以被系统识别的身份;

可以被判断的版本与兼容范围;

明确声明的权限和硬件需求;

可验证的内容完整性与签发来源;

安装失败时尽量不伤及旧版本的更新路径。

.app 文件,就是把这些信息装进同一个边界清楚、能够验证的容器里。

下一篇,我准备继续拆解权限模型:Manifest 里写了 displaytouch 和 storage 以后,系统 API 到底怎样在每一次调用时把权限真正执行起来。

如果让你给 ESP32 的小应用包增加一个新能力,你最希望先加入图标、资源文件,还是自动更新?欢迎留言告诉我。

我是阿白,感谢你的观看。

— END —

相关学习资料