搞过鸿蒙应用开发的都知道, 即便是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>
appid1009(写死在源码里的 DevEco 系列应用 ID)
code本机随机生成的 32 位十六进制(UUIDv4 去掉连字符)——不是账号验证码,是防伪造回调的随机串
回调服务127.0.0.1:<随机 port> 的一次性 HTTP 服务;Host 白名单只认 127.0.0.1:<port> / localhost:<port>
回调参数含 tempToken、siteId;siteId != '1' 直接拒绝(只支持中国大陆账号)
换 JWTGET 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 端点清单(实现照抄用)

用途MethodPath(基址 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/deletebody {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>
下载签名 URLPOST/api/amis/app-manage/v1/objects/url/reapply传对象 URL,换回临时下载链
登录页 / tokenGEThttps://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 内容非空重建
C3profile 没过期(validity.not-after)重建
C4profile 的 bundle-name == 本工程包名重建
C5profile 的 developer-id(teamId)== 当前账号团队重建
C6本机所有已连设备 UDID 都在 debug-info.device-ids 里重建(换设备必触发)
C7工程 ACL 权限 == profile 的 acls.allowed-acls重建(加权限必触发)
C8profile 里的开发证书指纹 ∈ 本机 .cer 证书链重建
C9该证书未过期重建
C10build-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. 复刻要点(给要自己造工具的人)

  1. 确定性命名是整条链的地基:证书名 auto_debug_<teamId>.cer、provisionName sha256(...)[:16]、材料文件名 {product}_{dir}_{sha256(前向斜杠路径) b64url 去 padding}=.{suffix}。任何一处不一致,就和 DevEco 的材料"对不上号",复用链路断掉。
  2. 成功判据看 ret.code,不是 HTTP 200;错误信息在 ret.msg(可能是对象也可能是 JSON 字符串)。
  3. 口令不要自己发明:必须与 hvigor 的 AesGcmKit 兼容(§3.7b),否则 hvigor 解不开;而"抄配置到别的机器"要连 material/ 一起抄。
  4. .p7b 解析别硬啃 ASN.1:里面是明文 JSON,找第一个 { 做配对扫描即可(注意二进制里 0x7D 就是 },别取"最后一个 }")。
  5. UDID 提取别猜文案:hdc 不同版本前缀不同,取最长十六进制串最稳。
  6. hdc 失败时退出码仍是 0,成败只能按输出文本判(install bundle successfully 之类)。
  7. SSRF 护栏:下载 URL 必须 HTTPS + 非内网;签名材料的下载链是临时的,别持久化。
  8. 先跑 C0–C10 再联网:把"日常构建"做成纯本地,是这套设计最省事的地方。
  9. 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

cj-tauri · 仓颉版 Tauri:用仓颉语言实现的桌面应用混合开发框架

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐