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。建议按以下顺序对照:
- 确认标签页确实获得 PRF 输出。分别记录两边是否存在
prf.results.first及其byteLength,不要只比较生物识别是否成功。 - 使用同一枚凭证。保存一次注册返回的
rawId,在标签页和 PWA 中分别调用getWithPrf(rawId)。不要分别执行createCredential(),再把得到的两个新凭证当成同一枚。相同的user.name不能证明凭证相同。 - 保持请求条件一致。比较 origin、RP ID、PRF label、浏览器完整版本及所属配置文件,并记录注册时实际选择的凭证提供方。相同设备和相同 origin 还不足以确认这些条件一致。
- 分别记录注册能力。如果还要比较两种窗口中新建凭证的行为,保留每次注册的凭证 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 或固定字符串替代密钥。
备注:内容仅供参考。
