HarmonyOS 7 Native Bundle:bundleName签名指纹三向自证
提审前检查文件名、版本号和截图很常见,却仍然可能把错误证书签出的包交到错误应用记录。文件叫什么是外部标签,真正安装后的身份至少还包括 bundleName、签名指纹以及由包名和签名信息共同决定的 appId。三者只看一个,都可能把“像正确包”误判成“就是正确包”。
本文把这项检查做成一个候选包内的只读诊断页。Demo 名为 ReleaseProofDesk,页面为 IdentityProofPage,候选任务号 REL-1536-944。演示包名是 com.example.releaseproof,指纹前缀为 A7:4C:91:2E:6B:38,证据清单完成 17/25,进度 68%,核心身份状态为 IDENTITY_MATCHED。这些字段用于解释方案,不代表真实上架、真实签名或审核结果。

一、结果反推:为什么一个包名不够
假设页面读到的 bundleName 与预期一致,最多只能说明安装包声明了同一个名称。若构建机换了签名材料,同名包的身份仍然可能不对。反过来,只保存一段指纹截图也不够:截图容易脱离候选任务,不知道它对应哪个 bundle,更不能说明运行中的安装实例就是那份产物。
Native_Bundle 模块提供当前应用包信息查询。官方模块说明包含应用包名、应用指纹和 appId;OH_NativeBundle_GetCurrentApplicationInfo() 返回当前应用信息,OH_NativeBundle_GetAppId() 返回 appId。官方对 appId 的描述尤其关键:它由应用包名和签名信息决定。也就是说,appId 不是 AppGallery Connect 页面上另一个随手抄来的编号,而是这里用于识别当前安装实例的系统侧标识。
Demo 把证据分成三个层次。第一层是“读到了什么”,记录原始 bundleName、完整指纹和完整 appId。第二层是“如何比较”,对指纹只做大小写和分隔符归一,不截断后再比较;bundleName 必须精确匹配;appId 必须与受控基线一致。第三层是“如何展示”,页面只显示指纹与 appId 的安全前缀,完整值仅进入本地诊断导出,而且导出必须由用户主动触发。
状态不是一个布尔值,而是 IDLE -> READING_NATIVE -> COMPARING -> IDENTITY_MATCHED,任何字段为空或不一致都进入 IDENTITY_REJECTED。这样日志能区分“接口读取失败”和“读取成功但不匹配”。前者是诊断链路问题,后者才是候选包身份问题。
二、先做一个窄而清楚的 Native 接口
Native 层只负责读取系统给出的身份,不负责决定是否通过。下面代码解决的具体问题是:一次读取 bundleName、fingerprint 和 appId,并把它们封装成普通对象交给 ArkTS。示例省略通用 N-API 错误宏,但保留了空指针和 appId 释放路径。
#include <cstdlib>
#include <string>
#include "napi/native_api.h"
#include "bundle/native_interface_bundle.h"
static void SetText(napi_env env, napi_value object,
const char* key, const char* value)
{
napi_value text = nullptr;
napi_create_string_utf8(env, value == nullptr ? "" : value,
NAPI_AUTO_LENGTH, &text);
napi_set_named_property(env, object, key, text);
}
static napi_value ReadIdentity(napi_env env, napi_callback_info info)
{
OH_NativeBundle_ApplicationInfo app =
OH_NativeBundle_GetCurrentApplicationInfo();
char* appId = OH_NativeBundle_GetAppId();
napi_value result = nullptr;
napi_create_object(env, &result);
SetText(env, result, "bundleName", app.bundleName);
SetText(env, result, "fingerprint", app.fingerprint);
SetText(env, result, "appId", appId);
if (appId != nullptr) {
free(appId);
appId = nullptr;
}
return result;
}
为什么不在 C++ 里直接与写死的常量比较?因为 Native 层越靠近接口,职责越应该单一。基线来自发布流程,可能按渠道、构建环境或应用记录变化;比较规则也可能增加。把规则留在 ArkTS 侧,更容易在一个页面里显示字段级结果,也更容易写单元测试。
内存处理必须和接口语义成对。官方说明要求 OH_NativeBundle_GetAppId() 返回的指针使用后手动释放,因此示例在对象属性创建完成后立即 free,并置空本地变量。不能把该指针保存到异步回调,不能释放后继续访问。OH_NativeBundle_GetCurrentApplicationInfo() 的结构字段按官方示例同步读取,不在本文凭空引入不存在的释放函数。
真实工程应为每个 N-API 调用检查 napi_status。为了让正文聚焦身份链路,示例没有展开统一错误宏;这不是建议忽略返回值。生产实现可在失败时抛出带稳定错误码的异常,例如 IDENTITY_NAPI_CREATE_FAILED,但不要把完整指纹拼进异常文本。
三、模块注册只注册一个名字
读取函数还需要导出给 ArkTS。官方 N-API 规范强调:一个 so 文件不要注册多个不同模块,nm_modname 要与模块名完全匹配,否则多线程加载时可能匹配错误。下面代码把 readIdentity 作为 releaseproof 模块的唯一导出。
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
napi_property_descriptor desc[] = {
{ "readIdentity", nullptr, ReadIdentity, nullptr, nullptr,
nullptr, napi_default, nullptr }
};
napi_define_properties(env, exports, 1, desc);
return exports;
}
EXTERN_C_END
static napi_module releaseProofModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "releaseproof",
.nm_priv = nullptr,
.reserved = { 0 },
};
extern "C" __attribute__((constructor))
void RegisterReleaseProofModule()
{
napi_module_register(&releaseProofModule);
}
这里最常见的错不是语法,而是名称漂移。CMake 产物叫 libreleaseproof.so,模块却仍复制模板里的 entry;ArkTS 声明文件又写成另一个名字。三处没有统一时,页面可能在开发构建里偶然可用,在并发加载或重构后失败。项目把模块名定义在发布核对表里,并在代码评审时同时查看 CMake、注册结构与 index.d.ts。
另一个边界是不要从 N-API 初始化函数里发起复杂业务。初始化只挂导出函数;真正读取在用户打开诊断页后同步执行一次。身份字段来自当前安装实例,读取开销有限,但页面仍应避免每次重组都调用 Native 层。Demo 用页面代次 944 缓存一次结果,刷新按钮会生成新代次并重新读取。
下面的 DevEco Studio 风格图展示了 cpp 目录、导出函数、右侧模拟器和底部 HiLog 的对应关系。它是根据本文数据生成的说明图,不冒充真实工程截图。图中红圈只标出 appId 释放与 IDENTITY_MATCHED 日志,避免把整张 IDE 变成标注海报。

四、比较规则必须比展示规则更严格
ArkTS 侧先声明窄接口,再读取受控基线。下面代码解决“页面只显示前缀,于是开发者误把前缀相等当成完整身份相等”的问题。比较始终使用完整值;脱敏只发生在生成视图模型时。
import releaseproof from 'libreleaseproof.so';
interface NativeIdentity {
bundleName: string;
fingerprint: string;
appId: string;
}
interface IdentityBaseline {
bundleName: string;
fingerprint: string;
appId: string;
}
function normalizeFingerprint(value: string): string {
return value.replaceAll(':', '').replaceAll('-', '').trim().toUpperCase();
}
@Component
struct IdentityProofPage {
@State state: string = 'IDLE';
@State progress: number = 0;
@State bundleText: string = '';
@State fingerprintText: string = '';
@State appIdText: string = '';
private generation: number = 944;
verify(baseline: IdentityBaseline): void {
this.state = 'READING_NATIVE';
const actual = releaseproof.readIdentity() as NativeIdentity;
this.state = 'COMPARING';
const matched = actual.bundleName === baseline.bundleName &&
normalizeFingerprint(actual.fingerprint) ===
normalizeFingerprint(baseline.fingerprint) &&
actual.appId === baseline.appId;
this.bundleText = actual.bundleName;
this.fingerprintText = actual.fingerprint.slice(0, 17);
this.appIdText = actual.appId.slice(0, 32);
this.progress = Math.round(17 * 100 / 25);
this.state = matched ? 'IDENTITY_MATCHED' : 'IDENTITY_REJECTED';
console.info(`[ReleaseProof] task=REL-1536-944 gen=${this.generation} ` +
`state=${this.state} progress=${this.progress}%`);
}
}
指纹归一只处理表现差异:冒号、连字符、空白和大小写。它不能删除任意字符,也不能只比较前六段。appId 不做模糊匹配,因为它本来就是由包名和签名信息决定的身份标识。若环境确实存在多套合法签名,应由发布系统明确提供允许列表,并给每条基线标注用途和有效期,不能在客户端用“任意一个前缀相同就通过”。
基线也不能由待检查页面自行生成。如果应用读取当前值,再立刻把当前值当预期值,检查永远成功。Demo 假定基线由构建流水线在产物冻结前注入,并与任务 REL-1536-944 绑定。更严格的团队可以让外部验收工具下发一次性挑战或对基线签名;本文只讨论包内诊断页的最小闭环,不把它夸成防篡改系统。
运行图显示候选包身份摘要:bundleName 完整显示,指纹和 appId 只显示安全前缀,三项比较均通过,清单进度为 68%。顶部时间为 15:36,状态栏包含 Wi‑Fi、5G、信号和 81% 电量。所有字段都与正文共享同一份演示口径。

五、失败页要告诉人“哪里不同”,不要泄露完整秘密
身份失败不能只给一个红色叉号。运维最需要知道的是:Native 读取是否成功、哪一项不一致、基线属于哪个任务、结果来自哪一代页面。Demo 的诊断页显示字段级布尔值和脱敏前缀,不显示完整 appId,不自动上传,也不把指纹写入普通崩溃日志。
建议把错误拆成四类。NATIVE_READ_FAILED 表示接口或 N-API 调用失败;BUNDLE_MISMATCH 表示包名不一致;FINGERPRINT_MISMATCH 表示签名身份不一致;APPID_MISMATCH 表示组合身份不一致。后两项同时出现时,不要急着判定系统异常,先核对是否使用了错误证书或错误应用记录。
页面离开时不需要释放已经复制成 ArkTS 字符串的数据,但要让异步导出任务失效。若用户点击“导出诊断”后立即返回,文件写入完成回调不能再修改销毁页面。做法与普通异步 UI 一样:记录页面代次或 alive 标志,完成时校验;Native 返回的 appId 则早已在同步桥接函数里释放,二者生命周期不能混为一谈。
诊断图与运行图明显不同。它显示原始读取阶段、三项比较、脱敏策略和资源释放事件。红色细箭头指向 free(appId) 对应的 NATIVE_BUFFER_RELEASED,另一个红圈标出 gen=944,证明当前结果没有被旧页面代次覆盖。图中的 17/25 与 68% 仍保持一致。

六、把自证插进发布流程,而不是只留在开发菜单
这张诊断页最适合出现在“候选包冻结之后、正式提交之前”。先安装候选包,打开只读身份页,记录任务号与三项结果,再由外部流程保存截图或导出摘要。之后若重新签名、重新打包或修改 bundleName,就必须生成新任务并重新取证,不能沿用旧截图。
证据清单的 25 项不必全是 API 字段。可以包括产物 SHA-256、构建号、调试标记、隐私声明版本、权限清单、测试账号可用性和关键跳转。本文只负责其中 17 项完成后的身份页,因此显示 68%。IDENTITY_MATCHED 代表三项核心身份比较通过,不代表其余八项自动通过,更不等于审核一定通过。
包内自证也有明确边界。它不能替代发布平台的签名校验,不能证明安装文件在传输链路上未被替换,也不能证明审核政策已经满足。它的价值是把“我以为装的是那一包”变成可核对的当前安装实例证据,尤其适合发现同包名错签、测试证书残留和候选任务串包。
测试时至少准备四个样本:三项全匹配的候选包;bundleName 错误的邻近应用;bundleName 相同但签名不同的内部包;Native 接口读取被故障注入打断的包。只有全通过样本进入 IDENTITY_MATCHED,其他样本要落到可解释的拒绝原因。不要只测成功路径,因为这个工具存在的理由就是抓错包。
七、三个不该省掉的收尾动作
第一,完整值的存储和传播范围要最小化。页面默认只展示前缀,普通日志只写布尔结果和任务号。若必须导出完整指纹与 appId,应加用户确认、明确保存位置和清理策略。身份信息不是口令,但也不该无节制散落。
第二,N-API 资源必须在最窄作用域内收口。OH_NativeBundle_GetAppId() 的返回指针在复制进 JS 字符串后立即释放;模块只注册一次;模块名与 so 保持一致。这些看似基础,却决定诊断工具会不会自己制造泄漏和加载问题。
第三,把演示和验证分开陈述。本文的 REL-1536-944、A7:4C:91:2E:6B:38、17/25 与 68% 是统一设计数据,用于说明页面、代码、日志和图片如何对账。正式提审前,必须在真实发布证书签出的候选包上重新读取真实值,并由团队自己的发布流程判定。
当包名、指纹和 appId 被绑到同一任务号里,候选包身份就不再靠文件名和记忆。它仍然不是完整的供应链证明,却足以堵住一类高频而低级的发布错误:拿着正确的文案、正确的截图,提交了身份不正确的包。
参考资料:
更多推荐




所有评论(0)