鸿蒙Flutter框架 0day适配实战:04 flutter build hap 自动签名:oh-flutter flutter_tools 签名模块的完整改造历程
前言
使用 oh-flutter 构建 HarmonyOS 应用时,flutter build hap 执行完之后,产出的 HAP 包还是未签名状态——要装到真机上运行,你还得手动打开 DevEco Studio,走一遍"生成签名材料 → 上传证书 → 创建 Profile → 写入 build-profile.json5"的完整流程。对开发者来说,这一步既打断心流,又依赖 IDE 环境;对 CI 场景来说,更是完全走不通的死路。
本文记录我们为 flutter_tools 增加 OHOS 自动签名能力的完整历程:如何参考 deveco-cli 开源实现(openharmony-sig/deveco-cli)的签名逻辑,在 packages/flutter_tools 里实现了一个 11 个文件、79 个测试、零外部 Dart 依赖 的自包含签名模块(lib/src/ohos/signing/),让 flutter build hap 一条命令直接产出可安装的签名 HAP。下面按"为什么必须字节级兼容 → 模块怎么切 → 判定链怎么短路 → 踩坑与修复"的顺序展开。
提示:本文所有代码均基于 oh-flutter 的
oh-3.47-dev分支。签名模块的落库提交是f2651e0b91(feat(ohos): add automatic signing for flutter build hap,2026-10-21,+5473 / −0),随 3.47.7-ohos-1.0.1 发布;仓库bin/internal/dart-sdk-url.ohos的注释把这个批次明确记为"纯框架变更(flutter_tools 新增 OHOS 自动签名lib/src/ohos/signing/,无引擎/Dart 变更)",11 个工件逐字节重挂载——也就是说:这次功能上线没有动任何引擎产物。当前对应发布线为 3.47.8-ohos-1.0.0。
一、背景与挑战
1.1 痛点:为什么非签名 HAP 装不上真机
HarmonyOS 对应用分发的安全要求是:安装到真机的 HAP 必须经过调试证书签名。DevEco Studio 的"自动签名"功能实际上做了四件事:
- 登录华为开发者账号(OAuth),确定团队(Team ID);
- 生成本地密钥库(
.p12)与证书签名请求(.csr); - 把 CSR 上传到 AGC(AppGallery Connect),申请并下载调试证书(
.cer)与调试配置 Profile(.p7b); - 把四样材料的加密信息写入项目的
build-profile.json5,hvigor 打包时再用它们完成真正的 HAP 签名。
原来 flutter_tools 的做法是在 assembleHap 之后调用 checkOhosSignedInfo 做构建后校验,发现没签名就打印一段引导语,让用户自己去 DevEco 操作。这在"偶尔构建一次"的场景下尚可接受,但每次构建都要中断流程去 IDE 里点一圈,体验非常割裂。
1.2 设计目标与约束
在动手之前,我们先明确了四条设计目标:
- 字节级兼容 DevEco:材料文件的命名规则、
build-profile.json5中密码的加密算法,必须与 hvigor 的DecipherUtil完全一致——因为最终解开这些密码、执行 HAP 签名的是 hvigor 自己(通过hap-sign-tool.jar),而不是 flutter_tools; - 高内聚低耦合:新逻辑全部收敛在一个独立目录
lib/src/ohos/signing/下,对外只暴露一个入口函数ensureOhosSignedInfo,方便日后提交 PR 合入上游仓库; - 非侵入:如果用户已经通过 DevEco 配好了签名(
build-profile.json5已有signingConfigs),自动签名静默跳过,不碰任何已有配置; - 失败可降级:自动签名任何一步失败,都回退到原有的 DevEco 引导语,绝不阻塞构建主流程。
关键认知:flutter_tools 只负责"生产材料 + 写配置",真正的 HAP 签名仍由 hvigor 完成。所以我们的兼容性目标不是"产出相同的 HAP",而是"产出让 hvigor 能认的材料"。
二、总体架构
2.1 模块划分:11 个文件各管一事
签名模块按职责拆成 11 个 Dart 文件,每个文件只负责一件事,互相之间只通过明确的接口通信:
| 文件 | 职责 | 依赖 |
|---|---|---|
aead.dart | 纯 Dart 实现 AES-128/256 + GCM + PBKDF2-HMAC-SHA256 | 无(仅 dart:typed_data) |
x509.dart | ASN.1 / PEM / PKCS#10 CSR 解析、指纹、SPKI 提取、有效期 | aead.dart |
keymanager.dart | DevEco DecipherUtil 兼容的密码加解密 | aead.dart |
auth.dart | OAuth loopback 登录、加密 token 存储、用户/团队 API | aead.dart, http |
material.dart | 材料文件命名规则、工具链解析(java/hap-sign-tool)、P12/CSR 生成 | base, process |
cloud.dart | AGC 云 API 客户端(证书上传/下载/删除、Profile 申请) | auth.dart, http |
device.dart | hdc 本地设备发现 + 云端设备注册 | cloud.dart, process |
profile.dart | .p7b 二进制解析、build-profile.json5 写入、一致性校验 | x509.dart, json5 |
acl.dart | 从 app.json5 收集权限 + SDK 权限白名单校验 | — |
regenerate.dart | C0–C10 短路重生成判定链 | material, profile, x509 |
signing.dart | 编排入口 ensureOhosSignedInfo(六阶段流程) | 以上全部 |
拆分原则很简单:密码学原语(aead/x509)不依赖任何 Flutter 内部 API,可以独立测试、独立复用;云端交互(auth/cloud/device)只依赖 http;本地文件操作(material/profile)只依赖 base/process。这样上游 review 时可以按维度分批看。
2.2 主流程:六阶段编排
signing.dart 中的 ensureOhosSignedInfo 是整个模块的唯一入口,在 hvigor.dart 和 ohos_builder.dart 的 4 个 assemble 调用点之前被调用:
- 入口检查:
build-profile.json5已有signingConfigs→ 静默返回(兼容已有 DevEco 用户); - 登录态检查:读本地加密 token → 调用
jwToken/check校验 → 过期则走 OAuth loopback 重新登录; - 重生成判定:跑 C0–C10 判定链,能复用现有材料就短路(跳过所有云端调用);
- 本地材料生成:调
hap-sign-tool.jar generate-keypair/generate-csr产出.p12和.csr; - 云端材料交换:AGC 删除旧证书 → 上传 CSR → 下载签发的
.cer→ 注册设备 → 下载.p7b; - 写配置:把四样材料路径 + 加密密码写入
build-profile.json5,交棒给 hvigor。
实际触发时只需要一条命令:
# 在 Flutter 工程的 example/ 目录下
flutter build hap --verbose
# 自动签名完成后 hvigor 继续 assembleHap,产物直接可安装
三、与 DevEco 的兼容:两个"字节级"难题
3.1 文件命名:一个隐藏的 SHA-256 哈希规则
DevEco 生成的材料文件不是随便命名的,而是遵循一套带哈希的规则:
{safeProduct}_{baseName}_{hash}{typeSegment}=.p12
例如:default_ohos_hZ_ZWrVriCSZPUHdd6qNU3b9HS6t1_4DYt8FtbvCGfQ=.p12
其中 hash 的计算方式是 sha256(项目绝对路径) → base64url 编码 → 只去掉填充符 =:
// material.dart:与 DevEco 完全一致的项目路径哈希
String getProjectRawHash(String projectPath) {
final digest = sha256.convert(utf8.encode(projectPath)).digest();
// 只剥离 base64url 的 padding '=',保留 '-' 和 '_'
return base64Url.encode(digest).replaceAll('=', '');
}
这里有两个极易踩错的细节:
- 文件名里
.p12扩展名前面那个=是字面量(模板的一部分),不是哈希的 base64 填充符——一开始我们以为那是填充符没去掉,结果生成的文件名和 DevEco 对不上,hvigor 找不到材料; - base64url 表里的
-和_必须保留。早期版本用replaceAll('=','').replaceAll('-','+').replaceAll('_','/')做"反 URL-safe"转换,哈希里一旦包含-或_,文件名就直接错了。
这条规则我们没有文档可查,最终是靠对照真实 DevEco 工程产出的文件名逆向验证的:拿一个 DevEco 已签名的工程,用上面两个规则算一遍,文件名逐字符一致才算通过。
经验:做工具链兼容时,"和参考实现逐字节对齐"的验证方式(golden file 式比对)比读文档可靠得多。
3.2 密码加密:纯 Dart 复刻 DecipherUtil
build-profile.json5 里的 storePassword / keyPassword 不是明文,而是 hvigor 的 DecipherUtil 用 AES-128-GCM 加密后的密文。hvigor 侧是 Node.js 代码:
// DevEco hvigor-ohos-plugin 的 decipher-util(Node 伪代码)
const key = crypto.pbkdf2Sync(
utf8(fd0 ^ fd1 ^ fd2 ^ FIXED_COMPONENT), // 4 组 16 字节材料异或
acSalt, 10000, 16, 'sha256');
const decipher = crypto.createDecipheriv('aes-128-gcm', key, iv);
其中 fd0/fd1/fd2/ac/ce 是写在 ~/.ohos/config/material/ 下的 5 个小文件,FIXED_COMPONENT 是一个写死的 16 字节常量。由于 Dart 生态没有现成的 GCM 实现可用(oh-flutter 自定义 SDK 也不允许随意加依赖),我们在 aead.dart 里从零实现了 AES + GCM + PBKDF2,并用 NIST 官方测试向量逐条验证。
keymanager.dart 则负责把整个"材料文件 → 密钥 → 解密"流程封装成与 DecipherUtil 对称的加解密接口。验证方式是往返测试:flutter_tools 加密 → 用 DevEco 同款算法解密 → 再用 keytool 打开 .p12,三步全通才说明字节级兼容。
四、密码学原语:纯 Dart 实现 AES-GCM 与 PBKDF2
4.1 AES 核心:照着 NIST 规范逐条实现
aead.dart 的 AES 部分严格按 NIST SP 800-38D 实现,包括密钥扩展(KeyExpansion)、加解密轮函数和 GF(2^8) 乘法。开发中修复了三个典型 bug:
- 密钥扩展列表越界:
w[i] = w[i-1] ^ w[i-Ns]的边界条件写错,128 位密钥能过、256 位密钥直接RangeError; final字段构造期赋值:Dart 的final列表字段不能在构造函数里赋值,改成在构造函数体内初始化;- GF 乘法实现:多项式乘法的按位异或循环少移了一位,只有特定字节模式下才暴露,纯单元测试(NIST 向量)能抓住、靠"看起来对"抓不住。
4.2 GCM:CTR 的 off-by-one 差点全废
GCM 模式最容易写错的地方是计数器初值:
// GCM 的 CTR 计数规则(NIST SP 800-38D 附录 C)
// J0 = IV(96bit) || 0x00000001
// 第 1 块密文 = E_K(J0 + 1) ← 注意:用 J0+1,不是 J0
// 认证 Tag = E_K(J0) XOR GHASH(...) ← Tag 才用 J0 本身
一开始我们把首块密文也算成 E_K(J0),结果所有 GCM 向量全挂。修正后 AesGcm.seal/open 对 NIST 的全部 GCM 测试向量通过。seal 的签名也刻意做得与 Web Crypto / Node 习惯一致:
// 返回 密文 || tag(16B)
List<int> seal(List<int> plaintext, List<int> nonce, [List<int>? aad]);
// 校验 tag 失败返回 null(恒时比较,防时序攻击)
List<int>? open(List<int> ciphertextWithTag, List<int> nonce, [List<int>? aad]);
4.3 PBKDF2-HMAC-SHA256
DecipherUtil 的密钥派生参数是 iterations=10000, dkLen=16, prf=HMAC-SHA256,标准 RFC 5869 实现。10000 轮在 Dart 里单次约 50–100ms,可接受——它只在每次构建时执行一两次,不在热路径上。
// aead.dart:与 Node pbkdf2Sync('sha256') 完全一致
List<int> pbkdf2HmacSha256(List<int> password, List<int> salt,
int iterations, int dkLen) {
final prf = Hmac(sha256, password);
final blocks = (dkLen / 32).ceil();
final dk = <int>[];
for (var i = 1; i <= blocks; i++) {
var u = prf.convert([...salt, i, 0, 0, 0]).digest();
final t = List<int>.from(u);
for (var j = 1; j < iterations; j++) {
u = prf.convert(u).digest();
for (var k = 0; k < 32; k++) {
t[k] ^= u[k];
}
}
dk.addAll(t.sublist(0, dkLen - dk.length));
}
return dk;
}
五、解析二进制文件:.p7b 与 X.509
5.1 .p7b 是二进制 PKCS#7 容器,不是文本
.p7b(Provision Profile)看起来像配置文件,实际是二进制 PKCS#7 SignedData(DER 编码)。真正的配置信息是一段 JSON,被嵌在 DER 树的某个 OCTET STRING 里。第一版代码用 readAsStringSync 直接读文本,二进制字节流一上来就抛异常。正确做法是读字节、遍历 DER 树定位内嵌 JSON:
// profile.dart:从 PKCS#7 DER 树中提取内嵌 JSON
Map<String, dynamic> _extractJsonFromPkcs7(List<int> der) {
var offset = 0;
// 自外向内遍历,找到包含可解析 JSON 的 OCTET STRING (tag 0x04)
for (var i = 0; i < 64; i++) {
final (tag, len, body) = _readTlv(der, offset);
if (tag == 0x04) {
final json = _tryParseJson(der.sublist(offset + 3, offset + 3 + len));
if (json != null) {
return json;
}
}
offset += _tlvSize(der, offset);
if (offset >= der.length) {
break;
}
}
throw const FormatException('.p7b: no embedded JSON found');
}
解析出来的 JSON 包含 validity(有效期)、bundle-info(包名)、development-certificate(开发证书指纹)、device-ids(绑定设备)和 acl(权限白名单)——后文的设备注册和一致性校验都依赖这些字段。
5.2 一个 RangeError 暴露的 ASN.1 长长度溢出 bug
写 ASN.1 解析器时遇到一个诡异的崩溃:RangeError (index): Invalid value: Not in inclusive range,且只在特定文件上复现。排查后发现是 DER 长形式长度字段的 64 位整数溢出:
// x509.dart:长形式长度(firstLengthByte & 0x80 != 0)的安全读取
int numBytes = firstLengthByte & 0x7f;
if (numBytes == 0 || numBytes > 4) {
throw const FormatException('DER: invalid long-form length');
}
// 最多 4 字节:5 字节以上在 64 位 int 里移位可能变负数,
// 负数会"通过"所有下界检查,然后在 sublist 处抛 RangeError
DER 标准里长度字段理论上可以任意长,但在证书场景下 4 字节(约 4GB)绰绰有余。超过 4 字节的"长度"基本可以断定是解析器跑偏到了非结构化的字节流(比如把证书内容当 TLV 读了)。如果不加这个上限,5 个字节的长度值移位后可能变成负数,所有 >= 0 的边界检查全部失效,最后以 RangeError 的形式在 sublist 处爆掉——离真正的出错点好几层代码,极难排查。
教训:解析不受信二进制数据时,对"结构字段"设合理的硬上限(而不是只信任标准理论值),能避免一整类"负数绕过边界检查"的诡异崩溃。
5.3 X.509 解析:指纹、有效期与 SPKI 提取
x509.dart 同时实现了 PEM 证书和 PKCS#10 CSR 的解析,提供三个能力:
- 指纹计算:SHA-256 指纹(与 AGC 上传/比对逻辑一致);
- 有效期判断:ASN.1 的
UTCTime/GeneralizedTime两种格式都要处理(2050 年之后证书会用后者); - SPKI 提取:从 TBS Certificate 的
SubjectPublicKeyInfo里取出公钥 DER 字节——这是第八节一致性校验的核心。
另一个小坑:X.509 v3 证书的 version 字段是 [0] EXPLICIT 上下文标记,构造式标签字节是 0xA0 而不是 0x00。isContextTag 的判断写成只查低 5 位标签号,会把 0xA0 误判成普通字段,导致 v3 证书全部解析失败。
六、云端流程:OAuth 登录与 AGC API
6.1 OAuth Loopback:本地 HTTP 服务器接回调
AGC 的登录走华为开发者账号的 OAuth 流程。CLI 场景没有 Web 回调地址,标准做法是 loopback 模式:
// auth.dart:loopback 登录核心步骤
// 1. 本地起一个 127.0.0.1 的 HTTP 服务器(随机端口)
// 2. 用系统浏览器打开 OAuth 授权页(redirect_uri = http://127.0.0.1:port)
// 3. 用户在浏览器里完成登录,华为重定向回本地服务器
// 4. 从回调参数拿到 tempToken
// 5. 调 token 接口把 tempToken 换成 JWT,加密存到 ~/.ohos/flutter-signing/token.enc
token 的存储同样用了 aead.dart 的 AES-GCM 加密(密钥来自设备派生材料),避免明文 JWT 落在 ~/.ohos 下。每次构建先调 authrouter/auth/api/jwToken/check(header 带 jwtToken)校验有效期,只有过期才重新拉起浏览器——正常情况下一周内都不会再看到登录窗口。
6.2 AGC API:两个 DELETE 的传参方式完全不同
AGC 的 REST API 有几个反直觉的地方,整理成对照表:
| 操作 | 方法 | 参数传递 | 说明 |
|---|---|---|---|
| 删除远程证书 | DELETE | JSON body:{"certIds": [id]} | 放 query string 会 400 |
| 删除调试 Profile | DELETE | query string:?provisionIds[]=<id> | 放 body 会 400 |
| 上传 CSR | POST | multipart + JSON 混合 | 返回新证书 ID |
| 下载证书/Profile | POST | JSON body | 返回 base64 内容 |
认证头统一是三个:uid、teamId、oauth2Token。其中 teamId 要登录后才能从用户 API 查到。这些细节全部来自对 deveco-cli 开源实现的逐行对照——它的 cert-api.ts 和 generate-profile.ts 是本文所有端点、字段名和传参方式的权威参考。
提示:AGC 的 API 错误信息非常含糊(很多直接返回 400 不带详情),调试时建议先用 deveco-cli 的同一账号复现一遍,确认是参数问题而不是权限问题。
七、智能复用:C0–C10 短路判定链
7.1 为什么要一条判定链
自动签名最贵的不是计算,而是云端往返:删证书、传 CSR、下证书、注册设备、下 Profile,一次完整流程要 5–8 个 HTTP 请求。如果每次构建都重新走一遍,不仅慢,还会把 AGC 的调试证书额度白白消耗掉(每次删除重建都会产生新证书 ID)。所以 regenerate.dart 设计了一条 C0–C10 短路判定链——按成本从低到高依次检查,任何一条判定"需要重生成"就立刻短路:
// regenerate.dart:判定链(按检查成本升序排列)
// C0 材料文件缺失(.p12/.csr/.cer/.p7b 任一不存在) → 需要
// C1 材料已过期(.cer 或 .p7b 有效期不足 7 天) → 需要
// C2 项目路径哈希变了(工程被移动/重命名) → 需要
// C3 bundleName 与 .p7b 内 bundle-info 不一致 → 需要
// C4 本地证书指纹与 .p7b 内 development-certificate 不符 → 需要
// C5 build-profile.json5 中的 storePassword 解密失败 → 需要
// C6 设备未绑定(当前 hdc 设备 UDID 不在 .p7b device-ids) → 需要
// C7 权限变化(app.json5 权限超出 .p7b acl 白名单) → 需要
// C8 keytool 验证 .p12 打不开(密码/别名不匹配) → 需要
// C9 AGC 侧证书已不存在(云端 404) → 需要
// C10 兜底:以上都通过但无法确认 → 保守重生成
7.2 复用路径的实测
这条链的价值在第二次构建时体现得最明显:
$ flutter build hap --verbose
[ ] Auto-signing: reusing existing material (C0-C10 all passed).
[ +917 ms] start hap build...
第二次构建零云端调用,直接跳过整个签名阶段进入 hvigor 打包。这是"自动签名"能日常可用的关键——它必须安静到用户感觉不到它的存在。
八、一致性校验:一次失败暴露的设计错误
8.1 keytool 指纹比对的陷阱
最初版本的材料一致性校验逻辑是:用 keytool 取出 .p12 里证书的指纹,和 .cer(AGC 签发的证书链)的指纹比对。端到端测试时这一步必然失败——比对结果永远是 false。
排查后发现这是个设计错误而不是实现 bug:.p12 里存的是我们自签名的证书(generate-keypair 当场生成的,subject 和 issuer 都是自己),而 .cer 是 AGC 用自己的 CA 重新签发的证书。两者的指纹永远不会相同——issuer、有效期、甚至签名算法都不同。拿两个不同签发者签的证书比指纹,等于拿两张不同银行发的身份证比照片尺寸。
8.2 修复:用 SPKI 公钥比对
正确的不变量是公钥:CSR 里请求的公钥,就是 AGC 签发证书里嵌入的公钥,也是 .p12 密钥对对应的公钥。三者通过 SubjectPublicKeyInfo(SPKI)的 DER 字节精确相等:
// profile.dart:基于 SPKI 的一致性校验(替代指纹比对)
bool checkCertificateInProfile({
required List<int> p12CertSpki, // .p12 自签名证书的 SPKI
required List<int> cerLeafSpki, // AGC 签发证书链叶子证书的 SPKI
required List<int> csrSpki, // CSR 的 SPKI
}) {
// 三个 SPKI 必须两两相等 —— 相等即证明
// "P12 里的私钥" 和 "AGC 签发的证书" 是同一对密钥
const eq = listEquals;
return eq(p12CertSpki, cerLeafSpki) && eq(cerLeafSpki, csrSpki);
}
这个修复还带来一个额外好处:不再依赖 keytool 子进程。SPKI 比对完全在纯 Dart 的 x509.dart 里完成,测试里可以直接用 openssl 生成的测试证书对做断言,不需要在 CI 里装 Java。
经验:校验"两个制品是否来自同一来源"时,先问一句——什么字段在两次签发中保持不变? 指纹、有效期、issuer 全都会变,只有密钥材料(公钥/私钥)是不变量。找错不变量,测试就会"永远失败",而且失败得很有迷惑性。
九、端到端验证
9.1 从零到签名 HAP 的完整运行
下面是真实账号下第一次构建的完整输出(密码已在日志层面脱敏):

图 1:一条命令触发自动签名——生成 P12/CSR、向 AGC 申请证书与 Profile、写入 build-profile.json5,随后交棒 hvigor 打包
注意输出里的几个关键节点:
Auto-signing: regeneration required—— C0–C10 判定链的结论(首次构建,材料缺失);generate-keypair success/generate-csr success—— 本地材料生成(走 DevEco JBR 的 java);Downloaded ...=.cer (2987 bytes)/.p7b (4343 bytes)—— AGC 云端材料到位;Wrote auto-signing config "default" into .../build-profile.json5—— 加密配置落盘。
9.2 产物交叉验证
签名 HAP 产出后,我们用 hap-sign-tool.jar verify-app 做了独立交叉验证(不依赖 hvigor 的打包链路):
# 独立验证签名 HAP 的完整命令
java -jar $SDK/default/openharmony/toolchains/lib/hap-sign-tool.jar verify-app \
-inFile /tmp/signed_test.hap \
-outCertChain /tmp/out_chain.cer \
-outProfile /tmp/out_profile.p7b
# 输出 Digest verify result: true / verify permission sign success / verify-app success
交叉验证还确认了一个细节:签名 HAP 内嵌的证书链顺序是 root → leaf → intermediate(不是直觉上的 root → intermediate → leaf),这是 hap-sign-tool 的行为,解析工具按"链里包含 AGC 叶子证书指纹"判断而不是按位置判断。
9.3 验证矩阵
整体验收覆盖了从单元到真机全链路,结果如下:
| 验证项 | 方式 | 结果 |
|---|---|---|
| 密码学原语 | NIST SP 800-38D / RFC 5869 官方测试向量 | 全部通过 |
| 静态分析 | dart analyze(模块 11 + 测试 9 文件) | 0 issue |
| 单元测试 | dart test test/general.shard/ohos/signing/(79 个) | 79/79 通过 |
| 文件命名兼容 | 与真实 DevEco 工程产物逐字符比对 | 一致 |
| 密码往返 | flutter_tools 加密 → DevEco 算法解密 → keytool 打开 .p12 | 通过 |
| AGC 全链路 | 真实账号:删旧证 → 传 CSR → 下载证书/Profile | 通过 |
| SPKI 一致性 | 证书 ↔ CSR ↔ P12 公钥三方比对 | 通过 |
| 真实签名/验签 | hap-sign-tool sign-app + verify-app | PASS |
| 复用短路 | 第二次构建日志确认零云端调用 | 通过 |
十、踩坑与经验
10.1 环境与工具链的坑
开发过程中环境类问题比算法类问题花的时间还多,整理一份清单:
- macOS 的
/usr/bin/java是个 stub:canRun检查能通过,但真正执行时没有 JVM。Java 解析顺序必须写成JAVA_HOME → DevEco JBR(/Applications/DevEco-Studio.app/Contents/jbr)→ PATH; DEVECO_SDK_HOME必须指向 SDK 目录(.../Contents/sdk):hvigor 从这里找hap-sign-tool.jar和hdc,指错一层目录就会报"工具链不存在";- oh-flutter 自定义 Dart SDK 的 API 差异:
bin/cache/dart-sdk-ohos把String.allMatches的参数类型从Pattern改成了String,签名模块里所有正则匹配都改用RegExp.allMatches(String)或纯循环,保证双 SDK 兼容; - hvigor/node 路径:DevEco 自带的 node v24 和 hvigorw 都在
/Applications/DevEco-Studio.app/Contents/tools/下,不在系统 PATH 里。
10.2 一个已知的、与签名无关的既有问题
端到端构建的最后一关(AOT 编译阶段)会遇到 Invalid SDK hash——aot_ohos_asset_bundle 的 IconTreeShaker 用 host 的 dart-sdk 去校验 dart-sdk-ohos 的构建产物,build ID 不匹配。这个问题在加入自动签名之前就存在,属于 oh-flutter 工具链自身的 SDK 管理问题,与本文的签名模块无关,后续会在工具链侧单独修复。
边界声明:自动签名模块解决的是"材料生产 + 配置写入",签名本身由 hvigor 完成。如果构建在 AOT 阶段失败,请优先排查工具链 SDK 问题,而不是签名配置。
十一、常见问题 FAQ
11.1 flutter build hap 成功,但安装时仍提示未签名?
先用 --verbose 查看是否有 Auto-signing 日志:自动签名的任何一步失败都会静默降级为 DevEco 引导语,不会阻塞构建。确认项目根目录的 build-profile.json5 已生成 signingConfigs,并检查 hvigor 读取的是同一份配置;已有 DevEco 签名配置时模块会主动跳过。
11.2 第二次构建会重新申请证书吗?
不会。C0–C10 判定链全部通过时会短路复用现有材料,日志出现 reusing existing material,整个签名阶段零云端调用。只有材料缺失/过期、工程路径变化、设备未绑定、权限超出 Profile 白名单等情况才会触发重生成。
11.3 删除 AGC 证书或 Profile 返回 400?
两种 DELETE 的传参位置不同:删除证书要把 {"certIds":[...]} 放在 JSON body,删除 Profile 要把 provisionIds[] 放在 query string;放错位置会直接 400。可先用 deveco-cli 的同账号复现,区分参数问题与权限问题。
11.4 .p12 打不开,或 build-profile.json5 里的密码解密失败?
先确认材料文件名中的项目路径哈希与当前工程绝对路径一致,再确认 storePassword / keyPassword 使用与 hvigor DecipherUtil 相同的 AES-128-GCM 流程加密。可用 keytool 打开 .p12 做独立验证;密码错误时优先检查材料文件是否来自另一台机器或旧工程。
11.5 macOS 上提示 java 不可用,或找不到 hap-sign-tool.jar?
macOS 的 /usr/bin/java 只是 stub,不能作为 JVM 使用。Java 解析顺序应为 JAVA_HOME → DevEco JBR(/Applications/DevEco-Studio.app/Contents/jbr)→ PATH;同时确认 DEVECO_SDK_HOME 指向 .../Contents/sdk,hvigor 才能找到 hap-sign-tool.jar 与 hdc。
11.6 构建在 AOT 阶段报 Invalid SDK hash,是签名失败吗?
不是。该问题在自动签名模块加入之前就存在,原因是 host 的 dart-sdk 与 dart-sdk-ohos 的 build ID 不匹配,属于工具链 SDK 管理问题;签名材料与 HAP 验签可以通过,优先按 10.2 节排查工具链。
十二、系列导航
本文是「鸿蒙Flutter框架 0day适配实战」系列的第 4 篇,也是消费侧的收官篇(工具链专题)。全系列 10 篇,分消费侧(4 篇)与生产侧(6 篇);与本文直接相关的是下面几篇:
| 篇目 | 主题 | 与本文的关系 |
|---|---|---|
| 第 1 篇 | 背景、分叉仓库、版本命名与 fvm fork 安装 | 装到含自动签名能力的 oh-3.47-dev 版本线 |
| 第 2 篇 | 手动 bootstrap 高版本 SDK 与版本核验 | tag 版本安装,本文代码所在 SDK 的获取方式 |
| 第 3 篇 | 鸿蒙模拟器/真机跑 Flutter 项目 | 第三章「签名:没有真机证书时怎么出包」——本文把它自动化了 |
| 第 5 篇 | 上游同步总览与 Breaking Changes | 工具链改造在同步批次中的位置与验证门口径 |
| 第 6 篇 | 同步 Phase 1+2:基线建立与 Framework 适配 | flutter_tools 侧 API 漂移修复,与本文同一模块 |
全系列 10 篇总览(消费侧 4 篇 + 生产侧 6 篇):
| # | 篇目 | 侧 | 一句话主题 |
|---|---|---|---|
| 01 | 背景、分叉仓库、版本命名与 fvm fork 安装 | 消费 | 三层架构、三个接缝、七层工作量、fvm 直装 tag、版本墙 |
| 02 | tag 版本的手动安装与三层核验 | 消费 | 浅克隆 bootstrap、首次初始化、SSH 修复、异常分诊 |
| 03 | 鸿蒙模拟器/真机跑 Flutter + 消费侧踩坑集 | 消费 | 工具链三件套、HAP 构建/安装/运行、占位签名、截图取证、CP1-CP14 |
| 04 | flutter build hap 自动签名(本篇) | 消费 | P12/CSR、AGC 证书与 Profile、纯 Dart AES-GCM、C0-C10 复用 |
| 05 | 上游同步总览、BC 全景与端到端 SOP | 生产 | 批次解剖、五阶段验证门、12 项适配表、端到端 SOP 与 P1-P29 索引 |
| 06 | Phase 1+2:基线建立与 Framework 适配 | 生产 | 代理 HTTP/1.1 修障、421 文件重打、Dart SDK 源码构建 |
| 07 | Phase 3 Engine:三方重放与冲突裁决 | 生产 | 826 文件、15 真冲突两分法、契约对齐 16 项 |
| 08 | 引擎构建链攻防 | 生产 | 依赖树、工具链版本、musl 交叉链接、测试面漂移 |
| 09 | 构建发布与产物分发 | 生产 | 版本锁、11 个工件(Dart SDK 双宿主)、11 条覆盖键、换新 tag 铁律 |
| 10 | 验证与真机验收 | 生产 | 四道质量闸、设备侧单测、三档引擎、字体静默故障 |
阅读顺序建议:第 3 篇 → 本篇。第 3 篇解决「没有证书怎么出包」,本篇解决「一条命令出可安装的签名包」,两篇合起来才是完整的真机调试链路。
总结
回顾整个改造,最有价值的三个认知是:第一,工具链兼容的本质是找对"不变量"——文件名哈希、DecipherUtil 密钥派生、SPKI 公钥,每一个都是逆向后逐字节验证的;第二,纯 Dart 实现密码学原语完全可行,前提是拿 NIST/RFC 官方测试向量当验收标准;第三,自动化的终点是"安静"——C0–C10 判定链让第二次构建完全无感,这才是日常可用的状态。
目前模块(lib/src/ohos/signing/ 11 个文件 + test/general.shard/ohos/signing/ 9 个测试文件,单笔提交 +5473 / −0 行、79 个单测)已随 3.47.7-ohos-1.0.1 进入 oh-flutter/flutter_flutter,下一步是整理 design doc 后向 flutter/flutter 上游提交 PR,并跟进 AOT SDK hash 工具链问题的修复。
下一篇(第 5 篇)切换视角进入生产侧:3.44.9→3.47.4 上游同步批次总览——2263 个文件 / +192740 行的适配构成、五阶段与五道验证门、3.47 Breaking Changes 适配表,以及可复用的端到端同步 SOP。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- oh-flutter 仓库(含本文全部代码):atomgit.com/oh-flutter/flutter_flutter
- 本文截图素材仓库:atomgit.com/oh-flutter/csdn-assets
- Flutter OH 适配版(3.47.8-ohos-1.0.0):https://atomgit.com/oh-flutter/flutter_flutter
- Flutter OH 环境搭建指导:https://atomgit.com/oh-flutter/flutter_samples/blob/master/ohos/docs/03_environment/OpenHarmony-flutter%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA%E6%8C%87%E5%AF%BC.md
- NIST SP 800-38D(AES 与 GCM 规范):csrc.nist.gov
- RFC 5869(PBKDF2 规范):datatracker.ietf.org
- DevEco Studio 官方页面:developer.huawei.com
- deveco-cli 开源实现(本文签名逻辑参考):atomgit.com/openharmony-sig/deveco-cli
- OpenHarmony 官方文档:OpenHarmony 官网
- Flutter 官方仓库:github.com/flutter/flutter
- 开源鸿蒙跨平台社区:openharmonycrossplatform.csdn.net
环境与版本
本文的示例与截图基于以下环境组合(2026-10 实测):
| 组件 | 版本 | 说明 |
|---|---|---|
| 上游 Flutter SDK | 3.47.8 | github.com/flutter/flutter |
| Flutter SDK(OH 适配版) | 3.47.8-ohos-1.0.0 | oh-flutter/flutter_flutter,oh-3.47-dev 分支 |
| DevEco Studio | 26.0.0 Release | 提供 hvigor / ohpm / hdc / JBR 等工具链 |
| OpenHarmony SDK / API | API 26(26.0.0) | 模拟器与 SDK 均为 API 26 |
| 运行设备 | OpenHarmony 模拟器 | API 26,1280×2832 |
| 签名执行方 | hvigor + hap-sign-tool.jar | flutter_tools 只负责生产材料与写配置 |
环境搭建(分叉获取、DevEco Studio 与 SDK 配置、模拟器创建)请按官方指导操作:OpenHarmony-flutter 环境搭建指导。
本文改造的是
packages/flutter_tools内的工具链代码,不是 pub.dev 三方库,因此没有pubspec.yaml依赖声明;获取代码请直接使用上面的 oh-flutter 仓库与oh-3.47-dev分支。
更多推荐


所有评论(0)