苹果一年前在 WWDC2022 中引入了 Passkey。这具有突破性意义,因为它允许将 FIDO 凭证中的私钥存储在 iCloud 密钥链中,并方便它在设备之间传播。虽然它使 FIDO 凭证易于使用,并解决了引导问题 (用户从新设备登录网站/应用程序),但它带来了一个安全缺点,即私钥不再存储在硬件 TPM 模块中。由于这个缺点,根据 NIST,Passkey 只能被分类为提供 AAL2 级保证。
下图显示了常见认证因素之间的安全强度权衡。
要达到最高安全级别,我们需要利用硬件设备。不幸的是,苹果和谷歌目前的 Passkey 实现并没有给开发者一个选择 TPM 模块的选项。
如果你真的想要很高的安全性保证怎么办?你显然可以使用物理安全密钥如 Yubikey,但这两者都昂贵且难以使用。这篇文章展示了如何利用 iOS 平台提供的原语来从零开始构建 FIDO 解决方案,利用内置的 TPM(也称为安全令牌)。
FIDO 背后的概念很简单。它利用公钥密码学,用户使用私钥对服务器的 nonce 进行签名,并证明其拥有私钥。在本文中,我们将演示如何使用原生 iOS API 来实现相同的认证流程。这不是 FIDO 协议的完整实现,但由于篇幅限制,它只关注私钥和公钥部分。但是如果你选择的话可以在这个例子的基础上扩展。
我们将使用 iOS 中的 Local Authentication 框架,它将生成一个 LAPublicKey 和 LAPrivateKey 对,存储在 TPM 模块中。然后,我们将演示如何使用 LAPrivateKey 进行签名,以及如何使用 LAPublicKey 验证签名。
首先,我们演示如何在注册过程中生成密钥对。我们利用 LARightStore,它会在安全区域中存储由独特密钥加密的 LAPersistedRight。我们创建一个 generateClientKeys() 函数来捕获全部逻辑。首先,我们初始化一个 LARight(),它是“一组控制对资源或操作的访问要求的集合”。当我们调用 LARightStore.shared.saveRight() 时,会生成一个密钥对,并持久化密钥和权限,并返回一个 LAPersistedRight。我们可以通过调用 persistedRight.key.publicKey 获取新生成的密钥的公钥引用。这个公钥会被返回,以便调用者可以在服务器端持久化这个公钥用于未来验证。
// 生成密钥对
func generateClientKeys() async throws -> Data {
let right = LARight()
// 如果之前生成过密钥,在生成新密钥对前清除
try await LARightStore.shared.removeRight(forIdentifier: "fido-key")
// 生成新密钥对
let persistedRight = try await LARightStore.shared.saveRight(right, identifier: keyIdentifier)
return try await persistedRight.key.publicKey.bytes
}
注意,在 saveRight() 之前我们也调用了 LARightStore.shared.removeRight()。这是为了在相同标识符下如果之前保存了旧密钥的话删除它。
在注册之后,当用户再次回到你的应用并需要登录时,我们需要通过认证流程来验证用户。下面的代码是一个简化的 FIDO 流程。首先,按照 FIDO 协议,我们需要调用服务器获取一个 nonce。nonce 用于防止重放攻击。然后,我们调用下面的函数对 nonce 进行签名。
func signServerChallenge(nonce: Data) async throws -> Data {
let persistedRight = try await LARightStore.shared.right(forIdentifier: "fido-key")
try await persistedRight.authorize(localizedReason: "Authenticating...")
// 验证我们可以签名
guard persistedRight.key.canSign(using: .ecdsaSignatureMessageX962SHA256) else {
throw NSError(domain: "SampleErrorDomain", code: -1, userInfo: [:])
}
return try await persistedRight.key.sign(nonce, algorithm: .ecdsaSignatureMessageX962SHA256)
}
这个函数先根据相同的标识符查找 LAPersistedRight。如果找到,它会请求用户授权使用该密钥,然后使用私钥对 nonce 进行签名。nonce 和它的签名应该发送到服务器进行验证。
FIDO in TPM 项目是一个演示项目,结合了上述代码片段,演示了注册和认证流程的用户界面。你也可以观看这个视频演示以查看注册和认证的用户体验。
为什么你不使用平台提供的原生 WebAuthn API(iOS 上的 Authentication Services API 或 Android 上的 Credential Manager API)?有几个原因使用本文概述的自制解决方案:
强安全性 (高达 AAL3 级)。该解决方案将允许你使用内置的平台认证器 (TouchID 或 FaceID) 提供强大的安全性保证,而无需购买独立的安全密钥 (如 Yubikey)。它既安全又易于使用,因为你不需要携带额外的硬件。
更可定制的 UX 和 UI。如演示视频所示,注册体验已经大大简化,甚至可以无需用户交互就默默完成。认证体验也更简单,有很大的定制空间。特别是,你可以选择对最终用户提及“生物识别”或其他更熟悉的术语,因为大多数用户都没有听说过 Passkey。这为你提供了灵活性,如果你不想将功能营销为 Passkey。
不需要绑定到网站。WebAuthn 是一个遵循 FIDO 标准的 Web API。为避免钓鱼攻击,WebAuthn 要求凭证绑定到 web 域。当 iOS 和 Android 引入等效的原生 API 时,它们遵循了这种设计,要求你的应用通过通用链接绑定到 web 域。但你的应用可能没有 web 存在。此解决方案可节省设置网站和配置通用链接的麻烦。
与浏览器内部仅限于 WebAuthn API 不同,在原生应用中你可以利用各种 API。如果你不想使用平台提供的 FIDO API,你实际上可以自己实现 FIDO 协议。希望这篇文章为你奠定了基础,如果你想深入进行更多定制化和更强的安全性保证。
注:本文也发表在 Passkey: AAL3 Strong Security with a Customizable Interface 来自:https://drhuanliu.medium.com/passkey-aal3-strong-security-with-a-customizable-interface-7dfdca6cb557