Skip to content
Open
Show file tree
Hide file tree
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 May 14, 2026
dd0cc30
Clean BE/KW regarding attestation
Diolor May 14, 2026
d37cdac
Clean BE/KW regarding attestation
Diolor Feb 9, 2026
4b197af
Enhance key attestation documentation with detailed explanations and …
Diolor Feb 9, 2026
19c80a6
WIP attestation
Diolor Feb 12, 2026
a165a22
Enhance Android Key Attestation
Diolor Apr 1, 2026
87a920e
Enhance Android Key Attestation
Diolor Apr 1, 2026
057773b
Enhance Android Key Attestation
Diolor Apr 1, 2026
b2d567b
Make descriptive content
Diolor Apr 1, 2026
02ac39a
grammar
Diolor Apr 1, 2026
bb98920
Fix irelevant info
Diolor Apr 2, 2026
b58d198
Add pointer
Diolor Apr 2, 2026
02e6af6
Indents
Diolor Apr 2, 2026
544ea58
Lint + fix prescriptive
Diolor Apr 8, 2026
6007387
Move Integrity Signals to BEST
Diolor Apr 8, 2026
76546c3
Update knowledge/android/MASVS-STORAGE/MASTG-KNOW-0045.md
Diolor Apr 8, 2026
aed967d
Use existing placeholders
Diolor May 12, 2026
0ef2111
Add tests
Diolor May 17, 2026
bdbf5ce
Rename tests
Diolor May 17, 2026
3f99519
Update best practices for Android Key Attestation and Play Integrity API
Diolor May 19, 2026
a62be1d
Create the test
Diolor May 19, 2026
d3364c4
Add demo
Diolor May 19, 2026
c428782
Minor changes on demo
Diolor May 20, 2026
5801a63
Cleanup Google Integrity API
Diolor May 20, 2026
f983501
Add App Check and App/Device Attest for iOS
Diolor May 20, 2026
8881bcc
Rename BP
Diolor May 20, 2026
591fdb7
Lint
Diolor May 20, 2026
597ce89
Add best practice guidelines for mitigating risks of hardcoded API keys
Diolor May 20, 2026
efeddb7
Use placeholder names
Diolor Jun 14, 2026
54a0075
Merge branch 'master' into key-attestation-best-practice
Diolor Jun 14, 2026
51ba825
Revert placeholder
Diolor Jun 14, 2026
7a4504c
Revert placeholder
Diolor Jun 14, 2026
ad572c5
Use KNOW-0044
Diolor Jun 14, 2026
b6578de
Fix temp ids
Diolor Jun 14, 2026
989879e
Merge branch 'master' into key-attestation-best-practice
Diolor Jun 14, 2026
efcb468
Revert
Diolor Jun 14, 2026
bcc6696
Restore fix_ids.py #3869 logic accidentally over-reverted
Diolor Jun 14, 2026
9ba815c
Add iOS App Attest test (MASTG-TEST-0x01)
Diolor Jun 20, 2026
667fcfb
Renumber attestation tests: Android 0x01, iOS 0x02
Diolor Jun 20, 2026
e954d30
Add placeholder iOS App Attest demo (MASTG-DEMO-0x02)
Diolor Jun 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 2 additions & 10 deletions Document/0x06a-Platform-Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,16 +170,8 @@ The following APIs [require user permission](https://www.apple.com/business/docs

### DeviceCheck

The DeviceCheck framework, including its components DeviceCheck and App Attest, helps you prevent fraudulent use of your services. It consists of a framework that you use from your app and an Apple server which is accessible only to your own server. DeviceCheck allows you to persistently store information on the device and on Apple servers. The stored information remains intact across app reinstallation, device transfers, or resets, with the option to reset this data periodically.

DeviceCheck is typically used to mitigate fraud by restricting access to sensitive resources. For example, limiting promotions to once per device, identify and flag fraudulent devices, etc. However, it definitely cannot prevent all fraud. For example, it is [not meant to detect compromised operating systems](https://swiftrocks.com/app-attest-apple-protect-ios-jailbreak "App Attest: How to prevent an iOS app's APIs from being abused") (aka. jailbreak detection).

For more information, refer to the [DeviceCheck documentation](https://developer.apple.com/documentation/devicecheck "DeviceCheck documentation").
See @MASTG-KNOW-0122.

#### App Attest

App Attest, available under the DeviceCheck framework, helps you verify instances of the app running on a device by enabling apps to attach a hardware-backed assertion to requests, ensuring they originate from the legitimate app on a genuine Apple device. This feature aids in preventing modified apps from communicating with your server.

The process involves generating and validating cryptographic keys, along with a set of verifications performed by your server, ensuring the authenticity of the request. It is important to note that while App Attest enhances security, it does not guarantee complete protection against all forms of fraudulent activities.

For more detailed information, refer to the [WWDC 2021](https://developer.apple.com/videos/play/wwdc2021/10244 "WWDC 2021") session, along with the ["DeviceCheck documentation"](https://developer.apple.com/documentation/devicecheck/) and ["Validating apps that connect to your server"](https://developer.apple.com/documentation/devicecheck/validating-apps-that-connect-to-your-server).
See @MASTG-KNOW-0123.
74 changes: 74 additions & 0 deletions best-practices/MASTG-BEST-0042.md
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.
73 changes: 73 additions & 0 deletions best-practices/MASTG-BEST-0044.md
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

Copy link
Copy Markdown
Collaborator Author

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.

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 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0109/MASTG-DEMO-0109.md
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 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0109/MastgTest.kt
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()
}
}
Loading