WikifitaGitHub live67e8de5
pesquisa · openai-research/attestation-flow

Attestation Flow — DeviceCheck, Challenge, e o Hierarchy com PoW Fallback

The complete integrity mechanism of the desktop app — DeviceCheck native addon, challenge flow, and the if/else hierarchy with PoW fallback for non-Apple platforms

Baixar raw

Attestation Flow — DeviceCheck, Challenge, and the PoW Hierarchy

Part of openai-research. Sources: runtime eval in the main process (bezetacil v4 harness), real captured traces, and main process code.

The if/else hierarchy

The desktop app has a boolean gate that determines which integrity mechanism is used:

vPr() = isDeviceCheckSupported() && appVersion !== '0.0.0'
       (both checked via window.electronBridge in the renderer)

if (vPr()) {
  → ATTESTATION FLOW (macOS arm64 with DeviceCheck addon)
    GET /ios/attestation_challenge + x-sentinel-dc
    → app_attest_challenge in the conversation body
} else {
  → SENTINEL FLOW (PoW fallback for non-Apple platforms)
    POST /sentinel/chat-requirements/prepare
    → OpenAI-Sentinel-* headers (PoW FNV-1a on desktop, SHA3-512 on web)
}

The desktop on macOS takes the attestation path. The sentinel/PoW path exists for platforms where Apple's DeviceCheck API is unavailable.

The DeviceCheck native addon

File: /Applications/ChatGPT.app/Contents/Resources/native/devicecheck.node (Mach-O 64-bit bundle, arm64; N-API C++ — napi_register_module_v1, Napi::AsyncWorker pattern; links /System/Library/Frameworks/DeviceCheck.framework)

API: single export generateToken() returning a Promise:

{
  "supported": true,
  "tokenBase64": "AgAAAK9Pf1MC0r1SMXZiiu+iiG…",  // 2980 chars
  "latencyMs": 17.02
}
  • It is a thin wrapper over macOS's DCDevice (DeviceCheck framework; the macOS 15+ DeviceCheck/AppAttest surface) — the token is an opaque device/app attestation (Apple server-validated with the developer's credentials).
  • NO com.apple.developer.devicecheck entitlement exists — verified on the addon AND the app (codesign -d --entitlements): the app carries com.apple.developer.team-identifier: 2DC432GLL2, aps-environment: production, application-groups (2DC432GLL2.com.openai.codex.notifications, 2DC432GLL2.com.openai.sky.CUAService), allow-jit/allow-unsigned-executable-memory, audio/camera/network/calendars, keychain-access-groups (2DC432GLL2.*, 2DC432GLL2.com.openai.shared).
  • Standalone loading verified: require() from a plain Node process (outside the app) → {supported: true, tokenBase64: "AgAA…"}no code-signature chain validation in the addon (earlier claim disproven: the loader does not check the caller; macOS DCDevice provides the attestation regardless).
  • Implication for flow A — UPDATED 2026-08-27: the addon is self-sufficient (no re-signing/entitlements), BUT the plain-Node token produced 403 at the challenge (2026-08-26, first run) and the documented acceptance continues only for tokens generated in the app context; the validated A1 path generates the token in the main process (the bezetacil devicecheck trigger) — see flow-definitions / flow-a-manual-step-by-step. The server-side validation is keyed to the developer's Team/bundle; acceptance of an out-of-bundle token was NOT observed (one 403 observed).

The challenge flow (step by step)

1. Generate DeviceCheck token:
   addon.generateToken() → {supported: true, tokenBase64: "AgAA…", latencyMs: 17}
   (Apple hardware attestation, ~17ms)

2. GET /backend-api/ios/attestation_challenge
   Headers:
     Authorization: Bearer <codex access token>
     chatgpt-account-id: <account id from JWT>
     OAI-Language: en-US
     originator: Codex Desktop
     oai-did: <device uuid>
     x-sentinel-dc: {"token": "<DeviceCheck tokenBase64>"}
   → 200 {"attestation_challenge": "gAAAAAB…"}

3. POST /f/conversation with the challenge in the body:
   "app_attest_challenge": "gAAAAAB…"

Device registration (the missing piece — runtime-verified 2026-08-27)

The main process DeviceCheckCookieManager (asar class Epe) implements the registration:

  • registerCookie(): POST /devicecheck body {bundle_id, device_token} with credentials: 'include' (the Electron session cookies — the web session __oailb), then stores the returned _devicecheck cookie (name const _devicecheck, domain .chatgpt.com, secure; freshness checked against ~60s).
  • ensureCookie(): reads oai-did from the request headers; if the did differs from the registered one or the cookie is stale → registerCookie().
  • attachToken(): x-sentinel-dc: {"token": <DeviceCheck token>} — attached to the challenge request.
  • Runtime state (verified via bezetacil): the Electron default session holds _devicecheck (.chatgpt.com, secure) + __oailb (chatgpt.com and chat.openai.com, HttpOnly — the link-session derived web session). installation_id in this install: 594e0bbc-3b0c-470a-833a-7943425e76ba (file ~/.codex/installation_id, resolved by the app-server at boot — uuid v4, regenerate-or-reuse per codex-rs/core/src/installation_id.rs).
  • Implication (facts, not yet exercised): an unregistered did+token pair gets 403 on /ios/attestation_challenge; the registration POST carries the web-session cookies — the standalone probe flow needs the __oailb (link-session) + the /devicecheck step before the challenge.

What the server validates

The server receives the DeviceCheck token via x-sentinel-dc and validates it against Apple's DeviceCheck API using the app's server-side credentials. This proves:

  • The request originates from a real Apple device
  • The app is the genuine ChatGPT desktop (bundle ID com.openai.codex)
  • The device hasn't been modified to bypass attestation

The attestation_challenge returned is a one-time-use token that the server validates when the conversation request arrives. It expires quickly (observed: valid for the duration of one message send).

The sentinel/PoW fallback (non-Apple platforms)

On Windows/Linux, or({arch, platform}) returns false (requires darwin && arm64), so generateToken() returns nullvPr() is false → the renderer falls back to the sentinel flow:

  • POST /sentinel/chat-requirements/prepare with fingerprint
  • PoW solving (FNV-1a on desktop, SHA3-512 on web)
  • OpenAI-Sentinel-* headers on the conversation request

This is the flow that OmniRoute implements (for web/cookies) and that our probes initially replicated — but it's the fallback, not the primary mechanism.

The x-sentinel-dc header format

{"token": "<Apple DeviceCheck tokenBase64>"}

Wrapped by attachToken() on the deviceCheckCookieManager (asar class Epe), which also manages a registration cookie (POST /devicecheck with {bundle_id, device_token}) to register the device with the backend before the first attested request.

DeviceCheck addon standalone loading (verified again 2026-08-26, plain Node)

The addon exports exactly one function. Loading it from a plain Node process (no app context) works — this is the flow-A basis for a standalone:

node -e "const a=require('/Applications/ChatGPT.app/Contents/Resources/native/devicecheck.node');
a.generateToken().then(t=>console.log(t.supported, t.tokenBase64.slice(0,12)))"
# → true AgAAAK9Pf1MC…

Object.keys(addon)["generateToken"]. (Note: the app's own main loads it via process.resourcesPath + '/native/' + 'devicecheck.node' through createRequire in the app — same addon.)

The platform gate (the if/else that decides attestation vs sentinel)

The main process gate is or({arch, platform}):

function or({arch = process.arch, platform = process.platform} = {})
  { return platform === 'darwin' && arch === 'arm64' }

Condition chain:

  • or() false → sr() (the addon loader) returns null → DeviceCheck unavailable
  • ar({platform}) — platform pre-gate: platform === 'darwin' || platform === 'win32' (Windows can generate but the arch gate filters to darwin arm64)
  • vPr() in the renderer — the final boolean: isDeviceCheckSupported() === true && appVersion !== '0.0.0'

To exercise the sentinel/PoW fallback from a Mac, the gate to patch at runtime is vPr() in the renderer (or or() in the main) — flipping either makes the app take the sentinel route.

What the server validates (DeviceCheck token)

  1. Sends the DeviceCheck token via x-sentinel-dc: {"token": "<tokenBase64>"}
  2. The server validates it against Apple's DeviceCheck API using its own server-side credentials for the app's Team ID + bundle ID (com.openai.codex)
  3. Returns the one-time attestation_challenge which the client must echo in the conversation body as app_attest_challenge

This is why the token is not forgeable outside an Apple device running the signed app: Apple's API rejects tokens from unregistered devices or other bundle IDs, and the server owns the server-side DeviceCheck verification secrets.

TypeScript types (addon + manager + wire)

// ── the native addon ─────────────────────────────────────────────────────
export interface GenerateTokenResponse {
  supported: boolean
  tokenBase64: string          // base64 App-Attest (2980 chars) — 'AgAA…'
  latencyMs: number            // ~17
}
export interface DeviceCheckAddon {
  generateToken(): Promise<GenerateTokenResponse>    // single export
}

// ── the main-process gate ────────────────────────────────────────────────
export type PlatformGate = { arch?: string; platform?: string }   // defaults: process.arch/process.platform
export const gate = (o: PlatformGate): boolean => o.platform === 'darwin' && o.arch === 'arm64'
export interface ElectronBridge {
  isDeviceCheckSupported(): boolean
  // plus appVersion from SentryInitOptions ('0.0.0' → gate off)
}
export const vPr = (isSupported: boolean, appVersion: string): boolean =>
  isSupported && appVersion !== '0.0.0'

// ── the DeviceCheckCookieManager (asar class Epe) ────────────────────────
export interface DeviceCheckRegistrationBody {       // POST /devicecheck
  bundle_id: string            // 'com.openai.codex'
  device_token: string
}
export interface DeviceCheckRegistrationResponse {
  is_ok: boolean
  status_code: number
  // …(server description fields)
}
export interface EpeOptions {
  bundleIdentifier: string
  generateToken(): Promise<GenerateTokenResponse>    // the addon loader
  url: string                  // '/devicecheck' resolved against the prod API base
}
export interface EpeState {
  registration: Promise<unknown> | null
  registeredCookie: Electron.Cookie | null
  registeredDeviceId: string | null
}
export interface DeviceCheckCookieManager {
  ensureCookie(headers: Record<string, string>): Promise<void>   // compare oai-did + cookie; register if stale/unknown
  attachToken(headers: Record<string, string>): Promise<void>    // headers['x-sentinel-dc'] = JSON.stringify({token})
  registerCookie(headers: Record<string, string>): Promise<Electron.Cookie>  // POST /devicecheck (credentials:'include')
}
export const DeviceCheckCookieName = '_devicecheck'   // the registration cookie name
export const DeviceCheckCookieFreshnessMs = 60_000    // the freshness window (Tpe)

// ── the challenge wire ───────────────────────────────────────────────────
export interface XSentinelDc { token: string }         // headers['x-sentinel-dc'] = JSON string of this
export interface AttestationChallengeResponse { attestation_challenge: string }  // 'gAAAAAB…', one-time

// ── entitlements (verified) ──────────────────────────────────────────────
export interface AppEntitlements {
  'com.apple.developer.team-identifier': '2DC432GLL2'
  'com.apple.developer.aps-environment': 'production'
  'com.apple.security.application-groups': string[]
  'com.apple.security.cs.allow-jit': boolean
  'com.apple.security.cs.allow-unsigned-executable-memory': boolean
  'com.apple.security.device.audio-input'?: boolean
  'com.apple.security.device.camera'?: boolean
  'com.apple.security.files.user-selected.read-write'?: boolean
  'com.apple.security.network.client': boolean
  'com.apple.security.personal-information.calendars'?: boolean
  'keychain-access-groups': string[]
  // NO com.apple.developer.devicecheck entitlement (verified — macOS DCDevice does not require it)
}

Cross-references