鸿蒙HarmonyOS应用HAP包的自动签名机制拆解:把 DevEco Studio 的“签名材料工厂“拆开看
搞过鸿蒙应用开发的都知道, 即便是debug在本机测试,也需要在DevStudio里登录AGC完成debug的自签名。这点很烦!为什么呢?因为在AI时代, 我们实在是连庞大的devStudio IDE都不想打开,直接让AI跑完不好么?官方没有解决之道,针对这个痛点,于是有了此文,绝对硬核干货!我被这痛点折磨了很久!此文就是拆解,如何绕开DevEco IDE,直接实现应用HAP包的自签名。
- 这篇文档解决什么问题:
flutter build hap在没配签名时能自己把 HAP 签成"真机可装"的样子,而大多数人(包括我们自己一开始)以为它只是"调了个 AGC API"。不是。 - 它复刻的是 DevEco Studio 内部的自动签名流程——从 OAuth 登录、团队/设备注册、密钥与 CSR 生成、证书申请、profile 申请、文件落盘命名,到把加密口令写进
build-profile.json5。
本文把这条链拆成可复刻的步骤,附真实端点、真实参数、真实文件名规则,以及"什么时候其实不用上云"。事实来源与核对方式见文末《出处与核对》。
写这篇的目的:给后续要自己造工具的团队一份可以照着实现的说明书,而不是一堆猜测。
0. 一句话结论
鸿蒙 debug 自动签名 = AGC(华为应用市场)上一套账号资产 + 本机一套材料文件 + 一段把两者串起来、并把结果写成 hvigor 能读的配置的胶水。
它被自动化的是"材料生产",不是"签名动作"——签名本身仍然由 hvigor 调 SDK 里的
hap-sign-tool.jar完成。

1. 背景:为什么"有 signed.hap"还是装不上
这是最容易踩的第一个坑,也是整条链存在的理由:
| 现象 | 真相 |
|---|---|
hvigor assembleHap 产出了 entry-default-signed.hap | 不代表能装。没有 signingConfigs 时它照样产出 “signed” 文件(用 OpenHarmony 默认材料或干脆自签) |
hdc install -r xxx.hap 失败 | code: 9568322 signature verification failed due to not trusted app source |
| 判定"能不能装"的真正条件 | 设备只信任华为签发、且 profile 的 device-ids 含本机 UDID 的证书 |
所以"签名"这件事在鸿蒙上不是一个文件操作,而是三方关系:
华为 AGC 账号 本机 鸿蒙设备
───────────── ──── ────────
证书 (cert) ──┐ .p12 私钥+证书 校验:
profile(描述文件) ├────────► .cer / .p7b ──────────► ① 证书链可信(华为签)
device(设备 UDID) ┘ (~/.ohos/config) ② profile 含本机 UDID
③ 未被吊销/未过期
自动签名要做的就是:把左边这堆资产生出来、下载到本机、把中间这堆文件写成 hvigor 认识的配置。
2. 全链路(一张图看完)
[flutter build hap / devecocli / DevEco IDE]
│
│ ① 工程 & 工具链自检(bundleName、java、hap-sign-tool.jar)
▼
┌──────────────────────────────────────────────────────────────┐
│ ② 登录:OAuth loopback │
│ https://cn.devecostudio.huawei.com/console/DevEcoIDE/apply │
│ ?port=<本机端口>&appid=1009&code=<clientSecret> │
│ → 浏览器登录 → 302 回 127.0.0.1:<port>/?tempToken=… │
│ → GET /authrouter/auth/api/temptoken/check?… → JWT │
│ → JWT 加密落盘(后续构建复用,不必每次登录) │
└──────────────────────────────────────────────────────────────┘
│ ③ 团队:GET /api/ups/user-permission-service/v1/user-team-list → teamId
▼
┌──────────────────────────────────────────────────────────────┐
│ ④ 设备:hdc list targets → hdc shell bm get -u(UDID) │
│ getprop hw_sc.build.os.deviceType(设备类型码) │
│ → 缺则 POST /api/cps/device-manage/v1/device/add │
└──────────────────────────────────────────────────────────────┘
│ ⑤ 复用判定 C0–C10:材料还有效?→ 跳过整个云流程(只写配置)
▼
┌──────────────────────────────────────────────────────────────┐
│ ⑥ 密钥与 CSR(本地,调 SDK 的 java jar) │
│ java -jar hap-sign-tool.jar generate-keypair → .p12 │
│ java -jar hap-sign-tool.jar generate-csr → .csr │
└──────────────────────────────────────────────────────────────┘
│ ⑦ 证书:cert/list 找 auto_debug_<teamId>.cer → 没有就 cert/add(csr)
│ → 拿签名下载 URL → 下载 .cer
▼
┌──────────────────────────────────────────────────────────────┐
│ ⑧ profile:POST /api/cps/provision-manage/v1/ide/test/ │
│ provision/add {certList, packageName, │
│ provisionName, deviceList, acl} │
│ → 返回 provisionFileUrl → 换签名 URL → 下载 .p7b │
└──────────────────────────────────────────────────────────────┘
│ ⑨ 落地:按确定性命名写 ~/.ohos/config/…=.{p12,csr,cer,p7b}
│ 口令用 AesGcmKit 兼容格式加密 → 写 build-profile.json5
▼
[普通 hvigor assembleHap] → 产物自带签名 → hdc install 可装
3. 逐步拆解
3.1 ② 登录:OAuth loopback(不是"账号密码直传")
自动化里最"反直觉"的一步:工具不存你的密码,它开一个本机端口,让浏览器把登录结果回灌回来。
| 元素 | 值(源码实测) |
|---|---|
| 登录页 | https://cn.devecostudio.huawei.com/console/DevEcoIDE/apply?port=<port>&appid=1009&code=<clientSecret> |
appid | 1009(写死在源码里的 DevEco 系列应用 ID) |
code | 本机随机生成的 32 位十六进制(UUIDv4 去掉连字符)——不是账号验证码,是防伪造回调的随机串 |
| 回调服务 | 127.0.0.1:<随机 port> 的一次性 HTTP 服务;Host 白名单只认 127.0.0.1:<port> / localhost:<port> |
| 回调参数 | 含 tempToken、siteId;siteId != '1' 直接拒绝(只支持中国大陆账号) |
| 换 JWT | GET https://cn.devecostudio.huawei.com/authrouter/auth/api/temptoken/check?tempToken=<…>&site=CN&version=1.0.0&appid=1009 |
| JWT 校验 | …/authrouter/auth/api/jwToken/check;返回用户信息(userId、userName、accessToken) |
| 落盘 | JWT 加密存盘,下次构建直接复用(目录可用 FLUTTER_OHOS_SIGNING_DIR 覆盖) |
| 打开浏览器 | macOS open / Windows cmd /c start "" <url> / Linux xdg-open;打不开就打印 URL 让人手点 |
这一点很关键:所谓"自动签名要登录",本质是要一次 OAuth 授权换长效 token; token 有效期内后续构建都不再打扰人。
3.2 ③ 团队(teamId)
AGC 的资产是按团队隔离的:证书名、设备列表、profile 都在某个 teamId 下。
登录后先拉团队列表(/api/ups/user-permission-service/v1/user-team-list),默认取第一个,也允许显式指定。之后每个 AGC 请求都带 teamId 头。
3.3 ④ 设备注册:把本机 UDID 加进账号
hdc list targets # 得到序列号(跳过 [Empty])
hdc -t <serial> shell bm get -u # 取 UDID(下一行那种 64 位十六进制)
hdc -t <serial> shell getprop hw_sc.build.os.deviceType # 设备类型码
- UDID 提取规则:取
bm get -u输出里目标行之后的内容(我们自己的实现更稳:扫最长十六进制串); - 设备类型码(源码里列出的子集):
1轻量穿戴 /2穿戴 /3智慧屏 /4手机 —— PC(2in1)等更多码来自
hw_sc.build.os.deviceType的取值,工具只是透传; - 已在账号里就不重复加:先
device/list(分页encodeFlag=0&start=<n>&pageSize=100)比对; - 最终 profile 申请要带完整云端设备 ID 列表,不是只带你这一台。
3.4 ⑥ 密钥与 CSR:本地跑 SDK 的 jar
签名私钥永远不上云,只在本地生成:
# 生成密钥对(默认参数与 DevEco 一致)
java -jar hap-sign-tool.jar generate-keypair \
-keyAlias debugKey -keyAlg ECC -keySize NIST-P-256 \
-keystoreFile <…>.p12 -keyPwd <pwd> -keystorePwd <pwd>
# 生成 CSR(证书签名请求)
java -jar hap-sign-tool.jar generate-csr \
-keyAlias debugKey -subject "CN=DebugKey" -signAlg SHA256withECDSA \
-keystoreFile <…>.p12 -keyPwd <pwd> -outFile <…>.csr
- 默认参数(DevEco 同款):
keyAlias=debugKey、keyAlg=ECC、keySize=NIST-P-256、subject=CN=DebugKey、signAlg=SHA256withECDSA; - 口令是随机生成的字母数字(8 随机字节 → base64url,去掉
-_=,保证能安全地当命令行参数); - Java 从
JAVA_HOME/PATH找,找不到就退到 DevEco 自带的 JBR; - 日志里要把
-keystorePwd/-keyPwd打码(源码专门做了 masked run)。
3.5 ⑦ 证书:能复用就复用
- 证书名是确定的:
auto_debug_<teamId>.cer(teamId 里的/ . :等非法字符去掉)——于是"这次要不要新建"变成一次cert/list里的名字查找; - 新建:
POST /api/cps/harmony-cert-manage/v1/cert/add,body{csr, certName, certType},成功判据是响应体里ret.code == 0(不是 HTTP 200 就完事); - 下载:先向
…/cert/download-url一类接口要带签名的临时 URL,再 HTTPS 下载;下载后校验sha256。URL 要过 SSRF 白名单(HTTPS + 非内网地址),这是防"被诱导去下内网资源"的护栏。
3.6 ⑧ profile:把"包名 + 设备 + 证书"绑成一张描述文件
POST /api/cps/provision-manage/v1/ide/test/provision/add # debug(test)
POST /api/cps/provision-manage/v1/ide/real/provision/add # release
body: {
"certList": ["<certId>"],
"packageName": "com.example.demo", # = bundleName
"provisionName": "<sha256(product_bundle_bundle)[:16]>",
"deviceList": ["<udid1>", "<udid2>", …], # 仅 debug/test 需要
"aclPermissionList": [ … ] # 项目申请的 ACL 权限
}
→ { id, provisionFileUrl }
deviceList是 debug、release 的分水岭:test profile 带设备白名单,real profile 不带(上架用);provisionName用sha256("{product}_{bundle}_{bundle}")前 16 位十六进制 —— 同样是"确定性命名",让重复申请可去重;aclPermissionList来自工程声明的权限(module.json5/acls),profile 会把它固化进allowed-acls。
3.7 ⑨ 落地:文件名与口令都是"确定性"的
这两处是整条链最容易被忽略、但决定"DevEco / ark-cli / flutter / 我们的工具能否互相复用"的地方。
(a) 材料文件名
~/.ohos/config/{product}_{工程目录名}_{hash}=.{p12|csr|cer|p7b}hash = base64url(sha256(工程根绝对路径,前向斜杠)) 去掉 = 填充
本机实测核对(不是抄文档):
| 项 | 值 |
|---|---|
| 工程根(DevEco 眼中的) | E:/test/rust/habit-tree/src-tauri/gen/ohos(前向斜杠) |
sha256 → base64url → 去 = | m7Gxh5QRtg0h6LnDjtArCp3iYObE9iWm8BEssdAfmHM |
| 实际文件名 | default_ohos_m7Gxh5QRtg0h6LnDjtArCp3iYObE9iWm8BEssdAfmHM=.p7b ✅ |
| 反例 | 同一路径用反斜杠算出的 hash 是 EuNnZ1gj… → 不匹配 |
踩点:hash 用前向斜杠,且后面那个
=是文件名的一部分(hash 去 padding 后再接一个字面=),不是"带 padding 的 base64"。两个细节错一个,就和 DevEco 生成的材料对不上号。
(b) 口令加密(material/ 目录 + AesGcmKit 兼容格式)
写进 build-profile.json5 的 storePassword / keyPassword 不是明文,是 DevEco/hvigor 的AesGcmKit 密文;密钥材料就躺在 .p12 旁边的 material/ 里:
<config>/material/
├── ac/ 盐(16 字节,随机文件名,一个文件)
├── ce/ workKey 的密文 [ len(4B,BE) | IV(12) | ct | tag(16) ]
└── fd/
├── 0/ 随机分量(16B)
├── 1/ 随机分量(16B)
└── 2/ 随机分量(16B)
推导(与 hvigor-ohos-plugin 的 decipher-util.js 对齐):
passphrase = lossy-utf8( XOR(fd0, fd1, fd2, kFixed16) )
rootKey = PBKDF2-HMAC-SHA256(passphrase, salt, 10000, 16)
workKey = AES-128-GCM-decrypt(rootKey, ce)
ciphertext = hex( [ len(4B,BE) | IV(12) | AES-128-GCM-encrypt(workKey, pwd) | tag(16) ] )
这就是为什么"光把
signingConfigs抄到另一台机器上跑不通"——material/得一起跟着走。 也是为什么我们在灵渠里从不试图解密口令:hvigor 自己会解,我们只负责不破坏它。
© build-profile.json5 写入形态
{
app: {
signingConfigs: [{
name: 'default',
type: 'HarmonyOS',
material: {
storeFile: '…=.p12', storePassword: '<hex 密文>',
keyAlias: 'debugKey', keyPassword: '<hex 密文>',
signAlg: 'SHA256withECDSA',
profile: '…=.p7b', certpath: '…=.cer',
},
}],
products: [{ name: 'default', signingConfig: 'default', /* … */ }],
},
}
4. AGC 端点清单(实现照抄用)
| 用途 | Method | Path(基址 https://connect-api.cloud.huawei.com) | 关键 body / query |
|---|---|---|---|
| 证书列表 | GET | /api/cps/harmony-cert-manage/v1/cert/list | 头:uid teamId oauth2Token |
| 证书新增 | POST | /api/cps/harmony-cert-manage/v1/cert/add | {csr, certName, certType} |
| 证书删除 | DELETE | /api/cps/harmony-cert-manage/v1/cert/delete | body {certIds:[id]}(用 query 会被 AGC 400) |
| 设备列表 | GET | /api/cps/device-manage/v1/device/list | ?encodeFlag=0&start=<n>&pageSize=100 |
| 设备新增 | POST | /api/cps/device-manage/v1/device/add | {deviceName, udid, deviceType} |
| profile 申请(debug) | POST | /api/cps/provision-manage/v1/ide/test/provision/add | {certList, packageName, provisionName, deviceList, aclPermissionList} |
| profile 申请(release) | POST | /api/cps/provision-manage/v1/ide/real/provision/add | 同上但不带 deviceList |
| profile 删除 | DELETE | /api/cps/provision-manage/v1/provision/delete | ?id=<id> |
| 下载签名 URL | POST | /api/amis/app-manage/v1/objects/url/reapply | 传对象 URL,换回临时下载链 |
| 登录页 / token | GET | https://cn.devecostudio.huawei.com/… | 见 §3.1 |
统一约定:JSON 交互;请求头带 uid / teamId / oauth2Token;成功判据是 ret.code == 0(HTTP 200 + ret.code != 0 是常见的失败形态,必须解析 body)。错误信息优先取 ret.msg(它可能是对象,也可能是被 JSON 编码过的字符串,两种都要认)。
5. 短路径:C0–C10 复用检查(“大部分时候根本不该上云”)
这是整条链里最值得学的设计——先花 10 个本地检查决定"要不要联网":
| 检查 | 含义 | 不过怎么办 |
|---|---|---|
| C0 | 显式 --force | 直接重建 |
| C1 | 四个材料文件都在(.p12/.csr/.cer/.p7b) | 重建 |
| C2 | .p7b 内容非空 | 重建 |
| C3 | profile 没过期(validity.not-after) | 重建 |
| C4 | profile 的 bundle-name == 本工程包名 | 重建 |
| C5 | profile 的 developer-id(teamId)== 当前账号团队 | 重建 |
| C6 | 本机所有已连设备 UDID 都在 debug-info.device-ids 里 | 重建(换设备必触发) |
| C7 | 工程 ACL 权限 == profile 的 acls.allowed-acls | 重建(加权限必触发) |
| C8 | profile 里的开发证书指纹 ∈ 本机 .cer 证书链 | 重建 |
| C9 | 该证书未过期 | 重建 |
| C10 | build-profile.json5 里的口令还能打开 .p12 | 重建(material/ 丢了就会在这里露馅) |
全部通过 → 只重写 build-profile.json5,一条网络请求都不发。这条设计让"日常 build"离线可用、且把云配额与失败面压到最小。
6. 与 DevEco Studio 的对应关系
自动签名工具做的每一步,在 DevEco 里都有一个"人工对应动作":
| 步骤 | DevEco Studio 里的操作 | 自动化后 |
|---|---|---|
| 登录 | 登录华为账号 | OAuth loopback(一次,token 落盘) |
| 团队 | 选团队 | team 列表取第一个 / 显式指定 |
| 设备 | 手机插线 → 自动识别 | hdc 取 UDID + 云端注册 |
| 密钥/CSR | 点"自动生成" | hap-sign-tool.jar generate-keypair/generate-csr |
| 证书 | (自动) | cert/list → cert/add |
| profile | (自动) | provision/add |
| 材料落盘 | 写 ~/.ohos/config | 同命名规则写同名文件 |
| 写配置 | 写 build-profile.json5 | 同结构写(口令为 AesGcmKit 密文) |
换句话说:自动签名 = 用 AGC API + 本地 jar 重放了 DevEco 的这一串点击。
也正因如此,它与 DevEco 共享同一批文件——两边可以互相接管(这点对我们很有用)。
7. 两条路:上云生成 vs 离线复用
把上面的链和"我们已经有一台能装的机器"这个事实放一起看,就有了两条路:
| 路线 A:AGC 云生成(flutter / deveco-cli 走的路) | 路线 B:离线复用本机材料(灵渠 t2h:sign 走的路) | |
|---|---|---|
| 需要华为账号 | 要(OAuth 授权一次) | 不要 |
| 需要联网 | 要(AGC) | 不要 |
| 需要 IDE | 不要 | 不要 |
| 能覆盖"新设备第一次装" | ✅(自动注册 UDID) | ❌(profile 里没有新设备 UDID → 只能上云或在 DevEco 里点一次) |
| 能覆盖"日常重复构建" | ✅ | ✅(更快:只读本地 p7b) |
| 主要风险 | token 失效、云配额、siteId 非大陆账号被拒 | 材料被删/换机器 → .p12+material/ 不在就必挂 |
| 实现量 | 大(OAuth + 6 个 API + ASN.1/AES-GCM + json5 写入) | 小(读 p7b 里的明文 JSON + 比对 UDID) |
结论:两者不是替代关系,是分层关系——
日常:路线 B(离线判定"能不能装") ← 99% 的构建属于这一类
边界:C6 检查不过(换了新设备)→ 路线 A ← 只有这时候才真需要账号
灵渠的 t2h:sign 就是路线 B 的实现(.p7b 是 PKCS#7 包着明文 JSON,直接在字节里配对扫出 JSON 即可,不需要解 ASN.1);它给出的结论就是"现在能不能直装,不能的话下一步动什么"。
8. 复刻要点(给要自己造工具的人)
- 确定性命名是整条链的地基:证书名
auto_debug_<teamId>.cer、provisionNamesha256(...)[:16]、材料文件名{product}_{dir}_{sha256(前向斜杠路径) b64url 去 padding}=.{suffix}。任何一处不一致,就和 DevEco 的材料"对不上号",复用链路断掉。 - 成功判据看
ret.code,不是 HTTP 200;错误信息在ret.msg(可能是对象也可能是 JSON 字符串)。 - 口令不要自己发明:必须与 hvigor 的
AesGcmKit兼容(§3.7b),否则 hvigor 解不开;而"抄配置到别的机器"要连material/一起抄。 .p7b解析别硬啃 ASN.1:里面是明文 JSON,找第一个{做配对扫描即可(注意二进制里0x7D就是},别取"最后一个}")。- UDID 提取别猜文案:
hdc不同版本前缀不同,取最长十六进制串最稳。 hdc失败时退出码仍是 0,成败只能按输出文本判(install bundle successfully之类)。- SSRF 护栏:下载 URL 必须 HTTPS + 非内网;签名材料的下载链是临时的,别持久化。
- 先跑 C0–C10 再联网:把"日常构建"做成纯本地,是这套设计最省事的地方。
- release 是另一条线:real profile 不带
deviceList,需要上架资质/企业证书,不能拿 debug 冒充。
9. 出处与核对
| 事实 | 来源与核对方式 |
|---|---|
自动签名的模块划分(auth / keymanager / material / cloud / device / profile / regenerate / aead / x509)、ensureOhosSignedInfo 在 assembleHap 前调用、无签名配置时才动手、失败回落到 DevEco 指引、79 个单测 | flutter 仓提交 e2793de 的 commit message(原文) |
登录 URL、appid=1009、siteId==1、tempToken→JWT、/api/jwToken/check、token 加密落盘、FLUTTER_OHOS_SIGNING_DIR | 读源码 packages/flutter_tools/lib/src/ohos/signing/auth.dart(常量与 login() 实现) |
AGC 端点、请求头 uid/teamId/oauth2Token、ret.code==0 判据、provision body、deviceList 仅 test、cert/delete 要 body、auto_debug_<teamId>.cer、provisionName 规则 | 读 …/signing/cloud.dart、…/signing/profile.dart |
| 材料命名规则、四个默认参数、随机口令、JDK/JBR 兜底、hap-sign-tool 参数 | 读 …/signing/material.dart(projectPathHash / signingFileBasePath / generate-keypair 调用) |
material/ 的 KDF 与 AES-128-GCM 密文布局 | 读 …/signing/keymanager.dart(文件头注释逐行给出推导;注明对齐 hvigor-ohos-plugin decipher-util.js) |
| C0–C10 检查清单 | 读 …/signing/regenerate.dart |
.p7b = PKCS#7 DER + 内嵌 JSON(走 ASN.1 找首个以 { 开头的 OCTET STRING) | 读 …/signing/profile.dart |
上游 TS 实现确实存在(signature/cert-api.ts、device-manager.ts、generate-profile.ts、download.ts、re-generate-sign.ts、key-manager.ts 等) | git ls-tree 核对 openharmony-sig/deveco-cli(packages/cli/src/signature/) |
文件名 hash 的实际算法(前向斜杠路径的 sha256 → base64url 去 padding → 文件名里再接 =) | 本机实测:sha256("E:/test/rust/habit-tree/src-tauri/gen/ohos") → m7Gxh5QR…,与 ~/.ohos/config/default_ohos_m7Gxh5QR….p7b 完全一致;用反斜杠路径算则不匹配 |
| 9568322 触发条件、“有 signed 文件 ≠ 能装” | 本项目真机实测(docs/进度记录.md 0.17.0) |
未逐一核对(本文不展开断言,仅按源码字面转述):设备类型码全集、certType 全集、AGC 各错误码语义、aclPermissionList 的具体生成规则。
相关文档:实操配方见 skills/t2h-ohos-port/references/signing.md;
本项目的离线判定实现见 src/compiler/sign.cj + t2h:sign(src/main.cj)。
我的开源项目:
cjh:仓颉语言实现的终端 coding agent(Cangjie Harness)——单二进制零依赖、TUI + Web 双驱动、MCP 插件生态,一个文件即一个 agent
更多推荐


所有评论(0)