用户在确认窗口里看到“签署展品借阅凭证”,服务端却收到另一份内容的签名,这种问题无法通过给按钮加锁图标解决。可信确认最核心的要求,是用户看见、明确同意和最终验证的内容保持一致。

数字盾相关能力强调可信界面、输入与签名保护。实验使用一份无实际法律效力的测试凭证。公开能力介绍不等于所有应用已经具备调用资格;在具体接口与接入资质确认前,不用普通弹窗或旧版密钥接口冒充数字盾调用。

先确定签名对象的唯一表达

同一业务内容可能有多种 JSON 表达:字段顺序不同、空字段省略、日期格式不同,都可能产生不同字节。客户端与服务端应选择一致的规范化规则,对最终字节计算摘要。

凭证编号、展品编号、借阅期限、动作类型和挑战值都需要纳入保护范围。界面展示的友好名称可以保留,但不能只签名称而忽略唯一编号。以下为应用自己的待确认结构。

interface TestReceipt {
  schemaVersion: number;
  receiptId: string;
  exhibitId: string;
  action: 'acknowledge-test-receipt';
  validUntil: string;
  challenge: string;
}

序列化算法应有固定测试向量。遇到未支持的版本直接拒绝,不在接收端默默补默认值后继续验证。测试材料明确标注模拟用途,避免让体验者误以为正在签署真实凭证。

可信界面承担什么职责

应用普通页面负责说明业务上下文,可信确认环节负责呈现实际待确认的重要信息并获取用户操作。两者之间不能隐藏关键差异,例如普通页面说“查看”,最终动作却是“授权”。

当系统可信界面对文本长度、字段类型或交互方式有约束时,应按文档映射业务信息。不能通过截断文本隐藏关键内容,也不能把所有信息塞进无法辨识的摘要后声称用户已理解。

需要系统证明的内容必须来自真实能力返回。普通 ArkUI 对话框只能验证业务流程和取消行为,不能证明可信输入、受保护显示或安全硬件参与了签名。

用户取消是正常结果

确认流程至少区分等待、成功、取消、失败和过期。取消后不自动再次拉起;失败重试时重新检查挑战是否仍然有效。应用切到后台后收到回调,也不能直接改变已经关闭的业务页面。

每次任务绑定唯一 requestId。旧任务完成时,只有当前业务仍等待同一个请求,才能消费结果。签名材料保存时间应尽可能短,日志里不记录可复用的完整结果。

如果用户在确认期间编辑了凭证内容,应使当前确认失效。不能把旧摘要对应的签名挂到新版本凭证上。服务端以签名覆盖的版本作为唯一依据。

服务端验证不能省略上下文

验证端首先按官方要求检查可信来源与签名,再核对凭证内容、挑战、动作和期限。密钥可信不等于任意业务都被授权;另一个场景产生的签名不能直接用于当前操作。

挑战的消费与凭证状态变更需要一致性保护。若网络在成功后中断,客户端重试应得到同一业务结果,不能创建第二份凭证。这里的幂等键属于业务协议,不能拿设备标识替代。

安全硬件保护私钥与业务语义正确是两层问题。即使密钥无法导出,错误的字段映射仍然可能让用户确认错误内容,因此字段一致性测试必须独立存在。

验收用例围绕内容变化展开

先签署固定测试凭证,再逐项修改展品编号、动作、期限和挑战,确认旧签名无法被接受。随后覆盖取消、超时、重复提交、旧页面回调和设备能力不可用。

对可信界面的验收需要支持设备和官方接入链路。录屏可以辅助说明用户看见什么,但不能单独证明安全能力执行成功;还需要脱敏的请求关联、验证结果及设备环境。

在接口未确认阶段,Demo 可以完成规范化测试、业务状态机和验证端契约。它们应被标为流程原型。取得实际能力后,再补系统调用和真实验签,才能把文章升级为完整接入实战。

让确认内容变化成为独立测试向量

修改内容需要验证的结果
只改变无语义的序列化字段顺序按选定规范化规则得到一致字节
改变展品或动作得到不同待签内容,旧结果不可复用
新挑战替换旧挑战旧确认不完成新请求

直接签任意 JSON 实现较短,但双方规范不一致时难以定位失败;固定版本的规范化契约增加前置工作,能把签名争议落到具体字节。先用固定向量核对规范化,再实现取消与幂等,最后接可信界面和正式验证。普通弹窗仅承担前两个阶段的流程调试。

参考:华为开发者能力介绍。数字盾具体版本、开放范围及可信界面接口需另行核实;旧有密钥存储或普通签名接口不能代替数字盾能力。

可信文本、挑战与认证令牌的归属

可信确认前先检查文本排版。结果非零时停止调用,保留不能展示的位置。确认入口需要已有 authID、合法图片和 32 字节挑战;挑战来自 HUKS 会话初始化,不自行生成一个随机数替代。

PIN 认证路径把相同文本放入 authContent,避免展示内容与认证内容分叉。用户取消和 SDK 错误向调用方传播,不返回成功令牌。页面只显示令牌字节数,不把完整令牌输出到日志。

export class TrustedProbe {
  async checkText(text: string): Promise<trustedAuthentication.TextCheckResult> {
    return trustedAuthentication.checkConfirmUITextFormat(InputRules.required(text, 'confirmation', 2048));
  }
  async confirm(challengeHex: string, authId: string, text: string,
    image: ArrayBuffer): Promise<trustedAuthentication.AuthToken> {
    const challenge: Uint8Array = InputRules.hex(challengeHex);
    if (!/^[1-9][0-9]*$/.test(authId)) { throw new Error('valid enrolled authID required'); }
    InputRules.required(text, 'confirmation', 2048);
    if (image.byteLength === 0) { throw new Error('TUI label image required'); }
    const checked: trustedAuthentication.TextCheckResult = await this.checkText(text);
    if (checked.result !== 0) { throw new Error('TUI format rejected at ' + checked.lastIndex); }
    return trustedAuthentication.procContentAuthentication(challenge, BigInt(authId),
      { reqType: trustedAuthentication.AuthType.AUTH_TYPE_TUI_PIN, authContent: [text] },
      { title: '图鉴测试确认', image: image });
  }
}

辅助校验与边界处理使用以下实现:

export class InputRules {
  static required(value: string, name: string, max: number = 256): string {
    if (typeof value !== 'string' || value.trim().length === 0 || value.length > max) {
      throw new Error(name + ' is missing or too long');
    }
    return value.trim();
  }
  static hex(value: string): Uint8Array {
    if (!/^[0-9a-fA-F]{64}$/.test(value)) { throw new Error('challenge must be 32 bytes of hex'); }
    const bytes: Uint8Array = new Uint8Array(32);
    for (let i: number = 0; i < bytes.length; i++) { bytes[i] = parseInt(value.substring(i * 2, i * 2 + 2), 16); }
    return bytes;
  }
}

用排版失败和认证调用次数核对源码

数字盾登记、HUKS 会话和合法 TUI 图片,令牌用于后续 HUKS 操作的完整流程。

检查层当前处理不能替代的结果
编译API 26 工程进行类型检查与打包目标设备支持
逻辑校验输入、状态或资源释放边界平台服务真实返回
联调按上面的输入和条件逐步执行不能以按钮文案代替核心结果

可信认证与 HUKS 会话的版本约束,以 HarmonyOS 官方文档中心和当前 SDK 声明为准。

排版不合格时不会进入认证

可信文本排版检查失败,认证接口不得继续执行;通过断言认证调用次数可检查这一前置条件。合法挑战仍须来自实际 HUKS 会话。宿主测试直接载入 TrustedProbe,仅以替身代替 trustedAuthentication;load 与 test 属于测试框架。

test('trusted: rejected format never opens authentication', async () => {
    let called = 0;
    const C = load('services/TrustedProbe', {
        '@kit.DeviceSecurityKit': {
            trustedAuthentication: {
                checkConfirmUITextFormat: async () => ({
                    result: 1019100011, lastIndex: 0
                }), procContentAuthentication: async () => called++, AuthType: {
                    AUTH_TYPE_TUI_PIN: 32
                }
            }
        }
    }).TrustedProbe;
    await assert.rejects(new C().confirm('ab'.repeat(32), '1', 'text', new ArrayBuffer(8)), /format/);
    assert.equal(called, 0);
});
操作或边界应检查的结果
输入不满足前提不调用后续平台操作
平台拒绝或抛错保留错误,不生成成功状态
再次进入或重试检查旧状态不会污染新任务
Logo

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

更多推荐