From f1c41237a990e214a77ef0d358d7167592eafc0b Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 8 Jan 2026 17:35:02 -0500 Subject: [PATCH 01/94] docs: add docs --- docs/api-reference.md | 49 ++++++++++++++ docs/architecture.md | 46 +++++++++++++ docs/contributing.md | 12 ++++ docs/getting-started.md | 37 +++++++++++ docs/index.md | 58 ++++++++++++++++ docs/prompt.md | 72 ++++++++++++++++++++ docs/providers.md | 136 +++++++++++++++++++++++++++++++++++++ docs/usage.md | 144 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 554 insertions(+) create mode 100644 docs/api-reference.md create mode 100644 docs/architecture.md create mode 100644 docs/contributing.md create mode 100644 docs/getting-started.md create mode 100644 docs/index.md create mode 100644 docs/prompt.md create mode 100644 docs/providers.md create mode 100644 docs/usage.md diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 00000000..cc122efd --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,49 @@ +# API Reference + +This section provides a high-level overview of the main classes and protocols within `AppCheckCore`. For detailed API documentation, please refer to the header files directly. + +## Core Classes + +### `GACAppCheck` +The central class for managing App Check tokens. It serves as the primary entry point for your application to interact with the App Check system. + +* **Purpose:** Manages the lifecycle of App Check tokens, including fetching, caching, and refreshing. It delegates attestation logic to an `GACAppCheckProvider` instance. +* **Key Methods:** + * `tokenForcingRefresh:completion:`: Requests an App Check token, with an option to force a refresh, bypassing the cache. + * `limitedUseTokenWithCompletion:`: Requests a limited-use App Check token, which does not affect the primary token's refresh cycle. + +### `GACAppCheckSettings` +Provides configurable settings for the `AppCheckCore` library. + +* **Purpose:** Allows customization of various behaviors, such as token refresh intervals or logging levels. + +### `GACAppCheckToken` +Represents an App Check token received from the App Check backend. + +* **Properties:** + * `token` (`NSString *`): The actual App Check token string. + * `expirationDate` (`NSDate *`): The date and time when the token expires. + +### `GACAppCheckTokenResult` +A wrapper object containing either an `GACAppCheckToken` upon success or an `NSError` upon failure. + +* **Properties:** + * `token` (`GACAppCheckToken * _Nullable`): The App Check token if the request was successful. + * `error` (`NSError * _Nullable`): An error object if the token request failed. + +## Protocols + +### `GACAppCheckProvider` +A protocol that defines the interface for App Check providers. Custom providers must conform to this protocol. + +* **Purpose:** Abstracts the specifics of how App Check tokens are obtained. Implementations interact with platform-specific attestation services or provide mock tokens. +* **Key Methods:** + * `getTokenWithCompletion:`: Asynchronously fetches a new App Check token. + * `getLimitedUseTokenWithCompletion:`: Asynchronously fetches a new limited-use App Check token. + +### `GACAppCheckTokenDelegate` +A protocol for delegates that wish to receive notifications about App Check token updates. + +* **Purpose:** Allows your application to react to changes in the App Check token, such as when a new token is fetched or an existing one is refreshed. +* **Key Methods:** + * `appCheck:didChangeToken:`: Notifies the delegate when the App Check token changes. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 00000000..c571e154 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,46 @@ +# Architecture & Design + +This document details the internal architecture of `AppCheckCore`, focusing on token storage, lifecycle management, and security mechanisms. + +## Token Storage +App Check tokens are sensitive credentials that grant access to your backend resources. `AppCheckCore` treats them with high security. + +### Keychain Storage +The `GACAppCheckStorage` class is responsible for persisting App Check tokens. +* **Mechanism:** It uses the iOS Keychain via `GULKeychainStorage`. +* **Service Name:** `com.google.app_check_core.token_storage`. +* **Data Protection:** Tokens are stored as `GACAppCheckStoredToken` objects (conforming to `NSSecureCoding`). +* **Access Groups:** Supports sharing tokens across apps/extensions via Keychain Access Groups (configurable during initialization). + +### Artifact Storage (App Attest) +For the App Attest provider, intermediate artifacts are also stored to maintain a stable device identity. +* **Class:** `GACAppAttestArtifactStorage` +* **Storage:** Keychain. +* **Key Suffix:** Keys are namespaced by the service name and resource name (e.g., `my-sdk.projects/123/apps/abc`) to prevent collisions. + +## Token Lifecycle Management +The `GACAppCheck` class acts as the central coordinator. + +1. **Request:** The app requests a token via `token(forcingRefresh:completion:)`. +2. **Cache Check:** + * If `forcingRefresh` is `NO`: Checks `GACAppCheckStorage` for a valid, non-expired token. + * **Buffer Time:** Tokens are considered "expired" slightly before their actual expiration time to account for clock skew and network latency. +3. **Fetch (if needed):** + * If the cache is empty or expired, or `forcingRefresh` is `YES`, a request is made to the configured `GACAppCheckProvider`. +4. **Storage:** + * Upon successful retrieval, the new token is written to `GACAppCheckStorage`. + * Any old token is overwritten. +5. **Completion:** The token (cached or new) is returned to the caller. + +## Backoff Strategy +To prevent overwhelming the backend or Apple's servers during failures, `AppCheckCore` implements an exponential backoff strategy. +* **Class:** `GACAppCheckBackoffWrapper` +* **Usage:** Providers (`GACAppAttestProvider`, `GACDeviceCheckProvider`) wrap their network and attestation calls in this backoff mechanism. +* **Behavior:** Retries with increasing delays on retryable errors (e.g., network timeouts, temporary server errors 503). Non-retryable errors (e.g., 403 Forbidden, 400 Bad Request) fail immediately. + +## Threading Model +* **Concurrency:** `AppCheckCore` is designed to be thread-safe. +* **Queues:** + * **Main Queue:** Completion handlers are typically dispatched to the main queue (or a user-specified queue if the API supported it, but currently defaults to main for top-level APIs). + * **Internal Queues:** Providers use private serial queues (e.g., `com.google.GACAppAttestProvider`) to manage state and sequentialize complex attestation flows (like generating a key, then attesting, then exchanging). + * **Background:** Network requests are performed on background queues (`QOS_CLASS_DEFAULT` or `QOS_CLASS_UTILITY`). diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 00000000..75599838 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,12 @@ +# Contributing to App Check Core + +We welcome contributions to the App Check Core library! + +For detailed information on how to contribute, including guidelines for code style, testing, and pull requests, please refer to the main [CONTRIBUTING.md](../../CONTRIBUTING.md) file in the root of the repository. + +## Development Setup + +To set up your development environment and get started with App Check Core: + +1. **Run Setup Scripts:** Execute `setup-scripts.sh` from the project root to install necessary tools and dependencies. +2. **Generate Project Files:** Use the appropriate commands (e.g., `pod install` for CocoaPods or Xcode's Swift Package Manager integration) to generate Xcode project files. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 00000000..957d6d3d --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,37 @@ +# Getting Started + +## Installation + +### CocoaPods +To integrate `AppCheckCore` using CocoaPods, add the following to your `Podfile`: + +```ruby +pod 'AppCheckCore' +``` + +Then, run `pod install` from your terminal. + +### Swift Package Manager +You can add `AppCheckCore` to your project using Swift Package Manager in Xcode or by adding it to your `Package.swift` file. + +**Xcode:** +1. In Xcode, go to `File > Add Packages...` +2. Enter the repository URL: `https://github.com/google/app-check` +3. Choose the desired version or branch. + +**Package.swift:** +Add the following to your `Package.swift` dependencies: + +```swift +.package(url: "https://github.com/google/app-check", from: "1.0.0"), // Replace with the latest version +``` + +And then add `"AppCheckCore"` to your target's dependencies. + +## Prerequisites +`AppCheckCore` supports the following minimum OS versions: +* iOS 12.0+ +* macCatalyst 13.0+ +* macOS 10.15+ +* tvOS 13.0+ +* watchOS 7.0+ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..1c0c44b6 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,58 @@ +# App Check Core - Documentation + +## Introduction +`AppCheckCore` is the underlying engine for app attestation and token management, primarily used by the Firebase iOS SDK but designed for broader internal Google use. It provides a robust and secure way to verify the authenticity of app instances accessing your backend resources. This library supports applications running on iOS, macOS, tvOS, and watchOS. + +## Key Features +* **Token Management:** Handles the lifecycle of App Check tokens, including caching and automatic refreshing to ensure continuous protection. +* **Provider Abstraction:** Abstracts different attestation providers, allowing for flexible integration with various platform-specific integrity mechanisms. +* **Limited-Use Tokens:** Supports the generation and management of limited-use tokens for scenarios requiring single-use or short-lived authentication. + +## Documentation Sections +* [Getting Started](getting-started.md): Installation and basic setup. +* [Usage Guide](usage.md): How to initialize and fetch tokens. +* [Providers Deep Dive](providers.md): Detailed architectural breakdown of App Attest, DeviceCheck, and Debug providers, including sequence diagrams. +* [Architecture](architecture.md): Internal design details regarding token storage, caching strategies, and threading. +* [API Reference](api-reference.md): High-level class overview. + +## High-Level Architecture +This diagram illustrates the core relationships. For detailed sequence flows, see [Providers Deep Dive](providers.md). + +```mermaid +classDiagram + class GACAppCheck { + +tokenForcingRefresh:completion: + +limitedUseTokenWithCompletion: + } + + class GACAppCheckProvider { + <> + +getTokenWithCompletion: + +getLimitedUseTokenWithCompletion: + } + + class GACAppAttestProvider { + -AppAttestService + -KeyStorage + -ArtifactStorage + } + + class GACDeviceCheckProvider { + -DCDevice + } + + class GACAppCheckDebugProvider { + -LocalDebugToken + } + + class GACAppCheckToken { + +token: String + +expirationDate: Date + } + + GACAppCheck "1" *-- "1" GACAppCheckProvider : uses + GACAppCheckProvider <|-- GACAppAttestProvider : implements + GACAppCheckProvider <|-- GACDeviceCheckProvider : implements + GACAppCheckProvider <|-- GACAppCheckDebugProvider : implements + GACAppCheck --> GACAppCheckToken : returns +``` \ No newline at end of file diff --git a/docs/prompt.md b/docs/prompt.md new file mode 100644 index 00000000..dabf6600 --- /dev/null +++ b/docs/prompt.md @@ -0,0 +1,72 @@ +Create a comprehensive documentation website structure in a `docs/` directory for the **App Check Core** library. The documentation should be written in Markdown and include the following pages. Use Mermaid diagrams where appropriate to illustrate architecture and flows. + +### 1. `docs/index.md` (Home/Overview) +* **Title:** App Check Core - Documentation +* **Introduction:** Explain that `AppCheckCore` is the underlying engine for app attestation and token management, primarily used by the Firebase iOS SDK but designed for broader internal Google use. Mention it supports iOS, macOS, tvOS, and watchOS. +* **Key Features:** + * Manages App Check tokens (caching, refreshing). + * Abstracts different attestation providers (DeviceCheck, AppAttest, Debug). + * Handles limited-use tokens. +* **Architecture Diagram (Mermaid):** Create a class diagram showing the relationship between: + * `GACAppCheck` (The core manager) + * `GACAppCheckProvider` (The protocol) + * Implementations: `GACAppAttestProvider`, `GACDeviceCheckProvider`, `GACAppCheckDebugProvider`. + * `GACAppCheckToken` (The result). + +### 2. `docs/getting-started.md` +* **Installation:** + * **CocoaPods:** Explain how to add `pod 'AppCheckCore'` to the Podfile. + * **Swift Package Manager:** Explain how to add the package via Xcode or `Package.swift` (URL: `https://github.com/google/app-check`). +* **Prerequisites:** List minimum OS versions (iOS 12.0+, macOS 10.15+, etc., based on `Package.swift`). + +### 3. `docs/usage.md` (Core Integration) +* **Initialization:** + * Show how to initialize a provider (e.g., `GACAppAttestProvider`). + * Show how to initialize `GACAppCheck` with that provider, a service name, and a resource name. + * *Code Example (Swift & Obj-C):* + ```swift + let provider = AppCheckCoreAppAttestProvider(serviceName: "my-sdk", resourceName: "projects/123/apps/abc", ...) + let appCheck = AppCheckCore(serviceName: "my-sdk", resourceName: "...", appCheckProvider: provider, ...) + ``` +* **Fetching Tokens:** + * Explain `token(forcingRefresh:completion:)`. + * Explain `limitedUseToken(completion:)`. + * *Code Example:* How to call these methods and handle the `GACAppCheckTokenResult`. +* **Sequence Diagram (Mermaid):** A sequence diagram showing the flow: + 1. App asks `GACAppCheck` for a token. + 2. `GACAppCheck` checks cache. + 3. If missing/expired, `GACAppCheck` asks `GACAppCheckProvider` for a token. + 4. Provider contacts Apple/Backend. + 5. Token returned and cached. + +### 4. `docs/providers.md` +* **Overview:** Explain the concept of providers. +* **AppAttest Provider (`GACAppAttestProvider`):** + * Best for modern iOS devices (A11 chip+). + * Wraps `DCAppAttestService`. + * Requires `DeviceCheck` framework linkage. +* **DeviceCheck Provider (`GACDeviceCheckProvider`): + * Uses Apple's older `DCDevice` API. + * Good fallback for older devices. +* **Debug Provider (`GACAppCheckDebugProvider`): + * Explain its use for local development and CI (simulators). + * Mention it generates a local debug secret that needs to be registered in the backend. + +### 5. `docs/api-reference.md` +* **Core Classes:** + * `GACAppCheck`: The main entry point. + * `GACAppCheckSettings`: Managing settings. + * `GACAppCheckToken`: Properties (token string, expiration date). + * `GACAppCheckTokenResult`: Wrapper for token + error. +* **Protocols:** + * `GACAppCheckProvider`: The interface for creating custom providers. + * `GACAppCheckTokenDelegate`: For listening to token changes. + +### 6. `docs/contributing.md` +* Link to the root `CONTRIBUTING.md`. +* Briefly mention the development setup (running `setup-scripts.sh`, generating project files). + +### General Guidelines +* Use clear, professional language. +* Provide Swift and Objective-C code snippets where relevant (the library is Obj-C but Swift-friendly). +* Ensure the directory structure matches the requested `docs/` layout. diff --git a/docs/providers.md b/docs/providers.md new file mode 100644 index 00000000..52ab270d --- /dev/null +++ b/docs/providers.md @@ -0,0 +1,136 @@ +# App Check Providers: Deep Dive + +This document details the internal design and detailed flows of each App Check provider. + +## AppAttest Provider (`GACAppAttestProvider`) +The most complex provider, interacting with `DCAppAttestService`. It maintains a stable key pair on the device to sign assertions. + +### Components +* **Service:** `DCAppAttestService` (Apple's API). +* **Storage:** + * `GACAppAttestKeyIDStorage`: Stores the generated App Attest Key ID. + * `GACAppAttestArtifactStorage`: Stores the "artifact" returned by the Firebase backend after a successful initial handshake. This artifact effectively links the on-device key to the backend session. + +### Flow 1: Initial Handshake (Attestation) +Occurs when the app runs for the first time or if the stored artifact is missing/corrupted. + +```mermaid +sequenceDiagram + participant App + participant Provider as GACAppAttestProvider + participant Apple as DCAppAttestService + participant API as GACAppAttestAPIService + participant Backend as Firebase Backend + + App->>Provider: getToken() + Provider->>API: getRandomChallenge() + API->>Backend: POST /generateAppAttestChallenge + Backend-->>API: { "challenge": "..." } + + par Parallel Execution + Provider->>Apple: generateKey() + Apple-->>Provider: Key ID + and + Provider->>API: (Challenge received) + end + + Provider->>Apple: attestKey(keyId, clientDataHash=SHA256(challenge)) + Apple-->>Provider: Attestation Object + + Provider->>API: attestKeyWithAttestation(attestation, keyID, challenge) + API->>Backend: POST /exchangeAppAttestAttestation + Note right of Backend: Verifies attestation validity
and app integrity. + Backend-->>API: { "token": "...", "artifact": "..." } + + Provider->>Provider: Store Artifact & Key ID + Provider-->>App: App Check Token +``` + +### Flow 2: Token Refresh (Assertion) +Occurs for subsequent requests. It's faster and uses the established key pair. + +```mermaid +sequenceDiagram + participant App + participant Provider as GACAppAttestProvider + participant Apple as DCAppAttestService + participant API as GACAppAttestAPIService + participant Backend as Firebase Backend + + App->>Provider: getToken() + Provider->>API: getRandomChallenge() + API->>Backend: POST /generateAppAttestChallenge + Backend-->>API: { "challenge": "..." } + + Provider->>Provider: Retrieve stored Artifact + Provider->>Provider: ClientData = Artifact + Challenge + Provider->>Apple: generateAssertion(keyId, clientDataHash=SHA256(ClientData)) + Apple-->>Provider: Assertion Object + + Provider->>API: getAppCheckTokenWithArtifact(artifact, challenge, assertion) + API->>Backend: POST /exchangeAppAttestAssertion + Note right of Backend: Verifies assertion signature
matches stored public key. + Backend-->>API: { "token": "..." } + + Provider-->>App: App Check Token +``` + +--- + +## DeviceCheck Provider (`GACDeviceCheckProvider`) +A simpler provider for older devices. + +### Components +* **Service:** `DCDevice` (Apple's API). +* **Generator:** `DCDevice.currentDevice` (can be mocked for testing). + +### Flow +```mermaid +sequenceDiagram + participant App + participant Provider as GACDeviceCheckProvider + participant Apple as DCDevice + participant API as GACDeviceCheckAPIService + participant Backend as Firebase Backend + + App->>Provider: getToken() + Provider->>Apple: generateToken() + Apple-->>Provider: Device Token (Ephemeral) + + Provider->>API: appCheckTokenWithDeviceToken(deviceToken) + API->>Backend: POST /exchangeDeviceCheckToken + Note right of Backend: Verifies device token with Apple. + Backend-->>API: { "token": "..." } + + Provider-->>App: App Check Token +``` + +--- + +## Debug Provider (`GACAppCheckDebugProvider`) +Used for local development and CI. + +### Configuration +The provider looks for a debug secret in the following order: +1. **Environment Variable:** `AppCheckDebugToken` (or legacy `FIRAAppCheckDebugToken`). +2. **Local Storage:** `NSUserDefaults` key `GACAppCheckDebugToken`. +3. **Generation:** If neither exists, it generates a new UUID, stores it in `NSUserDefaults`, and logs it to the console (warning level). + +### Flow +```mermaid +sequenceDiagram + participant App + participant Provider as GACAppCheckDebugProvider + participant API as GACAppCheckDebugProviderAPIService + participant Backend as Firebase Backend + + App->>Provider: getToken() + Provider->>Provider: Determine Debug Secret (Env Var or UUID) + + Provider->>API: appCheckTokenWithDebugToken(debugToken) + API->>Backend: POST /exchangeDebugToken + Note right of Backend: Checks if debug token is
registered in Console. + Backend-->>API: { "token": "..." } + + Provider-->>App: App Check Token +``` \ No newline at end of file diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 00000000..c7a79176 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,144 @@ +# Core Integration and Usage + +## Initialization + +To use `AppCheckCore`, you first need to initialize an `AppCheckCoreProvider` implementation (e.g., `AppCheckCoreAppAttestProvider`) and then use it to initialize `AppCheckCore`. + +### Swift +```swift +import AppCheckCore +import Foundation + +// 1. Initialize an App Check Provider +// Replace "my-sdk" with your SDK's unique identifier. +// Replace "projects/123/apps/abc" with your actual resource name. +// Set baseURL and APIKey if needed, otherwise nil. +// Provide a keychainAccessGroup if you use one. +let provider = AppCheckCoreAppAttestProvider(serviceName: "my-sdk", + resourceName: "projects/123/apps/abc", + baseURL: nil, + APIKey: nil, + keychainAccessGroup: nil, + requestHooks: nil) + +// 2. Initialize AppCheckCore +// You can optionally provide settings and a token delegate. +let appCheck = AppCheckCore(serviceName: "my-sdk", + resourceName: "projects/123/apps/abc", + appCheckProvider: provider, + settings: AppCheckCoreSettings(), // Use default settings or provide your own + tokenDelegate: nil, // Provide a delegate to observe token changes + keychainAccessGroup: nil) +``` + +### Objective-C +```objectivec +#import +#import + +// 1. Initialize an App Check Provider +// Replace "my-sdk" with your SDK's unique identifier. +// Replace "projects/123/apps/abc" with your actual resource name. +// Set baseURL and APIKey if needed, otherwise nil. +// Provide a keychainAccessGroup if you use one. +GACAppAttestProvider *provider = + [[GACAppAttestProvider alloc] initWithServiceName:@"my-sdk" + resourceName:@"projects/123/apps/abc" + baseURL:nil + APIKey:nil + keychainAccessGroup:nil + requestHooks:nil]; + +// 2. Initialize AppCheckCore +// You can optionally provide settings and a token delegate. +GACAppCheck *appCheck = + [[GACAppCheck alloc] initWithServiceName:@"my-sdk" + resourceName:@"projects/123/apps/abc" + appCheckProvider:provider + settings:[[GACAppCheckSettings alloc] init] // Use default settings or provide your own + tokenDelegate:nil // Provide a delegate to observe token changes + keychainAccessGroup:nil]; +``` + +## Fetching Tokens + +`AppCheckCore` provides methods to fetch App Check tokens, both for general use and for limited-use scenarios. + +### Fetching a Standard App Check Token +Use `token(forcingRefresh:completion:)` to retrieve an App Check token. The `forcingRefresh` parameter determines whether to use a cached token or request a new one. In most cases, `NO` (or `false` in Swift) should be used. + +### Swift +```swift +appCheck.token(forcingRefresh: false) { result in + if let token = result.token { + print("App Check Token: \(token.token)") + print("Token Expiration: \(token.expirationDate)") + } else if let error = result.error { + print("Error fetching App Check token: \(error.localizedDescription)") + } +} +``` + +### Objective-C +```objectivec +[appCheck tokenForcingRefresh:NO + completion:^(GACAppCheckTokenResult *result) { + if (result.token) { + NSLog(@"App Check Token: %@", result.token.token); + NSLog(@"Token Expiration: %@", result.token.expirationDate); + } else if (result.error) { + NSLog(@"Error fetching App Check token: %@", result.error.localizedDescription); + } +}]; +``` + +### Fetching a Limited-Use App Check Token +For scenarios where you need a token for a single, immediate request without affecting the primary token's refresh cycle, use `limitedUseToken(completion:)`. + +### Swift +```swift +appCheck.limitedUseTokenWithCompletion { result in + if let token = result.token { + print("Limited-Use App Check Token: \(token.token)") + } else if let error = result.error { + print("Error fetching limited-use App Check token: \(error.localizedDescription)") + } +} +``` + +### Objective-C +```objectivec +[appCheck limitedUseTokenWithCompletion:^(GACAppCheckTokenResult *result) { + if (result.token) { + NSLog(@"Limited-Use App Check Token: %@", result.token.token); + } else if (result.error) { + NSLog(@"Error fetching limited-use App Check token: %@", result.error.localizedDescription); + } +}]; +``` + +## Token Fetching Sequence Diagram +This diagram illustrates the typical flow when an application requests an App Check token. + +```mermaid +sequenceDiagram + participant App + participant GACAppCheck + participant Cache + participant GACAppCheckProvider + participant AppleBackend + + App->>GACAppCheck: Request Token (forcingRefresh: false) + GACAppCheck->>Cache: Check for valid token + alt Token in cache and valid + Cache-->>GACAppCheck: Cached Token + GACAppCheck-->>App: App Check Token Result + else Token missing or expired + GACAppCheck->>GACAppCheckProvider: Get Token + GACAppCheckProvider->>AppleBackend: Perform Attestation/Verification + AppleBackend-->>GACAppCheckProvider: Attestation/Verification Result + GACAppCheckProvider-->>GACAppCheck: New App Check Token + GACAppCheck->>Cache: Store New Token + GACAppCheck-->>App: App Check Token Result + end +``` \ No newline at end of file From abfa6d3d5d173c5012a4c6f9ffe8313c8af3ec0b Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 8 Jan 2026 17:50:31 -0500 Subject: [PATCH 02/94] review --- docs/architecture.md | 31 ++++++++++-- docs/providers.md | 115 ++++++++++++++++++++++++++++--------------- 2 files changed, 100 insertions(+), 46 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index c571e154..7608df19 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -32,15 +32,36 @@ The `GACAppCheck` class acts as the central coordinator. * Any old token is overwritten. 5. **Completion:** The token (cached or new) is returned to the caller. -## Backoff Strategy -To prevent overwhelming the backend or Apple's servers during failures, `AppCheckCore` implements an exponential backoff strategy. +## Exponential Backoff Strategy +To prevent overwhelming the backend or Apple's servers during failures, `AppCheckCore` implements a robust exponential backoff strategy via `GACAppCheckBackoffWrapper`. + +### Algorithm +The backoff interval is calculated as follows: +$$ +\text{Interval} = \min(\text{Base} \times \text{Jitter}, \text{MaxInterval}) +$$ +* **Base:** $2^{\text{retry\_count}}$ seconds. +* **Jitter:** A random multiplier between $1.0$ and $1.5$ (to prevent thundering herd problems). +* **MaxInterval:** 4 hours. + +### Error Policies +The backoff behavior depends on the error type, specifically HTTP status codes returned by the backend: + +| HTTP Status Code | Backoff Type | Reason | +| :--- | :--- | :--- | +| **< 400** | **None** | Network errors or successful requests do not trigger backoff. | +| **400 (Bad Request)**
**404 (Not Found)** | **1 Day** | Indicates a project misconfiguration or outdated app version. Unlikely to resolve quickly. | +| **403 (Forbidden)**
**429 (Too Many Requests)**
**503 (Service Unavailable)** | **Exponential** | Indicates soft deletion, rate limiting, or server overload. Retrying later is appropriate. | +| **Other 5xx** | **Exponential** | Standard server errors. | + +### Implementation * **Class:** `GACAppCheckBackoffWrapper` * **Usage:** Providers (`GACAppAttestProvider`, `GACDeviceCheckProvider`) wrap their network and attestation calls in this backoff mechanism. -* **Behavior:** Retries with increasing delays on retryable errors (e.g., network timeouts, temporary server errors 503). Non-retryable errors (e.g., 403 Forbidden, 400 Bad Request) fail immediately. +* **State:** The wrapper tracks the failure count and the last failure time. It resets to 0 upon a successful token fetch. ## Threading Model * **Concurrency:** `AppCheckCore` is designed to be thread-safe. * **Queues:** - * **Main Queue:** Completion handlers are typically dispatched to the main queue (or a user-specified queue if the API supported it, but currently defaults to main for top-level APIs). + * **Main Queue:** Completion handlers are typically dispatched to the main queue. * **Internal Queues:** Providers use private serial queues (e.g., `com.google.GACAppAttestProvider`) to manage state and sequentialize complex attestation flows (like generating a key, then attesting, then exchanging). - * **Background:** Network requests are performed on background queues (`QOS_CLASS_DEFAULT` or `QOS_CLASS_UTILITY`). + * **Background:** Network requests are performed on background queues (`QOS_CLASS_DEFAULT` or `QOS_CLASS_UTILITY`). \ No newline at end of file diff --git a/docs/providers.md b/docs/providers.md index 52ab270d..21deca96 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -1,6 +1,6 @@ # App Check Providers: Deep Dive -This document details the internal design and detailed flows of each App Check provider. +This document details the internal design and detailed flows of each App Check provider, including error handling, retries, and state resets. ## AppAttest Provider (`GACAppAttestProvider`) The most complex provider, interacting with `DCAppAttestService`. It maintains a stable key pair on the device to sign assertions. @@ -9,10 +9,12 @@ The most complex provider, interacting with `DCAppAttestService`. It maintains a * **Service:** `DCAppAttestService` (Apple's API). * **Storage:** * `GACAppAttestKeyIDStorage`: Stores the generated App Attest Key ID. - * `GACAppAttestArtifactStorage`: Stores the "artifact" returned by the Firebase backend after a successful initial handshake. This artifact effectively links the on-device key to the backend session. + * `GACAppAttestArtifactStorage`: Stores the "artifact" returned by the Firebase backend after a successful initial handshake. +* **Resiliency:** + * **Automatic Retry:** The provider wraps the entire flow in a retry loop. If a specific "Rejection Error" occurs (e.g., invalid key), it resets its internal state and retries the flow from scratch. ### Flow 1: Initial Handshake (Attestation) -Occurs when the app runs for the first time or if the stored artifact is missing/corrupted. +Occurs when the app runs for the first time, or if the stored artifact is missing, or **after a reset**. ```mermaid sequenceDiagram @@ -23,31 +25,45 @@ sequenceDiagram participant Backend as Firebase Backend App->>Provider: getToken() - Provider->>API: getRandomChallenge() - API->>Backend: POST /generateAppAttestChallenge - Backend-->>API: { "challenge": "..." } - par Parallel Execution - Provider->>Apple: generateKey() - Apple-->>Provider: Key ID - and - Provider->>API: (Challenge received) + loop Retry Loop (Max 1 Retry) + Provider->>API: getRandomChallenge() + API->>Backend: POST /generateAppAttestChallenge + Backend-->>API: { "challenge": "..." } + + par Parallel Execution + Provider->>Apple: generateKey() (If needed) + Apple-->>Provider: Key ID + and + Provider->>API: (Challenge received) + end + + Provider->>Apple: attestKey(keyId, clientDataHash=SHA256(challenge)) + + alt Attestation Failed (Invalid Key/Input) + Apple-->>Provider: DCErrorInvalidKey / Input + Provider->>Provider: RESET: Delete KeyID & Artifact + Note right of Provider: Throws RejectionError,
Triggering Loop Retry + else Attestation Success + Apple-->>Provider: Attestation Object + Provider->>API: attestKeyWithAttestation(...) + API->>Backend: POST /exchangeAppAttestAttestation + + alt Backend Rejection (403) + Backend-->>API: 403 Forbidden + Provider->>Provider: RESET: Delete KeyID & Artifact + Note right of Provider: Throws RejectionError,
Triggering Loop Retry + else Success + Backend-->>API: { "token": "...", "artifact": "..." } + Provider->>Provider: Store Artifact & Key ID + Provider-->>App: App Check Token + end + end end - - Provider->>Apple: attestKey(keyId, clientDataHash=SHA256(challenge)) - Apple-->>Provider: Attestation Object - - Provider->>API: attestKeyWithAttestation(attestation, keyID, challenge) - API->>Backend: POST /exchangeAppAttestAttestation - Note right of Backend: Verifies attestation validity
and app integrity. - Backend-->>API: { "token": "...", "artifact": "..." } - - Provider->>Provider: Store Artifact & Key ID - Provider-->>App: App Check Token ``` ### Flow 2: Token Refresh (Assertion) -Occurs for subsequent requests. It's faster and uses the established key pair. +Occurs for subsequent requests using the established key pair. ```mermaid sequenceDiagram @@ -58,21 +74,30 @@ sequenceDiagram participant Backend as Firebase Backend App->>Provider: getToken() - Provider->>API: getRandomChallenge() - API->>Backend: POST /generateAppAttestChallenge - Backend-->>API: { "challenge": "..." } - - Provider->>Provider: Retrieve stored Artifact - Provider->>Provider: ClientData = Artifact + Challenge - Provider->>Apple: generateAssertion(keyId, clientDataHash=SHA256(ClientData)) - Apple-->>Provider: Assertion Object - - Provider->>API: getAppCheckTokenWithArtifact(artifact, challenge, assertion) - API->>Backend: POST /exchangeAppAttestAssertion - Note right of Backend: Verifies assertion signature
matches stored public key. - Backend-->>API: { "token": "..." } - Provider-->>App: App Check Token + loop Retry Loop (Max 1 Retry) + Provider->>API: getRandomChallenge() + API->>Backend: POST /generateAppAttestChallenge + Backend-->>API: { "challenge": "..." } + + Provider->>Provider: Retrieve stored Artifact + Provider->>Provider: ClientData = Artifact + Challenge + Provider->>Apple: generateAssertion(keyId, clientDataHash=SHA256(ClientData)) + + alt Assertion Failed (Invalid Key/Input) + Apple-->>Provider: DCErrorInvalidKey / Input + Provider->>Provider: RESET: Delete KeyID & Artifact + Note right of Provider: Throws RejectionError,
Triggering Loop Retry
(Will fall back to Initial Handshake) + else Assertion Success + Apple-->>Provider: Assertion Object + + Provider->>API: getAppCheckTokenWithArtifact(...) + API->>Backend: POST /exchangeAppAttestAssertion + Backend-->>API: { "token": "..." } + + Provider-->>App: App Check Token + end + end ``` --- @@ -82,7 +107,7 @@ A simpler provider for older devices. ### Components * **Service:** `DCDevice` (Apple's API). -* **Generator:** `DCDevice.currentDevice` (can be mocked for testing). +* **Generator:** `DCDevice.currentDevice`. ### Flow ```mermaid @@ -94,15 +119,23 @@ sequenceDiagram participant Backend as Firebase Backend App->>Provider: getToken() + + Note right of Provider: Wrapped in Backoff Wrapper Provider->>Apple: generateToken() Apple-->>Provider: Device Token (Ephemeral) Provider->>API: appCheckTokenWithDeviceToken(deviceToken) API->>Backend: POST /exchangeDeviceCheckToken Note right of Backend: Verifies device token with Apple. - Backend-->>API: { "token": "..." } - Provider-->>App: App Check Token + alt Error (e.g., 503) + Backend-->>API: 503 Service Unavailable + Provider->>Provider: Record Failure (Backoff) + Provider-->>App: Error + else Success + Backend-->>API: { "token": "..." } + Provider-->>App: App Check Token + end ``` --- @@ -133,4 +166,4 @@ sequenceDiagram Backend-->>API: { "token": "..." } Provider-->>App: App Check Token -``` \ No newline at end of file +``` From 779bd6a10bffdbc0ac32d62a165716bd65e73d1b Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 15:15:50 -0500 Subject: [PATCH 03/94] wrap to 80 --- docs/architecture.md | 76 ++++++++++++++++++++++++++++++-------------- docs/index.md | 29 +++++++++++++---- docs/providers.md | 31 ++++++++++++------ 3 files changed, 97 insertions(+), 39 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 7608df19..36c9cee2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,39 +1,55 @@ # Architecture & Design -This document details the internal architecture of `AppCheckCore`, focusing on token storage, lifecycle management, and security mechanisms. +This document details the internal architecture of `AppCheckCore`, +focusing on token storage, lifecycle management, and security +mechanisms. ## Token Storage -App Check tokens are sensitive credentials that grant access to your backend resources. `AppCheckCore` treats them with high security. +App Check tokens are sensitive credentials that grant access to your +backend resources. `AppCheckCore` treats them with high security. ### Keychain Storage -The `GACAppCheckStorage` class is responsible for persisting App Check tokens. +The `GACAppCheckStorage` class is responsible for persisting App Check +tokens. * **Mechanism:** It uses the iOS Keychain via `GULKeychainStorage`. * **Service Name:** `com.google.app_check_core.token_storage`. -* **Data Protection:** Tokens are stored as `GACAppCheckStoredToken` objects (conforming to `NSSecureCoding`). -* **Access Groups:** Supports sharing tokens across apps/extensions via Keychain Access Groups (configurable during initialization). +* **Data Protection:** Tokens are stored as `GACAppCheckStoredToken` + objects (conforming to `NSSecureCoding`). +* **Access Groups:** Supports sharing tokens across apps/extensions + via Keychain Access Groups (configurable during initialization). ### Artifact Storage (App Attest) -For the App Attest provider, intermediate artifacts are also stored to maintain a stable device identity. +For the App Attest provider, intermediate artifacts are also stored to +maintain a stable device identity. * **Class:** `GACAppAttestArtifactStorage` * **Storage:** Keychain. -* **Key Suffix:** Keys are namespaced by the service name and resource name (e.g., `my-sdk.projects/123/apps/abc`) to prevent collisions. +* **Key Suffix:** Keys are namespaced by the service name and resource + name (e.g., `my-sdk.projects/123/apps/abc`) to prevent collisions. ## Token Lifecycle Management The `GACAppCheck` class acts as the central coordinator. -1. **Request:** The app requests a token via `token(forcingRefresh:completion:)`. +1. **Request:** The app requests a token via + `token(forcingRefresh:completion:)`. 2. **Cache Check:** - * If `forcingRefresh` is `NO`: Checks `GACAppCheckStorage` for a valid, non-expired token. - * **Buffer Time:** Tokens are considered "expired" slightly before their actual expiration time to account for clock skew and network latency. + * If `forcingRefresh` is `NO`: Checks `GACAppCheckStorage` for a + valid, non-expired token. + * **Buffer Time:** Tokens are considered "expired" slightly before + their actual expiration time to account for clock skew and + network latency. 3. **Fetch (if needed):** - * If the cache is empty or expired, or `forcingRefresh` is `YES`, a request is made to the configured `GACAppCheckProvider`. + * If the cache is empty or expired, or `forcingRefresh` is `YES`, + a request is made to the configured `GACAppCheckProvider`. 4. **Storage:** - * Upon successful retrieval, the new token is written to `GACAppCheckStorage`. + * Upon successful retrieval, the new token is written to + `GACAppCheckStorage`. * Any old token is overwritten. 5. **Completion:** The token (cached or new) is returned to the caller. ## Exponential Backoff Strategy -To prevent overwhelming the backend or Apple's servers during failures, `AppCheckCore` implements a robust exponential backoff strategy via `GACAppCheckBackoffWrapper`. +To prevent overwhelming the backend or Apple's servers during failures, +`AppCheckCore` implements a robust exponential backoff strategy via +`GACAppCheckBackoffWrapper`. ### Algorithm The backoff interval is calculated as follows: @@ -41,27 +57,41 @@ $$ \text{Interval} = \min(\text{Base} \times \text{Jitter}, \text{MaxInterval}) $$ * **Base:** $2^{\text{retry\_count}}$ seconds. -* **Jitter:** A random multiplier between $1.0$ and $1.5$ (to prevent thundering herd problems). +* **Jitter:** A random multiplier between $1.0$ and $1.5$ (to prevent + thundering herd problems). * **MaxInterval:** 4 hours. ### Error Policies -The backoff behavior depends on the error type, specifically HTTP status codes returned by the backend: +The backoff behavior depends on the error type, specifically HTTP status +codes returned by the backend: | HTTP Status Code | Backoff Type | Reason | | :--- | :--- | :--- | -| **< 400** | **None** | Network errors or successful requests do not trigger backoff. | -| **400 (Bad Request)**
**404 (Not Found)** | **1 Day** | Indicates a project misconfiguration or outdated app version. Unlikely to resolve quickly. | -| **403 (Forbidden)**
**429 (Too Many Requests)**
**503 (Service Unavailable)** | **Exponential** | Indicates soft deletion, rate limiting, or server overload. Retrying later is appropriate. | +| **< 400** | **None** | Network errors or successful requests do not + trigger backoff. | +| **400 (Bad Request)**
**404 (Not Found)** | **1 Day** | Indicates a + project misconfiguration or outdated app version. Unlikely to + resolve quickly. | +| **403 (Forbidden)**
**429 (Too Many Requests)**
**503 (Service + Unavailable)** | **Exponential** | Indicates soft deletion, rate + limiting, or server overload. Retrying later is appropriate. | | **Other 5xx** | **Exponential** | Standard server errors. | ### Implementation * **Class:** `GACAppCheckBackoffWrapper` -* **Usage:** Providers (`GACAppAttestProvider`, `GACDeviceCheckProvider`) wrap their network and attestation calls in this backoff mechanism. -* **State:** The wrapper tracks the failure count and the last failure time. It resets to 0 upon a successful token fetch. +* **Usage:** Providers (`GACAppAttestProvider`, `GACDeviceCheckProvider`) + wrap their network and attestation calls in this backoff mechanism. +* **State:** The wrapper tracks the failure count and the last failure + time. It resets to 0 upon a successful token fetch. ## Threading Model * **Concurrency:** `AppCheckCore` is designed to be thread-safe. * **Queues:** - * **Main Queue:** Completion handlers are typically dispatched to the main queue. - * **Internal Queues:** Providers use private serial queues (e.g., `com.google.GACAppAttestProvider`) to manage state and sequentialize complex attestation flows (like generating a key, then attesting, then exchanging). - * **Background:** Network requests are performed on background queues (`QOS_CLASS_DEFAULT` or `QOS_CLASS_UTILITY`). \ No newline at end of file + * **Main Queue:** Completion handlers are typically dispatched to + the main queue. + * **Internal Queues:** Providers use private serial queues (e.g., + `com.google.GACAppAttestProvider`) to manage state and + sequentialize complex attestation flows (like generating a key, + then attesting, then exchanging). + * **Background:** Network requests are performed on background + queues (`QOS_CLASS_DEFAULT` or `QOS_CLASS_UTILITY`). diff --git a/docs/index.md b/docs/index.md index 1c0c44b6..d4e61a70 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,22 +1,37 @@ # App Check Core - Documentation ## Introduction -`AppCheckCore` is the underlying engine for app attestation and token management, primarily used by the Firebase iOS SDK but designed for broader internal Google use. It provides a robust and secure way to verify the authenticity of app instances accessing your backend resources. This library supports applications running on iOS, macOS, tvOS, and watchOS. +`AppCheckCore` is the underlying engine for app attestation and token +management, primarily used by the Firebase iOS SDK but designed for +broader internal Google use. It provides a robust and secure way to +verify the authenticity of app instances accessing your backend +resources. This library supports applications running on iOS, macOS, +tvOS, and watchOS. ## Key Features -* **Token Management:** Handles the lifecycle of App Check tokens, including caching and automatic refreshing to ensure continuous protection. -* **Provider Abstraction:** Abstracts different attestation providers, allowing for flexible integration with various platform-specific integrity mechanisms. -* **Limited-Use Tokens:** Supports the generation and management of limited-use tokens for scenarios requiring single-use or short-lived authentication. +* **Token Management:** Handles the lifecycle of App Check tokens, + including caching and automatic refreshing to ensure continuous + protection. +* **Provider Abstraction:** Abstracts different attestation providers, + allowing for flexible integration with various platform-specific + integrity mechanisms. +* **Limited-Use Tokens:** Supports the generation and management of + limited-use tokens for scenarios requiring single-use or short-lived + authentication. ## Documentation Sections * [Getting Started](getting-started.md): Installation and basic setup. * [Usage Guide](usage.md): How to initialize and fetch tokens. -* [Providers Deep Dive](providers.md): Detailed architectural breakdown of App Attest, DeviceCheck, and Debug providers, including sequence diagrams. -* [Architecture](architecture.md): Internal design details regarding token storage, caching strategies, and threading. +* [Providers Deep Dive](providers.md): Detailed architectural + breakdown of App Attest, DeviceCheck, and Debug providers, + including sequence diagrams. +* [Architecture](architecture.md): Internal design details regarding + token storage, caching strategies, and threading. * [API Reference](api-reference.md): High-level class overview. ## High-Level Architecture -This diagram illustrates the core relationships. For detailed sequence flows, see [Providers Deep Dive](providers.md). +This diagram illustrates the core relationships. For detailed sequence +flows, see [Providers Deep Dive](providers.md). ```mermaid classDiagram diff --git a/docs/providers.md b/docs/providers.md index 21deca96..5b6607d1 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -1,20 +1,31 @@ # App Check Providers: Deep Dive -This document details the internal design and detailed flows of each App Check provider, including error handling, retries, and state resets. +This document details the internal design and detailed flows of each +App Check provider, including error handling, retries, and state +resets. ## AppAttest Provider (`GACAppAttestProvider`) -The most complex provider, interacting with `DCAppAttestService`. It maintains a stable key pair on the device to sign assertions. +The most complex provider, interacting with `DCAppAttestService`. It +maintains a stable key pair on the device to sign assertions. ### Components * **Service:** `DCAppAttestService` (Apple's API). * **Storage:** - * `GACAppAttestKeyIDStorage`: Stores the generated App Attest Key ID. - * `GACAppAttestArtifactStorage`: Stores the "artifact" returned by the Firebase backend after a successful initial handshake. + * `GACAppAttestKeyIDStorage`: Stores the generated App Attest Key + ID. + * `GACAppAttestArtifactStorage`: Stores the "artifact" returned by + the Firebase backend after a successful initial handshake. This + artifact effectively links the on-device key to the backend + session. * **Resiliency:** - * **Automatic Retry:** The provider wraps the entire flow in a retry loop. If a specific "Rejection Error" occurs (e.g., invalid key), it resets its internal state and retries the flow from scratch. + * **Automatic Retry:** The provider wraps the entire flow in a + retry loop. If a specific "Rejection Error" occurs (e.g., + invalid key), it resets its internal state and retries the flow + from scratch. ### Flow 1: Initial Handshake (Attestation) -Occurs when the app runs for the first time, or if the stored artifact is missing, or **after a reset**. +Occurs when the app runs for the first time, or if the stored artifact +is missing, or **after a reset**. ```mermaid sequenceDiagram @@ -145,9 +156,11 @@ Used for local development and CI. ### Configuration The provider looks for a debug secret in the following order: -1. **Environment Variable:** `AppCheckDebugToken` (or legacy `FIRAAppCheckDebugToken`). +1. **Environment Variable:** `AppCheckDebugToken` (or legacy + `FIRAAppCheckDebugToken`). 2. **Local Storage:** `NSUserDefaults` key `GACAppCheckDebugToken`. -3. **Generation:** If neither exists, it generates a new UUID, stores it in `NSUserDefaults`, and logs it to the console (warning level). +3. **Generation:** If neither exists, it generates a new UUID, stores it + in `NSUserDefaults`, and logs it to the console (warning level). ### Flow ```mermaid @@ -166,4 +179,4 @@ sequenceDiagram Backend-->>API: { "token": "..." } Provider-->>App: App Check Token -``` +``` \ No newline at end of file From f948d2a8a2a6d4f8bc55ded96f7cecd155ef94fd Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 15:21:24 -0500 Subject: [PATCH 04/94] emphasize types we dont own --- docs/providers.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/providers.md b/docs/providers.md index 5b6607d1..f6f49e8a 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -31,7 +31,7 @@ is missing, or **after a reset**. sequenceDiagram participant App participant Provider as GACAppAttestProvider - participant Apple as DCAppAttestService + participant Apple as DCAppAttestService
(Apple's DeviceCheck Framework) participant API as GACAppAttestAPIService participant Backend as Firebase Backend @@ -80,7 +80,7 @@ Occurs for subsequent requests using the established key pair. sequenceDiagram participant App participant Provider as GACAppAttestProvider - participant Apple as DCAppAttestService + participant Apple as DCAppAttestService
(Apple's DeviceCheck Framework) participant API as GACAppAttestAPIService participant Backend as Firebase Backend @@ -125,7 +125,7 @@ A simpler provider for older devices. sequenceDiagram participant App participant Provider as GACDeviceCheckProvider - participant Apple as DCDevice + participant Apple as DCDevice
(Apple's DeviceCheck Framework) participant API as GACDeviceCheckAPIService participant Backend as Firebase Backend From fc46b9b3ff28d8f51aa45cf7fbfc8d0dd2083fd0 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 15:22:33 -0500 Subject: [PATCH 05/94] reasier to read --- docs/providers.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/providers.md b/docs/providers.md index f6f49e8a..b15a0abc 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -31,7 +31,7 @@ is missing, or **after a reset**. sequenceDiagram participant App participant Provider as GACAppAttestProvider - participant Apple as DCAppAttestService
(Apple's DeviceCheck Framework) + participant Apple as DCAppAttestService

(Apple's DeviceCheck Framework) participant API as GACAppAttestAPIService participant Backend as Firebase Backend @@ -80,7 +80,7 @@ Occurs for subsequent requests using the established key pair. sequenceDiagram participant App participant Provider as GACAppAttestProvider - participant Apple as DCAppAttestService
(Apple's DeviceCheck Framework) + participant Apple as DCAppAttestService

(Apple's DeviceCheck Framework) participant API as GACAppAttestAPIService participant Backend as Firebase Backend @@ -125,7 +125,7 @@ A simpler provider for older devices. sequenceDiagram participant App participant Provider as GACDeviceCheckProvider - participant Apple as DCDevice
(Apple's DeviceCheck Framework) + participant Apple as DCDevice

(Apple's DeviceCheck Framework) participant API as GACDeviceCheckAPIService participant Backend as Firebase Backend From c45c8032393df7b9e5d54c8eb56b8edf26b1f01f Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 15:35:59 -0500 Subject: [PATCH 06/94] Feedback --- docs/providers.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/providers.md b/docs/providers.md index b15a0abc..016483e3 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -38,15 +38,14 @@ sequenceDiagram App->>Provider: getToken() loop Retry Loop (Max 1 Retry) - Provider->>API: getRandomChallenge() - API->>Backend: POST /generateAppAttestChallenge - Backend-->>API: { "challenge": "..." } - par Parallel Execution + Provider->>API: getRandomChallenge() + API->>Backend: POST /generateAppAttestChallenge + Backend-->>API: { "challenge": "..." } + API-->>Provider: Challenge + and Provider->>Apple: generateKey() (If needed) Apple-->>Provider: Key ID - and - Provider->>API: (Challenge received) end Provider->>Apple: attestKey(keyId, clientDataHash=SHA256(challenge)) From 9fca0bf928843914d2f38d37f63b83c49765b0f7 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 15:45:32 -0500 Subject: [PATCH 07/94] fdbk --- docs/providers.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/providers.md b/docs/providers.md index 016483e3..37fbd51c 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -23,6 +23,30 @@ maintains a stable key pair on the device to sign assertions. invalid key), it resets its internal state and retries the flow from scratch. +### Decision Logic & State Machine +Before executing a handshake, the provider determines the correct flow +based on the internal state and manages concurrent requests. + +```mermaid +flowchart TD + Start[getToken call] --> Coalesce{Ongoing Operation?} + + Coalesce -- Yes --> Reuse[Reuse or Chain Promise] + Coalesce -- No --> Backoff[Apply Backoff Wrapper] + + Backoff --> StateCheck{Check State
attestationState} + + StateCheck -->|Not Supported| Error[Return Error] + + StateCheck -->|Supported| KeyCheck{Key ID Stored?} + + KeyCheck -- No --> Flow1[Go to Flow 1
Initial Handshake] + KeyCheck -- Yes --> ArtifactCheck{Artifact Stored?} + + ArtifactCheck -- No --> Flow1 + ArtifactCheck -- Yes --> Flow2[Go to Flow 2
Token Refresh/Assertion] +``` + ### Flow 1: Initial Handshake (Attestation) Occurs when the app runs for the first time, or if the stored artifact is missing, or **after a reset**. @@ -178,4 +202,4 @@ sequenceDiagram Backend-->>API: { "token": "..." } Provider-->>App: App Check Token -``` \ No newline at end of file +``` From d795779a5cd1c6161ab55889b90fef2e6837f8fd Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 16:07:47 -0500 Subject: [PATCH 08/94] fdbk --- docs/providers.md | 50 ++++++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/docs/providers.md b/docs/providers.md index 37fbd51c..45e33a69 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -27,24 +27,38 @@ maintains a stable key pair on the device to sign assertions. Before executing a handshake, the provider determines the correct flow based on the internal state and manages concurrent requests. +**Note on Limited Use:** Limited-use tokens are never reused/coalesced. +If a limited-use token is requested (or if one is currently being +fetched), the new request will "chain" (wait for the ongoing one to +finish) and then start a fresh handshake to ensure a unique token is +generated. + ```mermaid flowchart TD - Start[getToken call] --> Coalesce{Ongoing Operation?} + Start[getToken call] --> CheckUse{Limited Use Request?} + + CheckUse -- Yes --> Chain[Chain Promise] + CheckUse -- No --> Coalesce{Ongoing Operation?} + + Coalesce -- No --> Backoff[Apply Backoff] + Coalesce -- Yes --> CheckOngoing{Ongoing is Limited?} + + CheckOngoing -- Yes --> Chain + CheckOngoing -- No --> Reuse[Reuse Ongoing Promise] - Coalesce -- Yes --> Reuse[Reuse or Chain Promise] - Coalesce -- No --> Backoff[Apply Backoff Wrapper] + Chain --> Backoff - Backoff --> StateCheck{Check State
attestationState} + Backoff --> StateCheck{Attestation State?} StateCheck -->|Not Supported| Error[Return Error] StateCheck -->|Supported| KeyCheck{Key ID Stored?} - KeyCheck -- No --> Flow1[Go to Flow 1
Initial Handshake] + KeyCheck -- No --> Flow1[Flow 1: Initial Handshake] KeyCheck -- Yes --> ArtifactCheck{Artifact Stored?} ArtifactCheck -- No --> Flow1 - ArtifactCheck -- Yes --> Flow2[Go to Flow 2
Token Refresh/Assertion] + ArtifactCheck -- Yes --> Flow2[Flow 2: Token Refresh/Assertion] ``` ### Flow 1: Initial Handshake (Attestation) @@ -59,7 +73,7 @@ sequenceDiagram participant API as GACAppAttestAPIService participant Backend as Firebase Backend - App->>Provider: getToken() + App->>Provider: getToken(limitedUse) loop Retry Loop (Max 1 Retry) par Parallel Execution @@ -80,8 +94,8 @@ sequenceDiagram Note right of Provider: Throws RejectionError,
Triggering Loop Retry else Attestation Success Apple-->>Provider: Attestation Object - Provider->>API: attestKeyWithAttestation(...) - API->>Backend: POST /exchangeAppAttestAttestation + Provider->>API: attestKeyWithAttestation(..., limitedUse) + API->>Backend: POST /exchangeAppAttestAttestation
{ limited_use: true/false } alt Backend Rejection (403) Backend-->>API: 403 Forbidden @@ -107,7 +121,7 @@ sequenceDiagram participant API as GACAppAttestAPIService participant Backend as Firebase Backend - App->>Provider: getToken() + App->>Provider: getToken(limitedUse) loop Retry Loop (Max 1 Retry) Provider->>API: getRandomChallenge() @@ -125,8 +139,8 @@ sequenceDiagram else Assertion Success Apple-->>Provider: Assertion Object - Provider->>API: getAppCheckTokenWithArtifact(...) - API->>Backend: POST /exchangeAppAttestAssertion + Provider->>API: getAppCheckTokenWithArtifact(..., limitedUse) + API->>Backend: POST /exchangeAppAttestAssertion
{ limited_use: true/false } Backend-->>API: { "token": "..." } Provider-->>App: App Check Token @@ -152,14 +166,14 @@ sequenceDiagram participant API as GACDeviceCheckAPIService participant Backend as Firebase Backend - App->>Provider: getToken() + App->>Provider: getToken(limitedUse) Note right of Provider: Wrapped in Backoff Wrapper Provider->>Apple: generateToken() Apple-->>Provider: Device Token (Ephemeral) - Provider->>API: appCheckTokenWithDeviceToken(deviceToken) - API->>Backend: POST /exchangeDeviceCheckToken + Provider->>API: appCheckTokenWithDeviceToken(deviceToken, limitedUse) + API->>Backend: POST /exchangeDeviceCheckToken
{ limited_use: true/false } Note right of Backend: Verifies device token with Apple. alt Error (e.g., 503) @@ -193,11 +207,11 @@ sequenceDiagram participant API as GACAppCheckDebugProviderAPIService participant Backend as Firebase Backend - App->>Provider: getToken() + App->>Provider: getToken(limitedUse) Provider->>Provider: Determine Debug Secret (Env Var or UUID) - Provider->>API: appCheckTokenWithDebugToken(debugToken) - API->>Backend: POST /exchangeDebugToken + Provider->>API: appCheckTokenWithDebugToken(debugToken, limitedUse) + API->>Backend: POST /exchangeDebugToken
{ limited_use: true/false } Note right of Backend: Checks if debug token is
registered in Console. Backend-->>API: { "token": "..." } From f151765d11eda1161829e4aa7e457f038f9e3c1e Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 16:12:25 -0500 Subject: [PATCH 09/94] fdbk --- docs/providers.md | 222 ++------------------------------- docs/providers/app-attest.md | 199 +++++++++++++++++++++++++++++ docs/providers/debug.md | 30 +++++ docs/providers/device-check.md | 36 ++++++ 4 files changed, 274 insertions(+), 213 deletions(-) create mode 100644 docs/providers/app-attest.md create mode 100644 docs/providers/debug.md create mode 100644 docs/providers/device-check.md diff --git a/docs/providers.md b/docs/providers.md index 45e33a69..c5b38d5a 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -1,219 +1,15 @@ # App Check Providers: Deep Dive -This document details the internal design and detailed flows of each +This section details the internal design and detailed flows of each App Check provider, including error handling, retries, and state resets. -## AppAttest Provider (`GACAppAttestProvider`) -The most complex provider, interacting with `DCAppAttestService`. It -maintains a stable key pair on the device to sign assertions. +Select a provider below for detailed documentation: -### Components -* **Service:** `DCAppAttestService` (Apple's API). -* **Storage:** - * `GACAppAttestKeyIDStorage`: Stores the generated App Attest Key - ID. - * `GACAppAttestArtifactStorage`: Stores the "artifact" returned by - the Firebase backend after a successful initial handshake. This - artifact effectively links the on-device key to the backend - session. -* **Resiliency:** - * **Automatic Retry:** The provider wraps the entire flow in a - retry loop. If a specific "Rejection Error" occurs (e.g., - invalid key), it resets its internal state and retries the flow - from scratch. - -### Decision Logic & State Machine -Before executing a handshake, the provider determines the correct flow -based on the internal state and manages concurrent requests. - -**Note on Limited Use:** Limited-use tokens are never reused/coalesced. -If a limited-use token is requested (or if one is currently being -fetched), the new request will "chain" (wait for the ongoing one to -finish) and then start a fresh handshake to ensure a unique token is -generated. - -```mermaid -flowchart TD - Start[getToken call] --> CheckUse{Limited Use Request?} - - CheckUse -- Yes --> Chain[Chain Promise] - CheckUse -- No --> Coalesce{Ongoing Operation?} - - Coalesce -- No --> Backoff[Apply Backoff] - Coalesce -- Yes --> CheckOngoing{Ongoing is Limited?} - - CheckOngoing -- Yes --> Chain - CheckOngoing -- No --> Reuse[Reuse Ongoing Promise] - - Chain --> Backoff - - Backoff --> StateCheck{Attestation State?} - - StateCheck -->|Not Supported| Error[Return Error] - - StateCheck -->|Supported| KeyCheck{Key ID Stored?} - - KeyCheck -- No --> Flow1[Flow 1: Initial Handshake] - KeyCheck -- Yes --> ArtifactCheck{Artifact Stored?} - - ArtifactCheck -- No --> Flow1 - ArtifactCheck -- Yes --> Flow2[Flow 2: Token Refresh/Assertion] -``` - -### Flow 1: Initial Handshake (Attestation) -Occurs when the app runs for the first time, or if the stored artifact -is missing, or **after a reset**. - -```mermaid -sequenceDiagram - participant App - participant Provider as GACAppAttestProvider - participant Apple as DCAppAttestService

(Apple's DeviceCheck Framework) - participant API as GACAppAttestAPIService - participant Backend as Firebase Backend - - App->>Provider: getToken(limitedUse) - - loop Retry Loop (Max 1 Retry) - par Parallel Execution - Provider->>API: getRandomChallenge() - API->>Backend: POST /generateAppAttestChallenge - Backend-->>API: { "challenge": "..." } - API-->>Provider: Challenge - and - Provider->>Apple: generateKey() (If needed) - Apple-->>Provider: Key ID - end - - Provider->>Apple: attestKey(keyId, clientDataHash=SHA256(challenge)) - - alt Attestation Failed (Invalid Key/Input) - Apple-->>Provider: DCErrorInvalidKey / Input - Provider->>Provider: RESET: Delete KeyID & Artifact - Note right of Provider: Throws RejectionError,
Triggering Loop Retry - else Attestation Success - Apple-->>Provider: Attestation Object - Provider->>API: attestKeyWithAttestation(..., limitedUse) - API->>Backend: POST /exchangeAppAttestAttestation
{ limited_use: true/false } - - alt Backend Rejection (403) - Backend-->>API: 403 Forbidden - Provider->>Provider: RESET: Delete KeyID & Artifact - Note right of Provider: Throws RejectionError,
Triggering Loop Retry - else Success - Backend-->>API: { "token": "...", "artifact": "..." } - Provider->>Provider: Store Artifact & Key ID - Provider-->>App: App Check Token - end - end - end -``` - -### Flow 2: Token Refresh (Assertion) -Occurs for subsequent requests using the established key pair. - -```mermaid -sequenceDiagram - participant App - participant Provider as GACAppAttestProvider - participant Apple as DCAppAttestService

(Apple's DeviceCheck Framework) - participant API as GACAppAttestAPIService - participant Backend as Firebase Backend - - App->>Provider: getToken(limitedUse) - - loop Retry Loop (Max 1 Retry) - Provider->>API: getRandomChallenge() - API->>Backend: POST /generateAppAttestChallenge - Backend-->>API: { "challenge": "..." } - - Provider->>Provider: Retrieve stored Artifact - Provider->>Provider: ClientData = Artifact + Challenge - Provider->>Apple: generateAssertion(keyId, clientDataHash=SHA256(ClientData)) - - alt Assertion Failed (Invalid Key/Input) - Apple-->>Provider: DCErrorInvalidKey / Input - Provider->>Provider: RESET: Delete KeyID & Artifact - Note right of Provider: Throws RejectionError,
Triggering Loop Retry
(Will fall back to Initial Handshake) - else Assertion Success - Apple-->>Provider: Assertion Object - - Provider->>API: getAppCheckTokenWithArtifact(..., limitedUse) - API->>Backend: POST /exchangeAppAttestAssertion
{ limited_use: true/false } - Backend-->>API: { "token": "..." } - - Provider-->>App: App Check Token - end - end -``` - ---- - -## DeviceCheck Provider (`GACDeviceCheckProvider`) -A simpler provider for older devices. - -### Components -* **Service:** `DCDevice` (Apple's API). -* **Generator:** `DCDevice.currentDevice`. - -### Flow -```mermaid -sequenceDiagram - participant App - participant Provider as GACDeviceCheckProvider - participant Apple as DCDevice

(Apple's DeviceCheck Framework) - participant API as GACDeviceCheckAPIService - participant Backend as Firebase Backend - - App->>Provider: getToken(limitedUse) - - Note right of Provider: Wrapped in Backoff Wrapper - Provider->>Apple: generateToken() - Apple-->>Provider: Device Token (Ephemeral) - - Provider->>API: appCheckTokenWithDeviceToken(deviceToken, limitedUse) - API->>Backend: POST /exchangeDeviceCheckToken
{ limited_use: true/false } - Note right of Backend: Verifies device token with Apple. - - alt Error (e.g., 503) - Backend-->>API: 503 Service Unavailable - Provider->>Provider: Record Failure (Backoff) - Provider-->>App: Error - else Success - Backend-->>API: { "token": "..." } - Provider-->>App: App Check Token - end -``` - ---- - -## Debug Provider (`GACAppCheckDebugProvider`) -Used for local development and CI. - -### Configuration -The provider looks for a debug secret in the following order: -1. **Environment Variable:** `AppCheckDebugToken` (or legacy - `FIRAAppCheckDebugToken`). -2. **Local Storage:** `NSUserDefaults` key `GACAppCheckDebugToken`. -3. **Generation:** If neither exists, it generates a new UUID, stores it - in `NSUserDefaults`, and logs it to the console (warning level). - -### Flow -```mermaid -sequenceDiagram - participant App - participant Provider as GACAppCheckDebugProvider - participant API as GACAppCheckDebugProviderAPIService - participant Backend as Firebase Backend - - App->>Provider: getToken(limitedUse) - Provider->>Provider: Determine Debug Secret (Env Var or UUID) - - Provider->>API: appCheckTokenWithDebugToken(debugToken, limitedUse) - API->>Backend: POST /exchangeDebugToken
{ limited_use: true/false } - Note right of Backend: Checks if debug token is
registered in Console. - Backend-->>API: { "token": "..." } - - Provider-->>App: App Check Token -``` +* [AppAttest Provider](providers/app-attest.md) + * The primary provider for modern iOS devices, wrapping `DCAppAttestService`. + * Features complex state management, automatic retries, and request coalescing. +* [DeviceCheck Provider](providers/device-check.md) + * A fallback provider for older devices using `DCDevice`. +* [Debug Provider](providers/debug.md) + * For local development and CI/CD environments. diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md new file mode 100644 index 00000000..7a4b0aa4 --- /dev/null +++ b/docs/providers/app-attest.md @@ -0,0 +1,199 @@ +# AppAttest Provider (`GACAppAttestProvider`) + +The most complex provider, interacting with `DCAppAttestService`. It +maintains a stable key pair on the device to sign assertions. + +## Components +* **Service:** `DCAppAttestService` (Apple's API). +* **Storage:** + * `GACAppAttestKeyIDStorage`: Stores the generated App Attest Key + ID. + * `GACAppAttestArtifactStorage`: Stores the "artifact" returned by + the Firebase backend after a successful initial handshake. This + artifact effectively links the on-device key to the backend + session. +* **Resiliency:** + * **Automatic Retry:** The provider wraps the entire flow in a + retry loop. If a specific "Rejection Error" occurs (e.g., + invalid key), it resets its internal state and retries the flow + from scratch. + +## Decision Logic & State Machine +Before executing a handshake, the provider determines the correct flow +based on the internal state and manages concurrent requests. + +**Note on Limited Use:** Limited-use tokens are never reused/coalesced. +If a limited-use token is requested (or if one is currently being +fetched), the new request will "chain" (wait for the ongoing one to +finish) and then start a fresh handshake to ensure a unique token is +generated. + +```mermaid +flowchart TD + Start[getToken call] --> CheckUse{Limited Use Request?} + + CheckUse -- Yes --> Chain[Chain Promise] + CheckUse -- No --> Coalesce{Ongoing Operation?} + + Coalesce -- No --> Backoff[Apply Backoff] + Coalesce -- Yes --> CheckOngoing{Ongoing is Limited?} + + CheckOngoing -- Yes --> Chain + CheckOngoing -- No --> Reuse[Reuse Ongoing Promise] + + Chain --> Backoff + + Backoff --> StateCheck{Attestation State?} + + StateCheck -->|Not Supported| Error[Return Error] + + StateCheck -->|Supported| KeyCheck{Key ID Stored?} + + KeyCheck -- No --> Flow1[Flow 1: Initial Handshake] + KeyCheck -- Yes --> ArtifactCheck{Artifact Stored?} + + ArtifactCheck -- No --> Flow1 + ArtifactCheck -- Yes --> Flow2[Flow 2: Token Refresh/Assertion] +``` + +## Concurrent Request Handling +The `GACAppAttestProvider` carefully manages concurrent calls to +`getToken(limitedUse:)` to ensure correctness and efficiency: + +* **No Ongoing Operation:** If no token fetching operation is in + progress, a new one is started, and its promise is stored as the + `ongoingGetTokenOperation`. +* **Reuse (Standard Tokens Only):** If a standard (non-limited use) + token is requested, and there's an `ongoingGetTokenOperation` that + is also for a standard token, the existing promise is reused. This + ensures only one actual token fetch occurs for multiple concurrent + standard requests. +* **Chaining (Limited-Use or Mismatched Requests):** + * If a limited-use token is requested, *or* + * If a standard token is requested but the `ongoingGetTokenOperation` + is for a limited-use token (or vice versa), + the new request will **chain**. This means it waits for the currently + `ongoingGetTokenOperation` to complete, and then initiates a *new*, separate + token fetching sequence. This prevents limited-use tokens from being + accidentally reused and ensures distinct token types are handled + independently. + +```mermaid +sequenceDiagram + participant AppA as App (Standard) + participant AppB as App (Limited) + participant AppC as App (Standard) + participant Provider as GACAppAttestProvider + + AppA->>Provider: getToken(false) + activate Provider + Note right of Provider: No ongoing op.
Start new op (standard).
Set ongoingGetTokenOperation. + Provider-->>Provider: Start Flow 1/2 sequence + + AppB->>Provider: getToken(true) + activate Provider + Note right of Provider: Ongoing op (standard) exists.
New request is limited-use.
Chain: Wait for ongoing, then start new op. + Provider->>Provider: Await ongoing op completion + deactivate Provider + + AppC->>Provider: getToken(false) + activate Provider + Note right of Provider: Ongoing op (standard) exists.
New request is standard.
Reuse ongoing op's promise. + Provider-->>AppC: App Check Token (from ongoing op) + deactivate Provider + + Provider-->>AppA: App Check Token (from completed op) + deactivate Provider + + Provider-->>Provider: Start new Flow 1/2 for AppB + activate Provider + Provider-->>AppB: App Check Token (from new op) + deactivate Provider +``` + +## Flow 1: Initial Handshake (Attestation) +Occurs when the app runs for the first time, or if the stored artifact +is missing, or **after a reset**. + +```mermaid +sequenceDiagram + participant App + participant Provider as GACAppAttestProvider + participant Apple as DCAppAttestService

(Apple's DeviceCheck Framework) + participant API as GACAppAttestAPIService + participant Backend as Firebase Backend + + App->>Provider: getToken(limitedUse) + + loop Retry Loop (Max 1 Retry) + par Parallel Execution + Provider->>API: getRandomChallenge() + API->>Backend: POST /generateAppAttestChallenge + Backend-->>API: { "challenge": "..." } + API-->>Provider: Challenge + and + Provider->>Apple: generateKey() (If needed) + Apple-->>Provider: Key ID + end + + Provider->>Apple: attestKey(keyId, clientDataHash=SHA256(challenge)) + + alt Attestation Failed (Invalid Key/Input) + Apple-->>Provider: DCErrorInvalidKey / Input + Provider->>Provider: RESET: Delete KeyID & Artifact + Note right of Provider: Throws RejectionError,
Triggering Loop Retry + else Attestation Success + Apple-->>Provider: Attestation Object + Provider->>API: attestKeyWithAttestation(..., limitedUse) + API->>Backend: POST /exchangeAppAttestAttestation
{ limited_use: true/false } + + alt Backend Rejection (403) + Backend-->>API: 403 Forbidden + Provider->>Provider: RESET: Delete KeyID & Artifact + Note right of Provider: Throws RejectionError,
Triggering Loop Retry + else Success + Backend-->>API: { "token": "...", "artifact": "..." } + Provider->>Provider: Store Artifact & Key ID + Provider-->>App: App Check Token + end + end + end +``` + +## Flow 2: Token Refresh (Assertion) +Occurs for subsequent requests using the established key pair. + +```mermaid +sequenceDiagram + participant App + participant Provider as GACAppAttestProvider + participant Apple as DCAppAttestService

(Apple's DeviceCheck Framework) + participant API as GACAppAttestAPIService + participant Backend as Firebase Backend + + App->>Provider: getToken(limitedUse) + + loop Retry Loop (Max 1 Retry) + Provider->>API: getRandomChallenge() + API->>Backend: POST /generateAppAttestChallenge + Backend-->>API: { "challenge": "..." } + + Provider->>Provider: Retrieve stored Artifact + Provider->>Provider: ClientData = Artifact + Challenge + Provider->>Apple: generateAssertion(keyId, clientDataHash=SHA256(ClientData)) + + alt Assertion Failed (Invalid Key/Input) + Apple-->>Provider: DCErrorInvalidKey / Input + Provider->>Provider: RESET: Delete KeyID & Artifact + Note right of Provider: Throws RejectionError,
Triggering Loop Retry
(Will fall back to Initial Handshake) + else Assertion Success + Apple-->>Provider: Assertion Object + + Provider->>API: getAppCheckTokenWithArtifact(..., limitedUse) + API->>Backend: POST /exchangeAppAttestAssertion
{ limited_use: true/false } + Backend-->>API: { "token": "..." } + + Provider-->>App: App Check Token + end + end +``` diff --git a/docs/providers/debug.md b/docs/providers/debug.md new file mode 100644 index 00000000..09cf0af5 --- /dev/null +++ b/docs/providers/debug.md @@ -0,0 +1,30 @@ +# Debug Provider (`GACAppCheckDebugProvider`) + +Used for local development and CI. + +## Configuration +The provider looks for a debug secret in the following order: +1. **Environment Variable:** `AppCheckDebugToken` (or legacy + `FIRAAppCheckDebugToken`). +2. **Local Storage:** `NSUserDefaults` key `GACAppCheckDebugToken`. +3. **Generation:** If neither exists, it generates a new UUID, stores it + in `NSUserDefaults`, and logs it to the console (warning level). + +## Flow +```mermaid +sequenceDiagram + participant App + participant Provider as GACAppCheckDebugProvider + participant API as GACAppCheckDebugProviderAPIService + participant Backend as Firebase Backend + + App->>Provider: getToken(limitedUse) + Provider->>Provider: Determine Debug Secret (Env Var or UUID) + + Provider->>API: appCheckTokenWithDebugToken(debugToken, limitedUse) + API->>Backend: POST /exchangeDebugToken
{ limited_use: true/false } + Note right of Backend: Checks if debug token is
registered in Console. + Backend-->>API: { "token": "..." } + + Provider-->>App: App Check Token +``` diff --git a/docs/providers/device-check.md b/docs/providers/device-check.md new file mode 100644 index 00000000..47cec758 --- /dev/null +++ b/docs/providers/device-check.md @@ -0,0 +1,36 @@ +# DeviceCheck Provider (`GACDeviceCheckProvider`) + +A simpler provider for older devices. + +## Components +* **Service:** `DCDevice` (Apple's API). +* **Generator:** `DCDevice.currentDevice`. + +## Flow +```mermaid +sequenceDiagram + participant App + participant Provider as GACDeviceCheckProvider + participant Apple as DCDevice

(Apple's DeviceCheck Framework) + participant API as GACDeviceCheckAPIService + participant Backend as Firebase Backend + + App->>Provider: getToken(limitedUse) + + Note right of Provider: Wrapped in Backoff Wrapper + Provider->>Apple: generateToken() + Apple-->>Provider: Device Token (Ephemeral) + + Provider->>API: appCheckTokenWithDeviceToken(deviceToken, limitedUse) + API->>Backend: POST /exchangeDeviceCheckToken
{ limited_use: true/false } + Note right of Backend: Verifies device token with Apple. + + alt Error (e.g., 503) + Backend-->>API: 503 Service Unavailable + Provider->>Provider: Record Failure (Backoff) + Provider-->>App: Error + else Success + Backend-->>API: { "token": "..." } + Provider-->>App: App Check Token + end +``` From 038f296d29c089c1f122b6fd6e8262d0dd879d34 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 16:17:38 -0500 Subject: [PATCH 10/94] fix ori --- docs/providers/app-attest.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 7a4b0aa4..bba66052 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,31 +29,31 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -flowchart TD - Start[getToken call] --> CheckUse{Limited Use Request?} +flowchart LR + Start[getToken] --> CheckUse{Limited Use?} CheckUse -- Yes --> Chain[Chain Promise] - CheckUse -- No --> Coalesce{Ongoing Operation?} + CheckUse -- No --> Coalesce{Ongoing Op?} - Coalesce -- No --> Backoff[Apply Backoff] - Coalesce -- Yes --> CheckOngoing{Ongoing is Limited?} + Coalesce -- No --> Backoff[Backoff] + Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} CheckOngoing -- Yes --> Chain - CheckOngoing -- No --> Reuse[Reuse Ongoing Promise] + CheckOngoing -- No --> Reuse[Reuse Promise] Chain --> Backoff Backoff --> StateCheck{Attestation State?} - StateCheck -->|Not Supported| Error[Return Error] + StateCheck -->|No| Error[Error] - StateCheck -->|Supported| KeyCheck{Key ID Stored?} + StateCheck -->|Yes| KeyCheck{Key ID?} - KeyCheck -- No --> Flow1[Flow 1: Initial Handshake] - KeyCheck -- Yes --> ArtifactCheck{Artifact Stored?} + KeyCheck -- No --> Flow1[Flow 1: Initial] + KeyCheck -- Yes --> ArtifactCheck{Artifact?} ArtifactCheck -- No --> Flow1 - ArtifactCheck -- Yes --> Flow2[Flow 2: Token Refresh/Assertion] + ArtifactCheck -- Yes --> Flow2[Flow 2: Refresh] ``` ## Concurrent Request Handling @@ -196,4 +196,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` +``` \ No newline at end of file From 316401f0420267c8f4e8948cad0d0bb146c46e16 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 16:27:06 -0500 Subject: [PATCH 11/94] docs --- docs/providers/app-attest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index bba66052..662d975c 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -45,8 +45,6 @@ flowchart LR Backoff --> StateCheck{Attestation State?} - StateCheck -->|No| Error[Error] - StateCheck -->|Yes| KeyCheck{Key ID?} KeyCheck -- No --> Flow1[Flow 1: Initial] @@ -54,6 +52,8 @@ flowchart LR ArtifactCheck -- No --> Flow1 ArtifactCheck -- Yes --> Flow2[Flow 2: Refresh] + + StateCheck -->|No| Error[Error] ``` ## Concurrent Request Handling From d2e7f9c451eed512c0fcf612a3dd66dd330dc281 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 16:30:43 -0500 Subject: [PATCH 12/94] b --- docs/providers/app-attest.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 662d975c..b0053a9a 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -32,16 +32,17 @@ generated. flowchart LR Start[getToken] --> CheckUse{Limited Use?} - CheckUse -- Yes --> Chain[Chain Promise] + CheckUse -- Yes --> Queue[Queue New Request] CheckUse -- No --> Coalesce{Ongoing Op?} - Coalesce -- No --> Backoff[Backoff] + Coalesce -- No --> StartNew[Start New Request] Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} - CheckOngoing -- Yes --> Chain - CheckOngoing -- No --> Reuse[Reuse Promise] + CheckOngoing -- Yes --> Queue + CheckOngoing -- No --> Reuse[Reuse Existing Request] - Chain --> Backoff + Queue --> StartNew + StartNew --> Backoff[Backoff] Backoff --> StateCheck{Attestation State?} @@ -196,4 +197,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` \ No newline at end of file +``` From 8821cc37b4d363bb5d51b0f17239c6a180813c10 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:05:20 -0500 Subject: [PATCH 13/94] b --- docs/providers/app-attest.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index b0053a9a..10fb3f45 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -55,6 +55,8 @@ flowchart LR ArtifactCheck -- Yes --> Flow2[Flow 2: Refresh] StateCheck -->|No| Error[Error] + + Reuse ~~~ Footnote[Note: Requests are represented as a
shared promise property 'ongoingGetTokenOperation'] ``` ## Concurrent Request Handling @@ -197,4 +199,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` +``` \ No newline at end of file From bc09dd51a70f02c350ac959cad04e2ebf94e1d17 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:07:26 -0500 Subject: [PATCH 14/94] padding --- docs/providers/app-attest.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 10fb3f45..51930ae2 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,6 +29,7 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid +%%{init: {"flowchart": {"diagramPadding": 150}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} @@ -199,4 +200,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` \ No newline at end of file +``` From 21ebcd0f02aa195817e7d0963307782779db218e Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:09:00 -0500 Subject: [PATCH 15/94] b --- docs/providers/app-attest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 51930ae2..89623206 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,7 +29,7 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 150}}}%% +%%{init: {"flowchart": {"diagramPadding": 150, "fontSize": 16}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} @@ -200,4 +200,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` +``` \ No newline at end of file From 211380025abfd9152bf7554ed26a816ce1ac8218 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:10:00 -0500 Subject: [PATCH 16/94] b --- docs/providers/app-attest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 89623206..1d51847f 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,7 +29,7 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 150, "fontSize": 16}}}%% +%%{init: {"flowchart": {"diagramPadding": 150, "fontSize": 36}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} @@ -200,4 +200,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` \ No newline at end of file +``` From 6e8b1afa9bd1206202d6f994f6f7f43a35f01158 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:12:40 -0500 Subject: [PATCH 17/94] b --- docs/providers/app-attest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 1d51847f..fd675540 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,7 +29,7 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 150, "fontSize": 36}}}%% +%%{init: {"flowchart": {"diagramPadding": 125, "fontSize": 36}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} @@ -200,4 +200,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` +``` \ No newline at end of file From 82a552917155e307b6c7718ebb43c55174a7cf65 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:14:01 -0500 Subject: [PATCH 18/94] b --- docs/providers/app-attest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index fd675540..4119e779 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,7 +29,7 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 125, "fontSize": 36}}}%% +%%{init: {"flowchart": {"diagramPadding": 50, "fontSize": 36}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} @@ -200,4 +200,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` \ No newline at end of file +``` From 608346510ef60ccdad97dcb7ddb7ec0352ac5680 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:15:13 -0500 Subject: [PATCH 19/94] b --- docs/providers/app-attest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 4119e779..f5a97eed 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,7 +29,7 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 50, "fontSize": 36}}}%% +%%{init: {"flowchart": {"diagramPadding": 75, "fontSize": 36}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} @@ -200,4 +200,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` +``` \ No newline at end of file From e7f525e766366c6aefe3aae10e1cbc3beb2f6404 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:21:29 -0500 Subject: [PATCH 20/94] b --- docs/providers/app-attest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index f5a97eed..59257d73 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,7 +29,7 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 75, "fontSize": 36}}}%% +%%{init: {"flowchart": {"diagramPadding": 75}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} @@ -200,4 +200,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` \ No newline at end of file +``` From 8ce6ad786931fc432f76f7d9de7fa9cc5b35602c Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:22:37 -0500 Subject: [PATCH 21/94] b --- docs/providers/app-attest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 59257d73..bbefea2c 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,7 +29,7 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 75}}}%% +%%{init: {"flowchart": {"diagramPadding": 130}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} @@ -200,4 +200,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` +``` \ No newline at end of file From fd2c1e6443bc8eb4b7dce8b9b098bce6c7d9fb54 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:23:32 -0500 Subject: [PATCH 22/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index bbefea2c..47315c96 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,7 +29,7 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 130}}}%% +%%{init: {"flowchart": {"diagramPadding": 125}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} From 9807398c69674308c1f4f0334110b0f5c1132d1c Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:24:25 -0500 Subject: [PATCH 23/94] b --- docs/providers/app-attest.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 47315c96..b88d96ca 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -56,8 +56,6 @@ flowchart LR ArtifactCheck -- Yes --> Flow2[Flow 2: Refresh] StateCheck -->|No| Error[Error] - - Reuse ~~~ Footnote[Note: Requests are represented as a
shared promise property 'ongoingGetTokenOperation'] ``` ## Concurrent Request Handling From e021f096fc6e8f48120f01af748de8820a095d39 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:24:43 -0500 Subject: [PATCH 24/94] b --- docs/providers/app-attest.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index b88d96ca..47315c96 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -56,6 +56,8 @@ flowchart LR ArtifactCheck -- Yes --> Flow2[Flow 2: Refresh] StateCheck -->|No| Error[Error] + + Reuse ~~~ Footnote[Note: Requests are represented as a
shared promise property 'ongoingGetTokenOperation'] ``` ## Concurrent Request Handling From a937cd78d15ac7101e48d9f788d4ab722f1e6693 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:28:55 -0500 Subject: [PATCH 25/94] b --- docs/providers/app-attest.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 47315c96..02b5cbdc 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,7 +29,7 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 125}}}%% +%%{init: {"flowchart": {"diagramPadding": 130}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} @@ -57,7 +57,9 @@ flowchart LR StateCheck -->|No| Error[Error] - Reuse ~~~ Footnote[Note: Requests are represented as a
shared promise property 'ongoingGetTokenOperation'] + Reuse -.- Footnote[Note: The 'ongoingGetTokenOperation' (with its 'ongoingGetTokenOperationLimitedUse' flag)
manages the active token fetch. Standard requests reuse it if types match;
otherwise, new requests are queued to run sequentially.] + Queue -.- Footnote + StartNew -.- Footnote ``` ## Concurrent Request Handling @@ -200,4 +202,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` \ No newline at end of file +``` From 67ea37fd3223b4dcddf27d28f9913d0c126bb627 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:29:47 -0500 Subject: [PATCH 26/94] b --- docs/providers/app-attest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 02b5cbdc..70c2096e 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -57,7 +57,7 @@ flowchart LR StateCheck -->|No| Error[Error] - Reuse -.- Footnote[Note: The 'ongoingGetTokenOperation' (with its 'ongoingGetTokenOperationLimitedUse' flag)
manages the active token fetch. Standard requests reuse it if types match;
otherwise, new requests are queued to run sequentially.] + Reuse -.- Footnote["Note: The 'ongoingGetTokenOperation' (with its 'ongoingGetTokenOperationLimitedUse' flag)
manages the active token fetch. Standard requests reuse it if types match;
otherwise, new requests are queued to run sequentially."] Queue -.- Footnote StartNew -.- Footnote ``` @@ -202,4 +202,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` +``` \ No newline at end of file From 2d7a055541b9898c03afe554e20098062c21fb13 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:30:42 -0500 Subject: [PATCH 27/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 70c2096e..c312a9cd 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,7 +29,7 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 130}}}%% +%%{init: {"flowchart": {"diagramPadding": 100}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} From 397b9cc291fdcdc57930d3d8891ceb238823606c Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:30:57 -0500 Subject: [PATCH 28/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index c312a9cd..3b660860 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,7 +29,7 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 100}}}%% +%%{init: {"flowchart": {"diagramPadding": 115}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} From 51c385498898fac84f6d041921ff1d81d2f2c539 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:31:12 -0500 Subject: [PATCH 29/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 3b660860..96db92c8 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,7 +29,7 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 115}}}%% +%%{init: {"flowchart": {"diagramPadding": 120}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} From 70fab4366b01a1389c72ed71639e5dc82ca62859 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:31:25 -0500 Subject: [PATCH 30/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 96db92c8..f561c75d 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,7 +29,7 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 120}}}%% +%%{init: {"flowchart": {"diagramPadding": 125}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} From 14310bbb8746cda87d552d70c33a11d48ed4ff64 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:31:42 -0500 Subject: [PATCH 31/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index f561c75d..70c2096e 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,7 +29,7 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 125}}}%% +%%{init: {"flowchart": {"diagramPadding": 130}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} From 08396e35bffc2bc5ccedc0163a1972a32186a341 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:32:53 -0500 Subject: [PATCH 32/94] b --- docs/providers/app-attest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 70c2096e..e5ad104a 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -49,11 +49,11 @@ flowchart LR StateCheck -->|Yes| KeyCheck{Key ID?} - KeyCheck -- No --> Flow1[Flow 1: Initial] + KeyCheck -- No --> Flow1[Flow 1: Initial Handshake
(Attestation)] KeyCheck -- Yes --> ArtifactCheck{Artifact?} ArtifactCheck -- No --> Flow1 - ArtifactCheck -- Yes --> Flow2[Flow 2: Refresh] + ArtifactCheck -- Yes --> Flow2[Flow 2: Token Refresh
(Assertion)] StateCheck -->|No| Error[Error] From 8117e715e7a1bb93c8265a6737fe78c5c9ca3529 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:33:14 -0500 Subject: [PATCH 33/94] b --- docs/providers/app-attest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index e5ad104a..89446514 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -49,11 +49,11 @@ flowchart LR StateCheck -->|Yes| KeyCheck{Key ID?} - KeyCheck -- No --> Flow1[Flow 1: Initial Handshake
(Attestation)] + KeyCheck -- No --> Flow1[Flow 1: Initial Handshake
(Attestation)] KeyCheck -- Yes --> ArtifactCheck{Artifact?} ArtifactCheck -- No --> Flow1 - ArtifactCheck -- Yes --> Flow2[Flow 2: Token Refresh
(Assertion)] + ArtifactCheck -- Yes --> Flow2[Flow 2: Token Refresh
(Assertion)] StateCheck -->|No| Error[Error] From 2e18fd06e9bc87202fbc28edad60298d016a38e3 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:33:31 -0500 Subject: [PATCH 34/94] b --- docs/providers/app-attest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 89446514..3f77747d 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -49,11 +49,11 @@ flowchart LR StateCheck -->|Yes| KeyCheck{Key ID?} - KeyCheck -- No --> Flow1[Flow 1: Initial Handshake
(Attestation)] + KeyCheck -- No --> Flow1[Flow 1: Initial Handshake (Attestation)] KeyCheck -- Yes --> ArtifactCheck{Artifact?} ArtifactCheck -- No --> Flow1 - ArtifactCheck -- Yes --> Flow2[Flow 2: Token Refresh
(Assertion)] + ArtifactCheck -- Yes --> Flow2[Flow 2: Token Refresh (Assertion)] StateCheck -->|No| Error[Error] From 620d979a88cc869304701f628c3e202dc952965e Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:33:52 -0500 Subject: [PATCH 35/94] b --- docs/providers/app-attest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 3f77747d..2be9d422 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -49,11 +49,11 @@ flowchart LR StateCheck -->|Yes| KeyCheck{Key ID?} - KeyCheck -- No --> Flow1[Flow 1: Initial Handshake (Attestation)] + KeyCheck -- No --> Flow1[Flow 1: Initial Handshake \(Attestation\)] KeyCheck -- Yes --> ArtifactCheck{Artifact?} ArtifactCheck -- No --> Flow1 - ArtifactCheck -- Yes --> Flow2[Flow 2: Token Refresh (Assertion)] + ArtifactCheck -- Yes --> Flow2[Flow 2: Token Refresh \(Assertion\)] StateCheck -->|No| Error[Error] From a59b2ba87c066de4015cdf3ff246ef60cdf8dd56 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:34:16 -0500 Subject: [PATCH 36/94] b --- docs/providers/app-attest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 2be9d422..40bca5cb 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -49,11 +49,11 @@ flowchart LR StateCheck -->|Yes| KeyCheck{Key ID?} - KeyCheck -- No --> Flow1[Flow 1: Initial Handshake \(Attestation\)] + KeyCheck -- No --> Flow1[Flow 1: Initial Handshake Attestation] KeyCheck -- Yes --> ArtifactCheck{Artifact?} ArtifactCheck -- No --> Flow1 - ArtifactCheck -- Yes --> Flow2[Flow 2: Token Refresh \(Assertion\)] + ArtifactCheck -- Yes --> Flow2[Flow 2: Token Refresh Assertion] StateCheck -->|No| Error[Error] From 835e83393677c2ba67ac056030fdf7cb2a95b271 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:34:25 -0500 Subject: [PATCH 37/94] b --- docs/providers/app-attest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 40bca5cb..9473611b 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -49,11 +49,11 @@ flowchart LR StateCheck -->|Yes| KeyCheck{Key ID?} - KeyCheck -- No --> Flow1[Flow 1: Initial Handshake Attestation] + KeyCheck -- No --> Flow1[Flow 1: Initial Handshake
Attestation] KeyCheck -- Yes --> ArtifactCheck{Artifact?} ArtifactCheck -- No --> Flow1 - ArtifactCheck -- Yes --> Flow2[Flow 2: Token Refresh Assertion] + ArtifactCheck -- Yes --> Flow2[Flow 2: Token Refresh
Assertion] StateCheck -->|No| Error[Error] From 56138ebd809332fbcbf551ee4ed19d8c64d74f1e Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:35:25 -0500 Subject: [PATCH 38/94] b --- docs/providers/app-attest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 9473611b..67212bac 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -49,11 +49,11 @@ flowchart LR StateCheck -->|Yes| KeyCheck{Key ID?} - KeyCheck -- No --> Flow1[Flow 1: Initial Handshake
Attestation] + KeyCheck -- No --> Flow1["Flow 1: Initial Handshake
(Attestation)"] KeyCheck -- Yes --> ArtifactCheck{Artifact?} ArtifactCheck -- No --> Flow1 - ArtifactCheck -- Yes --> Flow2[Flow 2: Token Refresh
Assertion] + ArtifactCheck -- Yes --> Flow2["Flow 2: Token Refresh
(Assertion)"] StateCheck -->|No| Error[Error] From 9af2ddb28407d60ba911b9f50b88a21b38a12995 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 17:35:54 -0500 Subject: [PATCH 39/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 67212bac..fede3926 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,7 +29,7 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 130}}}%% +%%{init: {"flowchart": {"diagramPadding": 140}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} From d2c05ecf37f4ac99fbf42c5a64af80cda26f01ef Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 18:21:47 -0500 Subject: [PATCH 40/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index fede3926..642aa512 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -42,7 +42,7 @@ flowchart LR CheckOngoing -- Yes --> Queue CheckOngoing -- No --> Reuse[Reuse Existing Request] - Queue --> StartNew + Queue --> Backoff StartNew --> Backoff[Backoff] Backoff --> StateCheck{Attestation State?} From 80cb6d2f271c7c6853c4a629d012a7c438aefab8 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 18:25:34 -0500 Subject: [PATCH 41/94] b --- docs/providers/app-attest.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 642aa512..5d2f9705 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,36 +29,38 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 140}}}%% +%%{init: {"flowchart": {"diagramPadding": 130}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} - CheckUse -- Yes --> Queue[Queue New Request] + CheckUse -- Yes --> Queue1[Queue New Request] CheckUse -- No --> Coalesce{Ongoing Op?} Coalesce -- No --> StartNew[Start New Request] Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} - CheckOngoing -- Yes --> Queue + CheckOngoing -- Yes --> Queue2[Queue New Request] CheckOngoing -- No --> Reuse[Reuse Existing Request] - Queue --> Backoff - StartNew --> Backoff[Backoff] + Queue1 --> Backoff[Backoff] + Queue2 --> Backoff + StartNew --> Backoff Backoff --> StateCheck{Attestation State?} StateCheck -->|Yes| KeyCheck{Key ID?} - KeyCheck -- No --> Flow1["Flow 1: Initial Handshake
(Attestation)"] + KeyCheck -- No --> Flow1[Flow 1: Initial] KeyCheck -- Yes --> ArtifactCheck{Artifact?} ArtifactCheck -- No --> Flow1 - ArtifactCheck -- Yes --> Flow2["Flow 2: Token Refresh
(Assertion)"] + ArtifactCheck -- Yes --> Flow2[Flow 2: Refresh] StateCheck -->|No| Error[Error] Reuse -.- Footnote["Note: The 'ongoingGetTokenOperation' (with its 'ongoingGetTokenOperationLimitedUse' flag)
manages the active token fetch. Standard requests reuse it if types match;
otherwise, new requests are queued to run sequentially."] - Queue -.- Footnote + Queue1 -.- Footnote + Queue2 -.- Footnote StartNew -.- Footnote ``` @@ -202,4 +204,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` \ No newline at end of file +``` From e13ec89136408422168cfee4d5a0c21afe491cce Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 18:31:12 -0500 Subject: [PATCH 42/94] b --- docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture.md b/docs/architecture.md index 36c9cee2..7ba2f1c6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -56,7 +56,7 @@ The backoff interval is calculated as follows: $$ \text{Interval} = \min(\text{Base} \times \text{Jitter}, \text{MaxInterval}) $$ -* **Base:** $2^{\text{retry\_count}}$ seconds. +* **Base:** $2^{\text{retry_count}}$ seconds. * **Jitter:** A random multiplier between $1.0$ and $1.5$ (to prevent thundering herd problems). * **MaxInterval:** 4 hours. From 6da0bbe5578b127935617c02fad933ed59edabbe Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 18:31:51 -0500 Subject: [PATCH 43/94] b --- docs/architecture.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 7ba2f1c6..2cfc7ad4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -67,14 +67,9 @@ codes returned by the backend: | HTTP Status Code | Backoff Type | Reason | | :--- | :--- | :--- | -| **< 400** | **None** | Network errors or successful requests do not - trigger backoff. | -| **400 (Bad Request)**
**404 (Not Found)** | **1 Day** | Indicates a - project misconfiguration or outdated app version. Unlikely to - resolve quickly. | -| **403 (Forbidden)**
**429 (Too Many Requests)**
**503 (Service - Unavailable)** | **Exponential** | Indicates soft deletion, rate - limiting, or server overload. Retrying later is appropriate. | +| **< 400** | **None** | Network errors or successful requests do not trigger backoff. | +| **400 (Bad Request)**
**404 (Not Found)** | **1 Day** | Indicates a project misconfiguration or outdated app version. Unlikely to resolve quickly. | +| **403 (Forbidden)**
**429 (Too Many Requests)**
**503 (Service Unavailable)** | **Exponential** | Indicates soft deletion, rate limiting, or server overload. Retrying later is appropriate. | | **Other 5xx** | **Exponential** | Standard server errors. | ### Implementation From e52dc698dedd056eb7e2a2f9cd782358f91a4f29 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 18:32:27 -0500 Subject: [PATCH 44/94] b --- docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture.md b/docs/architecture.md index 2cfc7ad4..ec1d4133 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -56,7 +56,7 @@ The backoff interval is calculated as follows: $$ \text{Interval} = \min(\text{Base} \times \text{Jitter}, \text{MaxInterval}) $$ -* **Base:** $2^{\text{retry_count}}$ seconds. +* **Base:** $2^{\text{retrycount}}$ seconds. * **Jitter:** A random multiplier between $1.0$ and $1.5$ (to prevent thundering herd problems). * **MaxInterval:** 4 hours. From 55c41497b5ef5ef52dfce90767c6ddb54027d8f7 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 18:42:59 -0500 Subject: [PATCH 45/94] b --- docs/providers/app-attest.md | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 5d2f9705..92a6abd1 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -29,7 +29,7 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 130}}}%% +%%{init: {"flowchart": {"diagramPadding": 75}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} @@ -42,21 +42,27 @@ flowchart LR CheckOngoing -- Yes --> Queue2[Queue New Request] CheckOngoing -- No --> Reuse[Reuse Existing Request] - Queue1 --> Backoff[Backoff] + subgraph Execution ["Backoff Wrapped Execution"] + direction LR + Backoff[Check Backoff] + StateCheck{Attestation State?} + + Backoff --> StateCheck + + StateCheck -->|Yes| KeyCheck{Key ID?} + + KeyCheck -- No --> Flow1[Flow 1: Initial] + KeyCheck -- Yes --> ArtifactCheck{Artifact?} + + ArtifactCheck -- No --> Flow1 + ArtifactCheck -- Yes --> Flow2[Flow 2: Refresh] + + StateCheck -->|No| Error[Error] + end + + Queue1 --> Backoff Queue2 --> Backoff StartNew --> Backoff - - Backoff --> StateCheck{Attestation State?} - - StateCheck -->|Yes| KeyCheck{Key ID?} - - KeyCheck -- No --> Flow1[Flow 1: Initial] - KeyCheck -- Yes --> ArtifactCheck{Artifact?} - - ArtifactCheck -- No --> Flow1 - ArtifactCheck -- Yes --> Flow2[Flow 2: Refresh] - - StateCheck -->|No| Error[Error] Reuse -.- Footnote["Note: The 'ongoingGetTokenOperation' (with its 'ongoingGetTokenOperationLimitedUse' flag)
manages the active token fetch. Standard requests reuse it if types match;
otherwise, new requests are queued to run sequentially."] Queue1 -.- Footnote @@ -204,4 +210,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` +``` \ No newline at end of file From b330e00551dbaf2091d1f7e8821fd21f53f48bbb Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 18:51:32 -0500 Subject: [PATCH 46/94] b --- docs/architecture.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index ec1d4133..b7bb1145 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -53,10 +53,8 @@ To prevent overwhelming the backend or Apple's servers during failures, ### Algorithm The backoff interval is calculated as follows: -$$ -\text{Interval} = \min(\text{Base} \times \text{Jitter}, \text{MaxInterval}) -$$ -* **Base:** $2^{\text{retrycount}}$ seconds. +$ Interval = min(Base * Jitter, MaxInterval) $ +* **Base:** $2^{retry\_count}$ seconds. * **Jitter:** A random multiplier between $1.0$ and $1.5$ (to prevent thundering herd problems). * **MaxInterval:** 4 hours. From 3277e797132431482482c2f633da8d445d65eb6b Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 19:01:21 -0500 Subject: [PATCH 47/94] b --- docs/architecture.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index b7bb1145..ad3c3799 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -53,8 +53,10 @@ To prevent overwhelming the backend or Apple's servers during failures, ### Algorithm The backoff interval is calculated as follows: -$ Interval = min(Base * Jitter, MaxInterval) $ -* **Base:** $2^{retry\_count}$ seconds. +```math +Interval = \min(Base \times Jitter, MaxInterval) +``` +* **Base:** $2^{retryCount}$ seconds. * **Jitter:** A random multiplier between $1.0$ and $1.5$ (to prevent thundering herd problems). * **MaxInterval:** 4 hours. From 3d81ca282a9ab5d1a19abea87979154270469466 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 19:05:06 -0500 Subject: [PATCH 48/94] b --- docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture.md b/docs/architecture.md index ad3c3799..698059a9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -56,7 +56,7 @@ The backoff interval is calculated as follows: ```math Interval = \min(Base \times Jitter, MaxInterval) ``` -* **Base:** $2^{retryCount}$ seconds. +* **Base:** $`2^{retryCount}`$ seconds. * **Jitter:** A random multiplier between $1.0$ and $1.5$ (to prevent thundering herd problems). * **MaxInterval:** 4 hours. From e5c4d8e66c2aec121796fcdef35d0325f403d1fc Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 19:18:25 -0500 Subject: [PATCH 49/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 92a6abd1..1b1cecd9 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -64,7 +64,7 @@ flowchart LR Queue2 --> Backoff StartNew --> Backoff - Reuse -.- Footnote["Note: The 'ongoingGetTokenOperation' (with its 'ongoingGetTokenOperationLimitedUse' flag)
manages the active token fetch. Standard requests reuse it if types match;
otherwise, new requests are queued to run sequentially."] + Reuse -.- Footnote["Note: The 'ongoingGetTokenOperation' (with its 'ongoingGetTokenOperationLimitedUse' flag) manages the active token fetch. Standard requests reuse it if
types match;otherwise, new requests are queued to run sequentially."] Queue1 -.- Footnote Queue2 -.- Footnote StartNew -.- Footnote From 76b5604c0a4ad968d43aaa1d9a9dee5b2a7b5167 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 19:18:57 -0500 Subject: [PATCH 50/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 1b1cecd9..c338f7df 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -64,7 +64,7 @@ flowchart LR Queue2 --> Backoff StartNew --> Backoff - Reuse -.- Footnote["Note: The 'ongoingGetTokenOperation' (with its 'ongoingGetTokenOperationLimitedUse' flag) manages the active token fetch. Standard requests reuse it if
types match;otherwise, new requests are queued to run sequentially."] + Reuse -.- Footnote["Note: The 'ongoingGetTokenOperation' (with its 'ongoingGetTokenOperationLimitedUse' flag) manages the active token fetch. Standard requests reuse it if types match;otherwise, new requests are queued to run sequentially."] Queue1 -.- Footnote Queue2 -.- Footnote StartNew -.- Footnote From 50f9dad29f0483f41400254a8e5b414a54d50459 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 19:21:12 -0500 Subject: [PATCH 51/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index c338f7df..2768790b 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -64,7 +64,7 @@ flowchart LR Queue2 --> Backoff StartNew --> Backoff - Reuse -.- Footnote["Note: The 'ongoingGetTokenOperation' (with its 'ongoingGetTokenOperationLimitedUse' flag) manages the active token fetch. Standard requests reuse it if types match;otherwise, new requests are queued to run sequentially."] + Reuse -.- Footnote["Note: The 'ongoingGetTokenOperation' tracks the active fetch.
Standard requests reuse it (unless the active fetch is Limited-use).
Limited-use requests always queue a new, sequential fetch."] Queue1 -.- Footnote Queue2 -.- Footnote StartNew -.- Footnote From ae9b6c3b0040834c5d0dc09be82c332d165158ce Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 9 Jan 2026 20:00:11 -0500 Subject: [PATCH 52/94] b --- docs/providers/app-attest.md | 34 +++++++++++++++++----------------- docs/providers/debug.md | 4 ++-- docs/providers/device-check.md | 4 ++-- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 2768790b..045d0d26 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -32,28 +32,28 @@ generated. %%{init: {"flowchart": {"diagramPadding": 75}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} - + CheckUse -- Yes --> Queue1[Queue New Request] CheckUse -- No --> Coalesce{Ongoing Op?} - + Coalesce -- No --> StartNew[Start New Request] Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} - + CheckOngoing -- Yes --> Queue2[Queue New Request] CheckOngoing -- No --> Reuse[Reuse Existing Request] - + subgraph Execution ["Backoff Wrapped Execution"] direction LR Backoff[Check Backoff] StateCheck{Attestation State?} - + Backoff --> StateCheck - + StateCheck -->|Yes| KeyCheck{Key ID?} - + KeyCheck -- No --> Flow1[Flow 1: Initial] KeyCheck -- Yes --> ArtifactCheck{Artifact?} - + ArtifactCheck -- No --> Flow1 ArtifactCheck -- Yes --> Flow2[Flow 2: Refresh] @@ -98,12 +98,12 @@ sequenceDiagram participant AppB as App (Limited) participant AppC as App (Standard) participant Provider as GACAppAttestProvider - + AppA->>Provider: getToken(false) activate Provider Note right of Provider: No ongoing op.
Start new op (standard).
Set ongoingGetTokenOperation. Provider-->>Provider: Start Flow 1/2 sequence - + AppB->>Provider: getToken(true) activate Provider Note right of Provider: Ongoing op (standard) exists.
New request is limited-use.
Chain: Wait for ongoing, then start new op. @@ -138,7 +138,7 @@ sequenceDiagram participant Backend as Firebase Backend App->>Provider: getToken(limitedUse) - + loop Retry Loop (Max 1 Retry) par Parallel Execution Provider->>API: getRandomChallenge() @@ -151,7 +151,7 @@ sequenceDiagram end Provider->>Apple: attestKey(keyId, clientDataHash=SHA256(challenge)) - + alt Attestation Failed (Invalid Key/Input) Apple-->>Provider: DCErrorInvalidKey / Input Provider->>Provider: RESET: Delete KeyID & Artifact @@ -160,7 +160,7 @@ sequenceDiagram Apple-->>Provider: Attestation Object Provider->>API: attestKeyWithAttestation(..., limitedUse) API->>Backend: POST /exchangeAppAttestAttestation
{ limited_use: true/false } - + alt Backend Rejection (403) Backend-->>API: 403 Forbidden Provider->>Provider: RESET: Delete KeyID & Artifact @@ -186,7 +186,7 @@ sequenceDiagram participant Backend as Firebase Backend App->>Provider: getToken(limitedUse) - + loop Retry Loop (Max 1 Retry) Provider->>API: getRandomChallenge() API->>Backend: POST /generateAppAttestChallenge @@ -195,18 +195,18 @@ sequenceDiagram Provider->>Provider: Retrieve stored Artifact Provider->>Provider: ClientData = Artifact + Challenge Provider->>Apple: generateAssertion(keyId, clientDataHash=SHA256(ClientData)) - + alt Assertion Failed (Invalid Key/Input) Apple-->>Provider: DCErrorInvalidKey / Input Provider->>Provider: RESET: Delete KeyID & Artifact Note right of Provider: Throws RejectionError,
Triggering Loop Retry
(Will fall back to Initial Handshake) else Assertion Success Apple-->>Provider: Assertion Object - + Provider->>API: getAppCheckTokenWithArtifact(..., limitedUse) API->>Backend: POST /exchangeAppAttestAssertion
{ limited_use: true/false } Backend-->>API: { "token": "..." } - + Provider-->>App: App Check Token end end diff --git a/docs/providers/debug.md b/docs/providers/debug.md index 09cf0af5..82c71747 100644 --- a/docs/providers/debug.md +++ b/docs/providers/debug.md @@ -20,11 +20,11 @@ sequenceDiagram App->>Provider: getToken(limitedUse) Provider->>Provider: Determine Debug Secret (Env Var or UUID) - + Provider->>API: appCheckTokenWithDebugToken(debugToken, limitedUse) API->>Backend: POST /exchangeDebugToken
{ limited_use: true/false } Note right of Backend: Checks if debug token is
registered in Console. Backend-->>API: { "token": "..." } - + Provider-->>App: App Check Token ``` diff --git a/docs/providers/device-check.md b/docs/providers/device-check.md index 47cec758..d43ae48e 100644 --- a/docs/providers/device-check.md +++ b/docs/providers/device-check.md @@ -16,7 +16,7 @@ sequenceDiagram participant Backend as Firebase Backend App->>Provider: getToken(limitedUse) - + Note right of Provider: Wrapped in Backoff Wrapper Provider->>Apple: generateToken() Apple-->>Provider: Device Token (Ephemeral) @@ -24,7 +24,7 @@ sequenceDiagram Provider->>API: appCheckTokenWithDeviceToken(deviceToken, limitedUse) API->>Backend: POST /exchangeDeviceCheckToken
{ limited_use: true/false } Note right of Backend: Verifies device token with Apple. - + alt Error (e.g., 503) Backend-->>API: 503 Service Unavailable Provider->>Provider: Record Failure (Backoff) From 3315a31ec5fd00ba69bc633580b24f22f6c73d09 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 12 Jan 2026 10:38:04 -0500 Subject: [PATCH 53/94] b --- docs/providers/app-attest.md | 50 ++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 045d0d26..970924bd 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -8,15 +8,21 @@ maintains a stable key pair on the device to sign assertions. * **Storage:** * `GACAppAttestKeyIDStorage`: Stores the generated App Attest Key ID. + * **Location:** `UserDefaults` (Suite: `com.firebase.GACAppAttestKeyIDStorage`). * `GACAppAttestArtifactStorage`: Stores the "artifact" returned by the Firebase backend after a successful initial handshake. This artifact effectively links the on-device key to the backend session. + * **Location:** Keychain (Service: `com.firebase.app_check.app_attest_artifact_storage`). * **Resiliency:** * **Automatic Retry:** The provider wraps the entire flow in a - retry loop. If a specific "Rejection Error" occurs (e.g., - invalid key), it resets its internal state and retries the flow - from scratch. + retry loop. It resets its internal state (clearing Key ID and + Artifact) and retries the flow from scratch if a specific + "Rejection Error" occurs. + * **Triggers:** + * `DCErrorInvalidKey` (Apple DeviceCheck error) + * `DCErrorInvalidInput` (Apple DeviceCheck error) + * HTTP 403 Forbidden (Backend rejection) ## Decision Logic & State Machine Before executing a handshake, the provider determines the correct flow @@ -29,31 +35,31 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 75}}}%% +%%{init: {"flowchart": {"diagramPadding": 130}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} - + CheckUse -- Yes --> Queue1[Queue New Request] CheckUse -- No --> Coalesce{Ongoing Op?} - + Coalesce -- No --> StartNew[Start New Request] Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} - + CheckOngoing -- Yes --> Queue2[Queue New Request] CheckOngoing -- No --> Reuse[Reuse Existing Request] - + subgraph Execution ["Backoff Wrapped Execution"] direction LR Backoff[Check Backoff] StateCheck{Attestation State?} - + Backoff --> StateCheck - + StateCheck -->|Yes| KeyCheck{Key ID?} - + KeyCheck -- No --> Flow1[Flow 1: Initial] KeyCheck -- Yes --> ArtifactCheck{Artifact?} - + ArtifactCheck -- No --> Flow1 ArtifactCheck -- Yes --> Flow2[Flow 2: Refresh] @@ -98,12 +104,12 @@ sequenceDiagram participant AppB as App (Limited) participant AppC as App (Standard) participant Provider as GACAppAttestProvider - + AppA->>Provider: getToken(false) activate Provider Note right of Provider: No ongoing op.
Start new op (standard).
Set ongoingGetTokenOperation. Provider-->>Provider: Start Flow 1/2 sequence - + AppB->>Provider: getToken(true) activate Provider Note right of Provider: Ongoing op (standard) exists.
New request is limited-use.
Chain: Wait for ongoing, then start new op. @@ -138,7 +144,7 @@ sequenceDiagram participant Backend as Firebase Backend App->>Provider: getToken(limitedUse) - + loop Retry Loop (Max 1 Retry) par Parallel Execution Provider->>API: getRandomChallenge() @@ -151,7 +157,7 @@ sequenceDiagram end Provider->>Apple: attestKey(keyId, clientDataHash=SHA256(challenge)) - + alt Attestation Failed (Invalid Key/Input) Apple-->>Provider: DCErrorInvalidKey / Input Provider->>Provider: RESET: Delete KeyID & Artifact @@ -160,7 +166,7 @@ sequenceDiagram Apple-->>Provider: Attestation Object Provider->>API: attestKeyWithAttestation(..., limitedUse) API->>Backend: POST /exchangeAppAttestAttestation
{ limited_use: true/false } - + alt Backend Rejection (403) Backend-->>API: 403 Forbidden Provider->>Provider: RESET: Delete KeyID & Artifact @@ -186,7 +192,7 @@ sequenceDiagram participant Backend as Firebase Backend App->>Provider: getToken(limitedUse) - + loop Retry Loop (Max 1 Retry) Provider->>API: getRandomChallenge() API->>Backend: POST /generateAppAttestChallenge @@ -195,19 +201,19 @@ sequenceDiagram Provider->>Provider: Retrieve stored Artifact Provider->>Provider: ClientData = Artifact + Challenge Provider->>Apple: generateAssertion(keyId, clientDataHash=SHA256(ClientData)) - + alt Assertion Failed (Invalid Key/Input) Apple-->>Provider: DCErrorInvalidKey / Input Provider->>Provider: RESET: Delete KeyID & Artifact Note right of Provider: Throws RejectionError,
Triggering Loop Retry
(Will fall back to Initial Handshake) else Assertion Success Apple-->>Provider: Assertion Object - + Provider->>API: getAppCheckTokenWithArtifact(..., limitedUse) API->>Backend: POST /exchangeAppAttestAssertion
{ limited_use: true/false } Backend-->>API: { "token": "..." } - + Provider-->>App: App Check Token end end -``` \ No newline at end of file +``` From bb77a509c1adcb61004d4c409059f92ceb80ec82 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 12 Jan 2026 10:55:41 -0500 Subject: [PATCH 54/94] b --- docs/providers/app-attest.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 970924bd..c9d237ff 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -15,7 +15,7 @@ maintains a stable key pair on the device to sign assertions. session. * **Location:** Keychain (Service: `com.firebase.app_check.app_attest_artifact_storage`). * **Resiliency:** - * **Automatic Retry:** The provider wraps the entire flow in a + * **Automatic Retry (Internal):** The provider wraps the entire flow in a retry loop. It resets its internal state (clearing Key ID and Artifact) and retries the flow from scratch if a specific "Rejection Error" occurs. @@ -23,6 +23,11 @@ maintains a stable key pair on the device to sign assertions. * `DCErrorInvalidKey` (Apple DeviceCheck error) * `DCErrorInvalidInput` (Apple DeviceCheck error) * HTTP 403 Forbidden (Backend rejection) + * **Backoff Strategy (External):** An outer wrapper protects the backend + from traffic spikes. + * **Triggers:** HTTP 404/400 (1 day backoff), HTTP 429/503 (Exponential backoff). + * **Behavior:** If a request fails with these errors, subsequent + requests are blocked immediately until the backoff interval passes. ## Decision Logic & State Machine Before executing a handshake, the provider determines the correct flow @@ -216,4 +221,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` +``` \ No newline at end of file From 18576a17dde800e8a94fd354cf5c4b0cd9617746 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 12 Jan 2026 12:05:30 -0500 Subject: [PATCH 55/94] b --- docs/providers/app-attest.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index c9d237ff..255ef70f 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -19,10 +19,15 @@ maintains a stable key pair on the device to sign assertions. retry loop. It resets its internal state (clearing Key ID and Artifact) and retries the flow from scratch if a specific "Rejection Error" occurs. - * **Triggers:** + * **Triggers for Reset & Retry:** * `DCErrorInvalidKey` (Apple DeviceCheck error) * `DCErrorInvalidInput` (Apple DeviceCheck error) - * HTTP 403 Forbidden (Backend rejection) + * HTTP 403 Forbidden (Backend rejection during attestation exchange) + * **Transient Error Handling (No Reset):** If `DCErrorServerUnavailable` + (indicating a temporary issue reaching Apple's App Attest service) occurs, + the request fails (allowing the app to retry) without resetting the + App Attest key or artifact. This aligns with Apple's recommendation to + preserve the device's risk metric. * **Backoff Strategy (External):** An outer wrapper protects the backend from traffic spikes. * **Triggers:** HTTP 404/400 (1 day backoff), HTTP 429/503 (Exponential backoff). @@ -145,6 +150,7 @@ sequenceDiagram participant App participant Provider as GACAppAttestProvider participant Apple as DCAppAttestService

(Apple's DeviceCheck Framework) + participant AppleServer as Apple Server participant API as GACAppAttestAPIService participant Backend as Firebase Backend @@ -162,6 +168,8 @@ sequenceDiagram end Provider->>Apple: attestKey(keyId, clientDataHash=SHA256(challenge)) + Apple->>AppleServer: Contact App Attest Service + AppleServer-->>Apple: Attestation Result alt Attestation Failed (Invalid Key/Input) Apple-->>Provider: DCErrorInvalidKey / Input @@ -221,4 +229,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` \ No newline at end of file +``` From fbd2489dc3eb8e13ffde0bfa2283b487d6447136 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 12 Jan 2026 12:15:47 -0500 Subject: [PATCH 56/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 255ef70f..2c21646f 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -52,8 +52,8 @@ flowchart LR CheckUse -- Yes --> Queue1[Queue New Request] CheckUse -- No --> Coalesce{Ongoing Op?} - Coalesce -- No --> StartNew[Start New Request] Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} + Coalesce -- No --> StartNew[Start New Request] CheckOngoing -- Yes --> Queue2[Queue New Request] CheckOngoing -- No --> Reuse[Reuse Existing Request] From 4b006f4af532d8584f9a26877903ad603f669d19 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 12 Jan 2026 12:17:13 -0500 Subject: [PATCH 57/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 2c21646f..255ef70f 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -52,8 +52,8 @@ flowchart LR CheckUse -- Yes --> Queue1[Queue New Request] CheckUse -- No --> Coalesce{Ongoing Op?} - Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} Coalesce -- No --> StartNew[Start New Request] + Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} CheckOngoing -- Yes --> Queue2[Queue New Request] CheckOngoing -- No --> Reuse[Reuse Existing Request] From 54897e2d74bd9a4695b21e855df8ca7fe9c45d27 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 12 Jan 2026 12:18:37 -0500 Subject: [PATCH 58/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 255ef70f..0d498c78 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -52,7 +52,7 @@ flowchart LR CheckUse -- Yes --> Queue1[Queue New Request] CheckUse -- No --> Coalesce{Ongoing Op?} - Coalesce -- No --> StartNew[Start New Request] + Coalesce -->|No| StartNew[Start New Request] Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} CheckOngoing -- Yes --> Queue2[Queue New Request] From 1149547e2be9e82c4e69d1d5f0a8c9f9d469c4c8 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 12 Jan 2026 12:19:16 -0500 Subject: [PATCH 59/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 0d498c78..255ef70f 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -52,7 +52,7 @@ flowchart LR CheckUse -- Yes --> Queue1[Queue New Request] CheckUse -- No --> Coalesce{Ongoing Op?} - Coalesce -->|No| StartNew[Start New Request] + Coalesce -- No --> StartNew[Start New Request] Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} CheckOngoing -- Yes --> Queue2[Queue New Request] From 362f40bc31e2cff2cd3ae80725b7d864cdf521f1 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 12 Jan 2026 12:22:27 -0500 Subject: [PATCH 60/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 255ef70f..2c21646f 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -52,8 +52,8 @@ flowchart LR CheckUse -- Yes --> Queue1[Queue New Request] CheckUse -- No --> Coalesce{Ongoing Op?} - Coalesce -- No --> StartNew[Start New Request] Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} + Coalesce -- No --> StartNew[Start New Request] CheckOngoing -- Yes --> Queue2[Queue New Request] CheckOngoing -- No --> Reuse[Reuse Existing Request] From 74c3f7d802780dc77f0b9ed21304bc4f07621681 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 12 Jan 2026 12:23:18 -0500 Subject: [PATCH 61/94] b --- docs/providers/app-attest.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 2c21646f..590266dc 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -84,6 +84,8 @@ flowchart LR Queue1 -.- Footnote Queue2 -.- Footnote StartNew -.- Footnote + + CheckOngoing ~~~ StartNew ``` ## Concurrent Request Handling From 8e5973c35a8c05b703e8daf38dd6fd56689ac9e6 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 12 Jan 2026 12:25:45 -0500 Subject: [PATCH 62/94] Revert "b" This reverts commit 74c3f7d802780dc77f0b9ed21304bc4f07621681. --- docs/providers/app-attest.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 590266dc..2c21646f 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -84,8 +84,6 @@ flowchart LR Queue1 -.- Footnote Queue2 -.- Footnote StartNew -.- Footnote - - CheckOngoing ~~~ StartNew ``` ## Concurrent Request Handling From fa2da39f7f9645a79cba516f2429f2a7631fc7e9 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 12 Jan 2026 12:26:42 -0500 Subject: [PATCH 63/94] renew --- docs/providers/app-attest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 2c21646f..a82f1408 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -51,10 +51,10 @@ flowchart LR CheckUse -- Yes --> Queue1[Queue New Request] CheckUse -- No --> Coalesce{Ongoing Op?} - + Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} Coalesce -- No --> StartNew[Start New Request] - + CheckOngoing -- Yes --> Queue2[Queue New Request] CheckOngoing -- No --> Reuse[Reuse Existing Request] From 382ba4fe836cc65b08f6a4966efe4191128978eb Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 12 Jan 2026 14:09:16 -0500 Subject: [PATCH 64/94] b --- docs/providers/app-attest.md | 40 +++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index a82f1408..d64f4093 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -16,23 +16,25 @@ maintains a stable key pair on the device to sign assertions. * **Location:** Keychain (Service: `com.firebase.app_check.app_attest_artifact_storage`). * **Resiliency:** * **Automatic Retry (Internal):** The provider wraps the entire flow in a - retry loop. It resets its internal state (clearing Key ID and - Artifact) and retries the flow from scratch if a specific - "Rejection Error" occurs. + retry loop (max 1 attempt). It resets its internal state (clearing Key + ID and Artifact) and retries the flow from scratch if specific + recoverable errors occur. * **Triggers for Reset & Retry:** - * `DCErrorInvalidKey` (Apple DeviceCheck error) - * `DCErrorInvalidInput` (Apple DeviceCheck error) - * HTTP 403 Forbidden (Backend rejection during attestation exchange) - * **Transient Error Handling (No Reset):** If `DCErrorServerUnavailable` - (indicating a temporary issue reaching Apple's App Attest service) occurs, - the request fails (allowing the app to retry) without resetting the - App Attest key or artifact. This aligns with Apple's recommendation to - preserve the device's risk metric. + * `DCErrorInvalidKey` / `DCErrorInvalidInput` (Apple DeviceCheck error). + * HTTP 403 (Attestation Rejected). * **Backoff Strategy (External):** An outer wrapper protects the backend - from traffic spikes. - * **Triggers:** HTTP 404/400 (1 day backoff), HTTP 429/503 (Exponential backoff). - * **Behavior:** If a request fails with these errors, subsequent - requests are blocked immediately until the backoff interval passes. + from traffic spikes by enforcing delays on subsequent attempts based on + the error type. + * **No Backoff (Immediate Retry):** Applied to non-HTTP errors, + including Apple's `DCError` (e.g., `serverUnavailable`), network + connectivity issues, and storage/parsing failures. + * **Exponential Backoff:** Applied to retryable server errors. + * HTTP 403 (Project/App Deleted) *if internal retry fails*. + * HTTP 429 (Too Many Requests). + * HTTP 503 (Server Overloaded). + * **1 Day Backoff:** Applied to configuration errors unlikely to resolve quickly. + * HTTP 400 (Bad Request). + * HTTP 404 (Not Found). ## Decision Logic & State Machine Before executing a handshake, the provider determines the correct flow @@ -51,10 +53,10 @@ flowchart LR CheckUse -- Yes --> Queue1[Queue New Request] CheckUse -- No --> Coalesce{Ongoing Op?} - - Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} + Coalesce -- No --> StartNew[Start New Request] - + Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} + CheckOngoing -- Yes --> Queue2[Queue New Request] CheckOngoing -- No --> Reuse[Reuse Existing Request] @@ -229,4 +231,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` +``` \ No newline at end of file From ddaf306d8e7601d25cccf2c346e6b5dec624dec3 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 16:10:41 -0500 Subject: [PATCH 65/94] b --- docs/providers/app-attest.md | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index d64f4093..5f48ee1f 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -15,19 +15,25 @@ maintains a stable key pair on the device to sign assertions. session. * **Location:** Keychain (Service: `com.firebase.app_check.app_attest_artifact_storage`). * **Resiliency:** - * **Automatic Retry (Internal):** The provider wraps the entire flow in a - retry loop (max 1 attempt). It resets its internal state (clearing Key - ID and Artifact) and retries the flow from scratch if specific - recoverable errors occur. - * **Triggers for Reset & Retry:** + * **Automatic Retry (Internal):** The provider includes an internal + retry loop (max 1 attempt) with a 0-second delay. This loop is + specifically triggered if an error wrapped as + `GACAppAttestRejectionError` occurs. + * **Triggers for Reset & Internal Retry:** * `DCErrorInvalidKey` / `DCErrorInvalidInput` (Apple DeviceCheck error). - * HTTP 403 (Attestation Rejected). - * **Backoff Strategy (External):** An outer wrapper protects the backend - from traffic spikes by enforcing delays on subsequent attempts based on - the error type. - * **No Backoff (Immediate Retry):** Applied to non-HTTP errors, - including Apple's `DCError` (e.g., `serverUnavailable`), network - connectivity issues, and storage/parsing failures. + * HTTP 403 (Attestation Rejected) from the backend during handshake. + * **Transient Error Handling (No Reset):** If `DCErrorServerUnavailable` + (indicating a temporary issue reaching Apple's App Attest service) occurs, + the request fails, but the App Attest key and artifact are **preserved**. + This allows the app to retry the request later using the same key, + aligning with Apple's recommendation to preserve the device's risk metric. + * **Backoff Strategy (External):** An outer `GACAppCheckBackoffWrapper` + protects the backend from traffic spikes by enforcing delays on subsequent + attempts based on the error type. + * **No Backoff (Immediately Permitted):** For non-HTTP errors (e.g., + Apple's `DCError` like `serverUnavailable`), network connectivity issues, + storage failures, or parsing errors, the backoff wrapper **does not** enforce + a delay. Subsequent `getToken` calls by the app are immediately permitted. * **Exponential Backoff:** Applied to retryable server errors. * HTTP 403 (Project/App Deleted) *if internal retry fails*. * HTTP 429 (Too Many Requests). @@ -188,7 +194,7 @@ sequenceDiagram Note right of Provider: Throws RejectionError,
Triggering Loop Retry else Success Backend-->>API: { "token": "...", "artifact": "..." } - Provider->>Provider: Store Artifact & Key ID + Provider-->>Provider: Store Artifact & Key ID Provider-->>App: App Check Token end end From fdaa82dacf8c58d6d9236e60720955cfe2d744ac Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 16:21:23 -0500 Subject: [PATCH 66/94] b --- docs/providers/app-attest.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 5f48ee1f..0dded167 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -38,6 +38,7 @@ maintains a stable key pair on the device to sign assertions. * HTTP 403 (Project/App Deleted) *if internal retry fails*. * HTTP 429 (Too Many Requests). * HTTP 503 (Server Overloaded). + * Other HTTP 5xx (Server Errors) or 4xx not listed above or handled by 1 day backoff. * **1 Day Backoff:** Applied to configuration errors unlikely to resolve quickly. * HTTP 400 (Bad Request). * HTTP 404 (Not Found). @@ -237,4 +238,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` \ No newline at end of file +``` From d53352d182a2feeab203ce498c235f9a22838069 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 16:22:25 -0500 Subject: [PATCH 67/94] b --- docs/providers/app-attest.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 0dded167..ec2711da 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -64,7 +64,7 @@ flowchart LR Coalesce -- No --> StartNew[Start New Request] Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} - CheckOngoing -- Yes --> Queue2[Queue New Request] + CheckOngoing -- Yes --> Queue1 CheckOngoing -- No --> Reuse[Reuse Existing Request] subgraph Execution ["Backoff Wrapped Execution"] @@ -86,12 +86,10 @@ flowchart LR end Queue1 --> Backoff - Queue2 --> Backoff StartNew --> Backoff Reuse -.- Footnote["Note: The 'ongoingGetTokenOperation' tracks the active fetch.
Standard requests reuse it (unless the active fetch is Limited-use).
Limited-use requests always queue a new, sequential fetch."] Queue1 -.- Footnote - Queue2 -.- Footnote StartNew -.- Footnote ``` From 7a1688dccefcaed9de0b76ad0d4803b651766b8e Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 16:22:52 -0500 Subject: [PATCH 68/94] Revert "b" This reverts commit d53352d182a2feeab203ce498c235f9a22838069. --- docs/providers/app-attest.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index ec2711da..0dded167 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -64,7 +64,7 @@ flowchart LR Coalesce -- No --> StartNew[Start New Request] Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} - CheckOngoing -- Yes --> Queue1 + CheckOngoing -- Yes --> Queue2[Queue New Request] CheckOngoing -- No --> Reuse[Reuse Existing Request] subgraph Execution ["Backoff Wrapped Execution"] @@ -86,10 +86,12 @@ flowchart LR end Queue1 --> Backoff + Queue2 --> Backoff StartNew --> Backoff Reuse -.- Footnote["Note: The 'ongoingGetTokenOperation' tracks the active fetch.
Standard requests reuse it (unless the active fetch is Limited-use).
Limited-use requests always queue a new, sequential fetch."] Queue1 -.- Footnote + Queue2 -.- Footnote StartNew -.- Footnote ``` From e9d9388563af789bb90c6fec3724c39ee4339c7b Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 16:43:05 -0500 Subject: [PATCH 69/94] b --- docs/providers/app-attest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 0dded167..879e042e 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -144,7 +144,7 @@ sequenceDiagram Provider-->>AppA: App Check Token (from completed op) deactivate Provider - Provider-->>Provider: Start new Flow 1/2 for AppB + Provider-->>Provider: Start new Flow 1/2 for App (Limited) activate Provider Provider-->>AppB: App Check Token (from new op) deactivate Provider @@ -186,7 +186,7 @@ sequenceDiagram Note right of Provider: Throws RejectionError,
Triggering Loop Retry else Attestation Success Apple-->>Provider: Attestation Object - Provider->>API: attestKeyWithAttestation(..., limitedUse) + Provider->>API: attestKeyWithAttestation(attestation, keyID, challenge, limitedUse) API->>Backend: POST /exchangeAppAttestAttestation
{ limited_use: true/false } alt Backend Rejection (403) From 85f2bf5943e0bfbef24938dc57cf789059c4ded7 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 16:55:53 -0500 Subject: [PATCH 70/94] b --- docs/providers/app-attest.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 879e042e..0d85887e 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -165,7 +165,7 @@ sequenceDiagram App->>Provider: getToken(limitedUse) - loop Retry Loop (Max 1 Retry) + loop Retry Loop (Max 1 Retry for GACAppAttestRejectionError) par Parallel Execution Provider->>API: getRandomChallenge() API->>Backend: POST /generateAppAttestChallenge @@ -183,7 +183,7 @@ sequenceDiagram alt Attestation Failed (Invalid Key/Input) Apple-->>Provider: DCErrorInvalidKey / Input Provider->>Provider: RESET: Delete KeyID & Artifact - Note right of Provider: Throws RejectionError,
Triggering Loop Retry + Note right of Provider: Throws GACAppAttestRejectionError,
Triggering Loop Retry else Attestation Success Apple-->>Provider: Attestation Object Provider->>API: attestKeyWithAttestation(attestation, keyID, challenge, limitedUse) @@ -191,11 +191,13 @@ sequenceDiagram alt Backend Rejection (403) Backend-->>API: 403 Forbidden + API-->>Provider: Error (403) Provider->>Provider: RESET: Delete KeyID & Artifact - Note right of Provider: Throws RejectionError,
Triggering Loop Retry + Note right of Provider: Throws GACAppAttestRejectionError,
Triggering Loop Retry else Success Backend-->>API: { "token": "...", "artifact": "..." } - Provider-->>Provider: Store Artifact & Key ID + API-->>Provider: { "token": "...", "artifact": "..." } + Provider->>Provider: Store Artifact & Key ID Provider-->>App: App Check Token end end @@ -215,7 +217,7 @@ sequenceDiagram App->>Provider: getToken(limitedUse) - loop Retry Loop (Max 1 Retry) + loop Retry Loop (Max 1 Retry for GACAppAttestRejectionError) Provider->>API: getRandomChallenge() API->>Backend: POST /generateAppAttestChallenge Backend-->>API: { "challenge": "..." } @@ -227,7 +229,7 @@ sequenceDiagram alt Assertion Failed (Invalid Key/Input) Apple-->>Provider: DCErrorInvalidKey / Input Provider->>Provider: RESET: Delete KeyID & Artifact - Note right of Provider: Throws RejectionError,
Triggering Loop Retry
(Will fall back to Initial Handshake) + Note right of Provider: Throws GACAppAttestRejectionError,
Triggering Loop Retry
(Will fall back to Initial Handshake) else Assertion Success Apple-->>Provider: Assertion Object From 3c47659530a5c59c72abee55be6554f1cfde6468 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 21:13:06 -0500 Subject: [PATCH 71/94] b --- docs/providers/app-attest.md | 58 +++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 0d85887e..7d9b1008 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -43,6 +43,28 @@ maintains a stable key pair on the device to sign assertions. * HTTP 400 (Bad Request). * HTTP 404 (Not Found). +## Attestation State Calculation +The provider determines its current state by checking for the presence of +a supported environment, a stored key ID, and a stored artifact. This state +dictates whether to perform an initial handshake or a token refresh. + +```mermaid +flowchart LR + Start([Calculate State]) --> CheckSupport{Is App Attest
Supported?} + + CheckSupport -- No --> Unsupported[State: Unsupported] + + CheckSupport -- Yes --> CheckKey{Stored Key ID?} + + CheckKey -- No --> SupportedInitial[State: SupportedInitial
(Ready for Handshake)] + + CheckKey -- Yes --> CheckArtifact{Stored Artifact
for Key ID?} + + CheckArtifact -- No --> KeyGenerated[State: KeyGenerated
(Key exists, no Artifact)] + + CheckArtifact -- Yes --> KeyRegistered[State: KeyRegistered
(Key & Artifact exist)] +``` + ## Decision Logic & State Machine Before executing a handshake, the provider determines the correct flow based on the internal state and manages concurrent requests. @@ -57,28 +79,28 @@ generated. %%{init: {"flowchart": {"diagramPadding": 130}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} - + CheckUse -- Yes --> Queue1[Queue New Request] CheckUse -- No --> Coalesce{Ongoing Op?} - + Coalesce -- No --> StartNew[Start New Request] Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} - + CheckOngoing -- Yes --> Queue2[Queue New Request] CheckOngoing -- No --> Reuse[Reuse Existing Request] - + subgraph Execution ["Backoff Wrapped Execution"] direction LR Backoff[Check Backoff] StateCheck{Attestation State?} - + Backoff --> StateCheck - + StateCheck -->|Yes| KeyCheck{Key ID?} - + KeyCheck -- No --> Flow1[Flow 1: Initial] KeyCheck -- Yes --> ArtifactCheck{Artifact?} - + ArtifactCheck -- No --> Flow1 ArtifactCheck -- Yes --> Flow2[Flow 2: Refresh] @@ -123,12 +145,12 @@ sequenceDiagram participant AppB as App (Limited) participant AppC as App (Standard) participant Provider as GACAppAttestProvider - + AppA->>Provider: getToken(false) activate Provider Note right of Provider: No ongoing op.
Start new op (standard).
Set ongoingGetTokenOperation. Provider-->>Provider: Start Flow 1/2 sequence - + AppB->>Provider: getToken(true) activate Provider Note right of Provider: Ongoing op (standard) exists.
New request is limited-use.
Chain: Wait for ongoing, then start new op. @@ -164,7 +186,7 @@ sequenceDiagram participant Backend as Firebase Backend App->>Provider: getToken(limitedUse) - + loop Retry Loop (Max 1 Retry for GACAppAttestRejectionError) par Parallel Execution Provider->>API: getRandomChallenge() @@ -179,7 +201,7 @@ sequenceDiagram Provider->>Apple: attestKey(keyId, clientDataHash=SHA256(challenge)) Apple->>AppleServer: Contact App Attest Service AppleServer-->>Apple: Attestation Result - + alt Attestation Failed (Invalid Key/Input) Apple-->>Provider: DCErrorInvalidKey / Input Provider->>Provider: RESET: Delete KeyID & Artifact @@ -188,7 +210,7 @@ sequenceDiagram Apple-->>Provider: Attestation Object Provider->>API: attestKeyWithAttestation(attestation, keyID, challenge, limitedUse) API->>Backend: POST /exchangeAppAttestAttestation
{ limited_use: true/false } - + alt Backend Rejection (403) Backend-->>API: 403 Forbidden API-->>Provider: Error (403) @@ -216,7 +238,7 @@ sequenceDiagram participant Backend as Firebase Backend App->>Provider: getToken(limitedUse) - + loop Retry Loop (Max 1 Retry for GACAppAttestRejectionError) Provider->>API: getRandomChallenge() API->>Backend: POST /generateAppAttestChallenge @@ -225,19 +247,19 @@ sequenceDiagram Provider->>Provider: Retrieve stored Artifact Provider->>Provider: ClientData = Artifact + Challenge Provider->>Apple: generateAssertion(keyId, clientDataHash=SHA256(ClientData)) - + alt Assertion Failed (Invalid Key/Input) Apple-->>Provider: DCErrorInvalidKey / Input Provider->>Provider: RESET: Delete KeyID & Artifact Note right of Provider: Throws GACAppAttestRejectionError,
Triggering Loop Retry
(Will fall back to Initial Handshake) else Assertion Success Apple-->>Provider: Assertion Object - + Provider->>API: getAppCheckTokenWithArtifact(..., limitedUse) API->>Backend: POST /exchangeAppAttestAssertion
{ limited_use: true/false } Backend-->>API: { "token": "..." } - + Provider-->>App: App Check Token end end -``` +``` \ No newline at end of file From 7c0585003f86cc9209ec18becd9eca4d07965c3a Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 21:13:39 -0500 Subject: [PATCH 72/94] b --- docs/architecture.md | 6 ++++- docs/index.md | 12 ++++++++++ docs/providers/app-attest.md | 46 ++++++++++++++++++------------------ 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 698059a9..9581be19 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,6 +4,10 @@ This document details the internal architecture of `AppCheckCore`, focusing on token storage, lifecycle management, and security mechanisms. +**Note:** This document describes internal implementation details that +are subject to change. Rely only on the public API surface for +integration (see [App Check Core Documentation](index.md#important-disclaimer)). + ## Token Storage App Check tokens are sensitive credentials that grant access to your backend resources. `AppCheckCore` treats them with high security. @@ -89,4 +93,4 @@ codes returned by the backend: sequentialize complex attestation flows (like generating a key, then attesting, then exchanging). * **Background:** Network requests are performed on background - queues (`QOS_CLASS_DEFAULT` or `QOS_CLASS_UTILITY`). + queues (`QOS_CLASS_DEFAULT` or `QOS_CLASS_UTILITY`). \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index d4e61a70..43a9e3fe 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,6 +19,18 @@ tvOS, and watchOS. limited-use tokens for scenarios requiring single-use or short-lived authentication. +## Important Disclaimer +**The detailed architectural and implementation documentation within this +`docs/` directory, especially the deep dives into provider internals and +decision logic, are provided for comprehensive understanding only.** + +**Consumers of the App Check Core library should exclusively rely on the +public API surface (as defined by the public header files, e.g., in +`AppCheckCore/Sources/Public`) for integration. Internal implementation +details, including specific error handling flows, storage mechanisms, and +concurrency management described herein, are subject to change without +notice across library versions.** + ## Documentation Sections * [Getting Started](getting-started.md): Installation and basic setup. * [Usage Guide](usage.md): How to initialize and fetch tokens. diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 7d9b1008..1851de6b 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -51,17 +51,17 @@ dictates whether to perform an initial handshake or a token refresh. ```mermaid flowchart LR Start([Calculate State]) --> CheckSupport{Is App Attest
Supported?} - + CheckSupport -- No --> Unsupported[State: Unsupported] - + CheckSupport -- Yes --> CheckKey{Stored Key ID?} - + CheckKey -- No --> SupportedInitial[State: SupportedInitial
(Ready for Handshake)] - + CheckKey -- Yes --> CheckArtifact{Stored Artifact
for Key ID?} - + CheckArtifact -- No --> KeyGenerated[State: KeyGenerated
(Key exists, no Artifact)] - + CheckArtifact -- Yes --> KeyRegistered[State: KeyRegistered
(Key & Artifact exist)] ``` @@ -79,28 +79,28 @@ generated. %%{init: {"flowchart": {"diagramPadding": 130}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} - + CheckUse -- Yes --> Queue1[Queue New Request] CheckUse -- No --> Coalesce{Ongoing Op?} - + Coalesce -- No --> StartNew[Start New Request] Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} - + CheckOngoing -- Yes --> Queue2[Queue New Request] CheckOngoing -- No --> Reuse[Reuse Existing Request] - + subgraph Execution ["Backoff Wrapped Execution"] direction LR Backoff[Check Backoff] StateCheck{Attestation State?} - + Backoff --> StateCheck - + StateCheck -->|Yes| KeyCheck{Key ID?} - + KeyCheck -- No --> Flow1[Flow 1: Initial] KeyCheck -- Yes --> ArtifactCheck{Artifact?} - + ArtifactCheck -- No --> Flow1 ArtifactCheck -- Yes --> Flow2[Flow 2: Refresh] @@ -145,12 +145,12 @@ sequenceDiagram participant AppB as App (Limited) participant AppC as App (Standard) participant Provider as GACAppAttestProvider - + AppA->>Provider: getToken(false) activate Provider Note right of Provider: No ongoing op.
Start new op (standard).
Set ongoingGetTokenOperation. Provider-->>Provider: Start Flow 1/2 sequence - + AppB->>Provider: getToken(true) activate Provider Note right of Provider: Ongoing op (standard) exists.
New request is limited-use.
Chain: Wait for ongoing, then start new op. @@ -186,7 +186,7 @@ sequenceDiagram participant Backend as Firebase Backend App->>Provider: getToken(limitedUse) - + loop Retry Loop (Max 1 Retry for GACAppAttestRejectionError) par Parallel Execution Provider->>API: getRandomChallenge() @@ -201,7 +201,7 @@ sequenceDiagram Provider->>Apple: attestKey(keyId, clientDataHash=SHA256(challenge)) Apple->>AppleServer: Contact App Attest Service AppleServer-->>Apple: Attestation Result - + alt Attestation Failed (Invalid Key/Input) Apple-->>Provider: DCErrorInvalidKey / Input Provider->>Provider: RESET: Delete KeyID & Artifact @@ -210,7 +210,7 @@ sequenceDiagram Apple-->>Provider: Attestation Object Provider->>API: attestKeyWithAttestation(attestation, keyID, challenge, limitedUse) API->>Backend: POST /exchangeAppAttestAttestation
{ limited_use: true/false } - + alt Backend Rejection (403) Backend-->>API: 403 Forbidden API-->>Provider: Error (403) @@ -238,7 +238,7 @@ sequenceDiagram participant Backend as Firebase Backend App->>Provider: getToken(limitedUse) - + loop Retry Loop (Max 1 Retry for GACAppAttestRejectionError) Provider->>API: getRandomChallenge() API->>Backend: POST /generateAppAttestChallenge @@ -247,18 +247,18 @@ sequenceDiagram Provider->>Provider: Retrieve stored Artifact Provider->>Provider: ClientData = Artifact + Challenge Provider->>Apple: generateAssertion(keyId, clientDataHash=SHA256(ClientData)) - + alt Assertion Failed (Invalid Key/Input) Apple-->>Provider: DCErrorInvalidKey / Input Provider->>Provider: RESET: Delete KeyID & Artifact Note right of Provider: Throws GACAppAttestRejectionError,
Triggering Loop Retry
(Will fall back to Initial Handshake) else Assertion Success Apple-->>Provider: Assertion Object - + Provider->>API: getAppCheckTokenWithArtifact(..., limitedUse) API->>Backend: POST /exchangeAppAttestAssertion
{ limited_use: true/false } Backend-->>API: { "token": "..." } - + Provider-->>App: App Check Token end end From 5c73e75d4d736406954cdbc5b14206022aeff05b Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 22:17:58 -0500 Subject: [PATCH 73/94] b --- docs/prompt.md | 72 -------------------------------------------------- 1 file changed, 72 deletions(-) delete mode 100644 docs/prompt.md diff --git a/docs/prompt.md b/docs/prompt.md deleted file mode 100644 index dabf6600..00000000 --- a/docs/prompt.md +++ /dev/null @@ -1,72 +0,0 @@ -Create a comprehensive documentation website structure in a `docs/` directory for the **App Check Core** library. The documentation should be written in Markdown and include the following pages. Use Mermaid diagrams where appropriate to illustrate architecture and flows. - -### 1. `docs/index.md` (Home/Overview) -* **Title:** App Check Core - Documentation -* **Introduction:** Explain that `AppCheckCore` is the underlying engine for app attestation and token management, primarily used by the Firebase iOS SDK but designed for broader internal Google use. Mention it supports iOS, macOS, tvOS, and watchOS. -* **Key Features:** - * Manages App Check tokens (caching, refreshing). - * Abstracts different attestation providers (DeviceCheck, AppAttest, Debug). - * Handles limited-use tokens. -* **Architecture Diagram (Mermaid):** Create a class diagram showing the relationship between: - * `GACAppCheck` (The core manager) - * `GACAppCheckProvider` (The protocol) - * Implementations: `GACAppAttestProvider`, `GACDeviceCheckProvider`, `GACAppCheckDebugProvider`. - * `GACAppCheckToken` (The result). - -### 2. `docs/getting-started.md` -* **Installation:** - * **CocoaPods:** Explain how to add `pod 'AppCheckCore'` to the Podfile. - * **Swift Package Manager:** Explain how to add the package via Xcode or `Package.swift` (URL: `https://github.com/google/app-check`). -* **Prerequisites:** List minimum OS versions (iOS 12.0+, macOS 10.15+, etc., based on `Package.swift`). - -### 3. `docs/usage.md` (Core Integration) -* **Initialization:** - * Show how to initialize a provider (e.g., `GACAppAttestProvider`). - * Show how to initialize `GACAppCheck` with that provider, a service name, and a resource name. - * *Code Example (Swift & Obj-C):* - ```swift - let provider = AppCheckCoreAppAttestProvider(serviceName: "my-sdk", resourceName: "projects/123/apps/abc", ...) - let appCheck = AppCheckCore(serviceName: "my-sdk", resourceName: "...", appCheckProvider: provider, ...) - ``` -* **Fetching Tokens:** - * Explain `token(forcingRefresh:completion:)`. - * Explain `limitedUseToken(completion:)`. - * *Code Example:* How to call these methods and handle the `GACAppCheckTokenResult`. -* **Sequence Diagram (Mermaid):** A sequence diagram showing the flow: - 1. App asks `GACAppCheck` for a token. - 2. `GACAppCheck` checks cache. - 3. If missing/expired, `GACAppCheck` asks `GACAppCheckProvider` for a token. - 4. Provider contacts Apple/Backend. - 5. Token returned and cached. - -### 4. `docs/providers.md` -* **Overview:** Explain the concept of providers. -* **AppAttest Provider (`GACAppAttestProvider`):** - * Best for modern iOS devices (A11 chip+). - * Wraps `DCAppAttestService`. - * Requires `DeviceCheck` framework linkage. -* **DeviceCheck Provider (`GACDeviceCheckProvider`): - * Uses Apple's older `DCDevice` API. - * Good fallback for older devices. -* **Debug Provider (`GACAppCheckDebugProvider`): - * Explain its use for local development and CI (simulators). - * Mention it generates a local debug secret that needs to be registered in the backend. - -### 5. `docs/api-reference.md` -* **Core Classes:** - * `GACAppCheck`: The main entry point. - * `GACAppCheckSettings`: Managing settings. - * `GACAppCheckToken`: Properties (token string, expiration date). - * `GACAppCheckTokenResult`: Wrapper for token + error. -* **Protocols:** - * `GACAppCheckProvider`: The interface for creating custom providers. - * `GACAppCheckTokenDelegate`: For listening to token changes. - -### 6. `docs/contributing.md` -* Link to the root `CONTRIBUTING.md`. -* Briefly mention the development setup (running `setup-scripts.sh`, generating project files). - -### General Guidelines -* Use clear, professional language. -* Provide Swift and Objective-C code snippets where relevant (the library is Obj-C but Swift-friendly). -* Ensure the directory structure matches the requested `docs/` layout. From a0f4ba3486bfa895033f100b312be7a9d5f3010b Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 22:29:49 -0500 Subject: [PATCH 74/94] b --- docs/contributing.md | 12 ------------ docs/getting-started.md | 37 ------------------------------------- docs/index.md | 2 +- 3 files changed, 1 insertion(+), 50 deletions(-) delete mode 100644 docs/contributing.md delete mode 100644 docs/getting-started.md diff --git a/docs/contributing.md b/docs/contributing.md deleted file mode 100644 index 75599838..00000000 --- a/docs/contributing.md +++ /dev/null @@ -1,12 +0,0 @@ -# Contributing to App Check Core - -We welcome contributions to the App Check Core library! - -For detailed information on how to contribute, including guidelines for code style, testing, and pull requests, please refer to the main [CONTRIBUTING.md](../../CONTRIBUTING.md) file in the root of the repository. - -## Development Setup - -To set up your development environment and get started with App Check Core: - -1. **Run Setup Scripts:** Execute `setup-scripts.sh` from the project root to install necessary tools and dependencies. -2. **Generate Project Files:** Use the appropriate commands (e.g., `pod install` for CocoaPods or Xcode's Swift Package Manager integration) to generate Xcode project files. diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index 957d6d3d..00000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,37 +0,0 @@ -# Getting Started - -## Installation - -### CocoaPods -To integrate `AppCheckCore` using CocoaPods, add the following to your `Podfile`: - -```ruby -pod 'AppCheckCore' -``` - -Then, run `pod install` from your terminal. - -### Swift Package Manager -You can add `AppCheckCore` to your project using Swift Package Manager in Xcode or by adding it to your `Package.swift` file. - -**Xcode:** -1. In Xcode, go to `File > Add Packages...` -2. Enter the repository URL: `https://github.com/google/app-check` -3. Choose the desired version or branch. - -**Package.swift:** -Add the following to your `Package.swift` dependencies: - -```swift -.package(url: "https://github.com/google/app-check", from: "1.0.0"), // Replace with the latest version -``` - -And then add `"AppCheckCore"` to your target's dependencies. - -## Prerequisites -`AppCheckCore` supports the following minimum OS versions: -* iOS 12.0+ -* macCatalyst 13.0+ -* macOS 10.15+ -* tvOS 13.0+ -* watchOS 7.0+ diff --git a/docs/index.md b/docs/index.md index 43a9e3fe..a933eaf5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -32,7 +32,7 @@ concurrency management described herein, are subject to change without notice across library versions.** ## Documentation Sections -* [Getting Started](getting-started.md): Installation and basic setup. + * [Usage Guide](usage.md): How to initialize and fetch tokens. * [Providers Deep Dive](providers.md): Detailed architectural breakdown of App Attest, DeviceCheck, and Debug providers, From cddc37f21b870f983326d26079d4332aa1302cf3 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 22:32:21 -0500 Subject: [PATCH 75/94] b --- docs/providers/app-attest.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 1851de6b..aac94090 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -56,13 +56,13 @@ flowchart LR CheckSupport -- Yes --> CheckKey{Stored Key ID?} - CheckKey -- No --> SupportedInitial[State: SupportedInitial
(Ready for Handshake)] + CheckKey -- No --> SupportedInitial["State: SupportedInitial
(Ready for Handshake)"] CheckKey -- Yes --> CheckArtifact{Stored Artifact
for Key ID?} - CheckArtifact -- No --> KeyGenerated[State: KeyGenerated
(Key exists, no Artifact)] + CheckArtifact -- No --> KeyGenerated["State: KeyGenerated
(Key exists, no Artifact)"] - CheckArtifact -- Yes --> KeyRegistered[State: KeyRegistered
(Key & Artifact exist)] + CheckArtifact -- Yes --> KeyRegistered["State: KeyRegistered
(Key & Artifact exist)"] ``` ## Decision Logic & State Machine From 00b64e59cff46025a81c2d8e4bac6ab9869a929f Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 22:36:27 -0500 Subject: [PATCH 76/94] b --- docs/providers/app-attest.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index aac94090..300641ca 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -52,18 +52,17 @@ dictates whether to perform an initial handshake or a token refresh. flowchart LR Start([Calculate State]) --> CheckSupport{Is App Attest
Supported?} - CheckSupport -- No --> Unsupported[State: Unsupported] - - CheckSupport -- Yes --> CheckKey{Stored Key ID?} - - CheckKey -- No --> SupportedInitial["State: SupportedInitial
(Ready for Handshake)"] - - CheckKey -- Yes --> CheckArtifact{Stored Artifact
for Key ID?} - - CheckArtifact -- No --> KeyGenerated["State: KeyGenerated
(Key exists, no Artifact)"] - - CheckArtifact -- Yes --> KeyRegistered["State: KeyRegistered
(Key & Artifact exist)"] -``` + CheckSupport -- Yes --> CheckKey{Stored Key ID?} + + CheckSupport -- No --> Unsupported["State: Unsupported"] + + CheckKey -- Yes --> CheckArtifact{Stored Artifact
for Key ID?} + + CheckKey -- No --> SupportedInitial["State: SupportedInitial
(Ready for Handshake)"] + + CheckArtifact -- Yes --> KeyRegistered["State: KeyRegistered
(Key & Artifact exist)"] + + CheckArtifact -- No --> KeyGenerated["State: KeyGenerated
(Key exists, no Artifact)"]``` ## Decision Logic & State Machine Before executing a handshake, the provider determines the correct flow From cbfeca4d7fbb8d1c4a5fd93cfb33e13d1e84c3b6 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 22:42:12 -0500 Subject: [PATCH 77/94] b --- docs/providers/app-attest.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 300641ca..f8b3cf82 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -62,7 +62,8 @@ flowchart LR CheckArtifact -- Yes --> KeyRegistered["State: KeyRegistered
(Key & Artifact exist)"] - CheckArtifact -- No --> KeyGenerated["State: KeyGenerated
(Key exists, no Artifact)"]``` + CheckArtifact -- No --> KeyGenerated["State: KeyGenerated
(Key exists, no Artifact)"] +``` ## Decision Logic & State Machine Before executing a handshake, the provider determines the correct flow From 96b075c6b248515ebfb516920406605735b28b79 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 22:44:20 -0500 Subject: [PATCH 78/94] b --- docs/providers/app-attest.md | 46 ++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index f8b3cf82..03a5c7ba 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -51,17 +51,17 @@ dictates whether to perform an initial handshake or a token refresh. ```mermaid flowchart LR Start([Calculate State]) --> CheckSupport{Is App Attest
Supported?} - + CheckSupport -- Yes --> CheckKey{Stored Key ID?} - + CheckSupport -- No --> Unsupported["State: Unsupported"] - + CheckKey -- Yes --> CheckArtifact{Stored Artifact
for Key ID?} - + CheckKey -- No --> SupportedInitial["State: SupportedInitial
(Ready for Handshake)"] - + CheckArtifact -- Yes --> KeyRegistered["State: KeyRegistered
(Key & Artifact exist)"] - + CheckArtifact -- No --> KeyGenerated["State: KeyGenerated
(Key exists, no Artifact)"] ``` @@ -79,28 +79,28 @@ generated. %%{init: {"flowchart": {"diagramPadding": 130}}}%% flowchart LR Start[getToken] --> CheckUse{Limited Use?} - + CheckUse -- Yes --> Queue1[Queue New Request] CheckUse -- No --> Coalesce{Ongoing Op?} - + Coalesce -- No --> StartNew[Start New Request] Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} - + CheckOngoing -- Yes --> Queue2[Queue New Request] CheckOngoing -- No --> Reuse[Reuse Existing Request] - + subgraph Execution ["Backoff Wrapped Execution"] direction LR Backoff[Check Backoff] StateCheck{Attestation State?} - + Backoff --> StateCheck - + StateCheck -->|Yes| KeyCheck{Key ID?} - + KeyCheck -- No --> Flow1[Flow 1: Initial] KeyCheck -- Yes --> ArtifactCheck{Artifact?} - + ArtifactCheck -- No --> Flow1 ArtifactCheck -- Yes --> Flow2[Flow 2: Refresh] @@ -145,12 +145,12 @@ sequenceDiagram participant AppB as App (Limited) participant AppC as App (Standard) participant Provider as GACAppAttestProvider - + AppA->>Provider: getToken(false) activate Provider Note right of Provider: No ongoing op.
Start new op (standard).
Set ongoingGetTokenOperation. Provider-->>Provider: Start Flow 1/2 sequence - + AppB->>Provider: getToken(true) activate Provider Note right of Provider: Ongoing op (standard) exists.
New request is limited-use.
Chain: Wait for ongoing, then start new op. @@ -186,7 +186,7 @@ sequenceDiagram participant Backend as Firebase Backend App->>Provider: getToken(limitedUse) - + loop Retry Loop (Max 1 Retry for GACAppAttestRejectionError) par Parallel Execution Provider->>API: getRandomChallenge() @@ -201,7 +201,7 @@ sequenceDiagram Provider->>Apple: attestKey(keyId, clientDataHash=SHA256(challenge)) Apple->>AppleServer: Contact App Attest Service AppleServer-->>Apple: Attestation Result - + alt Attestation Failed (Invalid Key/Input) Apple-->>Provider: DCErrorInvalidKey / Input Provider->>Provider: RESET: Delete KeyID & Artifact @@ -210,7 +210,7 @@ sequenceDiagram Apple-->>Provider: Attestation Object Provider->>API: attestKeyWithAttestation(attestation, keyID, challenge, limitedUse) API->>Backend: POST /exchangeAppAttestAttestation
{ limited_use: true/false } - + alt Backend Rejection (403) Backend-->>API: 403 Forbidden API-->>Provider: Error (403) @@ -238,7 +238,7 @@ sequenceDiagram participant Backend as Firebase Backend App->>Provider: getToken(limitedUse) - + loop Retry Loop (Max 1 Retry for GACAppAttestRejectionError) Provider->>API: getRandomChallenge() API->>Backend: POST /generateAppAttestChallenge @@ -247,18 +247,18 @@ sequenceDiagram Provider->>Provider: Retrieve stored Artifact Provider->>Provider: ClientData = Artifact + Challenge Provider->>Apple: generateAssertion(keyId, clientDataHash=SHA256(ClientData)) - + alt Assertion Failed (Invalid Key/Input) Apple-->>Provider: DCErrorInvalidKey / Input Provider->>Provider: RESET: Delete KeyID & Artifact Note right of Provider: Throws GACAppAttestRejectionError,
Triggering Loop Retry
(Will fall back to Initial Handshake) else Assertion Success Apple-->>Provider: Assertion Object - + Provider->>API: getAppCheckTokenWithArtifact(..., limitedUse) API->>Backend: POST /exchangeAppAttestAssertion
{ limited_use: true/false } Backend-->>API: { "token": "..." } - + Provider-->>App: App Check Token end end From 3d0ec5d22aaa153c616ebc32c657b74031cff57d Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 23:16:27 -0500 Subject: [PATCH 79/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 03a5c7ba..f5744feb 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -92,7 +92,7 @@ flowchart LR subgraph Execution ["Backoff Wrapped Execution"] direction LR Backoff[Check Backoff] - StateCheck{Attestation State?} + StateCheck{Attestation State?}
(See 'Attestation State Calculation' above) Backoff --> StateCheck From 486eeb66524982e5cabc25de45fe92f172e3c102 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 23:18:07 -0500 Subject: [PATCH 80/94] B --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index f5744feb..a0217a13 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -92,7 +92,7 @@ flowchart LR subgraph Execution ["Backoff Wrapped Execution"] direction LR Backoff[Check Backoff] - StateCheck{Attestation State?}
(See 'Attestation State Calculation' above) + StateCheck["Attestation State?
(See 'Attestation State Calculation' above)"] Backoff --> StateCheck From a2df167de03fd930b0469e7f6ea101487c460f15 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 23:24:31 -0500 Subject: [PATCH 81/94] b --- docs/providers/app-attest.md | 72 +++++++++++++++++------------------- 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index a0217a13..e0eb7f10 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -51,19 +51,18 @@ dictates whether to perform an initial handshake or a token refresh. ```mermaid flowchart LR Start([Calculate State]) --> CheckSupport{Is App Attest
Supported?} - + CheckSupport -- Yes --> CheckKey{Stored Key ID?} - + CheckSupport -- No --> Unsupported["State: Unsupported"] - + CheckKey -- Yes --> CheckArtifact{Stored Artifact
for Key ID?} - + CheckKey -- No --> SupportedInitial["State: SupportedInitial
(Ready for Handshake)"] - + CheckArtifact -- Yes --> KeyRegistered["State: KeyRegistered
(Key & Artifact exist)"] - - CheckArtifact -- No --> KeyGenerated["State: KeyGenerated
(Key exists, no Artifact)"] -``` + + CheckArtifact -- No --> KeyGenerated["State: KeyGenerated
(Key exists, no Artifact)"]``` ## Decision Logic & State Machine Before executing a handshake, the provider determines the correct flow @@ -78,42 +77,37 @@ generated. ```mermaid %%{init: {"flowchart": {"diagramPadding": 130}}}%% flowchart LR - Start[getToken] --> CheckUse{Limited Use?} - - CheckUse -- Yes --> Queue1[Queue New Request] - CheckUse -- No --> Coalesce{Ongoing Op?} - - Coalesce -- No --> StartNew[Start New Request] - Coalesce -- Yes --> CheckOngoing{Ongoing Limited?} - - CheckOngoing -- Yes --> Queue2[Queue New Request] - CheckOngoing -- No --> Reuse[Reuse Existing Request] - + Start[getToken] --> Ongoing{Ongoing Op?} + + Ongoing -- No --> StartNew[Start New Request] + Ongoing -- Yes --> Conflict{Limited Request
or Mismatch?} + + Conflict -- Yes --> Queue[Queue New Request] + Conflict -- No --> Reuse[Reuse Existing Request] + + Queue --> Backoff + StartNew --> Backoff + subgraph Execution ["Backoff Wrapped Execution"] direction LR Backoff[Check Backoff] StateCheck["Attestation State?
(See 'Attestation State Calculation' above)"] - + Backoff --> StateCheck - + StateCheck -->|Yes| KeyCheck{Key ID?} - + KeyCheck -- No --> Flow1[Flow 1: Initial] KeyCheck -- Yes --> ArtifactCheck{Artifact?} - + ArtifactCheck -- No --> Flow1 ArtifactCheck -- Yes --> Flow2[Flow 2: Refresh] StateCheck -->|No| Error[Error] end - Queue1 --> Backoff - Queue2 --> Backoff - StartNew --> Backoff - Reuse -.- Footnote["Note: The 'ongoingGetTokenOperation' tracks the active fetch.
Standard requests reuse it (unless the active fetch is Limited-use).
Limited-use requests always queue a new, sequential fetch."] - Queue1 -.- Footnote - Queue2 -.- Footnote + Queue -.- Footnote StartNew -.- Footnote ``` @@ -145,12 +139,12 @@ sequenceDiagram participant AppB as App (Limited) participant AppC as App (Standard) participant Provider as GACAppAttestProvider - + AppA->>Provider: getToken(false) activate Provider Note right of Provider: No ongoing op.
Start new op (standard).
Set ongoingGetTokenOperation. Provider-->>Provider: Start Flow 1/2 sequence - + AppB->>Provider: getToken(true) activate Provider Note right of Provider: Ongoing op (standard) exists.
New request is limited-use.
Chain: Wait for ongoing, then start new op. @@ -186,7 +180,7 @@ sequenceDiagram participant Backend as Firebase Backend App->>Provider: getToken(limitedUse) - + loop Retry Loop (Max 1 Retry for GACAppAttestRejectionError) par Parallel Execution Provider->>API: getRandomChallenge() @@ -201,7 +195,7 @@ sequenceDiagram Provider->>Apple: attestKey(keyId, clientDataHash=SHA256(challenge)) Apple->>AppleServer: Contact App Attest Service AppleServer-->>Apple: Attestation Result - + alt Attestation Failed (Invalid Key/Input) Apple-->>Provider: DCErrorInvalidKey / Input Provider->>Provider: RESET: Delete KeyID & Artifact @@ -210,7 +204,7 @@ sequenceDiagram Apple-->>Provider: Attestation Object Provider->>API: attestKeyWithAttestation(attestation, keyID, challenge, limitedUse) API->>Backend: POST /exchangeAppAttestAttestation
{ limited_use: true/false } - + alt Backend Rejection (403) Backend-->>API: 403 Forbidden API-->>Provider: Error (403) @@ -238,7 +232,7 @@ sequenceDiagram participant Backend as Firebase Backend App->>Provider: getToken(limitedUse) - + loop Retry Loop (Max 1 Retry for GACAppAttestRejectionError) Provider->>API: getRandomChallenge() API->>Backend: POST /generateAppAttestChallenge @@ -247,19 +241,19 @@ sequenceDiagram Provider->>Provider: Retrieve stored Artifact Provider->>Provider: ClientData = Artifact + Challenge Provider->>Apple: generateAssertion(keyId, clientDataHash=SHA256(ClientData)) - + alt Assertion Failed (Invalid Key/Input) Apple-->>Provider: DCErrorInvalidKey / Input Provider->>Provider: RESET: Delete KeyID & Artifact Note right of Provider: Throws GACAppAttestRejectionError,
Triggering Loop Retry
(Will fall back to Initial Handshake) else Assertion Success Apple-->>Provider: Assertion Object - + Provider->>API: getAppCheckTokenWithArtifact(..., limitedUse) API->>Backend: POST /exchangeAppAttestAssertion
{ limited_use: true/false } Backend-->>API: { "token": "..." } - + Provider-->>App: App Check Token end end -``` \ No newline at end of file +``` From 4087dbe43c8a0555e21982891f6df14ed335a210 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 23:26:57 -0500 Subject: [PATCH 82/94] b --- docs/providers/app-attest.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index e0eb7f10..77ad826c 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -62,7 +62,8 @@ flowchart LR CheckArtifact -- Yes --> KeyRegistered["State: KeyRegistered
(Key & Artifact exist)"] - CheckArtifact -- No --> KeyGenerated["State: KeyGenerated
(Key exists, no Artifact)"]``` + CheckArtifact -- No --> KeyGenerated["State: KeyGenerated
(Key exists, no Artifact)"] +``` ## Decision Logic & State Machine Before executing a handshake, the provider determines the correct flow From acfcc32cef1363feb21994b1ea433ee7671325c8 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 23:27:58 -0500 Subject: [PATCH 83/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 77ad826c..3d785aae 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -76,7 +76,7 @@ finish) and then start a fresh handshake to ensure a unique token is generated. ```mermaid -%%{init: {"flowchart": {"diagramPadding": 130}}}%% +%%{init: {"flowchart": {"diagramPadding": 100}}}%% flowchart LR Start[getToken] --> Ongoing{Ongoing Op?} From c01956bf50e6349334cbc061393e29d10ec40a78 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 23:28:41 -0500 Subject: [PATCH 84/94] b --- docs/providers/app-attest.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 3d785aae..0e49d366 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -75,8 +75,9 @@ fetched), the new request will "chain" (wait for the ongoing one to finish) and then start a fresh handshake to ensure a unique token is generated. -```mermaid %%{init: {"flowchart": {"diagramPadding": 100}}}%% + +```mermaid flowchart LR Start[getToken] --> Ongoing{Ongoing Op?} From c447f255c441ba0d21f1d0bc9bdf16527fed0e9b Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 14 Jan 2026 23:42:38 -0500 Subject: [PATCH 85/94] b --- docs/providers/app-attest.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 0e49d366..06a22b1d 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -75,8 +75,6 @@ fetched), the new request will "chain" (wait for the ongoing one to finish) and then start a fresh handshake to ensure a unique token is generated. -%%{init: {"flowchart": {"diagramPadding": 100}}}%% - ```mermaid flowchart LR Start[getToken] --> Ongoing{Ongoing Op?} From b0ec775c853eb109b552f19be6a2f6f1d30df01d Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 20 Jan 2026 14:38:32 -0500 Subject: [PATCH 86/94] b --- docs/providers/app-attest.md | 51 +++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 06a22b1d..bbd69917 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -3,6 +3,26 @@ The most complex provider, interacting with `DCAppAttestService`. It maintains a stable key pair on the device to sign assertions. +## Overview +The App Attest provider uses a two-phase process that minimizes +network usage and preserves privacy: + +1. **Initial Handshake (Attestation):** The app generates a + cryptographic key pair via Apple's `DCAppAttestService`. Apple + certifies this key. The app sends this certification to the + Firebase backend, which validates it and returns an + **App Check Token** and an **Artifact**. +2. **Token Refresh (Assertion):** For subsequent requests, the app + uses the stored Key ID to sign a challenge (Assertion) from Apple. + The Firebase backend verifies this signature against the stored + Artifact to issue a new token. + +**Stored Artifacts:** +* **Key ID:** Identifies the key pair on the device (persisted in + UserDefaults). +* **Artifact:** An opaque object from the Firebase backend linking + the device's key to the user's session (persisted in Keychain). + ## Components * **Service:** `DCAppAttestService` (Apple's API). * **Storage:** @@ -22,24 +42,29 @@ maintains a stable key pair on the device to sign assertions. * **Triggers for Reset & Internal Retry:** * `DCErrorInvalidKey` / `DCErrorInvalidInput` (Apple DeviceCheck error). * HTTP 403 (Attestation Rejected) from the backend during handshake. - * **Transient Error Handling (No Reset):** If `DCErrorServerUnavailable` - (indicating a temporary issue reaching Apple's App Attest service) occurs, - the request fails, but the App Attest key and artifact are **preserved**. - This allows the app to retry the request later using the same key, - aligning with Apple's recommendation to preserve the device's risk metric. + * **Other Non-Resetting Errors (State Preserved):** For errors that + do not trigger the internal retry loop or an explicit external + backoff (e.g., `DCErrorServerUnavailable`, generic network issues, + storage errors), the request fails, but the App Attest key and + artifact are **preserved**. This allows the app to retry the + request later using the same key, aligning with Apple's + recommendation to preserve the device's risk metric. * **Backoff Strategy (External):** An outer `GACAppCheckBackoffWrapper` - protects the backend from traffic spikes by enforcing delays on subsequent - attempts based on the error type. + protects the backend from traffic spikes by enforcing delays on + subsequent attempts based on the error type. * **No Backoff (Immediately Permitted):** For non-HTTP errors (e.g., - Apple's `DCError` like `serverUnavailable`), network connectivity issues, - storage failures, or parsing errors, the backoff wrapper **does not** enforce - a delay. Subsequent `getToken` calls by the app are immediately permitted. + Apple's `DCError` like `serverUnavailable`), network connectivity + issues, storage failures, or parsing errors, the backoff wrapper + **does not** enforce a delay. Subsequent `getToken` calls by the + app are immediately permitted. * **Exponential Backoff:** Applied to retryable server errors. * HTTP 403 (Project/App Deleted) *if internal retry fails*. * HTTP 429 (Too Many Requests). * HTTP 503 (Server Overloaded). - * Other HTTP 5xx (Server Errors) or 4xx not listed above or handled by 1 day backoff. - * **1 Day Backoff:** Applied to configuration errors unlikely to resolve quickly. + * Other HTTP 5xx (Server Errors) or 4xx not listed above or + handled by 1 day backoff. + * **1 Day Backoff:** Applied to configuration errors unlikely to + resolve quickly. * HTTP 400 (Bad Request). * HTTP 404 (Not Found). @@ -256,4 +281,4 @@ sequenceDiagram Provider-->>App: App Check Token end end -``` +``` \ No newline at end of file From 7fa305bcf44077570d28caf17894694a408d899d Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 20 Jan 2026 14:42:36 -0500 Subject: [PATCH 87/94] b --- docs/providers/app-attest.md | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index bbd69917..4f38a3bf 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -4,14 +4,13 @@ The most complex provider, interacting with `DCAppAttestService`. It maintains a stable key pair on the device to sign assertions. ## Overview -The App Attest provider uses a two-phase process that minimizes -network usage and preserves privacy: +The App Attest provider uses a two-phase process: 1. **Initial Handshake (Attestation):** The app generates a - cryptographic key pair via Apple's `DCAppAttestService`. Apple - certifies this key. The app sends this certification to the - Firebase backend, which validates it and returns an - **App Check Token** and an **Artifact**. + cryptographic key pair via Apple's `DCAppAttestService`. Apple then + certifies this key via a call to an Apple server. The app sends this + certification to the Firebase backend, which validates it and returns + an **App Check Token** and an **Artifact**. 2. **Token Refresh (Assertion):** For subsequent requests, the app uses the stored Key ID to sign a challenge (Assertion) from Apple. The Firebase backend verifies this signature against the stored @@ -42,13 +41,6 @@ network usage and preserves privacy: * **Triggers for Reset & Internal Retry:** * `DCErrorInvalidKey` / `DCErrorInvalidInput` (Apple DeviceCheck error). * HTTP 403 (Attestation Rejected) from the backend during handshake. - * **Other Non-Resetting Errors (State Preserved):** For errors that - do not trigger the internal retry loop or an explicit external - backoff (e.g., `DCErrorServerUnavailable`, generic network issues, - storage errors), the request fails, but the App Attest key and - artifact are **preserved**. This allows the app to retry the - request later using the same key, aligning with Apple's - recommendation to preserve the device's risk metric. * **Backoff Strategy (External):** An outer `GACAppCheckBackoffWrapper` protects the backend from traffic spikes by enforcing delays on subsequent attempts based on the error type. @@ -67,6 +59,13 @@ network usage and preserves privacy: resolve quickly. * HTTP 400 (Bad Request). * HTTP 404 (Not Found). + * **Other Non-Resetting Errors (State Preserved):** For errors that + do not trigger the internal retry loop or an explicit external + backoff (e.g., `DCErrorServerUnavailable`, generic network issues, + storage errors), the request fails, but the App Attest key and + artifact are **preserved**. This allows the app to retry the + request later using the same key, aligning with Apple's + recommendation to preserve the device's risk metric. ## Attestation State Calculation The provider determines its current state by checking for the presence of From b630696d5eab8d2ed93c698691030246f77fa434 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 20 Jan 2026 15:49:13 -0500 Subject: [PATCH 88/94] b --- docs/providers/app-attest.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 4f38a3bf..c16c51ef 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -8,9 +8,9 @@ The App Attest provider uses a two-phase process: 1. **Initial Handshake (Attestation):** The app generates a cryptographic key pair via Apple's `DCAppAttestService`. Apple then - certifies this key via a call to an Apple server. The app sends this - certification to the Firebase backend, which validates it and returns - an **App Check Token** and an **Artifact**. + certifies this key via a call to an Apple server. The AppCheckCore SDK + sends this certification to the Firebase backend, which validates it + and returns an **App Check Token** and an **Artifact**. 2. **Token Refresh (Assertion):** For subsequent requests, the app uses the stored Key ID to sign a challenge (Assertion) from Apple. The Firebase backend verifies this signature against the stored @@ -242,6 +242,12 @@ sequenceDiagram end end end + Note right of App: Errors not explicitly handled in this flow (e.g., + Note right of App: network issues, storage failures) will result in the + Note right of App: promise being rejected. Such errors may be subject + Note right of App: to external backoff if applicable, and all errors + Note right of App: eventually bubble up to the caller (unless successfully + Note right of App: resolved by an internal retry). ``` ## Flow 2: Token Refresh (Assertion) @@ -280,4 +286,10 @@ sequenceDiagram Provider-->>App: App Check Token end end + Note right of App: Errors not explicitly handled in this flow (e.g., + Note right of App: network issues, storage failures) will result in the + Note right of App: promise being rejected. Such errors may be subject + Note right of App: to external backoff if applicable, and all errors + Note right of App: eventually bubble up to the caller (unless successfully + Note right of App: resolved by an internal retry). ``` \ No newline at end of file From 41a3258e50b0b0a1756264b81ac2da11d13d396e Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 20 Jan 2026 15:52:18 -0500 Subject: [PATCH 89/94] b --- docs/providers/app-attest.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index c16c51ef..61689267 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -194,6 +194,12 @@ sequenceDiagram Occurs when the app runs for the first time, or if the stored artifact is missing, or **after a reset**. +**Note on Error Handling:** Errors not explicitly handled in this flow (e.g., +network issues, storage failures) will result in the promise being rejected. +Such errors may be subject to external backoff if applicable, and all errors +eventually bubble up to the caller (unless successfully resolved by an +internal retry). + ```mermaid sequenceDiagram participant App @@ -242,17 +248,17 @@ sequenceDiagram end end end - Note right of App: Errors not explicitly handled in this flow (e.g., - Note right of App: network issues, storage failures) will result in the - Note right of App: promise being rejected. Such errors may be subject - Note right of App: to external backoff if applicable, and all errors - Note right of App: eventually bubble up to the caller (unless successfully - Note right of App: resolved by an internal retry). ``` ## Flow 2: Token Refresh (Assertion) Occurs for subsequent requests using the established key pair. +**Note on Error Handling:** Errors not explicitly handled in this flow (e.g., +network issues, storage failures) will result in the promise being rejected. +Such errors may be subject to external backoff if applicable, and all errors +eventually bubble up to the caller (unless successfully resolved by an +internal retry). + ```mermaid sequenceDiagram participant App @@ -286,10 +292,4 @@ sequenceDiagram Provider-->>App: App Check Token end end - Note right of App: Errors not explicitly handled in this flow (e.g., - Note right of App: network issues, storage failures) will result in the - Note right of App: promise being rejected. Such errors may be subject - Note right of App: to external backoff if applicable, and all errors - Note right of App: eventually bubble up to the caller (unless successfully - Note right of App: resolved by an internal retry). ``` \ No newline at end of file From 7b221f99a19f38f900eb2ba1376159b9d92b9fe7 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 20 Jan 2026 15:59:29 -0500 Subject: [PATCH 90/94] callouts --- docs/architecture.md | 7 ++++--- docs/index.md | 22 +++++++++++----------- docs/providers/app-attest.md | 35 ++++++++++++++++++----------------- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 9581be19..88458a96 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,9 +4,10 @@ This document details the internal architecture of `AppCheckCore`, focusing on token storage, lifecycle management, and security mechanisms. -**Note:** This document describes internal implementation details that -are subject to change. Rely only on the public API surface for -integration (see [App Check Core Documentation](index.md#important-disclaimer)). +> [!WARNING] +> This document describes internal implementation details that are subject +> to change. Rely only on the public API surface for integration +> (see [App Check Core Documentation](index.md#important-disclaimer)). ## Token Storage App Check tokens are sensitive credentials that grant access to your diff --git a/docs/index.md b/docs/index.md index a933eaf5..74710e96 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,17 +19,17 @@ tvOS, and watchOS. limited-use tokens for scenarios requiring single-use or short-lived authentication. -## Important Disclaimer -**The detailed architectural and implementation documentation within this -`docs/` directory, especially the deep dives into provider internals and -decision logic, are provided for comprehensive understanding only.** - -**Consumers of the App Check Core library should exclusively rely on the -public API surface (as defined by the public header files, e.g., in -`AppCheckCore/Sources/Public`) for integration. Internal implementation -details, including specific error handling flows, storage mechanisms, and -concurrency management described herein, are subject to change without -notice across library versions.** +> [!IMPORTANT] +> **The detailed architectural and implementation documentation within this +> `docs/` directory, especially the deep dives into provider internals and +> decision logic, are provided for comprehensive understanding only.** +> +> **Consumers of the App Check Core library should exclusively rely on the +> public API surface (as defined by the public header files, e.g., in +> `AppCheckCore/Sources/Public`) for integration. Internal implementation +> details, including specific error handling flows, storage mechanisms, and +> concurrency management described herein, are subject to change without +> notice across library versions.** ## Documentation Sections diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 61689267..bded074b 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -93,11 +93,12 @@ flowchart LR Before executing a handshake, the provider determines the correct flow based on the internal state and manages concurrent requests. -**Note on Limited Use:** Limited-use tokens are never reused/coalesced. -If a limited-use token is requested (or if one is currently being -fetched), the new request will "chain" (wait for the ongoing one to -finish) and then start a fresh handshake to ensure a unique token is -generated. +> [!IMPORTANT] +> **Note on Limited Use:** Limited-use tokens are never reused/coalesced. +> If a limited-use token is requested (or if one is currently being +> fetched), the new request will "chain" (wait for the ongoing one to +> finish) and then start a fresh handshake to ensure a unique token is +> generated. ```mermaid flowchart LR @@ -192,13 +193,12 @@ sequenceDiagram ## Flow 1: Initial Handshake (Attestation) Occurs when the app runs for the first time, or if the stored artifact -is missing, or **after a reset**. - -**Note on Error Handling:** Errors not explicitly handled in this flow (e.g., -network issues, storage failures) will result in the promise being rejected. -Such errors may be subject to external backoff if applicable, and all errors -eventually bubble up to the caller (unless successfully resolved by an -internal retry). +> [!NOTE] +> **Note on Error Handling:** Errors not explicitly handled in this flow +> (e.g., network issues, storage failures) will result in the promise being +> rejected. Such errors may be subject to external backoff if applicable, +> and all errors eventually bubble up to the caller (unless successfully +> resolved by an internal retry). ```mermaid sequenceDiagram @@ -253,11 +253,12 @@ sequenceDiagram ## Flow 2: Token Refresh (Assertion) Occurs for subsequent requests using the established key pair. -**Note on Error Handling:** Errors not explicitly handled in this flow (e.g., -network issues, storage failures) will result in the promise being rejected. -Such errors may be subject to external backoff if applicable, and all errors -eventually bubble up to the caller (unless successfully resolved by an -internal retry). +> [!NOTE] +> **Note on Error Handling:** Errors not explicitly handled in this flow +> (e.g., network issues, storage failures) will result in the promise being +> rejected. Such errors may be subject to external backoff if applicable, +> and all errors eventually bubble up to the caller (unless successfully +> resolved by an internal retry). ```mermaid sequenceDiagram From dce848bbb88b625ef7012c72fe6ea9e0e3234aa0 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 20 Jan 2026 17:29:17 -0500 Subject: [PATCH 91/94] better summary --- docs/providers/app-attest.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index bded074b..997c72a1 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -6,11 +6,15 @@ maintains a stable key pair on the device to sign assertions. ## Overview The App Attest provider uses a two-phase process: -1. **Initial Handshake (Attestation):** The app generates a - cryptographic key pair via Apple's `DCAppAttestService`. Apple then - certifies this key via a call to an Apple server. The AppCheckCore SDK - sends this certification to the Firebase backend, which validates it - and returns an **App Check Token** and an **Artifact**. +1. **Initial Handshake (Attestation):** The SDK generates a cryptographic key + pair containing a random challenge and a Key ID. The random challenge is + provided by a Firebase backend. The Key ID is provided by an Apple backend, + via Apple's `DCAppAttestService` (from the DeviceCheck framework). The + key pair is then attested by an Apple backend, via `DCAppAttestService`, + which provides an attestation result. This attestation, Key ID, and + challenge are sent to a Firebase backend for verification and exchange + for an **App Check Token** and an **Attestation Artifact**, which is + used to validate token refreshes (assertions). 2. **Token Refresh (Assertion):** For subsequent requests, the app uses the stored Key ID to sign a challenge (Assertion) from Apple. The Firebase backend verifies this signature against the stored From a6a2b1e0f41dbda86be9a43c1b2c08a9d27d3a59 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 20 Jan 2026 17:50:36 -0500 Subject: [PATCH 92/94] tables --- docs/providers/app-attest.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 997c72a1..5a353350 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -197,6 +197,12 @@ sequenceDiagram ## Flow 1: Initial Handshake (Attestation) Occurs when the app runs for the first time, or if the stored artifact + +| Component | Details | +| :--- | :--- | +| **Inputs** | `limitedUse` (Bool)
Optional existing `Key ID` | +| **Outputs** | `App Check Token` (Returned)
`Key ID` (Persisted)
`Artifact` (Persisted) | + > [!NOTE] > **Note on Error Handling:** Errors not explicitly handled in this flow > (e.g., network issues, storage failures) will result in the promise being @@ -257,6 +263,11 @@ sequenceDiagram ## Flow 2: Token Refresh (Assertion) Occurs for subsequent requests using the established key pair. +| Component | Details | +| :--- | :--- | +| **Inputs** | `limitedUse` (Bool)
`Key ID` (From Storage)
`Artifact` (From Storage) | +| **Outputs** | `App Check Token` (Returned) | + > [!NOTE] > **Note on Error Handling:** Errors not explicitly handled in this flow > (e.g., network issues, storage failures) will result in the promise being From ac47a330d1d37038b5323070f278c1217be48513 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 20 Jan 2026 19:50:01 -0500 Subject: [PATCH 93/94] b --- docs/providers/app-attest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index 5a353350..fd313065 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -286,11 +286,11 @@ sequenceDiagram App->>Provider: getToken(limitedUse) loop Retry Loop (Max 1 Retry for GACAppAttestRejectionError) + Provider->>Provider: Retrieve Key ID & Artifact Provider->>API: getRandomChallenge() API->>Backend: POST /generateAppAttestChallenge Backend-->>API: { "challenge": "..." } - Provider->>Provider: Retrieve stored Artifact Provider->>Provider: ClientData = Artifact + Challenge Provider->>Apple: generateAssertion(keyId, clientDataHash=SHA256(ClientData)) From b73c44f8f2a94ec3fa6a89b782bba82f4db11d8f Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 21 Apr 2026 15:58:41 -0400 Subject: [PATCH 94/94] fixes --- docs/providers/app-attest.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md index fd313065..be9f5095 100644 --- a/docs/providers/app-attest.md +++ b/docs/providers/app-attest.md @@ -290,6 +290,7 @@ sequenceDiagram Provider->>API: getRandomChallenge() API->>Backend: POST /generateAppAttestChallenge Backend-->>API: { "challenge": "..." } + API-->>Provider: Challenge Provider->>Provider: ClientData = Artifact + Challenge Provider->>Apple: generateAssertion(keyId, clientDataHash=SHA256(ClientData))