HarmonyOS 7 HAR:三方交付包声明与哈希证据验收门禁【鸿蒙心迹】
一个三方 HAR 在工程里能够被引用,和它具备可以归档、可复核的交付证据,是两件事。前者回答「能不能被编译系统找到」,后者回答「这次交付到底包含了什么」。构建脚本显示成功,并不能自动说明供应商声明的文件全部进入了最终包;文件名相同,也不能保证两次拿到的字节内容相同。
本文设计一个小工具 HarProofDesk,不去重复讨论依赖锁文件冲突、接口导出签名或者三方返回值中的零值问题,而是把验收粒度收窄到交付包内文件清单、文件指纹和声明证据。演示输入为虚构的 geo-utils-1.3.4.har,任务号 HP-1009-07。原始样本声明必须包含 6 个文件,实际发现 5 个,缺少 LICENSE;已存在的 5 个文件哈希均匹配,因而状态是 EVIDENCE_HOLD。这些名称和计数是教学夹具,不代表任何真实三方 SDK 存在许可证缺陷。

一、不要把「包能导入」当作交付完成
做三方依赖集成时,第一反应往往是看编译是否报错。这个动作没问题,但它只能覆盖依赖关系、模块入口和当前使用到的一部分接口。比如一个 HAR 携带了 ArkTS 源码、资源文件、Native 库与说明材料,应用只使用其中一个导出函数,即便 README.md 或许可证说明遗漏,也可能暂时不影响程序启动。若把运行时成功作为唯一验收依据,后续升级、分发和审计时就很难回溯第一次拿到的包是什么样。
华为官方 HAR 构建文档明确,HAR 构建模式和产物类型不同,包内可出现源码、资源、配置、字节码或本地二进制等内容;.ohpmignore 也可能影响某些文件是否被打入包内。开发者不能因为源工程目录里存在一个文件,就推断最终交付 HAR 里也有它。本文只对已经解包、已经交付给验收方的文件集合做本地验证,不把源目录扫描当成产物扫描。
另一个边界是「许可证存在」与「许可证义务已经履行」不能画等号。工具只能证明某个名为 LICENSE 的文件是否出现在受检目录、内容散列是否等于预期值,以及声明字段与文件集合是否自洽。是否满足某种开源协议、NOTICE 要求、二进制再分发条件或第三方商标条款,需要法律与合规人员结合实际组件逐项判断。本文不写「扫描通过即可上架」这种无法由代码支持的结论。
二、锁定一份可以重放的输入清单
HarProofDesk 的输入不是一个随机遍历的文件夹,而是两个相互独立的证据源:供应方或项目组固定的 expected-artifact.json,以及解包后实际看到的文件目录。前者记录允许进入验收集的相对路径和 SHA-256 摘要,后者是待验证对象。两者不能在同一次验收中相互覆盖,否则只要程序先扫描现有文件、再把它们写成「预期清单」,任何遗漏都会被自动洗成合法结果。
本例使用包名 @harproof/geo-utils、版本 1.3.4,从手工选定的 geo-utils-1.3.4.har 解包目录读取。六个预期路径为 oh-package.json5、Index.ets、README.md、LICENSE、resources/base/element/string.json 和 libs/arm64-v8a/libgeoarith.so。这是刻意设计的教学产物结构,并不意味着任何 HAR 都必须包含这六种文件。字节码 HAR 的文件布局可能与源码 HAR 不同,真实项目应按产物类型分别定义基线。
主页面 PackageAuditPage 只展示本轮验收的汇总:declared=6、found=5、missing=1、hashPassed=5/5、EVIDENCE_HOLD。详情页 ArtifactDetailPage 则展示遗漏路径和风险解释,避免用户把「五个哈希都匹配」误读成「六项要求均通过」。把比例的分母固定为五个实际存在且有合法预期散列的文件,是为了区分完整性和一致性两种指标:文件没来和文件来了但变了不是同一种失败。
这次采用 10:24 作为演示截图状态栏时间,但它只是视觉素材时间,不代表真实构建于该时刻。项目中真正应记录的时间来自受控 CI 任务或签名的构建证明,不能用一张设计图上的时钟代替。
三、文件名能骗过肉眼,路径规则不能靠肉眼
最先做的不是哈希,而是路径治理。清单如果允许 ../secrets.txt,扫描器就可能沿着相对路径读到待检根目录之外的内容。若解包目录包含指向外部位置的符号链接,单纯 existsSync 也会掩盖文件真实来源。即便源头是内部团队,也值得以不可信输入的方式处理,因为同一脚本可能以后被接入 CI 和供应链网关。
下面第一段 Node.js 代码解决「只读取清单明确列出的普通文件」的问题。它是标准 Node.js 逻辑,不是 HarmonyOS 特有的 API。示例采用已知可信的 JSON 清单;对于 .json5 元数据,本文不借助正则去假装完整解析 JSON5。需要读取任意 JSON5 语法时,应引入经过版本锁定和评估的解析器,或者在上游导出严格 JSON 格式供脚本消费。
import fs from 'node:fs';
import path from 'node:path';
export function safeArtifactFile(root, relativePath) {
if (typeof relativePath !== 'string' || relativePath.length === 0) {
throw new Error('BAD_MANIFEST_PATH');
}
const pieces = relativePath.replaceAll('\\', '/').split('/');
if (pieces.some(part => part === '..' || part === '' || part === '.')) {
throw new Error('PATH_TRAVERSAL');
}
const base = path.resolve(root);
const target = path.resolve(base, ...pieces);
if (!target.startsWith(base + path.sep)) {
throw new Error('OUTSIDE_ARTIFACT_ROOT');
}
const stat = fs.lstatSync(target, { throwIfNoEntry: false });
if (!stat) { return { status: 'MISSING', path: relativePath }; }
if (!stat.isFile() || stat.isSymbolicLink()) {
return { status: 'UNSAFE_TYPE', path: relativePath };
}
return { status: 'REGULAR_FILE', path: relativePath, absolute: target };
}
路径检查把 / 和 \ 归一化后拆段,是为了让同一份清单在不同宿主机上具有一致的语义。真实工程还应设置受检目录只读权限、限制解包后总体积、预防压缩炸弹,并在解包工具本身执行路径穿越检测。上面函数不负责安全解包:必须先通过可信解包流程得到受检目录,再让它检查常规文件。将这两个阶段混成一个函数,容易让读取前防护无从落实。
lstatSync 可识别最终路径是否是符号链接,但对中间目录的符号链接还需要逐级验证。正式验收系统应使用可信且隔离的临时目录、拒绝目录链中的符号链接,并在扫描期间禁止其他进程修改内容,降低检查到读取之间的竞争。本文的简化函数用于说明路径边界,不能宣称覆盖所有文件系统攻击。
四、先判缺件,再算散列,不让分母偷换
哈希检查要处理两个层次:文件在不在,以及文件内容有没有与期望指纹保持一致。如果 LICENSE 缺失,不能给它填写一个空字符串的 SHA-256 后继续统计成功,也不能让扫描器只对已存在的五个文件打勾就自动返回 PASS。这里采用一个 missing 集合和一个 mismatch 集合;任一非空都不允许进入下一轮人工验收。
第二段代码给出核心计算,调用 Node.js 内建 crypto.createHash('sha256')。示例为了便于审阅使用 readFileSync,正式工具对大体积原生库宜换成流式读取,并限制单文件最大值。expected 的散列由独立、可信的基线生成过程记录,不由本次扫描自行覆盖。
import fs from 'node:fs';
import crypto from 'node:crypto';
export function verifyManifest(root, expected, resolveFile) {
const result = { declared: expected.files.length, found: 0,
hashPassed: 0, missing: [], mismatch: [], unsafe: [] };
for (const item of expected.files) {
const file = resolveFile(root, item.path);
if (file.status === 'MISSING') {
result.missing.push(item.path); continue;
}
if (file.status !== 'REGULAR_FILE') {
result.unsafe.push(item.path); continue;
}
result.found += 1;
const actualHash = crypto.createHash('sha256')
.update(fs.readFileSync(file.absolute)).digest('hex');
if (actualHash === item.sha256.toLowerCase()) {
result.hashPassed += 1;
} else {
result.mismatch.push(item.path);
}
}
return result;
}
这里有一个容易造成误解的细节:found 表示在清单里、被判定为普通文件的数量,不表示解包目录全部文件数。额外出现的未知文件需要另一条策略。如果验收目标是「不允许存在清单以外的文件」,还必须遍历整个产物,按路径白名单发现 EXTRA_FILE。本文的六项固定基线演示了必要文件的闭包检查;实际发布级门禁建议同时做反向枚举,否则未经申报的新 .so 文件可能完全逃过验证。
expected.files 也需要在前置阶段排查重复路径和不同大小写仅相差一个字母的路径。在大小写敏感与不敏感文件系统之间移动产物时,重复项可能造成覆盖或报告翻倍。散列本身只代表相同字节序列的证据;它不验证二进制是否安全、Native ABI 是否匹配设备、更不证明依赖项的功能正确性。若供应商发布了同名新版文件,必须修改清单版本并走重新评审,而不是手动把 hash 刷新到最新。

五、为什么这里要把构建描述和包内容分开
oh-package.json5 是模块及依赖的重要描述文件,官方文档支持 name、version、main、dependencies、dynamicDependencies 等字段,说明构建系统怎样解析模块引用。它不是法律合规数据库,也不保证源码根目录与最终 HAR 的每个文件完全同构。这里把它当作产物内部需要核对的一个证据对象,而不是把其中的 license 字段当作唯一足够的许可证证明。
例如清单里的版本写 1.3.4,从包里读取的描述却写 1.3.3,即使所有文件的哈希都来自某份可信清单,也要产生 VERSION_MISMATCH。又比如 main 指向 Index.ets,但最终产物是字节码形态,入口文件的存在性和可用性需要按照该产物对应的构建规则解释,不能机械要求每一种 HAR 都有相同的源码布局。对于包含 Native .so 的包,还应核对 ABI 目录、编译目标与消费者设备覆盖矩阵;这不属于本次六项文件存在性检验的完成范围。
对工程负责人而言,有用的是把报告中的字段拆成来源:declaredFrom 指向签名或审批过的清单,observedFrom 指向受检 HAR 解包目录,buildProfile 表示 debug、release 或字节码产物类型。即使以后更换构建工具,旧报告仍可解释为什么某个文件在那一轮被要求存在。反过来,若只保存一个 checkPassed=true,升级半年后几乎无法追溯判断的前提。
本轮特别选择在已经拿到 HAR 之后执行验收。DevEco Studio 构建是否成功、ohpm install 是否解析依赖、HAP 最终发布包含哪些模块,属于不同阶段。放在最外侧的产物验收最好有自己的编号和可重放的输入,避免某个「本机能跑」状态直接覆盖它。演示任务 HP-1009-07 用于连接日志、详情页和输出 JSON;没有将它误称为系统构建任务 ID。
六、把两种失败做成用户能分辨的结果
EVIDENCE_HOLD 的语义在这里非常具体:六个预期文件中缺少 LICENSE,所以本轮证据不完整,即使其余五个 SHA-256 全部通过,也不能完成内部验收。它不是 AppGallery Connect 审核状态,也不是官方平台错误码。画面上使用醒目提示,是为了避免看惯绿色勾选的开发者忽略一个关键的灰色缺件。
如果同一份 HAR 后续被重新交付,必须新开一次验收记录。不要直接把 missing 从 1 改成 0 来模拟修复完成。一个更可靠的步骤是保留旧任务为 EVIDENCE_HOLD,针对新包生成新的构建散列与清单,然后跑全套相同规则。新轮次只有在文件集合、哈希与描述信息均满足门槛之后,才能得到 READY_FOR_MANUAL_REVIEW。这个名字仍特意保留「人工」二字:文件证据齐备不等于整个软件供应链审查已经结束。
第三段代码解决报告状态决策的问题,同时把遗漏路径保存在机器可读的明细中。示例里的 licenseRequired 来自这次内部验收合同,不代表华为对全部三方 HAR 强制采用完全相同的固定文件名。
export function decideEvidence(result, expected) {
const requiredPaths = new Set(expected.files.map(v => v.path));
const hasLicenseRule = requiredPaths.has('LICENSE');
const licenseMissing = hasLicenseRule && result.missing.includes('LICENSE');
const blocked = result.missing.length > 0 ||
result.mismatch.length > 0 || result.unsafe.length > 0;
return {
taskId: 'HP-1009-07', artifact: 'geo-utils-1.3.4.har',
declared: result.declared, found: result.found,
hashPassed: `${result.hashPassed}/${result.found}`,
issues: [...result.missing, ...result.mismatch, ...result.unsafe],
reason: licenseMissing ? 'LICENSE_NOT_IN_ARCHIVE' : (blocked ? 'ARTIFACT_DIFF' : 'NONE'),
state: blocked ? 'EVIDENCE_HOLD' : 'READY_FOR_MANUAL_REVIEW'
};
}
注意这个函数不能单独用于判定「不存在任何额外文件」,因为其输入 result 并未实现全集枚举。它也不代替软件成分分析和恶意代码扫描。在正式脚本里应增加 checkedAt、主机平台、解包器版本、输入 HAR 全文件 SHA-256、清单签名或审批号,以及报告本身的生成工具版本,便于对照重跑。业务如果对许可证文件名不敏感,还可以支持标准化别名映射,但映射规则必须事先冻结,不要在发现遗漏后现场修改通过条件。
主页面示意图把 HP-1009-07 固定为演示 ID,显示 6 / 5 / 1 三组数量;真正影响状态的是 missing=1 而不是已通过散列的 5/5。这张图不是应用市场或开发工具真实审核截图,它只是说明报告怎样以可读形式呈现。

七、把文件系统边界也放进验收清单
当代码跑通一组正常数据之后,更应该关注输入的恶意组合。第一类是路径穿越与符号链接:清单里出现 ../、绝对路径、非法片段,或者文件夹通过符号链接逃到根目录之外。第二类是竞态:扫描后、真正哈希计算前有其他进程替换文件。第三类是压缩包异常:目录重复、文件体积极大、压缩比异常、解包后产生覆盖。第四类是元数据缺陷:清单有重复路径、摘要不是 64 位十六进制、版本字段缺失、大小写碰撞。
这四类问题不能只靠一个 try/catch 把异常都转换为 MISSING,否则攻击输入和普通交付遗漏会在报告里看起来一样。更合适的报告结构包括 MANIFEST_INVALID、UNSAFE_PATH、HASH_MISMATCH、MISSING_FILE、EXTRA_FILE 等不同原因,让安全团队和供应商明确谁需要处理哪个问题。本文演示只触发 LICENSE_NOT_IN_ARCHIVE 一种主要原因,保留其他状态作为扩展点,不伪称已经完成全覆盖安全审计。
对于 Native 库,需要特别检查目标 ABI 和符号信息。不过本篇没有用工具去解析 ELF、没有加载 .so,所以 libgeoarith.so 的 SHA-256 通过,至多证明它与审批基线是同一组字节,不能证明可在某台 HarmonyOS 设备被正确链接。把二进制一致性报告和 ABI 兼容测试分开保存,能让 CI 失败时更快定位是「拿错包」还是「包正确但平台不兼容」。
有些交付团队习惯把许可证文件直接放在源码仓根目录,而不放进 HAR。是否允许这样做,要看项目约定和实际分发方案。在本轮教学契约中明示要求 LICENSE 存在于被验收的解包产物,因此少它就是拦截;若实际组织约定采用独立证据包,应该设计可以追踪到外部文件散列的正式规则,而不是把工具的缺失检查删掉。证据需要与具体交付版号绑定,不能用任意同名文件替代。
04 图展示这次诊断的具体原因、受检相对路径和已存在文件哈希情况。它比主页面更重要,因为供应商要根据这些字段定位问题。图中文字、计数和日志是固定夹具的演示信息,不对应真实 HAR 出包记录。

八、复核时要让结论可撤回
一个可维护的验收工具需要区分「当前这次扫描的结果」和「机构对这个包作出的最终决策」。扫描结果来自输入与算法,可以重跑;审批决策还可能依据供应商说明、许可义务以及最终的应用发布策略。若把工具的绿色状态自动写成「法律审查通过」,就把程序的能力边界扩张了。这里即便六项全部匹配也只输出 READY_FOR_MANUAL_REVIEW,人工确认以后再由外部系统给出独立记录。
复核也应允许撤回。假设已经验收的包后来发现其预期散列基线本身来源不可信,必须能标记该版报告失效。版本号并不天然保证不变性,同名包可以被覆盖下载,所以应同时归档包整体 SHA-256 和固定存储位置。对每个历史报告保存输入源哈希、规则版本和生成时间,才能在发布版本回溯时理解当时作出的判断。缺少这些材料时,应明确标记「证据不足」,而不是替过去补写一段漂亮结论。
本地模型测试至少包含五组输入:完整清单全部匹配;缺少 LICENSE;同名文件内容变动;清单路径穿越;解包目录额外增加未申报的 .so。如果只测本例的五个已存在文件都能算出散列,实际上没有证明门禁会在真正异常时拒绝通过。运行 Node.js 夹具可以验证业务比较算法,却不能替代 DevEco 构建真实 HAR、跨平台解包以及法律合规复核。
更细一点,日志需要按照「开始扫描→确认输入→校验路径→计算摘要→形成报告」分阶段记录,不能把一条 hashPassed=5/5 放到最醒目位置却隐藏 missing=1。本例状态 EVIDENCE_HOLD、declared=6、found=5、missing=1 与 hashPassed=5/5 应在不同界面保持一致。它们表达的是同一轮事实的不同侧面,并无矛盾:五个找到的文件都符合预期,但第六个文件没有进入交付包。
九、上线前保留哪些工程产物
为了让这套门禁真的服务协作,至少需要原始 HAR、验收基线 JSON、受检包整体散列、机器可读报告、差异明细以及当时采用的规则版本。团队内部可以把这些文件放在不可变的构建归档中,并将报告 ID 关联到实际发布候选。对于每个告警,还应记录处理意见和复验链接,而不是通过修改旧日志来「解决」异常。
它的适用场景包括来自供应商的 HAR、内部共享静态库交付、包含资源文件的 SDK,以及需确认二进制与文档同时交付的发布流水线。它不替代依赖漏洞扫描、签名验签、恶意样本检测、AppGallery 合规审核或真机兼容性测试。这些环节应并行组成证据链,而不是被折叠成一次「一键通过」。
本例要交付的工程判断很朴素:**能导入,是集成问题;能重放清单与散列,是证据问题;能否正式分发,是更大的产品和合规决定。**把这三层分开,团队就不会因为一个构建成功而误判所有后续环节都完成。等到真实 geo-utils 类交付出现时,先定义双方签字确认的清单规则,再让自动化程序执行,不要反过来让程序临时定义验收条件。
官方资料与验证边界
- 华为开发者文档《构建HAR》(2025-02-08):https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-v5/ide-hvigor-build-har-V5
- 华为开发者文档《oh-package.json5》(2026年可访问版本):https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-hmos-oh-package-json5
- 华为开发者文档《HAR转HSP指导》(2026-06-12):https://developer.huawei.com/consumer/cn/doc/doccenter-getting-started/har-to-hsp
验证声明:本文代码基于 Node.js 内建文件与散列 API,说明如何验收模拟的解包产物;图像、审计 ID、包名、校验计数均为教学夹具。尚未将代码作为实际 DevEco/HAR 构建脚本跑通,也没有对真实供应商或应用市场作出法律合规结论。
更多推荐



所有评论(0)