-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Attestation (Android+ iOS): Best Practises, Know, Tests, Demos #3756
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Diolor
wants to merge
40
commits into
OWASP:master
Choose a base branch
from
Diolor:key-attestation-best-practice
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 28 commits
Commits
Show all changes
40 commits
Select commit
Hold shift + click to select a range
d31306f
Move MASTG-KNOW-0044 from MASVS-STORAGE to MASVS-RESILIENCE
Diolor dd0cc30
Clean BE/KW regarding attestation
Diolor d37cdac
Clean BE/KW regarding attestation
Diolor 4b197af
Enhance key attestation documentation with detailed explanations and …
Diolor 19c80a6
WIP attestation
Diolor a165a22
Enhance Android Key Attestation
Diolor 87a920e
Enhance Android Key Attestation
Diolor 057773b
Enhance Android Key Attestation
Diolor b2d567b
Make descriptive content
Diolor 02ac39a
grammar
Diolor bb98920
Fix irelevant info
Diolor b58d198
Add pointer
Diolor 02e6af6
Indents
Diolor 544ea58
Lint + fix prescriptive
Diolor 6007387
Move Integrity Signals to BEST
Diolor 76546c3
Update knowledge/android/MASVS-STORAGE/MASTG-KNOW-0045.md
Diolor aed967d
Use existing placeholders
Diolor 0ef2111
Add tests
Diolor bdbf5ce
Rename tests
Diolor 3f99519
Update best practices for Android Key Attestation and Play Integrity API
Diolor a62be1d
Create the test
Diolor d3364c4
Add demo
Diolor c428782
Minor changes on demo
Diolor 5801a63
Cleanup Google Integrity API
Diolor f983501
Add App Check and App/Device Attest for iOS
Diolor 8881bcc
Rename BP
Diolor 591fdb7
Lint
Diolor 597ce89
Add best practice guidelines for mitigating risks of hardcoded API keys
Diolor efeddb7
Use placeholder names
Diolor 54a0075
Merge branch 'master' into key-attestation-best-practice
Diolor 51ba825
Revert placeholder
Diolor 7a4504c
Revert placeholder
Diolor ad572c5
Use KNOW-0044
Diolor b6578de
Fix temp ids
Diolor 989879e
Merge branch 'master' into key-attestation-best-practice
Diolor efcb468
Revert
Diolor bcc6696
Restore fix_ids.py #3869 logic accidentally over-reverted
Diolor 9ba815c
Add iOS App Attest test (MASTG-TEST-0x01)
Diolor 667fcfb
Renumber attestation tests: Android 0x01, iOS 0x02
Diolor e954d30
Add placeholder iOS App Attest demo (MASTG-DEMO-0x02)
Diolor File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| --- | ||
| title: Use Hardware-Backed Key Attestation for Device and App Integrity | ||
| alias: android-hardware-backed-attestation | ||
| id: MASTG-BEST-0042 | ||
| platform: android | ||
| knowledge: [MASTG-KNOW-0035, MASTG-KNOW-0044, MASTG-KNOW-0047, MASTG-KNOW-0119, MASTG-KNOW-0120] | ||
| --- | ||
|
|
||
| Applications that perform business-critical operations, such as financial transactions, multi-factor authentication, or sensitive data handling, should verify the integrity of the device environment before trusting it as well as the integrity of the application binary. | ||
|
|
||
| For the majority of apps, **Google Play Integrity API** (@MASTG-KNOW-0035) is the recommended starting point. It provides a managed, server-backed attestation signal covering device integrity, app integrity, and licensing - without requiring apps to implement low-level certificate verification themselves. | ||
|
|
||
| **This best practice covers the manual attestation approach** using Android's hardware-backed Key Attestation (@MASTG-KNOW-0044). This approach should be used when Play Integrity limitations need to be addressed, or when the application requires more control. In this scenario, use Android's Key Attestation to cryptographically verify that the client's keys reside in hardware-backed storage and that the device has not been compromised. | ||
|
|
||
| ## Implement Server-Driven Attestation with a Fresh Challenge | ||
|
|
||
| Always drive attestation from the server using the challenge-response flow described in @MASTG-KNOW-0044. Generate a unique, cryptographically random challenge (nonce) for each attestation request using a Cryptographically Secure Pseudorandom Number Generator (CSPRNG). Never reuse challenges across requests. Never implement attestation verification solely on the client side. | ||
|
|
||
| Since attestation reflects the state of the device and application at the time of key generation rather than at the time of use, require fresh key generation: | ||
|
|
||
| - At critical moments such as the first app launch, account binding, or sensitive operation. | ||
| - When the installed app version falls below the minimum acceptable threshold. | ||
|
|
||
| Enforce short-lived keys or periodic re-attestation policies to reduce the window of exposure between a device's state changing and that change being detected. | ||
|
|
||
| ## Verify the Attestation Certificate Chain | ||
|
|
||
| On the server side, verify the attestation certificate chain (@MASTG-KNOW-0044): | ||
|
|
||
| - Verify the chain of trust up to the Google Hardware Attestation Root Certificate. | ||
| - Check each certificate against Google's Certificate Revocation Status List. | ||
| - Confirm that the embedded challenge matches the one the server originally issued. | ||
|
|
||
| ## Verify Device Integrity | ||
|
|
||
| Using the `rootOfTrust` fields described in @MASTG-KNOW-0120, confirm the device is in a trusted state: | ||
|
|
||
| - Require that `attestationSecurityLevel` is `TrustedEnvironment` or `StrongBox`, confirming the device integrity data is hardware-enforced and cannot be falsified by the Android OS. | ||
| - Verify `verifiedBootState` is `Verified`, indicating the full boot chain was validated against OEM keys. | ||
| - Verify `deviceLocked` is `true`, confirming the bootloader is locked and the system partition cannot be modified without detection. | ||
|
|
||
| The following conditions indicate low or no device integrity: | ||
|
|
||
| - **`verifiedBootState` is not `Verified`**: The boot chain was not fully verified against OEM keys. A `SelfSigned` state means the device is running a custom ROM with a user-installed key; `Unverified` means no verification was performed at all; `Failed` means verification was attempted and failed. | ||
| - **`deviceLocked` is `false`**: The bootloader is unlocked, meaning the system partition can be modified without triggering a boot failure. This is a strong signal that the device may have been tampered with. | ||
| - **`attestationSecurityLevel` is `Software`**: The attestation was generated entirely in the Android OS with no hardware involvement. It can be trusted as long as the device is running an operating system that complies with the [Android Platform Security Model](https://arxiv.org/pdf/1904.05572) (that is, the `deviceLocked` is `true` and the `verifiedBootState` is `Verified`). | ||
|
|
||
| ## Verify Application Integrity | ||
|
|
||
| Using the `attestationApplicationId` fields described in @MASTG-KNOW-0119, confirm the key was generated by the legitimate application: | ||
|
|
||
| - Verify `packageName` matches the expected application identifier. | ||
| - Verify `signatureDigests` match the expected signing certificate digests pre-provisioned on the server (e.g., from the app's release certificate). A mismatch indicates the APK has been repackaged or signed with a different key. | ||
| - Verify `version` is within an acceptable range to reject outdated or known-vulnerable versions. | ||
|
|
||
| The following conditions indicate low or no application integrity: | ||
|
|
||
| - **`signatureDigests` do not match**: The SHA-256 digests of the app's signing certificates differ from the legitimate app's known digests at the time of release. This is the primary indicator that the APK has been repackaged or signed with a different key (e.g., a malicious clone or a patched version of the app). | ||
| - **`version` is below the minimum acceptable version**: The app version recorded at key generation time is too old, indicating a version with known vulnerabilities or one that predates a security-relevant update. | ||
|
|
||
| ## Enforce Key Properties | ||
|
|
||
| Use the attestation extension data (@MASTG-KNOW-0044) to confirm that the attested key pair was generated with the expected properties: | ||
|
|
||
| - Restrict the key purpose to only the intended operations (e.g., signing, encryption). | ||
| - Require user authentication before key use when applicable (e.g., biometric binding via [`setUserAuthenticationRequired`](https://developer.android.com/reference/kotlin/android/security/keystore/KeyGenParameterSpec.Builder#setuserauthenticationrequired)). | ||
|
|
||
| ## Handle Attestation Failures Securely | ||
|
|
||
| If attestation verification fails, the server must not grant the client elevated trust or access to sensitive operations. Depending on the application's risk profile: | ||
|
|
||
| - Deny access to high-assurance features entirely. | ||
| - Fall back to alternative verification mechanisms with appropriate risk acceptance. | ||
| - Log the failure for monitoring and incident response. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| --- | ||
| title: Mitigate the Risk of API Keys Hardcoded in the App Package | ||
| alias: mitigate-hardcoded-api-keys | ||
| id: MASTG-BEST-0044 | ||
| platform: generic | ||
| knowledge: [MASTG-KNOW-0015, MASTG-KNOW-0035, MASTG-KNOW-0072, MASTG-KNOW-0118, MASTG-KNOW-0121, MASTG-KNOW-0122, MASTG-KNOW-0123] | ||
| --- | ||
|
|
||
| API keys embedded in the app package - whether in source code, resource files, or build artifacts - can be extracted through static analysis or binary inspection, even without a rooted device or special tooling. The ideal solution is to move the key server-side entirely so it never ships with the app binary. Often, that is not possible, so the following measures reduce the window of exploitation. | ||
|
|
||
| In all cases, never expose sensitive data such as API keys to clients whose integrity has not been verified. | ||
|
|
||
| ## Deliver API Keys Over-The-Air After Integrity Verification | ||
|
|
||
| Instead of embedding the key in the binary, fetch it at runtime from your backend only after the device and app have been attested: | ||
|
|
||
| 1. At app startup, perform app and device attestation (see @MASTG-BEST-0043). | ||
| 2. If attestation passes, the backend issues an API key (or a short-lived derivative token) over a secure, pinned channel. | ||
| 3. The app holds the key in memory for the session. If persistence is required, store it in platform-provided secure storage (Keychain on iOS, Android Keystore) rather than plain files. | ||
|
|
||
| This approach keeps the key out of the binary entirely, allows rotation without a new app release, and ensures only verified app instances on genuine devices ever receive it. It combines the protections described in the sections below into a single cohesive architecture. | ||
|
|
||
| ### Prefer a Server-Side Proxy for Third-Party API Calls | ||
|
|
||
| Where the architecture allows it, do not ship the API key in the app at all. Instead: | ||
|
|
||
| - Have the mobile app call your own backend (sometimes known as API proxy or API Gateway), which authenticates the request, then calls the third-party API using a server-stored key. | ||
| - This eliminates the key from the app package entirely and gives you full control over auditing and rotation. | ||
|
|
||
| ### Enforce App and Device Integrity Verification Before Use | ||
|
|
||
| Require the client to pass app and device integrity verification before any API key or scoped token is issued or accepted. Do not treat attestation as an optional layer on top of a hardcoded key - make it a precondition enforced on the server: | ||
|
|
||
| - **Android**: Play Integrity API (@MASTG-KNOW-0035) or Firebase App Check (@MASTG-KNOW-0121). | ||
| - **iOS**: App Attest (@MASTG-KNOW-0123) or DeviceCheck (@MASTG-KNOW-0122) via Firebase App Check (@MASTG-KNOW-0121). | ||
|
|
||
| See @MASTG-BEST-0043 for server-side enforcement requirements. | ||
|
|
||
| ### Enforce Network Integrity with Certificate Pinning | ||
|
|
||
| A network-level attacker who intercepts traffic can observe or replay API keys even if they were never in the binary. Pin the server certificate so that the key is only ever transmitted over a channel the app explicitly trusts: | ||
|
|
||
| - **Android**: configure pinning via the Network Security Configuration (@MASTG-KNOW-0015). | ||
| - **iOS**: implement server trust evaluation (@MASTG-KNOW-0072). | ||
|
|
||
| Security controls such as Certificate Pinning can only be trusted if the app's integrity has been verified and the device's controls can be trusted. | ||
|
|
||
| ### Apply RASP Controls Before and Between Attestations | ||
|
|
||
| Attestation is a point-in-time check - it verifies the environment at session start but does not detect changes that occur during execution. RASP (see @MASTG-KNOW-0118) fills this gap by running continuous in-process checks throughout the app's lifetime: | ||
|
|
||
| - **Before attestation**: run environment checks (debugger detection, emulator detection, hooking framework detection) before initiating the attestation flow. Abort early if the environment is hostile - do not expose network calls or key requests to an already-compromised process. | ||
| - **Between attestations**: monitor for runtime changes that could indicate an active attack - hook injection, memory tampering, or the appearance of reverse engineering tools after a previously clean attestation. Respond by revoking the locally held key, clearing it from memory, and requiring re-attestation before resuming sensitive operations. | ||
|
|
||
| See @MASTG-BEST-0029 for RASP signal implementation guidance. | ||
|
|
||
| ### Limit Credential Lifetime | ||
|
|
||
| Credentials - whether static API keys or issued tokens - should be treated as perishable. The shorter their effective lifetime, the smaller the window of exploitation if they are extracted or intercepted. | ||
|
|
||
| **If you control the API and issue tokens to the app**, do not issue long-lived static keys. Issue short-lived tokens instead: | ||
|
|
||
| - Use short-lived JWTs with an `exp` claim of minutes to a few hours, depending on the sensitivity of the operation. A stolen token becomes useless once it expires. | ||
| - Issue a refresh token alongside the access token and invalidate it on each use (refresh token rotation). This limits the damage window if a refresh token is intercepted and makes reuse detectable. | ||
| - Bind tokens to the attested client by including attestation claims in the JWT payload (e.g., device integrity verdict, app version, platform). The server can then reject tokens presented from a context that no longer passes integrity checks. | ||
| - Scope tokens narrowly to the minimum set of operations the app needs at that point in the session. | ||
| - Support revocation via a server-side denylist for high-risk scenarios where waiting for natural expiry is unacceptable. | ||
|
|
||
| **If you rely on third-party API keys that cannot be moved server-side**, treat them as perishable static secrets: | ||
|
|
||
| - Rotate keys on a defined schedule and immediately upon any suspected compromise. | ||
| - Use distinct keys per platform and per environment (development, staging, production) so that a leak in one context does not affect others. | ||
| - Automate rotation where the API provider supports it, and ensure the app can receive updated keys without a forced upgrade (e.g., via a remote configuration service backed by attestation). | ||
31 changes: 31 additions & 0 deletions
31
demos/android/MASVS-RESILIENCE/MASTG-DEMO-0109/MASTG-DEMO-0109.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| --- | ||
| platform: android | ||
| title: Key Attestation Without a Server-Issued Challenge | ||
| id: MASTG-DEMO-0109 | ||
| code: [kotlin] | ||
| test: MASTG-TEST-0342 | ||
| --- | ||
|
|
||
| ## Sample | ||
|
|
||
| This sample generates an EC key pair in the Android KeyStore (hardware-backed via the TEE) and retrieves the resulting certificate chain to send to a server for verification. However, no attestation challenge is set via [`setAttestationChallenge`](https://developer.android.com/reference/kotlin/android/security/keystore/KeyGenParameterSpec.Builder#setattestationchallenge) during key generation, so the server cannot determine when the attestation was produced. | ||
|
|
||
| {{ MastgTest.kt # MastgTest_reversed.java }} | ||
|
|
||
| ## Steps | ||
|
|
||
| Let's run @MASTG-TOOL-0110 against the reversed Java code. | ||
|
|
||
| {{ ../../../../rules/mastg-android-key-attestation-missing-challenge.yml }} | ||
|
|
||
| {{ run.sh }} | ||
|
|
||
| ## Observation | ||
|
|
||
| The rule identifies one location where a `KeyGenParameterSpec` is built via method chaining without `setAttestationChallenge`. The certificate chain created from this spec is sent to the server without any embedded nonce. | ||
|
|
||
| {{ output.txt }} | ||
|
|
||
| ## Evaluation | ||
|
|
||
| The test fails because the output shows that `KeyGenParameterSpec.build()` is invoked without `setAttestationChallenge` anywhere in the chain. The `attestationChallenge` field in the leaf certificate will be null, so the server cannot verify the freshness of the attestation and cannot distinguish a freshly generated certificate chain from a replayed one. See @MASTG-BEST-0042 for the correct server-driven challenge-response flow. |
51 changes: 51 additions & 0 deletions
51
demos/android/MASVS-RESILIENCE/MASTG-DEMO-0109/MastgTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package org.owasp.mastestapp | ||
|
|
||
| import android.content.Context | ||
| import android.security.keystore.KeyGenParameterSpec | ||
| import android.security.keystore.KeyProperties | ||
| import android.util.Base64 | ||
| import java.security.KeyPairGenerator | ||
| import java.security.KeyStore | ||
| import java.security.spec.ECGenParameterSpec | ||
|
|
||
| class MastgTest(private val context: Context) { | ||
|
|
||
| private val keyAlias = "mastgAttestationKey" | ||
|
|
||
| fun mastgTest(): String { | ||
| val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } | ||
|
|
||
| if (keyStore.containsAlias(keyAlias)) { | ||
| keyStore.deleteEntry(keyAlias) | ||
| } | ||
|
|
||
| val kpg = KeyPairGenerator.getInstance( | ||
| KeyProperties.KEY_ALGORITHM_EC, | ||
| "AndroidKeyStore" | ||
| ) | ||
|
|
||
| // INSECURE: No attestation challenge is set. | ||
| // The server cannot verify when this attestation was produced. | ||
| val spec = KeyGenParameterSpec.Builder( | ||
| keyAlias, | ||
| KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY | ||
| ) | ||
| .setDigests(KeyProperties.DIGEST_SHA256) | ||
| .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) | ||
| .build() | ||
|
|
||
| kpg.initialize(spec) | ||
| kpg.generateKeyPair() | ||
|
|
||
| val certChain = keyStore.getCertificateChain(keyAlias) | ||
|
|
||
| val sb = StringBuilder() | ||
| sb.appendLine("Sending certificate chain to https://example.com/attestation-verify ...") | ||
| sb.appendLine("Chain length: ${certChain.size}") | ||
| certChain.forEachIndexed { i, cert -> | ||
| val pem = Base64.encodeToString(cert.encoded, Base64.DEFAULT) | ||
| sb.appendLine("Certificate[$i] (first 60 chars): ${pem.take(60)}...") | ||
| } | ||
| return sb.toString() | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a best for MASWE-0005. Attestation is a critical component of it.