# 〖共创稿事节〗HarmonyOS 7 新特性实战(51):发布包可追溯——离线签名、双层验签与换包台账

上架流程里最容易被忽略的一段,是从"构建成功"到"审核员手里的那个包"之间发生了什么。构建日志一片绿,不代表上传的那个文件就是你以为的那个文件;签名配置写着发布证书,也不代表包内每一个 HAP 都被签过。

这个工程有一条额外的约束:发布证书的私钥口令不允许以明文形式写进仓库里的任何文件。DevEco 的自动签名会把口令落在构建配置里,这条路直接断了。于是整条链路改成离线执行,每一步都留下可复算的输出。

从构建产物到审核包:四道工序与两次验签

一、一个发布包要回答三个问题

审核员不需要读构建脚本,但他手里的包必须能回答三件事:

问题靠什么回答失败时的表现
这个包是谁签的叶证书指纹与证书链叶证书是调试证书,装上就被判非法
包在传输中没被改过摘要校验(SHA-256 / SHA-384)Digest verify result: false
内层 HAP 也被签过解包后单独验签只签外层,内层 HAP 无签名块

三件事对应三次不同的检查,任何一次缺失,"可追溯"就不成立。

二、材料先备齐,再谈流程

发布证书不是本地随便生成的,它必须和 AGC 上的 Profile 绑定。这套材料一共五件,缺一件后面都会卡住:

材料内容字节数
发布证书AGC 证书管理新增,有效期至 2029-10-312,943
发布 Profile绑定应用 ID 与上述证书,分发类型 app_gallery4,057
私钥库本地生成的 EC P-384(secp384r1),别名 shanhaiRelease1,188
证书请求生成证书时提交给 AGC 的 CSR572
口令密文20 位随机口令,仅以当前 Windows 用户的 DPAPI 密文保存529

口令这一件最关键:它只以密文存在,构建配置里没有它,代码仓库里也没有它。签名时在进程内解密,用完即弃。代价是换机器就解不开,私钥库、证书、Profile 与口令密文必须一起备份。

手动签名的材料准备与 ACL 配置,官方说明见 应用/元服务签名。这套流程和自动签名的差别,主要在后半段:证书、Profile、口令都由自己持有,因此每一步都要自己留下证据。

三、六步链路

常规签名配置不可用,就把签名从构建里摘出来,拆成六步:

步骤动作关键观察点
1临时移除构建配置里的签名引用只删引用,不写口令
2Release 模式产出未签名 HAP 与 APP日志出现 No signingConfig found for product default
3用发布证书签 HAP日志出现 Generate signing block success
4复刻 APP 结构(HAP + 清单 + 描述)目录结构与官方打包一致
5用发布证书签 APP外层获得签名块
6对 APP 与包内 HAP 分别验签两次都要 Verify success

第 2 步的日志是这条链路的地基:看到 No signingConfig found,才能确认产物真的是未签名的。如果这一步跳过,后面签出来的东西可能"叠签",验签能过但叶证书不一定是你以为的那张。

四、签名与验签的实际命令

先签 HAP。注意这里没有 -compatibleVersion,原因写在第六节:

java -jar hap-sign-tool.jar sign-app -mode localSign `
  -keyAlias shanhaiRelease -signAlg SHA256withECDSA `
  -appCertFile release.cer -profileFile release.p7b `
  -inFile entry-default.hap -outFile entry-default-signed.hap `
  -keystoreFile release.p12 -keyPwd $pw -keystorePwd $pw

再复刻 APP 结构。官方打包工具在超过百兆的 HAP 上会报压缩失败,换成系统自带的压缩命令,目录结构保持一致:

$stage = 'app-stage'
New-Item -ItemType Directory -Path $stage -Force | Out-Null
Copy-Item entry-default-signed.hap $stage
Copy-Item pack.info $stage
Copy-Item pac.json $stage
Compress-Archive -Path "$stage\*" -DestinationPath release-unsigned.app -Force

然后签 APP。sign-app 只处理输入包本身,不会递归处理内层 HAP,所以顺序不能反过来:

java -jar hap-sign-tool.jar sign-app -mode localSign `
  -keyAlias shanhaiRelease -signAlg SHA256withECDSA `
  -appCertFile release.cer -profileFile release.p7b `
  -inFile release-unsigned.app -outFile app-harmony-release.app `
  -keystoreFile release.p12 -keyPwd $pw -keystorePwd $pw

最后两层各验一次:

java -jar hap-sign-tool.jar verify-app -inFile app-harmony-release.app `
  -outCertChain chain.cer -outProfile prof.p7b
keytool -printcert -file chain.cer

验签的输出不是一句"成功"就够的。APP 层的完整输出是:

Digest verify result: true, DigestAlgorithm: SHA-256
verify: Verify success
verify-app success

解包后的 HAP 单独验签,输出里必须出现签名块与权限签名两段:

Find Hap Signing Block success, version: 3, block count: 3
verify codesign success
verify permission sign success.
verify: Verify success

五、证书链与「签名隔离」

验签通过只说明签名结构正确,还要看签的到底是谁。证书链打印出来是三段:

层级指纹(节选)
根证书DF:21:A3:C0:9F:79:54:57:93:05:F8:5C:64:F8:0C:AD:...
中间证书0D:AA:8A:96:5B:51:53:12:4F:79:D4:B4:23:78:D0:AC:...
叶证书(发布)C2:1F:18:0F:AB:4C:79:06:75:24:47:4C:F0:29:CB:D0:...

关键不是记住这串指纹,而是拿它和调试包对照。同一个工程用调试签名打出来的包,叶证书是 DD:3A:F9:55:...:44:7F,和上面这张完全不同。两张证书并存,才证明"签的确实是发布证书",而不是构建流程悄悄退回了调试签名。

六、五个坑,每个都沉默

这条链路里最危险的不是报错,而是不报错。下面五个坑里,前两个都是日志写着成功、产物却是错的:

现象真正的原因处置
HAP 验签报 signature not found,但签名日志写 Sign Hap success传了兼容版本参数,签名块被静默跳过去掉该参数重签,日志出现 Generate signing block success
打包工具报 --pack-info-path is empty参数名不是想象的那个用 --pack-info-path 传清单
打包工具报 --out-path must end with .app输出参数名同样不是想象的那个用 --out-path 传目标文件名
大 HAP 报 Compress failed,加内存参数仍失败压缩路径对超大文件不稳定换 Compress-Archive 复刻同结构
签名报 11014003 Init keystore failed口令经 stdin 管道传入,行尾换行进了密码用 -keyPwd / -keystorePwd 直接传参

第一条最值得记。签名工具会打印 Sign Hap success,但只有验签才会告诉你签名块根本没写进去。签名成功和验签通过是两件事,不能互相替代。

第二条和第三条属于同一类:工具的参数名和文档示例经常不一致,报错信息里其实已经把正确名字写出来了,照着报错改比查文档快。

第五条是环境问题伪装成密码错误。口令本身没错,错的是传输方式——管道会在末尾补一个换行,而密钥库校验的是逐字节相等的口令。

七、换包台账:用数字记住每个包

版本对齐之后,同一份代码先后产出过三个包。只靠"最新那个"是记不住的,必须建台账:

阶段包体字节数SHA256(节选)说明
首次发布签名125,321,34212F8AEC7...328FD4版本仍为旧值,未上传
版本对齐 + 权限裁剪125,321,260F0CB6F46...42DB上传后合法性「不通过」
崩溃修复后重签125,321,04891315EEB...82B1上传后合法性「已达标」

三个包的体积差只有几百字节,肉眼完全分不出来,哈希是唯一可靠的区分方式。台账记录的字段不止哈希:还包括签名算法、叶证书指纹、Profile 里的版本号与分发类型、上传时间、AGC 的合法性结论。

三份产物的字节数与哈希台账

bundle-name = com.shanhai.spirits
type = release
app-distribution-type = app_gallery
version-name = 2.0.0
version-code = 2
validity = 2026-10-31 ~ 2029-10-31
issuer = app_gallery

Profile 的版本号必须和包内版本号一致。这个工程第一版上传时就栽在这里:Profile 写 2.0.0 (2),包内还是 1.0.0 (1000000)。处置是把应用级配置的两个版本字段改成 2.0.0 与 2,重新走一遍六步链路,而不是只改 Profile。

八、权限裁剪:一次被动的减法

版本对齐后第一次上传,合法性检测 15 项里唯一失败的是「ACL 权限声明一致性」:Profile 里没有 FLOAT_VIEW 与 USE_FLOAT_BALL,而模块配置里声明了它们。

Profile 的新建表单没有 ACL 勾选,受限权限要走单独的 ACL 申请,需要企业资质与演示材料。这一版的选择是从模块配置里删掉这两条声明,让包与 Profile 对齐:

// 详情页的闪控入口改为三段探测:
// 1. 查询系统是否支持该能力
// 2. 查询应用是否持有权限
// 3. 两者任一不满足,走普通悬浮卡片的分支
const granted: boolean = await canIUse('SystemCapability.Window.FloatingBall');
if (!granted) {
  this.floatingEnabled = false;
  this.floatingHint = '当前设备不支持悬浮球,已切换为页内卡片';
}

代价是闪控球与闪控窗两个实验入口在这一版里只能走降级分支。这是产品层面的取舍,不是签名问题,但必须写进台账:同一个版本号下,权限集合变了,包也就变了。

九、崩溃修复:一个 API 26 属性值一次重建

第三个包的产生原因是一次上架自检崩溃。报告给的栈是 undefined is not callable,落在底部导航壳的构建函数里。根因是代码无条件调用了 Tabs.barFloatingStyle——这个接口从 API 26 才有,而工程的可安装版本是 6.0.0(20)。

原来的护栏只挡住了传参,没有挡住调用:

// 错误写法:属性传 undefined,方法本身在低版本不存在
Tabs({ barPosition: BarPosition.End }) {
  ForEach(this.tabs, (item: TabItem) => {
    TabContent() { /* ... */ }
  })
}
.barFloatingStyle(this.useNativeFloatingBar() ? { /* 参数 */ } : undefined)

改成两条构建分支,低版本分支完全不下发这个属性:

if (this.useNativeFloatingBar()) {
  Tabs({ barPosition: BarPosition.End }) {
    ForEach(this.tabs, (item: TabItem) => {
      TabContent() { /* ... */ }
    })
  }
  .barFloatingStyle({ /* API 26 参数 */ })
  .barHeight(0)
} else {
  Tabs({ barPosition: BarPosition.End }) {
    ForEach(this.tabs, (item: TabItem) => {
      TabContent() { /* ... */ }
    })
  }
  .barHeight(0)
}

差别只有一行 .barFloatingStyle,但它决定了低版本设备是"启动成功"还是"冷启动即崩"。改完按第四节的四道工序重建:未签名 HAP 139,524,467 字节 → 签后 139,654,209 字节 → 打包 125,306,309 字节 → 签 APP 125,321,048 字节,两层验签都过。

编译产物也要核对一遍:构建出来的中间代码里,barFloatingStyle 只出现在 if 分支内,else 分支没有这个调用。只改源码不看产物,没法确认修复真的进了包。

十、自检报告与提交

新包上传后,AGC 的合法性检测直接通过,上架自检在 5 台设备上跑了 117 个用例:

维度结论
兼容性核心功能通过率 100%,「启动」项通过
稳定性未捕获崩溃
功耗 / 性能良好
UX 导航栏不通过 ×9(折叠屏展开态)
UX 状态栏不通过 ×1
警告基础布局 ×1、滑动动效 ×1、转场动效 ×1

总评依然是「不通过」,但失败项已经从"启动崩溃"收敛成沉浸适配建议。这两项不影响本次内容与功能审核,留到 2.0.1 处理;报告里缺平板与 2in1 机型,也在台账里如实标成覆盖不全。

十一、收个尾

可追溯不是一句形容词,而是四份东西:证书链能证明谁签的,双层验签能证明包没被动过,哈希台账能证明换的是哪一个包,自检报告能证明这个包跑得起来。 少任何一份,"最新那个包"就只是一个文件名。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐