How to use Apple App Attest in your Unity game

Apple App Attest lets your server check that a request comes from your real iOS game on real Apple hardware. Here is how to set it up and call it from Unity with a small native bridge.

By Tim UhlottFounder|Last updated: November 11, 2026|8 minutes read
cybersecurityiosintegrity
How to use Apple App Attest in your Unity game
On Android, Google gives you the Play Integrity API. On iPhone and iPad, Apple has its own answer: App Attest. It lets your server check that a request comes from a real, unmodified copy of your game running on real Apple hardware. Unity has no official plugin for it, so we will write a small native bridge ourselves. It is less than 30 lines of Objective-C. But let us start with what App Attest actually checks.

What is App Attest?

App Attest is part of Apple's DeviceCheck framework and works on iOS 14 and newer. Your game creates a cryptographic key inside the device's Secure Enclave. Apple then signs a statement that says "this key belongs to a genuine instance of this app on genuine Apple hardware". Your server checks that statement once and stores the public key. After that, your game signs important requests with the key. Apple calls these signatures assertions, and your server verifies each one with the stored public key. A modified game cannot produce a valid assertion, and neither can a script that only replays network traffic. Apple says it plainly (opens in a new tab): a compromised app can fake the result of any check it runs on itself. So the trust comes from the hardware and from Apple's servers, and your server makes the decision.

How App Attest works

StepWhenWhat happens
Generate keyOnce per install and usergenerateKey creates a key in the Secure Enclave and returns a keyId
Attest keyOnce, right afterThe game hashes a server challenge, calls attestKey, and the server verifies the result with Apple's root certificate
AssertOn important requestsgenerateAssertion signs the request; the server checks the signature and a counter
The challenge is a random value from your server, at least 16 bytes, so nobody can replay an old attestation. The counter in each assertion has to go up every time, so nobody can replay an old assertion either. Keys survive app updates. A reinstall, a restore from backup, or a new device loses the key, and the game just creates a new one.

How to set up App Attest

  1. In your Apple Developer account, open your App ID under Certificates, Identifiers & Profiles and enable the App Attest capability.
  2. Add the entitlement com.apple.developer.devicecheck.appattest-environment to the app. During development Apple uses a sandbox environment. Builds from TestFlight and the App Store always use production.
  3. Link DeviceCheck.framework to the Xcode project.
Unity regenerates the Xcode project on every build, so I would do steps 2 and 3 in a post-process script instead of by hand:
#if UNITY_IOS using UnityEditor; using UnityEditor.Callbacks; using UnityEditor.iOS.Xcode; public static class AppAttestPostProcess { [PostProcessBuild] public static void OnPostProcess(BuildTarget target, string path) { if (target != BuildTarget.iOS) return; string projPath = PBXProject.GetPBXProjectPath(path); var proj = new PBXProject(); proj.ReadFromFile(projPath); string mainTarget = proj.GetUnityMainTargetGuid(); proj.AddFrameworkToProject(proj.GetUnityFrameworkTargetGuid(), "DeviceCheck.framework", false); var ent = new PlistDocument(); ent.root.SetString("com.apple.developer.devicecheck.appattest-environment", EditorUserBuildSettings.development ? "development" : "production"); ent.WriteToFile(path + "/game.entitlements"); proj.AddFile("game.entitlements", "game.entitlements"); proj.SetBuildProperty(mainTarget, "CODE_SIGN_ENTITLEMENTS", "game.entitlements"); proj.WriteToFile(projPath); } } #endif

How to use App Attest in Unity

We need a small Objective-C bridge. Put this file at Assets/Plugins/iOS/AppAttestBridge.mm:
#import <DeviceCheck/DeviceCheck.h> static void Send(const char* method, NSString* msg) { UnitySendMessage("AppAttest", method, msg.UTF8String); } extern "C" bool AppAttest_IsSupported() { return DCAppAttestService.sharedService.isSupported; } extern "C" void AppAttest_Attest(const char* challengeHashB64) { NSData* hash = [[NSData alloc] initWithBase64EncodedString:@(challengeHashB64) options:0]; DCAppAttestService* service = DCAppAttestService.sharedService; [service generateKeyWithCompletionHandler:^(NSString* keyId, NSError* error) { if (error) { Send("OnError", error.localizedDescription); return; } [service attestKey:keyId clientDataHash:hash completionHandler:^(NSData* att, NSError* err) { if (err) { Send("OnError", err.localizedDescription); return; } Send("OnAttested", [NSString stringWithFormat:@"%@|%@", keyId, [att base64EncodedStringWithOptions:0]]); }]; }]; }
And the C# side, on a GameObject named AppAttest:
using System; using System.Runtime.InteropServices; using System.Security.Cryptography; using UnityEngine; public class AppAttest : MonoBehaviour { #if UNITY_IOS && !UNITY_EDITOR [DllImport("__Internal")] private static extern bool AppAttest_IsSupported(); [DllImport("__Internal")] private static extern void AppAttest_Attest(string challengeHashB64); #else private static bool AppAttest_IsSupported() => false; private static void AppAttest_Attest(string _) { } #endif public void Attest(byte[] serverChallenge) { if (!AppAttest_IsSupported()) return; // Simulator or old device. Tell the server. using var sha = SHA256.Create(); AppAttest_Attest(Convert.ToBase64String(sha.ComputeHash(serverChallenge))); } private void OnAttested(string msg) { string[] parts = msg.Split('|'); PlayerPrefs.SetString("AppAttestKeyId", parts[0]); // The ID is not secret. // Send parts[0] (keyId) and parts[1] (attestation) to your server. } private void OnError(string error) => Debug.LogWarning("App Attest: " + error); }
For assertions, add a second native function that calls generateAssertion:clientDataHash:completionHandler: with the stored key ID. Use a hash of the request body plus a fresh server challenge.

How to verify on your server

Apple does not ship a server SDK, so your backend follows Apple's validation steps (opens in a new tab). Your server:
  • checks the certificate chain against Apple's App Attest root certificate
  • checks that the nonce matches your challenge, so the attestation is fresh
  • checks that the App ID hash matches your Team ID and bundle ID
  • checks that the counter is 0 and the key ID matches the public key
  • stores the public key, and later verifies every assertion signature and a rising counter
Open-source libraries exist for several server languages. If you use one, compare it against Apple's list before you trust it. The attestation also contains a receipt. Your server can send it to Apple and get back a risk metric: roughly how many keys your app created on that device in the last 30 days. A high number can mean one compromised device is signing requests for many fake clients.

Why App Attest helps your game

iOS is less locked down than many developers assume. There are jailbreaks, re-signed IPAs, and tools that replay network traffic. With App Attest your server can reject reward claims, leaderboard scores, or purchase grants that do not come from your real build. I would roll it out gently. Apple suggests adding no more than 10 million users per day (opens in a new tab), and it can throttle attestation traffic. Devices without support and the simulator need a fallback path, too. Keep its limits in mind. Like Play Integrity, App Attest does not stop memory editing on a jailbroken phone that still holds a valid key. Once the key is attested, the server trusts what that device signs.
Takeaway: Sign the requests that give something away first: rewards, scores, and purchases. Start with soft consequences, like no ranked play, before you block anyone.

Conclusion

Enable the capability, add the entitlement in a post-process script, attest once, and sign the requests that matter. Your server checks everything. If you already use Play Integrity on Android (opens in a new tab), the same backend logic covers both platforms. For the client side, the part App Attest cannot see, take a look at GuardingPearSoftware Anti-Cheat.
00 views
00 shares

Discussion about this post

Comments are reviewed before they appear on the article.

No comments yet. Be the first to start the discussion.

Frequently asked questions

Newsletter

Stay in the Loop.

Subscribe to our newsletter to receive the latest news, updates, and special offers directly in your inbox. Don't miss out!