Device binding
Device Authentication (also known as 'DeviceAuthn', or device binding) is a way for a user to authenticate with a hardware resident private key.
Since the key cannot leave the device, once the key has been added to the identity, it gives a high assurance that the user is who they say they are, and is using a trusted, known device, without needing to remember something like a password.
This is very similar to passkeys with one crucial difference: passkeys are usually synced in the cloud among many devices, whereas a DeviceAuthn key cannot leave the hardware where it was created.
Using this approach, the system can restrict the use of an application on specific, whitelisted devices.
The strategy supports two modes:
- Second factor (step-up): an enrolled device key satisfies a higher authenticator assurance level (AAL2 or AAL3) after the user has already signed in with another method.
- First factor (passwordless): with
first_factorenabled, a device key protected by an app PIN or platform biometrics (Face ID, fingerprint), combined with the device's hardware-resident key, signs the user in with a single request and grants an AAL2 session — no password, no one-time code.
Key properties of the PIN and biometric first factor:
- The PIN never leaves the device and is never stored anywhere — not on the device, not on the server. It unlocks a secret on the device; the server verifies a cryptographic proof derived from that secret.
- The server is the only place a PIN guess can be tested. A stolen device, a stolen backup, or a full database dump cannot be used to guess the PIN offline.
- Wrong PIN attempts are counted server-side. After
pin_max_attemptsconsecutive failures (default 5), the key is locked and its secret destroyed. - The secret is delivered to the device exactly once at enrollment, end-to-end encrypted (HPKE) — TLS-terminating intermediaries and request logs only ever see ciphertext.
Since this is a strategy, it supports all the same hooks as the other strategies.
Short summary
- Available on Ory Network and with the Ory Enterprise License; implemented by the
deviceauthnstrategy, in spirit similar toWebAuthn. - Every key is addressed by its
client_key_id— a server-assigned fingerprint: the lowercase-hex SHA-256 of the key's public key in SubjectPublicKeyInfo (DER) form. Clients don't choose it; they derive it locally or read it from the settings flow. - The settings flow is used to manage keys (create, delete).
- The login flow is used to step-up the AAL. Hardware-backed keys (TEE) satisfy AAL2, while keys stored in a dedicated security chip (StrongBox) may eventually be categorized as AAL3.
- With
first_factorenabled, a key protected by an app PIN or platform biometrics is a complete passwordless login granting AAL2 — see How it works and Configuration. - Using the admin API, it is possible to delete all keys for a device on behalf of the user in case of theft or loss.
- A device may have multiple keys, to support multiple user accounts on the same device.
- Only these platforms are currently supported, because they offer native APIs, strong hardware, and trust guarantees:
- iOS: 14.0+
- iPadOS: 14.0+
- tvOS: 15.0+ (untested)
- visionOS 1.0+ (untested)
- Android SDK 24.0+. Older versions are unlikely to be supported.
Acronyms
- TPM: Trusted Platform Module
- TEE: Trusted Execution Environment
- CA: Certificate Authority
- AAL: Authenticator Assurance Level
How it works
Three keys
A PIN-protected enrollment involves three distinct keys:
| Key | Purpose | Lifetime | Known to the server |
|---|---|---|---|
| Device signing key | Signs the login challenge — the possession factor | Persistent, hardware-resident | Public half only |
| Sealing key | Wraps the stored PIN secret on the device | Persistent, hardware-resident, local | Never |
| Transport key (X25519) | Receives the one-time pin_secret at enrollment (HPKE) | Ephemeral — destroyed after enrollment | Public half only |
User verification levels
Every key records a user_verification level at enrollment. It is fixed at enrollment and not negotiable at login:
user_verification | How the user is verified | First factor | Second factor (step-up) |
|---|---|---|---|
pin | App PIN, proven to the server per login | Yes | Yes (PIN proof required) |
platform | Platform biometrics gate the signing key | Yes¹ | Yes |
none | Not verified | No | Yes |
¹ On iOS, biometric-only first factor requires the ios_biometric_first_factor opt-in — see Configuration.
The PIN mechanism
At enrollment the server generates a random 32-byte pin_secret, stores it encrypted, and sends it to the device exactly once,
sealed to the enrollment's transport key. The device never stores it in the clear: it derives a key from the user's PIN
(Argon2id), encrypts the secret with it (unauthenticated AES-CTR), and wraps the result under a non-exportable hardware key.
At login the device reverses the wrapping with the entered PIN and proves possession of the secret with an HMAC over the login challenge. A wrong PIN yields 32 bytes of garbage — indistinguishable from the real secret on the device — so the resulting proof is wrong and the server counts the failure. Nothing on the device (or in a stolen backup) can test whether a PIN guess is correct.
Biometric keys need none of this machinery: the platform gates the signing key itself and shows the biometric prompt when the key signs.
Assurance level
A successful first-factor login records possession (the device signature) plus knowledge (PIN) or inherence (biometrics) and grants an AAL2 session in a single submission. This matches NIST SP 800-63B's multi-factor cryptographic authenticator model: the PIN acts as an activation secret whose verification failures the server rate-limits.
Platform guides
Device binding is implemented with native platform APIs. We recommend using the Ory SDK to communicate with Kratos, although this is not required. Since device binding is only 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.
Each platform guide covers the full journey: second-factor device binding, the PIN and biometric first factor, and key recovery.
Configuration
Enable the deviceauthn strategy and configure its behavior in the Kratos configuration. This is the canonical configuration
example — enabling the strategy with no config block gives you second-factor device binding; the config keys below add the
first-factor PIN and biometric modes.
selfservice:
methods:
deviceauthn:
enabled: true
config:
# Allow deviceauthn keys with user_verification "pin" or "platform"
# to act as a complete first factor. Default: false.
first_factor: true
# Consecutive wrong-PIN attempts before a key is locked and its
# secret destroyed. Default 5, hard ceiling 10.
pin_max_attempts: 5
# Allow iOS biometric ("platform") keys as the sole first factor.
# Default false: App Attest cannot prove that a Secure Enclave key is
# biometric-gated, so this is an explicit opt-in.
ios_biometric_first_factor: false
# Bind enrollments (and iOS logins) to your apps. Empty lists disable
# the check — configure both in production.
ios_app_ids:
- "TEAMID.com.example.app"
android_app_ids:
- "0123…ef" # lowercase-hex SHA-256 of your app signing certificate
first_factor— enables the first-factor login path and its UI nodes. Without it, all keys are step-up only.pin_max_attempts— server-side lockout limit. Values above 10 are clamped (NIST SP 800-63B ceiling).ios_biometric_first_factor— on Android, the attestation proves that aplatformkey requires user authentication; on iOS it cannot, so iOSplatformkeys are step-up only unless you opt in. PIN keys are unaffected.ios_app_ids/android_app_ids— allow-lists checked against the attestation. Use the Apple App ID (<TeamID>.<BundleID>) and the SHA-256 digest of the Android app signing certificate (package names are forgeable).- PIN length and complexity are client-side concerns — see Client implementation requirements.
On Ory Network all of these are available through the project configuration. Relaxed attestation for emulator testing is described in Relaxed attestation for testing and only takes effect in development environments.
Relaxed attestation for testing
For testing purposes, you can relax the enrollment checks so that software-based attestations (such as those produced by the
Android emulator) are accepted. This relaxes the checks for software roots, expired certificates, and software security level. Add
config.insecure_allow_relaxed_attestation to the strategy configuration:
selfservice:
methods:
deviceauthn:
enabled: true
config:
insecure_allow_relaxed_attestation: true
On Ory Network, this is exposed as a toggle in the Console under MFA → Device Authentication, and is only available on development projects.
Keep the following in mind when using relaxed attestation:
- Keys enrolled while relaxed attestation is enabled carry a 30-day expiry. Hardware-attested keys are unaffected.
- Relaxed keys are refused at login as soon as the setting is disabled, the key expires, or (on Ory Network) the project is no
longer in the
developmentenvironment.
Relaxed attestation is intended for development and testing only. Never enable it for production traffic, as it removes the hardware-binding guarantees that this strategy relies on.
Protocol reference
All flows are native (API) flows. The flow's UI contains a hidden deviceauthn_nonce node; its value is the base64 encoding of
the JSON {"nonce":"<base64 of 32 raw bytes>"}. Decode twice to obtain the raw nonce bytes. The nonce is single-use and bound to
the flow.
Enrollment
- The
DeviceAuthnstrategy is enabled in the Kratos configuration — see Configuration. This strategy implements the settings and login flow. - The client creates a new settings flow and the existing keys for the identity are in the response. The settings flow has a
field
noncewhich contains a random nonce. This is the server challenge. This value is opaque and should not be assigned meaning. It may be a random string, or a hash of something. The important part is that it is not guessable by an attacker. - The client generates a private-public Elliptic Curve (EC) key pair in the TEE/TPM of the device using the server challenge, using native mobile APIs.
- The client completes the settings flow to enroll a new key by sending these fields:
- device name (human readable, picked by the user, for example
My work phone) - certificate chain (Android) or attestation (iOS), which contains the signature of the server challenge, and the public key (in the leaf certificate)
- device name (human readable, picked by the user, for example
- The server:
- Checks that the certificate chain is valid, using Google and Apple root CAs
- Checks the certificate revocation lists to ensure no root/intermediate CA in the chain has been revoked
- Checks that the challenge sent is the same as the challenge in the database (stored in the settings flow)
- Checks that the key is indeed in the TEE/TPM based on the device attestation information. A key in software is rejected. A key in the TPM (e.g. Strongbox) may warrant a higher AAL e.g. AAL3 in the future.
- Checks that the device is not emulated, modified in some way, etc based on the device attestation information
- Records the public key in the database
- Assigns the key's
client_key_id: the lowercase-hex SHA-256 fingerprint of the public key in SubjectPublicKeyInfo (DER) form. The device can recompute it locally. Keys enrolled before server-assigned IDs keep their original client-chosen value. - Erases the challenge value in the database to prevent re-use
- Replies with 200
Enrollment also records a user_verification level (none, platform, or pin) that determines whether the key can act as a
first factor. PIN enrollment computes the attestation challenge differently — see PIN enrollment.
At this point the key is enrolled for the identity.
PIN enrollment
Runs in a settings flow under a privileged session.
-
Generate an ephemeral X25519 keypair — the transport key (
t_priv,t_pub). It must exist before attestation, because its public half is baked into the attestation challenge. -
Compute the attestation challenge:
challenge = SHA256(nonce ‖ t_pub)The raw 32-byte nonce concatenated with the raw 32-byte transport public key, in that order, hashed once. This binds the transport key to the attested device: an intermediary that swaps
t_pubinvalidates the attestation.warningDo not use the bare nonce as the challenge — that is the second-factor device-binding form. Using it here fails with
Unable to validate the key attestation: wrong challenge. -
Create and attest the device signing key with that challenge. For PIN keys, create the key without platform user-verification gating — the PIN is the gate. On iOS pass the digest as
clientDataHashtoattestKey; on Android pass it tosetAttestationChallenge. -
Complete the settings flow:
{"method": "deviceauthn","add": {"device_name": "My work phone","version": 1,"pin_protected": true,"transport_public_key": "<base64 of t_pub>","attestation_ios": "<base64 CBOR attestation>"}}Android submits
certificate_chain_android(the attestation chain, leaf first) instead ofattestation_ios.user_verificationmay be omitted:pin_protected: trueimplies"pin". -
The server verifies the attestation, recomputes the challenge from the nonce it issued and the submitted
transport_public_key, assigns the key'sclient_key_id, generates thepin_secret, stores it encrypted, and returns it — exactly once — HPKE-sealed in the response'scontinue_with:{"continue_with": [{"action": "show_pin_entry_ui","data": {"enc": "<base64 HPKE encapsulated key>","ciphertext": "<base64 HPKE-sealed pin_secret>"}}]}The HPKE parameters are fixed and non-negotiable: DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM, with
info = "ory/deviceauthn/pin-secret/v1"and theclient_key_idstring as AAD. -
client_key_idis the key's deterministic fingerprint — the lowercase-hex SHA-256 of the device public key in PKIX, ASN.1 DER (SubjectPublicKeyInfo) form. It is not included incontinue_with: derive it locally (trivial on Android) or read it from the updated flow'sdeviceauthn_removenode for the new key (see the platform guides). -
The client opens the sealed secret with
t_priv, captures the user's PIN, seals the secret per the client requirements, and destroys the transport keypair. The key is immediately usable (confirmed).
Biometric enrollment
Same settings flow, three differences: the challenge is the bare nonce; the signing key is created with platform
user-verification gating (setUserAuthenticationRequired on Android, .biometryCurrentSet access control on iOS); and the
payload declares "user_verification": "platform" with no pin_protected and no transport_public_key. There is no secret and
no continue_with. On Android the server cross-checks the declaration against the attestation; on iOS it is trusted at enrollment
(which is why first-factor use is opt-in there).
Proof of device enrollment
- When the user creates the login flow with the DeviceAuthn strategy, the client receives a server challenge.
- Using the private key in the hardware of the device, the client signs the server challenge using ECDSA. The signature is only emitted after a biometric/PIN prompt has been passed. The client then sends the signature to the server using the login flow update endpoint.
- The server:
- Checks that the signature is valid using the recorded public key in the database
- Checks that no CA in the certificate chain (when the device has been enrolled) has been revoked
- Erases the challenge value in the database to prevent re-use.
- Replies with 200 with a fresh session token and a higher AAL e.g. AAL2 or AAL3
First-factor login
Requires first_factor: true. The client creates a native login flow and submits:
{
"method": "deviceauthn",
"client_key_id": "<the key's fingerprint>",
"signature": "<base64>",
"pin_proof": "<base64, PIN keys only>"
}
-
signature— the device key over the raw nonce. Android: an ASN.1/DER ECDSA signature over the SHA-256 of the nonce (Signature("SHA256withECDSA")fed the nonce bytes). iOS: the CBOR App Attest assertion fromgenerateAssertion(keyId, clientDataHash: nonce). -
pin_proof— PIN keys only:pin_proof = HMAC-SHA256(key: pin_secret,msg: "ory/deviceauthn/pin-proof/v1" ‖ client_key_id ‖ nonce)The domain string and
client_key_idas UTF-8 bytes, the nonce raw, concatenated without separators.
The server resolves the identity from client_key_id, verifies the signature, and — for PIN keys — verifies the proof. Success
grants an AAL2 session in this single submission.
Every rejection (unknown key, ineligible key, bad signature, wrong PIN, locked key) returns the same error, so enrollment status
can't be probed. The lockout counter moves only on a valid signature with a wrong proof — the combination that proves the physical
device made a wrong-PIN attempt. At the limit the key state becomes locked and the stored secret is destroyed. A correct proof
resets the counter.
Step-up with a PIN key
At AAL2 step-up, a PIN key must send pin_proof alongside the signature — the device signature alone does not bypass the PIN. A
pin_proof submitted for a non-PIN key is rejected as a downgrade attempt. Biometric and none keys step up with the signature
alone, as described in Proof of device enrollment.
Rotating the PIN secret
Re-issues a fresh pin_secret for an existing PIN key — the recovery path for a forgotten PIN or a locked key. The device signing
key is unchanged; no re-attestation happens. Runs in a settings flow under a privileged session and additionally requires proof of
possession of the enrolled key:
{
"method": "deviceauthn",
"rotate_secret": {
"client_key_id": "<the key's fingerprint>",
"transport_public_key": "<base64 of a fresh t_pub>",
"signature": "<base64>"
}
}
signaturecovers the challengenonce ‖ t_pub— the raw 64-byte concatenation of the settings-flow nonce and the fresh transport public key, not hashed by the caller. Android signs it withSHA256withECDSAas usual; iOS passes it asclientDataHashtogenerateAssertion. This binding ensures a session-level attacker cannot rotate the secret to a transport key they control.- Only PIN keys can be rotated.
- Effects: fresh secret (delivered via the same one-time
continue_with), failure counter reset,lockedstate cleared.
Key Revocation
- The user can revoke a key themselves (e.g. because the device is stolen, lost, broken, etc) using the settings flow. This action can be done from any device (e.g. from the browser), as it is the case for other methods e.g. WebAuthn.
- An admin using the admin API can revoke all keys on a device on behalf of the user. This is useful when the user only owns one device which is the one that should be revoked (e.g. one mobile phone) and which has been lost/stolen
Revocation is done by removing the key from the database.
Device list
The settings flow contains all keys for the identity. This is used to present the list of keys (including device name) in the UI.
Key lifecycle on the device
- Creation: When the device enrollment process is started for the user
- Deletion:
- When the app is uninstalled or when the phone is reset, the mobile OSes automatically remove all keys for the app. This means that if the device was enrolled, the public key subsists server-side but the private key does not exist anymore, so no one can sign any challenge for this public key. This database entry is thus useless, but poses no security risks.
Client implementation requirements
The security of the PIN path depends on the client following this recipe exactly. Each rule exists to preserve one invariant: the server's rate-limited proof check is the only place a PIN guess can be tested.
Sealing the secret
After opening the one-time pin_secret:
- Derive a key from the PIN:
pinKey = Argon2id(PIN, salt)with a fresh random salt. - Inner layer: encrypt the secret with unauthenticated AES-CTR under
pinKey, with a fresh random IV. CTR is deliberate: a wrong PIN yields plausible garbage, never a locally detectable failure. - 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.
- 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(neverUserDefaults), so uninstalling the app purges it. Android Keystore keys are purged on uninstall automatically. - Wipe the PIN,
pinKey,pin_secret, and the transport private key from memory.
Hard rules
- Hardware sealing key or refuse. If no StrongBox/TEE/Secure Enclave key is available, refuse PIN enrollment. Never fall back to a software key — the offline-guessing resistance rests entirely on this.
- No local PIN-correctness signal. Never wrap the secret in anything that reveals whether a PIN guess was right: no MAC or AEAD tag on the inner layer, no checksum, magic bytes, length or format marker, and no "did it decrypt sensibly" heuristics. Any such artifact is an offline PIN-testing oracle.
- Fresh salt and IV on every seal — at enrollment, on PIN change, and after secret rotation. Reusing either leaks the secret through CTR keystream reuse.
- Zeroize. Fresh PIN entry for every authentication; never cache the PIN,
pinKey, orpin_secret. Hold them in mutable byte buffers (ByteArray,[UInt8],Data) — never in immutable strings — and overwrite with zeros after use. - Require an unlocked device. Create the sealing key so it is usable only while the device is unlocked
(
setUnlockedDeviceRequired(true)on Android;kSecAttrAccessibleWhenUnlockedThisDeviceOnlyon iOS). - Separate keys. The signing key and the sealing key are distinct hardware keys; the transport key is ephemeral and destroyed after enrollment.
PIN policy
The server never sees the PIN, so PIN policy is enforced by your app, not by Ory: require at least 6 digits (recommended; 4 is the
absolute floor) and reject common values (repeated or sequential digits, 123456, birthdate shapes). Server-side lockout bounds
the damage of a weak PIN; it does not make weak PINs safe.
Argon2id calibration
Mobile hardware varies widely. Ship pre-tuned parameter tiers, benchmark once on first launch to pick a tier, and store the chosen parameters with the sealed blob so unsealing always uses the enrollment-time values. As a starting point: 64 MiB memory, 3 iterations, parallelism 4.
Error routing
| Symptom | Meaning | Action |
|---|---|---|
| Server rejects login; local unseal succeeded | Wrong PIN (or key locked) | Let the user retry; count local retries |
| Repeated rejections despite correct-looking PIN | Key locked server-side | Recover: fallback login + rotate secret |
| Outer unseal fails / sealing key or blob missing | Local state lost (reinstall) | Re-enroll the key |
Never present a failed outer unseal as "wrong PIN" — it is structural and retrying cannot fix it.
Recovery and lockout
| Situation | Path |
|---|---|
| User wants to change the PIN (knows it) | Client-only: unseal with the old PIN, re-seal with the new one (fresh salt and IV). No server call. |
| PIN forgotten, or sealed secret lost (reinstall) | Log in with another method, then rotate the secret. The device key and its attestation are kept. |
| Key locked after too many wrong PINs | Same: another login method, then rotate (clears the lock) — or delete and re-enroll the key. |
| Device lost or stolen | Delete the key from a session on another device, or via the admin identity APIs. |
A locked key is refused for both first-factor login and step-up. Locking destroys the stored secret, so a lock can never be silently undone — recovery always issues a fresh secret. Consider notifying users whenever a key is enrolled or its secret rotated, and delaying sensitive operations after either event.
Troubleshooting
Unable to validate the key attestation: wrong challengeat PIN enrollment — the attestation was created over the bare nonce. PIN enrollment binds the transport key: useSHA256(nonce ‖ transport_public_key)as the challenge. The bare nonce is correct only for second-factor device binding and biometric enrollment.The rotation signature is invalid.— the rotation signature must cover the raw concatenationnonce ‖ transport_public_key(64 bytes, not hashed by the caller, transport key generated fresh for this rotation).- Login always fails with the same generic error — by design the server returns one identical error for an unknown key, a bad
signature, a wrong PIN, and a locked key. Track wrong-PIN retries locally; after
pin_max_attemptsconsecutive failures assume the key is locked and offer recovery. - Keys enrolled before user verification existed — legacy keys cannot log in and must be re-enrolled.
- Relaxed-attestation keys stop working — keys enrolled with relaxed attestation expire after 30 days and are refused as soon as the setting is turned off. See relaxed attestation.
Security
This section covers the cryptographic design, the second-factor attack surface, and the security model of the PIN and biometric first factor.
Cryptography
The security of this design relies on a chain of trust anchored in hardware and standard cryptographic primitives.
- Asymmetric Cryptography: ECDSA with P-256 is used for the device key pair. This is a modern, efficient, and widely supported standard for digital signatures. It is less computationally expensive than RSA.
- Hardware-Backed Keys: Private keys are generated and stored as non-exportable within the device's Secure Enclave (iOS) or Trusted Execution Environment (TEE)/StrongBox (Android). They cannot be accessed by the OS or any application, providing strong protection against extraction. As much as the APIs allow it, the keys are marked as requiring user authentication (the phone is unlocked) and a biometrics/PIN prompt.
- Hashing: SHA-256 is used for generating nonces and hashing challenges, providing standard collision resistance.
- Certificate Chains: X.509 certificates are used to establish the chain of trust. The device's attestation is signed by a key that is, in turn, certified by a platform authority (Apple or Google), ensuring the attestation's authenticity.
- No configurability: Intentionally, for simplicity, performance, auditability, and to avoid downgrade attacks, all cryptographic primitives are fixed.
Attack Surface and Mitigations
- Man-in-the-Middle (MitM) Attack
- Threat: An attacker intercepts and tries to modify the communication between the client and server.
- Mitigation: All communication occurs over TLS, encrypting the channel. More importantly, the core payloads (attestation and login signatures) are themselves digitally signed using the hardware-bound key. Any tampering would invalidate the signature, causing the server to reject the request.
- Replay Attacks
- Threat: An attacker captures a valid attestation or login payload and "replays" it to the server at a later time to gain access.
- Mitigation: The server generates a unique, single-use cryptographic challenge for every new enrollment or login attempt. This challenge is embedded in the certificate chain. The server verifies that the challenge in the payload is the exact one it issued for that specific session and reject any duplicates or expired challenges.
- Emulation & Software-Based Attacks
- Threat: An attacker attempts to enroll a software-based "device" (e.g., an emulator, a script) by faking an attestation.
- Mitigation: This is the central problem that hardware attestation solves. The server verifies the entire certificate chain of
the attestation object up to a trusted root CA (Apple or Google). Only genuine hardware can obtain a valid certificate chain.
The server also inspects attestation flags (e.g., Android's
attestationSecurityLevel) to explicitly reject any keys that are not certified as hardware-backed.
- Physical Attacks & Key Extraction
- Threat: An attacker with physical possession of the device attempts to extract the private signing key from memory.
- Mitigation: Keys are generated as non-exportable inside the hardware security module (Secure Enclave/TEE). This is a physical countermeasure that makes it computationally infeasible to extract key material, even with advanced hardware probing techniques.
- Compromised OS (Rooting/Jailbreaking)
- Threat: An attacker gains root access to the device's operating system.
- Mitigation: The attestation object contains signals about the integrity of the operating system. Android's attestation
includes
VerifiedBootState, which indicates if the bootloader is locked and the OS is unmodified. The server can enforce a policy to only accept attestations from devices in a secure state.
- Cross-App/Cross-Site Attacks
- Threat: An attacker tricks a user into generating an attestation for a malicious app that is then used to attack the service.
- Mitigation: The attestation object includes an identifier for the application that requested it. On iOS, the
authDatacontains therpIdHash(a hash of the App ID). The server can verify that this hash matches its own app's identifier to ensure the attestation originated from the legitimate, code-signed application.
- Malicious App Key Theft/Usage
- Threat: A different, malicious app installed on the same device attempts to access and use the private key generated by the legitimate app to impersonate the user.
- Mitigation: This is prevented by the fundamental application sandbox security model of both iOS and Android. Keys generated in the hardware-backed key store are cryptographically bound to the application identifier that created them. The operating system and the secure hardware enforce this separation, making it impossible for "App B" to access, request, or use a key generated by "App A".
- Malware and Keyloggers on a Compromised Device
- Threat: Malware, such as a keylogger, screen scraper, or accessibility service exploit, is active on the user's device and attempts to intercept credentials.
- Mitigation: This design is highly resistant to such attacks. The entire flow is passwordless, meaning there is no "typeable" secret for a keylogger to capture. The core secret (the private key) never leaves the secure hardware. The user authorizes its use via a biometric prompt, which is managed by a privileged part of the OS, isolated from the application space where malware would reside. A keylogger can neither intercept the biometric data nor the signing operation itself.
- Device Backup, Restore, and Cloning
- Threat: An attacker steals a user's cloud backup (e.g., iCloud or Google One) and restores it to a new device they control, hoping to clone the trusted device and its keys.
- Mitigation: This is mitigated by the non-exportable property of hardware-backed keys. While application data and metadata may be backed up and restored, the actual private key material never leaves the Secure Enclave or TEE. When the app is restored on a new device, the reference to the old key will be invalid, effectively breaking the binding and forcing the user to perform a new enrollment. Furthermore, resetting the device automatically erases all keys in the TEE/TPM.
- Biometric System Bypass
- Threat: An attacker with physical possession of the device attempts to bypass biometric authentication (e.g., using a lifted fingerprint, high-resolution photo, or 3D mask).
- Mitigation: The design relies on the platform-level biometric security. Since the hardware key is only unlocked for signing after the hardware confirms a match, the attacker must defeat the hardware manufacturer's physical anti-spoofing technologies.
- Server-Side Compromise (Database Leak)
- Threat: An attacker breaches the server and steals the database containing public keys and device IDs for all enrolled devices.
- Mitigation: Because this is an asymmetric system, the public keys are useless for authentication without the corresponding private keys. Even with a full database leak, the attacker cannot impersonate users because they cannot sign the login challenges.
- Server-Side Compromise (CA Trust Anchor)
- Threat: An attacker gains enough server access to modify the list of trusted Root CAs, allowing them to accept attestations from a rogue CA they control.
- Mitigation: The Root CA certificates for Apple and Google are hard-coded within the server-side application logic rather than relying on the general OS trust store. This prevents an attacker from using a compromised system-wide trust store to validate fraudulent device attestations. However, if the attacker can modify the server executable, all bets are off, because they can modify the in-memory root CAs or bypass the validation logic entirely.
- UI Redressing / Overlay Attack (Android)
- Threat: A malicious app with the "Draw over other apps" permission creates a transparent overlay on top of your app. When the user thinks they are clicking "Enroll Device" or approving a "Transaction Signing" prompt, they are actually clicking through a malicious flow hidden beneath.
- Mitigation:
- iOS: Inherently protected by the OS (overlays are not permitted over other apps).
- Android: We use the
setFilterTouchesWhenObscured(true)flag on sensitive UI components. This tells the Android OS to discard touch events if the window is obscured by another visible window. See tapjacking.
- Dependency / Supply Chain Attack
- Threat: An attacker compromises the Mobile SDK or a dependency. They inject code that leaks the challenge, or subtly alters device attestation.
- Mitigation:
- Minimized dependencies
- Automated dependency scanning
- Certificate pinning: The Ory server CA can be pinned in the mobile application/SDK to ensure the device is talking to the legitimate server.
- TLS & URL whitelisting: Both Android and iOS allow for URL whitelisting to avoid attacker controlled servers from being contacted.
- Signed Device Information: The TEE/TPM on the device signs the device information. Using Apple/Google root CAs, the server checks that this information, e.g. the application id, has not been altered.
- Attestation Misbinding Attack
- Threat: The attack manages to leak the challenge meant for another user (e.g. due to a supply chain attack in the mobile app code), they sign the challenge with the attacker device, and they submit that to the server before the legitimate user can, in order to register the attacker device for the other user account.
- Mitigation:
- Challenge bound to the identity id: The challenge is bound to the identity in the database (stored in the same row). Since the identity is detected from the session token, an attacker cannot tamper with the identity id (unless they steal the session token, at which point they are the user, from the server perspective).
Security model for PIN and biometric first factor
| Attacker has | Outcome |
|---|---|
| Stolen, locked device | The sealing key requires an unlocked device; the sealed secret is hardware-bound and PIN guesses can't be tested. |
| Exfiltrated backup or sealed blob | Opaque — the outer layer is sealed by a non-exportable hardware key that never leaves the original device. |
| Code execution on the unlocked device | Can unseal, but a wrong PIN yields garbage; the only oracle is the server, which locks the key after pin_max_attempts attempts (default 5). |
| Ory database dump (even with the encryption secrets) | The pin_secret — but login still requires the device's hardware key, and the PIN itself is not derivable. |
| TLS-terminating intermediary, request logs | Only HPKE ciphertext of the one-time secret delivery; the PIN never transits at all. |
Stated limitations:
- On iOS, biometric gating of a
platformkey cannot be proven by App Attest — it is trusted at enrollment. This is why iOS biometric-only first factor is an explicit opt-in (ios_biometric_first_factor). - PIN strength cannot be enforced server-side; the server never sees the PIN. Enforce policy in your app and rely on the lockout to bound weak-PIN damage.
- Locking a key destroys its secret. An attacker with deep device compromise can deliberately burn the attempt budget to force a recovery; recovery paths require a different login method.
Comparison with WebAuthn and Passkeys
It is useful to compare this custom implementation with the FIDO WebAuthn standard and the user-facing concept of Passkeys. While they share core cryptographic principles, their goals and scope are fundamentally different.
Similarities
- Core Cryptography: Both approaches are built on public-key cryptography (typically ECDSA), and use a challenge-response protocol
Key Differences
- Standard vs. proprietary:
- WebAuthn/passkeys: An open, interoperable standard from the W3C and FIDO Alliance, designed to work across different websites, apps, browsers, and operating systems.
- This Design: A proprietary implementation tailored specifically for Ory's native application and server. It is not intended to be interoperable with any other system. However the design is based on building blocks that are fully open and standardized: PKI, TPM 2.0, ASN1, iOS & Android device attestation, etc.
- Goal: Device Binding vs. synced credentials:
- WebAuthn/passkeys: The primary goal is to create a convenient and portable user credential (a Passkey). Passkeys are often syncable via a cloud service (like iCloud Keychain or Google Password Manager), allowing a user who enrolls on their phone to seamlessly sign in on their laptop without re-enrolling.
- This design: The primary goal is strict device binding. We are proving that a specific, individual piece of hardware is authorized. The key is explicitly non-exportable and bound to a single installation of an app on a single device. It physically cannot be synced or used elsewhere.
- Role of attestation:
- WebAuthn/passkeys: Attestation is an optional feature. While a server can request it to verify the properties of an authenticator, many services skip it in favor of a simpler user experience. The focus is on proving possession of the key, not on scrutinizing the device itself.
- This design: Attestation is mandatory and central to the entire security model. The main purpose of the enrollment ceremony is for the server to validate the device's hardware and software integrity.