[codex] request desktop attestation from app (#20619)

## Summary

TL;DR: teaches `codex-rs` / app-server to request a desktop-provided
attestation token and attach it as `x-oai-attestation` on the scoped
ChatGPT Codex request paths.

![DeviceCheck attestation
interface](https://raw.githubusercontent.com/openai/codex/dev/jm/devicecheck-diagram-assets/pr-assets/devicecheck-attestation-interface.png)

## Details

This PR teaches the Codex app-server runtime how to request and attach
an attestation token. It does not generate DeviceCheck tokens directly;
instead, it relies on the connected desktop app to advertise that it can
generate attestation and then asks that app for a fresh header value
when needed.

The flow is:

1. The Codex desktop app connects to app-server.
2. During `initialize`, the app can advertise that it supports
`requestAttestation`.
3. Before app-server calls selected ChatGPT Codex endpoints, it sends
the internal server request `attestation/generate` to the app.
4. app-server receives a pre-encoded header value back.
5. app-server forwards that value as `x-oai-attestation` on the scoped
outbound requests.

The code in this repo is mostly protocol and runtime plumbing: it adds
the app-server request/response shape, introduces an attestation
provider in core, wires that provider into Responses / compaction /
realtime setup paths, and covers the intended scoping with tests. The
signed macOS DeviceCheck generation remains owned by the desktop app PR.

## Related PR

- Codex desktop app implementation:
https://github.com/openai/openai/pull/878649

## Validation

<details>
<summary>Tests run</summary>

```sh
cargo test -p codex-app-server-protocol
cargo test -p codex-core attestation --lib
cargo test -p codex-app-server --lib attestation
```

Also ran:

```sh
just fix -p codex-core
just fix -p codex-app-server
just fix -p codex-app-server-protocol
just fmt
just write-app-server-schema
```

</details>

<details>
<summary>E2E DeviceCheck validation</summary>

First validated the signed desktop app boundary directly: launched a
packaged signed `Codex.app`, sent `attestation/generate`, decoded the
returned `v1.` attestation header, and validated the extracted
DeviceCheck token with `personal/jm/verify_devicecheck_token.py` using
bundle ID `com.openai.codex`. Apple returned `status_code: 200` and
`is_ok: true`.

Then ran the fuller app + app-server flow. The packaged `Codex.app`
launched a current-branch app-server via `CODEX_CLI_PATH`, and a local
MITM proxy intercepted outbound `chatgpt.com` traffic. The app-server
requested `attestation/generate` from the real Electron app process, and
the intercepted `/backend-api/codex/responses` traffic included
`x-oai-attestation` on both routes:

```text
GET  /backend-api/codex/responses  Upgrade: websocket  x-oai-attestation: present
POST /backend-api/codex/responses  Upgrade: none       x-oai-attestation: present
```

The captured header decoded to a DeviceCheck token that also validated
with Apple for `com.openai.codex` (`status_code: 200`, `is_ok: true`,
team `2DC432GLL2`).

</details>

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Jiaming ZhangandCodex authored and GitHub committed 2026-05-08 12:36:02 -07:00
1 parent 61142b6169
commit 5f4d0ec343
65 files changed
+1086 -39

No files matched your search

+34 -5
View File
@@ -199,11 +199,16 @@ impl ThreadEntry {
#[derive(Default)]
struct ThreadStateManagerInner {
live_connections: HashSet<ConnectionId>,
live_connections: HashMap<ConnectionId, ConnectionCapabilities>,
threads: HashMap<ThreadId, ThreadEntry>,
thread_ids_by_connection: HashMap<ConnectionId, HashSet<ThreadId>>,
}
#[derive(Clone, Copy, Default)]
pub(crate) struct ConnectionCapabilities {
pub(crate) request_attestation: bool,
}
#[derive(Clone, Default)]
pub(crate) struct ThreadStateManager {
state: Arc<Mutex<ThreadStateManagerInner>>,
@@ -214,12 +219,36 @@ impl ThreadStateManager {
Self::default()
}
pub(crate) async fn connection_initialized(&self, connection_id: ConnectionId) {
pub(crate) async fn connection_initialized(
&self,
connection_id: ConnectionId,
capabilities: ConnectionCapabilities,
) {
self.state
.lock()
.await
.live_connections
.insert(connection_id);
.insert(connection_id, capabilities);
}
pub(crate) async fn first_attestation_capable_connection_for_thread(
&self,
thread_id: ThreadId,
) -> Option<ConnectionId> {
let state = self.state.lock().await;
state
.threads
.get(&thread_id)?
.connection_ids
.iter()
.filter_map(|connection_id| {
state
.live_connections
.get(connection_id)?
.request_attestation
.then_some(*connection_id)
})
.min_by_key(|connection_id| connection_id.0)
}
pub(crate) async fn subscribed_connection_ids(&self, thread_id: ThreadId) -> Vec<ConnectionId> {
@@ -338,7 +367,7 @@ impl ThreadStateManager {
) -> Option<Arc<Mutex<ThreadState>>> {
let thread_state = {
let mut state = self.state.lock().await;
if !state.live_connections.contains(&connection_id) {
if !state.live_connections.contains_key(&connection_id) {
return None;
}
state
@@ -366,7 +395,7 @@ impl ThreadStateManager {
connection_id: ConnectionId,
) -> bool {
let mut state = self.state.lock().await;
if !state.live_connections.contains(&connection_id) {
if !state.live_connections.contains_key(&connection_id) {
return false;
}
state