diff --git a/src/components/Shared/kratos/passwordless/deviceauthn/index.mdx b/src/components/Shared/kratos/passwordless/deviceauthn/index.mdx index 7e2b4e324..57350d029 100644 --- a/src/components/Shared/kratos/passwordless/deviceauthn/index.mdx +++ b/src/components/Shared/kratos/passwordless/deviceauthn/index.mdx @@ -487,8 +487,11 @@ After opening the one-time `pin_secret`: 3. Outer layer: seal the result under a **non-exportable hardware key** created without user-verification gating — an AES-256-GCM Android Keystore key (StrongBox where available), or an iOS Secure Enclave P-256 key used via ECIES. 4. Store the sealed blob together with the salt, KDF parameters, IV, a format version, and the key alias. On iOS use the Keychain - with `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` (never `UserDefaults`), so uninstalling the app purges it. Android Keystore - keys are purged on uninstall automatically. + with `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` (never `UserDefaults`) so the blob stays off encrypted backups and device + migrations. Keychain items survive app deletion, so you must detect a reinstall yourself and delete the stale artifacts before + the app uses them: write a marker to `UserDefaults` at first launch, which the system clears on deletion, and treat a stored + blob without its marker as a reinstall. Android Keystore keys are purged on uninstall automatically, along with their + hardware-backed sealing key. 5. Wipe the PIN, `pinKey`, `pin_secret`, and the transport private key from memory. ### Hard rules diff --git a/src/components/Shared/kratos/passwordless/deviceauthn/ios.mdx b/src/components/Shared/kratos/passwordless/deviceauthn/ios.mdx index e78e2e95b..e1363f567 100644 --- a/src/components/Shared/kratos/passwordless/deviceauthn/ios.mdx +++ b/src/components/Shared/kratos/passwordless/deviceauthn/ios.mdx @@ -1,16 +1,22 @@ A notable difference with Android is that Apple's app attestation APIs require a network call to Apple's servers from a real device. -This means that the emulator cannot be used. +This means the iOS Simulator cannot produce real attestations. For development and testing you can still exercise the flow on the +Simulator by enabling relaxed +attestation, which accepts software-backed attestations. The Ory client SDK for Swift ships an +`OryClientSimulator` product that generates such an attestation on the Simulator; its code compiles to nothing in device builds, +so it can never ship in production. Relaxed attestation is refused once the project leaves the development environment. It does +not, however, make the hardware-backed flows themselves work in the Simulator — see [Prerequisites](#prerequisites) below. Since Device Binding only is supported on native devices (not in the browser), all corresponding API calls should be done using the endpoints for native apps, to avoid having to pass cookies around manually. ## Prerequisites -The [second-factor guide](#second-factor-device-binding) below runs on a real device (App Attest and the Secure Enclave are -unavailable in the simulator, so device binding cannot run there) with iOS 14 or newer. The first-factor PIN path has the same -base requirements and additionally needs: +The [second-factor guide](#second-factor-device-binding) below runs on a real device with iOS 14 or newer, because App Attest and +the Secure Enclave are unavailable in the Simulator. Relaxed attestation stands in for App Attest during development, but nothing +stands in for the Secure Enclave, so the PIN path — whose sealing key must be hardware-backed — can only be exercised end to end +on a device. The first-factor PIN path has the same base requirements and additionally needs: - The App Attest entitlement `com.apple.developer.devicecheck.appattest-environment` set to `production` in your app's entitlements. @@ -334,7 +340,7 @@ signing key from the [second-factor guide](#second-factor-device-binding) above This listing is one complete recipe: nonce decoding, the enrollment and rotation ceremony (transport key, attestation, sealed secret), the PIN vault (Secure Enclave sealing key, Argon2id, AES-CTR, seal and unseal), the PIN proof, and first-factor login. -The `SettingsFlow` and `UpdateLoginFlowWithDeviceAuthnMethod` types are Ory Swift SDK models. +The `SettingsFlow` and `UpdateLoginFlowWithDeviceAuthnMethod` types are models from the Ory client SDK for Swift. ```swift import CommonCrypto @@ -423,9 +429,19 @@ func clientKeyId(fromUpdatedFlow flow: SettingsFlow) -> String? { .max { $0.1 < $1.1 }?.0 } -/// The local artifacts persisted after sealing. Store in the Keychain with -/// kSecAttrAccessibleWhenUnlockedThisDeviceOnly — never in UserDefaults — so -/// deleting the app purges them. None of these are secret on their own. +/// The local artifacts persisted after sealing. Store the whole struct in the +/// Keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly, never in +/// UserDefaults. The ThisDeviceOnly attribute keeps the blob out of encrypted +/// backups and device migrations, so the sealed secret cannot follow the user +/// onto another device. +/// +/// Keychain items survive app deletion, so you must detect a reinstall yourself +/// and delete the stale artifacts before the app uses them. The system does +/// clear UserDefaults on deletion, so write a marker there at first launch: if +/// the Keychain holds a PinArtifacts but the marker is gone, the app was +/// reinstalled. +/// +/// None of these fields are secret on their own. struct PinArtifacts: Codable { let version: Int // format version of this recipe let clientKeyId: String @@ -591,14 +607,23 @@ so its transport key is destroyed. ## Biometric keys -Biometric (`platform`) keys skip the PIN machinery entirely — no transport key, no sealed secret, no `pin_proof`. The Secure -Enclave gates the signing key itself and shows the Face ID or Touch ID prompt when the key signs. Three differences from the PIN -flow: +Biometric (`platform`) keys skip the PIN machinery entirely — no transport key, no sealed secret, no `pin_proof`. Unlike a PIN +key, the client gates each signature behind a biometric check. App Attest signing is silent, and an App Attest key cannot be bound +to Face ID or Touch ID through its access control, because App Attest keys are system-managed and accept no access-control flags. +The app must therefore run the biometric check itself immediately before it signs. The server trusts the `platform` declaration on +iOS because it cannot verify biometric gating remotely, which makes the client-side gate the security-critical part of this mode. +Three differences from the PIN flow: - The attestation challenge is the **bare nonce**, not `SHA256(nonce ‖ t_pub)`. Pass the raw nonce bytes straight to `attestKey` as `clientDataHash`. -- Create the signing key with `.biometryCurrentSet` in its access control, and enroll it with `"user_verification": "platform"` - and no `pin_protected` or `transport_public_key`. +- Enroll with `"user_verification": "platform"` and no `pin_protected` or `transport_public_key`. There is no access-control flag + to set on the key; instead, gate signing with a `LocalAuthentication` check — call + `evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason:)` on an `LAContext` instance — that must succeed + before every `generateAssertion` call, at both enrollment and login. To approximate `.biometryCurrentSet` semantics — + invalidating the key when the enrolled biometric set changes — record the biometric domain state at enrollment and reject a + login when it no longer matches. Read it from `context.domainState.biometry.stateHash` on iOS 18 and newer, or from the + deprecated `evaluatedPolicyDomainState` on iOS 14–17. Treat a mismatch as "re-enroll", not "account lost": the legacy value can + also change across a major OS upgrade even when the enrolled biometrics did not. - At login, submit only `client_key_id` and `signature` (the assertion over the bare nonce) — omit `pin_proof`. For the App Attest `generateKey` / `attestKey` / `generateAssertion` scaffolding, reuse the `OryApi` helper from the