PAYATHON 2026

macOS 上 WebAuthn PRF 在标签页可用,安装为 PWA 后为何缺少结果?

数据小许

结论

现有日志表明,PWA 中的 WebAuthn 操作成功了,但没有得到可用于解密的 PRF 输出。仅凭这些信息,无法认定这是 macOS Chromium standalone PWA 的统一限制,也无法确定是否缺少某个 flag。

你的注册写法 extensions: { prf: {} } 和认证请求中的 prf.eval.first 结构符合 PRF 扩展的用法。WebAuthn 标准没有为 standalone PWA 单独规定 PRF 开关。应先核对使用的凭证以及两种窗口中的实际返回值,再判断是否属于浏览器实现问题。WebAuthn PRF 扩展规范

两个返回值分别说明什么

返回值含义
注册时 { prf: { enabled: false } }注册结果报告,这枚新凭证无法使用 PRF,并非仅表示注册时没有执行 PRF 求值。
认证时 { prf: {} }本次认证没有返回 PRF 求值结果,不能继续读取 prf.results.first。

enabled 只在注册阶段报告,认证结果中没有这个字段是正常的。注册时仅传入 prf: {} 并未请求求值,因此没有 results 本身不构成异常。你这里更需要关注的是 enabled: false。PRF 输出字段定义

生物识别成功、创建了 resident credential,或设置了 userVerification: "required",都不足以单独证明该凭证支持 PRF。

先确认标签页与 PWA 是否在比较同一件事

原始描述确认了普通标签页中的生物识别流程正常,但没有展示标签页实际返回的 prf.results.first。建议按以下顺序对照:

  1. 确认标签页确实获得 PRF 输出。分别记录两边是否存在 prf.results.first 及其 byteLength,不要只比较生物识别是否成功。
  2. 使用同一枚凭证。保存一次注册返回的 rawId,在标签页和 PWA 中分别调用 getWithPrf(rawId)。不要分别执行 createCredential(),再把得到的两个新凭证当成同一枚。相同的 user.name 不能证明凭证相同。
  3. 保持请求条件一致。比较 origin、RP ID、PRF label、浏览器完整版本及所属配置文件,并记录注册时实际选择的凭证提供方。相同设备和相同 origin 还不足以确认这些条件一致。
  4. 分别记录注册能力。如果还要比较两种窗口中新建凭证的行为,保留每次注册的凭证 ID 和 prf.enabled,避免后续调用混用凭证。

如果同一枚已确认能返回 PRF 的凭证,在相同请求条件下稳定表现为“标签页有输出、PWA 无输出”,才更有理由怀疑存在与窗口模式相关的实现差异。这一判断仍需结合具体浏览器和 macOS 版本确认。

在代码中检查 PRF,缺失时停止解密

下面的函数可以替换原来的 getWithPrf()。它显式填写与注册一致的 rpId,方便对照,并在返回 PRF 数据前检查结果。这不能说明原代码省略 rpId 就是故障原因。

async function getWithPrf(rawId) {
  const label = new TextEncoder().encode("bio-demo-prf-label-v1");

  const assertion = await navigator.credentials.get({
    publicKey: {
      challenge: crypto.getRandomValues(new Uint8Array(32)),
      rpId: location.hostname,
      allowCredentials: [
        {
          type: "public-key",
          id: rawId,
          transports: ["internal"]
        }
      ],
      userVerification: "required",
      extensions: {
        prf: {
          eval: { first: label }
        }
      },
      timeout: 60000
    }
  });

  if (!assertion) {
    throw new Error("No credential returned");
  }

  const first = assertion.getClientExtensionResults().prf?.results?.first;

  console.log("PRF diagnostics:", {
    origin: location.origin,
    standalone: matchMedia("(display-mode: standalone)").matches,
    credentialId: assertion.id,
    authenticatorAttachment: assertion.authenticatorAttachment,
    prfBytes: first?.byteLength ?? 0
  });

  if (!first || first.byteLength !== 32) {
    throw new Error("PRF output unavailable; decryption stopped");
  }

  return new Uint8Array(first);
}

注册后也应检查 cred.getClientExtensionResults().prf?.enabled。如果为 false,应用应明确提示该凭证无法用于当前 PRF 加密流程。首次加密前,还应实际执行一次认证求值,确认能够取得输出。

如何处理兼容性与已有密文

暂时不要通过反复修改 flag 或重建凭证来尝试恢复已有数据。PRF 与凭证关联;将相同输入用于另一枚新凭证,不能保证得到原来的解密材料。若更换凭证,应在仍能解密的环境中先完成密钥迁移,或使用事先配置的恢复方式。W3C PRF 扩展说明

如果已验证普通标签页可以使用原凭证返回正确输出,可以暂时引导用户在那里解锁。若需要报告浏览器问题,请附上最小复现、macOS 和浏览器完整版本、两种窗口的扩展返回值,以及是否使用同一凭证的说明。

诊断日志只保留输出是否存在和长度即可,不要记录 PRF 字节或派生出的密钥。缺少 PRF 时,也不要用用户名、凭证 ID 或固定字符串替代密钥。

备注:内容仅供参考。