[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 Zhang
2026-05-08 12:36:02 -07:00
committed by GitHub
Unverified
parent 61142b6169
commit 5f4d0ec343
65 changed files with 1086 additions and 39 deletions
@@ -1312,6 +1312,12 @@ server_request_definitions! {
response: v2::ChatgptAuthTokensRefreshResponse,
},
/// Generate a fresh upstream attestation result on demand.
AttestationGenerate => "attestation/generate" {
params: v2::AttestationGenerateParams,
response: v2::AttestationGenerateResponse,
},
/// DEPRECATED APIs below
/// Request to approve a patch.
/// This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).
@@ -1910,6 +1916,7 @@ mod tests {
},
capabilities: Some(v1::InitializeCapabilities {
experimental_api: true,
request_attestation: true,
opt_out_notification_methods: Some(vec![
"thread/started".to_string(),
"item/agentMessage/delta".to_string(),
@@ -1930,6 +1937,7 @@ mod tests {
},
"capabilities": {
"experimentalApi": true,
"requestAttestation": true,
"optOutNotificationMethods": [
"thread/started",
"item/agentMessage/delta"
@@ -1955,6 +1963,7 @@ mod tests {
},
"capabilities": {
"experimentalApi": true,
"requestAttestation": true,
"optOutNotificationMethods": [
"thread/started",
"item/agentMessage/delta"
@@ -1975,6 +1984,7 @@ mod tests {
},
capabilities: Some(v1::InitializeCapabilities {
experimental_api: true,
request_attestation: true,
opt_out_notification_methods: Some(vec![
"thread/started".to_string(),
"item/agentMessage/delta".to_string(),
@@ -2091,6 +2101,28 @@ mod tests {
Ok(())
}
#[test]
fn serialize_attestation_generate_request() -> Result<()> {
let params = v2::AttestationGenerateParams {};
let request = ServerRequest::AttestationGenerate {
request_id: RequestId::Integer(9),
params: params.clone(),
};
assert_eq!(
json!({
"method": "attestation/generate",
"id": 9,
"params": {}
}),
serde_json::to_value(&request)?,
);
let payload = ServerRequestPayload::AttestationGenerate(params);
assert_eq!(request.id(), &RequestId::Integer(9));
assert_eq!(payload.request_with_id(RequestId::Integer(9)), request);
Ok(())
}
#[test]
fn serialize_server_response() -> Result<()> {
let response = ServerResponse::CommandExecutionRequestApproval {
@@ -46,6 +46,9 @@ pub struct InitializeCapabilities {
/// Opt into receiving experimental API methods and fields.
#[serde(default)]
pub experimental_api: bool,
/// Opt into `attestation/generate` requests for upstream `x-oai-attestation`.
#[serde(default)]
pub request_attestation: bool,
/// Exact notification method names that should be suppressed for this
/// connection (for example `thread/started`).
#[ts(optional = nullable)]
@@ -0,0 +1,17 @@
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Default)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AttestationGenerateParams {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AttestationGenerateResponse {
/// Opaque client attestation token.
pub token: String,
}
@@ -2,6 +2,7 @@ mod shared;
mod account;
mod apps;
mod attestation;
mod collaboration_mode;
mod command_exec;
mod config;
@@ -26,6 +27,7 @@ mod windows_sandbox;
pub use account::*;
pub use apps::*;
pub use attestation::*;
pub use collaboration_mode::*;
pub use command_exec::*;
pub use config::*;