Highlight
这场 Session 讲了 LocalAuthentication 框架在 iOS 16、macOS 13 上的新变化。核心概念是把”认证”(authentication)和”授权”(authorization)区分开。
核心内容
很多 App 都要保护敏感资源:用户资料、私钥、文件解密能力、交易确认按钮。旧做法通常从“验证用户身份”开始:创建 LAContext,让系统显示 Face ID、Touch ID 或密码界面,然后把这个上下文交给 Security 框架继续使用。
这个流程能工作,但开发者要自己串起多个概念。资源是什么,允许执行哪个操作,访问控制列表(ACL)要求什么认证方式,授权结果要绑定到哪个查询里,都要在代码里处理。Session 用 Secure Enclave 私钥签名举例:私钥在 Secure Enclave 中,App 不直接拿到私钥,只请求 Secure Enclave 完成签名操作。
Apple 在 iOS 16 和 macOS 13 的 LocalAuthentication 中加入了面向授权的高层 API。核心抽象是 LARight:一个 Right 代表 App 定义的受保护操作。开发者可以把问题从“用户身份验证”推进到“这个用户现在能不能执行这个操作”。
LARight 适合保护 App 内部资源,例如进入用户资料区。LAPersistedRight 适合跨启动保存授权要求,并由 Secure Enclave 中的唯一密钥支撑。私钥操作仍然必须先授权,公钥操作可以直接执行,这让二次验证、挑战签名这类流程少写很多底层拼装代码。
详细内容
用 LAContext 授权一次 Secure Enclave 签名(04:58)
Session 先回顾现有做法。签名前,App 要取出 Secure Enclave 私钥关联的访问控制列表。这里依赖 Security 框架的 SecItemCopyMatching,并在查询中要求返回属性。
let query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecAttrApplicationTag as String: "com.example.app.key",
kSecReturnAttributes as String: true,
]
var item: CFTypeRef? = nil
guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess, let attrs = item as? NSDictionary, let accessControl = attrs[kSecAttrAccessControl] else {
throw .aclNotFound
}
关键点:
kSecClassKey表示查询目标是密钥。kSecAttrTokenIDSecureEnclave把范围限制在 Secure Enclave 里的密钥。kSecAttrApplicationTag用稳定标签找到 App 创建的那把密钥。kSecReturnAttributes要求返回密钥属性,从而读取kSecAttrAccessControl。SecItemCopyMatching成功后,代码从返回属性中取出 ACL;失败就抛出aclNotFound。
拿到 ACL 后,LAContext 可以在 App 选择的时机触发授权 UI(05:15)。
let context = LAContext()
try await context.evaluateAccessControl(accessControl as! SecAccessControl,
operation: .useKeySign,
localizedReason: "Authentication is required to proceed")
关键点:
LAContext()创建本次授权使用的上下文。evaluateAccessControl直接评估前面取出的 ACL。operation: .useKeySign指明这次授权是为了执行签名操作。localizedReason是用户在系统授权界面中看到的原因说明。
授权完成后,要把同一个 LAContext 绑定到取私钥引用的查询里(05:44)。
let query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecAttrApplicationTag as String: "com.example.app.key",
kSecReturnRef as String: true,
kSecUseAuthenticationContext as String: context
]
var item: CFTypeRef? = nil
guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess, item != nil else {
throw .keyNotFound
}
关键点:
kSecReturnRef要求返回可用于后续操作的密钥引用。kSecUseAuthenticationContext把已经授权过的context传给 Security 查询。- 同一个上下文绑定到私钥引用后,执行签名时不会再弹一次认证界面。
- 这个免重复提示的效果持续到
LAContext失效。
最后,App 用返回的 SecKey 执行签名(06:00)。
let privateKey = item as! SecKey
var error: Unmanaged<CFError>?
guard let sgt = SecKeyCreateSignature(privateKey, self.algorithm, blob, &error) as Data? else {
throw .signatureFailure
}
关键点:
item被转换为SecKey,它是私钥引用。SecKeyCreateSignature请求 Secure Enclave 用私钥对blob签名。- 私钥本身不会交给 App;App 只得到签名结果。
- 签名失败时,代码抛出
signatureFailure。
用 LARight 表达 App 自己的受保护操作(08:28)
如果 App 只想保护一个应用内资源,例如用户资料页,直接使用 LARight 更简单。Right 的要求可以写成“需要生物识别,允许设备密码作为 fallback”。
// LARight: Basic usage
func login() async {
self.loginRight = LARight(requirement: .biometry(fallback: .devicePasscode))
do {
try await loginRight.checkCanAuthorize()
} catch {
navigateTo(section: .public)
return
}
do {
try await self.loginRight.authorize(localizedReason: self.localizedReason)
navigateTo(section: .protected)
} catch {
showError(.authenticationRequired)
}
}
关键点:
LARight(requirement:)创建一个受保护的授权对象。.biometry(fallback: .devicePasscode)表示优先使用生物识别,也允许用设备密码完成授权。checkCanAuthorize()先检查当前用户能否获得这个 Right,不能授权时进入公开区域。authorize(localizedReason:)触发系统驱动的授权 UI。- 授权成功后进入受保护区域,失败时显示认证必需的错误。
Right 有状态。它从 unknown 进入 authorizing,再根据结果进入 authorized 或 notAuthorized。App 可以读取 state,也可以用 KVO、Combine,或者监听默认 NotificationCenter 中的 didBecomeAuthorized 和 didBecomeUnauthorized 通知(09:38)。
登出时,App 主动撤销这个 Right(11:01)。
// LARight: Basic usage
func login() async {
self.loginRight = LARight(requirement: .biometry(fallback: .devicePasscode))
// ...
do {
try await self.loginRight.authorize(localizedReason: self.localizedReason)
navigateTo(section: .protected)
} catch {
showError(.authenticationRequired)
}
}
func logout() async {
await self.loginRight.deauthorize()
}
关键点:
self.loginRight需要被强引用保存;实例释放后授权状态也会丢失。deauthorize()把已授权的 Right 变回未授权状态。- 下次登录时,用户需要重新走授权流程。
- 这个模型适合“本次 App 运行期间有效”的保护场景。
用 LAPersistedRight 做跨会话二次验证(13:44)
LAPersistedRight 由 Right Store 创建。保存后,它由唯一的 Secure Enclave 密钥支撑,授权要求保持不可变。公钥可以直接导出;私钥签名、解密、密钥交换等操作必须先通过授权。
// LAPersistedRight: Retrieval and private key usage
func generateClientKeys() async throws -> Data {
let login2FA = LARight(requirement: .biometryCurrentSet)
let persisted2FA = try await LARightStore.shared.saveRight(login2FA, identifier: "2fa")
return try await persisted2FA.key.publicKey.bytes
}
func signChallenge(_ challenge: Data, algorithm: SecKeyAlgorithm) async throws -> Data {
let persisted2FA = try await LARightStore.shared.right(forIdentifier: "2fa")
let localizedReason = "Biometric authentication is required to proceed"
try await persisted2FA.authorize(localizedReason: localizedReason)
return try await persisted2FA.key.sign(challenge, algorithm: algorithm)
}
关键点:
.biometryCurrentSet要求授权绑定到创建 Right 时设备上已登记的生物识别集合。LARightStore.shared.saveRight把普通 Right 保存为持久化 Right,并使用唯一 identifier。persisted2FA.key.publicKey.bytes导出公钥字节,适合交给后端保存或验证。right(forIdentifier:)在之后的 App 会话中取回同一个持久化 Right。authorize(localizedReason:)成功后,私钥才允许执行受保护操作。persisted2FA.key.sign用私钥签名后端下发的 challenge。
这个示例对应一个常见登录增强流程:注册阶段生成设备绑定的公钥,后端保存公钥;登录或高风险操作时,后端下发 challenge,设备在用户完成生物识别后用私钥签名,后端用公钥验证签名。
核心启发
-
做什么:给敏感页面加本地解锁。 例如密码库、健康记录、财务详情页。为什么值得做:
LARight可以直接表达“进入这个区域前要授权”。怎么开始:为页面持有一个LARight(requirement: .biometry(fallback: .devicePasscode)),进入前调用checkCanAuthorize()和authorize(localizedReason:)。 -
做什么:给登出按钮做真正的本地撤销。 为什么值得做:Right 的授权状态可以由 App 主动撤销,避免用户离开设备后受保护区域继续可见。怎么开始:在登出、切换账号或进入后台策略触发时调用
deauthorize(),再次访问时重新授权。 -
做什么:用 Secure Enclave 做设备绑定的二次验证。 为什么值得做:
LAPersistedRight背后的私钥只能在授权后使用,适合高风险操作签名。怎么开始:注册时保存LARight(requirement: .biometryCurrentSet)并上传公钥;操作时取回 Right,授权后对后端 challenge 签名。 -
做什么:把旧的 LAContext + Security 代码逐步收敛。 为什么值得做:如果只是保护 App 内容,
LARight能减少手写 ACL 查询、上下文绑定和状态管理。怎么开始:保留需要底层密钥控制的路径,把普通内容解锁改成 Right 模型,再用状态观察刷新 UI。
关联 Session
- Discover Managed Device Attestation — 继续讲 Secure Enclave 私钥和设备证明如何用于服务端信任判断。
- Meet passkeys — 从账号登录角度讲平台认证能力和抗钓鱼凭证的集成。
- Replace CAPTCHAs with Private Access Tokens — 讲如何在保护隐私的前提下提高请求可信度。
- Improve DNS security for apps and servers — 讲网络层安全能力,适合和本地授权一起纳入安全改造清单。
评论
GitHub Issues · utterances