Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
7
.claude/skills/axiom-app-attest/.openskills.json
Normal file
7
.claude/skills/axiom-app-attest/.openskills.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"source": "CharlesWiltgen/Axiom",
|
||||
"sourceType": "git",
|
||||
"repoUrl": "https://github.com/CharlesWiltgen/Axiom",
|
||||
"subpath": "axiom-codex/skills/axiom-app-attest",
|
||||
"installedAt": "2026-04-12T08:05:42.610Z"
|
||||
}
|
||||
335
.claude/skills/axiom-app-attest/SKILL.md
Normal file
335
.claude/skills/axiom-app-attest/SKILL.md
Normal file
@@ -0,0 +1,335 @@
|
||||
---
|
||||
name: axiom-app-attest
|
||||
description: Use when implementing app integrity verification, preventing fraud with DCAppAttestService, validating requests from legitimate app instances, using DeviceCheck for promotional abuse prevention, or needing server-side attestation/assertion validation. Covers key generation, attestation, assertion, rollout strategy, and risk metrics.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# App Attest
|
||||
|
||||
Device-backed app integrity verification for fraud prevention. Proves three things to your server: the request came from a genuine Apple device, running your genuine app, with an untampered payload.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use when you need to:
|
||||
- Verify requests come from legitimate app instances (not modified/cloned apps)
|
||||
- Prevent fraud in purchases, promotions, or competitive features
|
||||
- Implement DCAppAttestService attestation or assertion flows
|
||||
- Handle DeviceCheck 2-bit per-device state for promotional abuse
|
||||
- Build server-side validation for attestation objects or assertion signatures
|
||||
- Plan a gradual App Attest rollout for a large install base
|
||||
|
||||
## Example Prompts
|
||||
|
||||
"How do I verify my app hasn't been tampered with?"
|
||||
"DCAppAttestService attestKey keeps failing with serverUnavailable"
|
||||
"How do I prevent users from claiming a free trial multiple times?"
|
||||
"What's the difference between attestation and assertion?"
|
||||
"How do I validate an attestation object on my server?"
|
||||
"isSupported returns false — should I block the user?"
|
||||
"We have 2M DAU, how do I roll out App Attest safely?"
|
||||
"How do I detect if someone is creating fake app instances?"
|
||||
|
||||
## Red Flags
|
||||
|
||||
Signs you're headed for trouble:
|
||||
|
||||
- **Validating app integrity on-device** — Modified apps control the runtime. Any local check can be patched out. Verification MUST happen server-side.
|
||||
- **Not guarding with isSupported** — DCAppAttestService crashes on unsupported devices. Always check before calling any API.
|
||||
- **Blocking users when isSupported returns false** — Some legitimate devices return false. Treat as risk signal, not hard block.
|
||||
- **Reusing keys across multiple users on same device** — One key per user per device. Shared keys break account-level trust association.
|
||||
- **Enabling App Attest for all users at once** — `attestKey` calls Apple's servers. At scale, rate limiting causes failures. Gradual rollout required (WWDC 2021-10244).
|
||||
- **Using assertions for every API call** — Cryptographic cost per call. Reserve for sensitive operations (purchases, account changes), not routine fetches.
|
||||
- **Discarding key on serverUnavailable error** — Transient Apple server issue. Retry with same key. Only discard on other errors.
|
||||
- **Skipping counter validation on server** — Counter must be ever-increasing. Without this, replay attacks succeed.
|
||||
|
||||
## Three Properties Verified
|
||||
|
||||
App Attest proves three things about each request:
|
||||
|
||||
| Property | What It Proves | How |
|
||||
|----------|---------------|-----|
|
||||
| Genuine device | Request comes from real Apple hardware | Hardware-backed key in Secure Enclave |
|
||||
| Genuine app | Your app binary, unmodified | App identity hash in attestation |
|
||||
| Untampered payload | Request data hasn't been altered | Digest signing in assertions |
|
||||
|
||||
**Privacy design**: Anonymous. No hardware identifiers. Keys don't survive reinstall/migration/restore. Apple can't correlate across apps or users.
|
||||
|
||||
## Key Generation
|
||||
|
||||
```swift
|
||||
import DeviceCheck
|
||||
|
||||
func generateAppAttestKey(for userId: String) async throws -> String {
|
||||
let service = DCAppAttestService.shared
|
||||
|
||||
guard service.isSupported else {
|
||||
// NOT an error — use as risk signal, not blocker
|
||||
reportUnattestedDevice()
|
||||
throw AppAttestError.unsupported
|
||||
}
|
||||
|
||||
let keyId = try await service.generateKey()
|
||||
// Cache persistently — one key per user per device
|
||||
UserDefaults.standard.set(keyId, forKey: "appAttestKeyId_\(userId)")
|
||||
return keyId
|
||||
}
|
||||
```
|
||||
|
||||
**Key lifecycle**: One key per user per device. Cache `keyId` persistently (Keychain or UserDefaults). Keys don't survive reinstall, migration, or restore. App Clips share identity with full app. Generate new key on sign-out.
|
||||
|
||||
## Attestation Flow
|
||||
|
||||
Attestation registers the key with Apple and your server. Happens once per key.
|
||||
|
||||
```dot
|
||||
digraph attestation {
|
||||
"Server issues\nchallenge" [shape=ellipse];
|
||||
"SHA256 hash\nchallenge" [shape=box];
|
||||
"attestKey API\n(Apple servers)" [shape=box];
|
||||
"Send attestation\nto your server" [shape=box];
|
||||
"Server validates\ncertificate chain" [shape=box];
|
||||
"Store public key\n+ key association" [shape=doublecircle];
|
||||
|
||||
"Error?" [shape=diamond];
|
||||
"serverUnavailable?" [shape=diamond];
|
||||
"Retry same key" [shape=box];
|
||||
"Discard key\ngenerate new" [shape=box];
|
||||
|
||||
"Server issues\nchallenge" -> "SHA256 hash\nchallenge";
|
||||
"SHA256 hash\nchallenge" -> "attestKey API\n(Apple servers)";
|
||||
"attestKey API\n(Apple servers)" -> "Error?" ;
|
||||
"Error?" -> "Send attestation\nto your server" [label="success"];
|
||||
"Error?" -> "serverUnavailable?" [label="error"];
|
||||
"serverUnavailable?" -> "Retry same key" [label="yes"];
|
||||
"serverUnavailable?" -> "Discard key\ngenerate new" [label="no"];
|
||||
"Send attestation\nto your server" -> "Server validates\ncertificate chain";
|
||||
"Server validates\ncertificate chain" -> "Store public key\n+ key association";
|
||||
}
|
||||
```
|
||||
|
||||
```swift
|
||||
func attestKey(userId: String) async throws {
|
||||
guard let keyId = storedKeyId(for: userId) else {
|
||||
throw AppAttestError.noKey
|
||||
}
|
||||
|
||||
// 1. Get one-time challenge from YOUR server (minimum 16 bytes)
|
||||
let challenge = try await server.fetchAttestationChallenge()
|
||||
|
||||
// 2. Hash the challenge
|
||||
let hash = Data(SHA256.hash(data: challenge))
|
||||
|
||||
// 3. Request attestation from Apple
|
||||
do {
|
||||
let attestation = try await service.attestKey(keyId, clientDataHash: hash)
|
||||
// 4. Send attestation object to YOUR server for validation
|
||||
try await server.verifyAttestation(attestation, keyId: keyId, challenge: challenge)
|
||||
} catch DCError.serverUnavailable {
|
||||
// Transient — retry with SAME key later
|
||||
scheduleAttestationRetry(keyId: keyId, userId: userId)
|
||||
} catch {
|
||||
// Other error — key is compromised or invalid
|
||||
// Discard and generate a new key
|
||||
clearStoredKey(for: userId)
|
||||
try await generateAndAttestNewKey(userId: userId)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Challenge requirements**: Server-generated, single-use, minimum 16 bytes, short-lived (expire after minutes, not hours).
|
||||
|
||||
## Assertion Flow
|
||||
|
||||
Assertions prove ongoing request integrity. No Apple server involvement — on-device only.
|
||||
|
||||
```swift
|
||||
func assertRequest(payload: Data, userId: String) async throws -> Data {
|
||||
guard let keyId = storedKeyId(for: userId) else {
|
||||
throw AppAttestError.noKey
|
||||
}
|
||||
|
||||
// Hash the payload you want to protect
|
||||
let hash = Data(SHA256.hash(data: payload))
|
||||
|
||||
// Generate assertion (on-device, no network)
|
||||
let assertion = try await service.generateAssertion(keyId, clientDataHash: hash)
|
||||
|
||||
// Send assertion + original payload to server
|
||||
// Server verifies signature and checks counter
|
||||
return assertion
|
||||
}
|
||||
```
|
||||
|
||||
**When to assert**: Reserve for moments that cost you money or trust if faked.
|
||||
|
||||
| Assert | Don't Assert |
|
||||
|--------|-------------|
|
||||
| In-app purchases | Content fetches |
|
||||
| Account changes (email, password) | Read-only API calls |
|
||||
| Competitive actions (leaderboard scores) | Analytics events |
|
||||
| Promotional claims (free trial) | UI configuration |
|
||||
| Reward redemptions | Search queries |
|
||||
|
||||
**Performance**: Secure Enclave operations. Fast enough for individual actions, expensive on every request.
|
||||
|
||||
## Server-Side Validation
|
||||
|
||||
Your server does the actual trust verification. The app only generates cryptographic material.
|
||||
|
||||
### Attestation Validation (once per key)
|
||||
|
||||
1. **Certificate chain** — Verify roots to Apple's App Attest root CA (Apple Private PKI)
|
||||
2. **Nonce** — Recompute SHA256(challenge || clientDataHash), match against credential certificate
|
||||
3. **App identity hash** — SHA256(teamId + "." + bundleId) must match your app
|
||||
4. **Counter** — Store initial value (assertions increment from here)
|
||||
5. **Key association** — Extract and store public key, associate with user account
|
||||
|
||||
### Assertion Validation (per sensitive request)
|
||||
|
||||
1. **Signature** — Verify using stored public key from attestation
|
||||
2. **App identity hash** — Must match attestation's hash (prevents cross-app replay)
|
||||
3. **Counter** — Must be strictly greater than last seen value (replay protection)
|
||||
4. **Client data hash** — Recompute from request payload, must match what was signed
|
||||
|
||||
**Counter is critical**: Without strictly-increasing counter validation, replay attacks succeed indefinitely.
|
||||
|
||||
## Rollout Strategy
|
||||
|
||||
From WWDC 2021-10244: `attestKey` makes a network call to Apple's servers. Apple rate-limits these calls per app.
|
||||
|
||||
| Install Base | Recommended Ramp Time |
|
||||
|-------------|----------------------|
|
||||
| <100K DAU | Days |
|
||||
| ~1M DAU | ~1 day gradual ramp |
|
||||
| ~100M DAU | Weeks |
|
||||
| ~1B DAU | 1+ month gradual ramp |
|
||||
|
||||
### Gradual Enablement Pattern
|
||||
|
||||
```swift
|
||||
func shouldEnableAppAttest(userId: String) -> Bool {
|
||||
guard DCAppAttestService.shared.isSupported else { return false }
|
||||
// Server controls rollout percentage — start at 1%, ramp daily
|
||||
return server.isAppAttestEnabled(for: userId)
|
||||
}
|
||||
```
|
||||
|
||||
**Rollout process**: Start at 1%. Monitor attestation success rate. If above 95%, double daily. If rate limiting errors spike, pause. Treat unattested requests as lower-trust during rollout (additional fraud signals), not blocked.
|
||||
|
||||
## DeviceCheck Integration
|
||||
|
||||
DeviceCheck stores 2 bits of state per device on Apple's servers. Different purpose from App Attest.
|
||||
|
||||
| Feature | App Attest | DeviceCheck |
|
||||
|---------|-----------|-------------|
|
||||
| Purpose | Verify app integrity | Track per-device state |
|
||||
| Survives reinstall | No | Yes (tied to hardware) |
|
||||
| Apple servers | Attestation only | Every query |
|
||||
|
||||
### Promotional Fraud Prevention
|
||||
|
||||
```swift
|
||||
import DeviceCheck
|
||||
|
||||
func checkTrialEligibility() async throws -> Bool {
|
||||
guard DCDevice.current.isSupported else { return true }
|
||||
|
||||
let token = try await DCDevice.current.generateToken()
|
||||
// Server calls Apple: POST https://api.devicecheck.apple.com/v1/query_two_bits
|
||||
let state = try await server.queryDeviceState(token: token)
|
||||
return !state.bit0 // bit0 = has claimed trial
|
||||
}
|
||||
|
||||
func markTrialClaimed() async throws {
|
||||
let token = try await DCDevice.current.generateToken()
|
||||
// Server calls Apple: POST https://api.devicecheck.apple.com/v1/update_two_bits
|
||||
try await server.updateDeviceState(token: token, bit0: true)
|
||||
}
|
||||
```
|
||||
|
||||
**2 bits, your rules**: Apple stores bits + timestamp. Semantics are yours (e.g., bit0=trial claimed, bit1=abuse flagged). Reset on your schedule. Shared across all apps from the same developer team — coordinate meaning across your portfolio.
|
||||
|
||||
## Risk Metric Service
|
||||
|
||||
After attestation, redeem the receipt with Apple to get risk metrics:
|
||||
|
||||
**Server-side**: POST receipt to `https://data.appattest.apple.com/v1/attestationData` (use `data-development.appattest.apple.com` for sandbox). Response includes approximate key count for the device.
|
||||
|
||||
**How to use**: Most devices have 1-3 keys. High key counts signal an attacker creating many fake identities. Redeem periodically (Apple rate-limits), establish a baseline for your app, and combine with other fraud signals (velocity, behavioral analysis).
|
||||
|
||||
## Anti-Rationalization Table
|
||||
|
||||
| Rationalization | Why It Fails | What To Do Instead |
|
||||
|----------------|-------------|-------------------|
|
||||
| "We'll validate integrity on-device" | Modified apps control the runtime and can patch out any local check | All validation on your server. Device only generates crypto material. |
|
||||
| "isSupported is always true on modern devices" | Some configurations and enterprise MDM setups return false | Always guard. Handle false as risk signal, not crash. |
|
||||
| "One key per device is enough" | Multi-user devices need per-user keys for accurate account association | One key per user per device. New key on sign-out. |
|
||||
| "We'll enable App Attest for everyone on launch day" | Apple rate-limits attestKey calls. Large install bases will see widespread failures. | Server-controlled gradual rollout. Monitor success rate. |
|
||||
| "Assert every API call for maximum security" | Secure Enclave operations have real cost. Assertion latency on every request degrades UX. | Assert sensitive operations only. Use session tokens for routine calls. |
|
||||
| "serverUnavailable means the key is bad" | It's a transient Apple server issue. Discarding the key forces re-attestation unnecessarily. | Retry with same key. Only discard on non-transient errors. |
|
||||
| "We don't need counter validation" | Without strictly-increasing counters, replay attacks succeed indefinitely. | Store counter server-side. Reject assertions with counter <= last seen. |
|
||||
| "DeviceCheck replaces App Attest" | DeviceCheck is 2-bit state storage, not integrity verification. Different threat models. | Use both: App Attest for integrity, DeviceCheck for per-device flags. |
|
||||
|
||||
## Pressure Scenarios
|
||||
|
||||
### Scenario 1: "Block users who fail attestation"
|
||||
|
||||
**Pressure**: "If they can't attest, they're probably running a modified app. Block them."
|
||||
|
||||
**Reality**: `isSupported` returns false on legitimate devices (older hardware, enterprise MDM, simulator). During rollout, most users simply haven't been enrolled yet. Blocking = blocking real customers.
|
||||
|
||||
**Correct action**: Trust tiers on server. Attested = high trust. Unattested = lower trust with additional fraud signals. Never hard-block on attestation failure alone.
|
||||
|
||||
**Push-back template**: "Some legitimate devices return isSupported=false. Let's use attestation as one signal in a risk score — high trust for attested, additional checks for unattested."
|
||||
|
||||
### Scenario 2: "Enable App Attest for everyone at once"
|
||||
|
||||
**Pressure**: "We've been building this for weeks. Ship it to everyone."
|
||||
|
||||
**Reality**: `attestKey` calls Apple's servers. Apple rate-limits per app. At 5M DAU, flipping the switch causes a thundering herd — mass failures, error floods, confused users. WWDC 2021-10244 explicitly recommends gradual rollout.
|
||||
|
||||
**Correct action**: Server-controlled rollout starting at 1%. At 5M DAU, expect ~1 week to full rollout.
|
||||
|
||||
**Push-back template**: "Apple rate-limits attestKey calls — their WWDC session recommends gradual rollout. I'll set up server-side percentage control starting at 1%, ramping to 100% over about a week."
|
||||
|
||||
## Checklist
|
||||
|
||||
Before shipping App Attest:
|
||||
|
||||
**Key Generation**:
|
||||
- [ ] `isSupported` checked before any DCAppAttestService call
|
||||
- [ ] Graceful handling when `isSupported` returns false (risk signal, not block)
|
||||
- [ ] Key ID cached persistently per user
|
||||
- [ ] One key per user per device (not shared)
|
||||
|
||||
**Attestation**:
|
||||
- [ ] Challenge from server is single-use, minimum 16 bytes, short-lived
|
||||
- [ ] `serverUnavailable` retries with same key
|
||||
- [ ] Other errors discard key and generate new
|
||||
- [ ] Attestation object sent to server for validation (not validated on-device)
|
||||
|
||||
**Assertion**:
|
||||
- [ ] Used only for sensitive operations (not every API call)
|
||||
- [ ] Payload hash covers the actual request data being protected
|
||||
- [ ] Server validates signature with stored public key
|
||||
- [ ] Server validates counter is strictly increasing
|
||||
|
||||
**Server**:
|
||||
- [ ] Certificate chain validated against Apple's App Attest root CA
|
||||
- [ ] App identity hash (teamId + bundleId) verified
|
||||
- [ ] Counter stored and checked for strict increase
|
||||
- [ ] Public key associated with user account
|
||||
|
||||
**Rollout**:
|
||||
- [ ] Server-controlled percentage (not client-side)
|
||||
- [ ] Gradual ramp with monitoring
|
||||
- [ ] Unattested users handled gracefully (lower trust, not blocked)
|
||||
- [ ] Rollback plan if attestation success rate drops
|
||||
|
||||
## Resources
|
||||
|
||||
**WWDC**: 2021-10244
|
||||
|
||||
**Docs**: /devicecheck, /devicecheck/establishing-your-app-s-integrity, /devicecheck/validating-apps-that-connect-to-your-server
|
||||
|
||||
**Skills**: axiom-cryptokit
|
||||
3
.claude/skills/axiom-app-attest/agents/openai.yaml
Normal file
3
.claude/skills/axiom-app-attest/agents/openai.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
interface:
|
||||
display_name: "App Attest"
|
||||
short_description: "Implementing app integrity verification, preventing fraud with DCAppAttestService, validating requests from legitimat..."
|
||||
Reference in New Issue
Block a user