认识适用于网页的触控 ID 和面容 ID

人们常认为密码是网络身份验证的原罪。密码易于猜测,且容易被泄露。在网络上频繁重复使用相同密码,使数据泄露更具盈利性。当密码变得更强且更独特时,它们很快就会变得让许多用户难以使用。密码确实名声不佳,但问题出在密码本身,还是出在将密码用作唯一身份验证因素上?

许多人认为后者才是问题,因此多重身份验证变得越来越流行。引入第二个因素确实解决了密码的大部分安全问题,但它不可避免地增加了额外的步骤,使整个身份验证体验变得繁琐。因此,多重身份验证尚未成为网络上事实上的身份验证机制。适用于网页的触控 ID 和面容 ID 提供了多重身份验证的安全保障和便捷性。它可以在一个步骤中提供多重身份验证。利用这项技术(已在超过十亿台支持的 Apple 设备上可用),Web 开发者现在可以广泛提供传统的多重身份验证,带来流畅、便捷的体验。此外,基于Web Authentication API 构建,触控 ID 和面容 ID 也具备防网络钓鱼能力。

这篇博文通过提供详细示例,扩展了WWDC 2020 “认识适用于网页的触控 ID 和面容 ID” 会议的内容,旨在帮助开发者采用这项新技术,包括如何管理不同的用户代理用户界面,如何将用户手势从用户激活事件传播到 WebAuthn API 调用,以及如何解释 Apple 匿名认证。本文将最后总结 Apple 平台认证器的独特特性和安全密钥支持的当前状态。如果您以前没有听说过 WebAuthn,强烈建议您先观看涵盖基本概念的WWDC 2020 会议。否则,请尽情阅读。

管理用户体验

尽管用户代理在 WebAuthn 流程中不被要求向用户提供 UI 指引,但实际上它们都这样做了。这使得用户代理能够分担网站管理用户体验的部分负担,但也给网站带来了新的复杂性,因为每个用户代理在 UI 中呈现 WebAuthn 仪式的方式都不同。WebAuthn 仪式可以是身份验证过程,也可以是注册过程。本节介绍了 WebAuthn 仪式选项如何映射到 WebKit/Safari 的 UI,以及适用于网页的触控 ID 和面容 ID 的推荐用户体验。

一个挑战是如何管理平台认证器和安全密钥之间不同的用户体验。虽然 WebAuthn API 允许同时向用户呈现这两种选项,但这并不是最佳方法。首先,大多数用户可能只熟悉平台认证器的品牌,即 Apple 平台上的面容 ID 和触控 ID,但对安全密钥不熟悉。同时提供这两种选项可能会让用户感到困惑,并难以决定如何操作。其次,平台认证器与安全密钥具有不同的行为和使用场景。例如,面容 ID 和触控 ID 适合用作更便捷的替代登录机制,而大多数安全密钥则不适用。此外,存储在安全密钥中的凭据通常可以在不同设备和平台之间使用,而存储在平台认证器中的凭据通常与特定的平台和设备绑定。因此,最好是分别向用户呈现这两种选项。

单独呈现面容 ID 和触控 ID

以下是调用适用于网页的触控 ID 和面容 ID 的推荐方法。下方是注册仪式对应的 Safari UI。此处,信赖方 ID 被选中显示在对话框中。

以下是显示上述对话框的相应代码片段。

const options = {
    publicKey: {
        rp: { name: "example.com" },
        user: {
            name: "john.appleseed@example.com",
            id: userIdBuffer,
            displayName: "John Appleseed"
        },
        pubKeyCredParams: [ { type: "public-key", alg: -7 } ],
        challenge: challengeBuffer,
        authenticatorSelection: { authenticatorAttachment: "platform" }
    }
};

const publicKeyCredential = await navigator.credentials.create(options);

关键选项是指定authenticatorSelection: { authenticatorAttachment: "platform" },这会告诉 WebKit 只调用平台认证器。返回 publicKeyCredential 后,最佳实践之一是将凭据 ID 存储在一个由服务器设置的、安全的、httpOnly 的 cookie 中,并将其传输方式标记为"internal"。此 cookie 可用于改善未来身份验证仪式中的用户体验。

为了保护用户免受跟踪,WebAuthn API 不允许网站查询设备上是否存在凭据。然而,这一重要的隐私功能要求网站付出额外努力,将已配置的凭据 ID 存储在单独的源中,并在身份验证仪式之前进行查询。这个单独的源通常位于后端服务器上。鉴于安全密钥可以在不同平台之间使用,这种做法对其非常有效。不幸的是,它不适用于平台认证器,因为凭据只能在创建它们的设备上使用。服务器端的源无法判断特定的平台认证器是否确实保存了凭据。因此,cookie 特别有用。不应通过 document.cookie API 设置此 cookie,因为 Safari 的智能跟踪预防功能会将此类 cookie 的有效期限制为七天。将这些凭据标记为"internal"也很重要,这样网站就可以在身份验证仪式选项中提供它,以防止 WebKit 同时要求用户使用安全密钥。

下方是两种不同的身份验证仪式 UI。第一种针对用户代理只有单个凭据的情况进行了简化,而第二种则展示了用户代理如何允许用户从多个凭据中选择一个。在这两种情况下,都只显示在注册仪式中提交的user.name。对于第二种情况,列表的顺序是根据凭据的最后使用日期排序的。WebKit 会跟踪最后使用日期。因此网站无需担心。

以下是显示上述对话框的相应代码片段。

const options = {
    publicKey: {
        challenge: challengeBuffer,
        allowCredentials: [
            { type: "public-key", id: credentialIdBuffer1, transports: ["internal"] },
            // ... more Credential IDs can be supplied.
        ]
    }
};

const publicKeyCredential = await navigator.credentials.get(options);

需要注意的是,即使 WebKit 可以改进,只要所有允许的凭据都在平台认证器中找到,就不再需要transports: ["internal"]来防止 WebKit 要求用户使用安全密钥,但这仅限于正常情况。在没有找到凭据的情况下,这个额外属性可以告诉 WebKit 显示错误消息,而不是要求用户使用安全密钥。

同时呈现面容 ID、触控 ID 和安全密钥

尽管不鼓励以下用法,WebKit/Safari 仍准备了专用 UI,允许用户在平台认证器之外选择安全密钥。下方是注册仪式对应的 UI。

通过从上述注册仪式的代码片段中删除authenticatorSelection: { authenticatorAttachment: "platform" },可以获得上述对话框。

如果上述身份验证仪式代码片段中allowCredentials数组的任何条目没有transports: ["internal"]属性,将显示上述对话框。

需要注意的是,在 UI 显示后,两种情况下安全密钥都可以立即使用。“使用安全密钥”和“安全密钥账户”选项用于显示如何与安全密钥交互的说明。

是否指定 allowCredentials

allowCredentials对于身份验证仪式是可选的。然而,省略它将导致 WebKit/Safari UI 中出现不确定的行为。如果找到凭据,将显示上述身份验证仪式 UI。如果没有找到凭据,WebKit 将要求用户提供其安全密钥。因此,强烈建议不要省略此选项。

传播用户手势

未经请求的权限提示很烦人。Mozilla 已经进行了调查 [1, 2] 来证实这一点。尽管 WebAuthn 提示在网络上不像今天的通知提示那样常见,但随着适用于网页的触控 ID 和面容 ID 的发布,这种情况将会改变。

网站请求通知权限并非为了好玩。他们请求是因为通知可以将用户带回其网站,并增加其每日活跃用户指标。在 WebAuthn 提示中也可以发现类似的经济动机,特别是当平台认证器可用时,因为一个成功的身份验证请求会产生一个高保真度、持久的用户唯一标识符。这是关于身份验证的一个普遍真理,这也是为什么许多网站在用户甚至尚未与网站交互之前就要求进行身份验证。尽管 WebAuthn 凭据不可避免地会被用于向用户提供定向广告,但至少可以利用 Mozilla 在 Firefox 中对通知权限提示所做的类似保护,使 WebAuthn 提示对用户而言不那么烦人,即要求 WebAuthn API 具备用户手势,以消除恼人的“加载时”提示。

我们早在之前就预见了这个问题,并在 WebAuthn 规范中提交了一个问题,但当时并未引起太多关注。一个原因是它是一个破坏性变更。另一个原因是安全密钥的风险不高,因为它们不那么流行,也不总是连接到平台。未经请求的提示数量一直出奇的低。随着适用于网页的触控 ID 和面容 ID 的发布,情况有所不同。因此,适用于网页的触控 ID 和面容 ID 需要用户手势才能运行。(出于向后兼容性考虑,安全密钥不需要用户手势。)

用户手势是一种指示,用于向 WebKit 标记当前 JavaScript 上下文的执行是用户交互的直接结果,或者更准确地说,是来自用户激活事件(例如 touchendclickdoubleclickkeydown 事件 [3])的处理程序的结果。要求 WebAuthn API 具备用户手势意味着 API 调用必须在上述 JavaScript 上下文内发生。通常,用户手势不会传播到上下文中的任何异步执行器。鉴于网站在调用 WebAuthn API 之前从服务器异步获取挑战是很常见的做法,WebKit 允许 WebAuthn API 接受通过 XHR 事件和 Fetch API 传播的用户手势。以下是网站如何从用户激活事件中调用适用于网页的触控 ID 和面容 ID 的示例。

直接从用户激活事件调用 API

// Fetching the challengeBuffer before the onclick event.

button.addEventListener("click", async () => {
    const options = {
        publicKey: {
            ...
            challenge: challengeBuffer,
            ...
        }
    };

    const publicKeyCredential = await navigator.credentials.create(options);
});

通过 XHR 事件传播用户手势

button.addEventListener("click", () => {
    const xhr = new XMLHttpRequest();
    xhr.onreadystatechange = async function() {
        if (this.readyState == 4 && this.status == 200) {
            const challenge = this.responseText;
            const options = {
                publicKey: {
                    ...
                    challenge: hexStringToUint8Array(challenge), // a custom helper
                    ...
                }
            };

            const publicKeyCredential = await navigator.credentials.create(options);
        }
    };
    xhr.open("POST", "/WebKit/webauthn/challenge", true);
    xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    xhr.send();
});

通过 Fetch API 传播用户手势

button.addEventListener("click", async () => {
    const response = await fetch("/WebKit/webauthn/challenge", { method: "POST" });
    const challenge = await response.text();

    const options = {
        publicKey: {
            ...
            challenge: hexStringToUint8Array(challenge), // a custom helper
            ...
        }
    };
    const publicKeyCredential = await navigator.credentials.create(options);
});

需要注意的是,可读流尚无法传播用户手势(相关错误)。此外,用户手势对于 XHR 事件和 Fetch API 都将在 10 秒后过期。

彩蛋:通过 setTimeout 传播用户手势

button.addEventListener("click", () => {
    setTimeout(async () => {
        const options = { ... };
        const publicKeyCredential = await navigator.credentials.create(options);
    }, 500);
});

上述示例中的用户手势将在 1 秒后过期。

在 iOS 14、iPadOS 14 和 macOS Big Sur Beta Seed 1 上,仅支持第一个案例。感谢开发者的早期反馈,我们得以识别限制并添加了后续案例。这也帮助我们认识到,用户手势在 Web 开发者中并非一个被充分理解的概念。因此,我们将为 HTML 规范做出贡献,并帮助建立一个定义明确的用户手势概念,以确保浏览器厂商之间的一致性。根据进展情况,我们可能会重新考虑将用户手势要求扩展到安全密钥。

解释 Apple 匿名认证

认证是一项可选功能,它为网站提供了认证器来源的加密证明,以便受特殊法规限制的网站可以做出信任决定。适用于网页的触控 ID 和面容 ID 提供 Apple 匿名认证。一旦验证,此认证保证了真实的 Apple 设备执行了 WebAuthn 注册仪式,但它不保证该设备上运行的操作系统未被篡改。如果操作系统未被篡改,它还保证刚生成的凭据的私钥受到安全隔区的保护,并且私钥的使用受到面容 ID 或触控 ID 的守护。(注意:如果生物识别连续多次失败,保护将回退到设备密码。)

Apple 匿名认证是同类首创,它提供了一种类似匿名 CA 的服务,其中认证器与由其制造商拥有的云端 CA 协同工作,动态生成每个凭据的认证证书,从而确保认证器的任何识别信息都不会在认证声明中透露给网站。此外,在与注册仪式相关的数据中,只有凭据的公钥以及连接的认证器数据和客户端数据的哈希值会被发送给 CA 进行认证,且 CA 不会存储这些数据。这种方法使整个认证过程保持隐私。此外,这种方法避免了基本认证的安全陷阱,即单个设备的泄露会导致所有具有相同认证证书的设备的证书被吊销。

启用 Apple 匿名认证

const options = {
    publicKey: {
        ...
        attestation: "direct", // the essential option
        ...
    }
};

const publicKeyCredential = await navigator.credentials.create(options);

验证声明格式

这是 Apple 匿名认证声明格式的定义。问题 1453 正在跟踪将此声明格式添加到 WebAuthn 标准的进展。

$$attStmtType //= (
                       fmt: "apple",
                       attStmt: appleStmtFormat
                   )

appleStmtFormat = {
                       x5c: [ credCert: bytes, * (caCert: bytes) ]
                   }

上述字段的语义如下
x5c
credCert 及其证书链,每个都采用 X.509 格式编码。
credCert
用于认证的凭据公钥证书,采用 X.509 格式编码。

以下是在给定输入 attStmtauthenticatorDataclientDataHash 时的验证过程:

  1. 验证 attStmt 是否是符合上述语法的有效 CBOR,并对其进行 CBOR 解码以提取所包含的字段。
  2. 连接 authenticatorDataclientDataHash 以形成 nonceToHash
  3. nonceToHash 执行 SHA-256 哈希以生成 nonce
  4. 验证 nonce 是否与 credCert 中 OID ( 1.2.840.113635.100.8.2 ) 扩展的值匹配。这里的 nonce 用于证明认证是实时的,并保护 authenticatorData 和客户端数据的完整性。
  5. 验证凭据公钥是否与 credCert 的主题公钥匹配。
  6. 如果成功,返回表示认证类型为匿名 CA 和认证信任路径 x5c 的实现特定值。

最后一步是验证 x5c 是否是从 credCertApple WebAuthn 根证书的有效证书链,从而证明认证。(此步骤通常在利用 x5c 的不同类型认证中共享 [4]。)需要注意的是,即使启用了认证,AAGUID 也是全零,因为所有支持适用于网页的触控 ID 和面容 ID 的 Apple 设备都应具有本节开头所述的相同属性,并且没有其他设备可以请求 Apple 匿名认证。

Apple 平台认证器的独特特性

以下是 Apple 平台认证器(即适用于网页的触控 ID 和面容 ID)的独特特性总结。

  • 不同的选项设置会导致不同的 UI,因此请明智地指定。
  • UI 中仅显示 RP ID 和 user.name
  • 调用平台认证器需要用户手势。
  • Apple 匿名认证可用。仅当您需要认证时才使用它。
  • 即使使用认证,AAGUID 也全是零。
  • 适用于网页的触控 ID 和面容 ID 在 iOS 14、iPadOS 14 和 macOS Big Sur 上的 Safari、SFSafariViewController 和 ASWebAuthenticationSession 中可用。对于 macOS,运行较低版本操作系统的 Safari 14 将不会获得此功能,因为认证依赖于新的系统框架。
  • 无论指定何种选项,平台认证器生成的所有公钥凭据都是常驻密钥。
  • 凭据只能通过 Mac Safari 上的“Safari”>“历史记录”>“清除历史记录…”或 iOS 和 iPadOS 上的“设置”>“Safari”>“清除历史记录和网站数据”来全部清除。
  • 签名计数器未实现,因此它始终为零。安全隔区用于防止凭据私钥泄露,而非软件保护。

安全密钥支持的当前状态

除了引入适用于网页的触控 ID 和面容 ID 外,iOS 14、iPadOS 14 以及所有支持的 macOS 上的 Safari 14 也改进了安全密钥支持,包括 PIN 码输入和账户选择。以下是目前支持的功能列表。除上述两项外,所有功能自 iOS 13.3、iPadOS 13.3 和 Safari 13 起均已支持。

  • WebAuthn Level 1 中的所有 MUST 功能和所有可选功能,除了 CollectedClientData.tokenBinding 和大部分扩展。仅支持 appid 扩展。
  • 所有 CTAP 2.0 认证器 API,除了 setPin 和 changePin。
  • 支持 USB、Lightning 和 NFC 传输协议的设备。
  • U2F 安全密钥通过 CTAP 2.0 支持,但不通过 CTAP 1/U2F JS 支持。
  • 与适用于网页的触控 ID 和面容 ID 类似,安全密钥支持在 Safari、SFSafariViewController 和 ASWebAuthenticationSession 中可用。

反馈

在这篇博文中,我们介绍了适用于网页的触控 ID 和面容 ID。我们相信这是网络身份验证的一大飞跃。它提供了一种极佳的替代登录方式,特别是对于传统的多种因素认证机制。借助这项技术,我们相信多重身份验证将取代单一因素密码,成为网络上事实上的身份验证机制。开发者们,请立即开始测试此功能,并通过 Twitter (@webkit, @alanwaketan, @jonathandavis) 发送反馈或提交错误,让我们知道它的使用体验。