云计算百科
云计算领域专业知识百科平台

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

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

三、六步链路

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

步骤动作关键观察点
1 临时移除构建配置里的签名引用 只删引用,不写口令
2 Release 模式产出未签名 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,342 12F8AEC7…328FD4 版本仍为旧值,未上传
版本对齐 + 权限裁剪 125,321,260 F0CB6F46…42DB 上传后合法性「不通过」
崩溃修复后重签 125,321,048 91315EEB…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 机型,也在台账里如实标成覆盖不全。

十一、收个尾

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

赞(0)
未经允许不得转载:网硕互联帮助中心 » 〖共创稿事节〗HarmonyOS 7 新特性实战(51):发布包可追溯——离线签名、双层验签与换包台账
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!