From 5cada24434589413d218331f08f3ffe245473f22 Mon Sep 17 00:00:00 2001 From: jif Date: Thu, 20 Aug 2026 18:22:03 +0000 Subject: [PATCH] Verify Codex app signatures before launch or install (#39776) ## What changed - Require macOS Desktop app bundles to pass strict `codesign` verification for the Codex bundle identifier and OpenAI signing team. - Verify downloaded apps before installation and existing apps before launch. - Add tests that reject unsigned bundles, ad-hoc signatures, and attempts to launch an unsigned existing app. GitOrigin-RevId: 9b23739281d8abf7fc30c5366960eb1bb07a1b7d --- codex-rs/cli/src/desktop_app/mac.rs | 104 ++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/codex-rs/cli/src/desktop_app/mac.rs b/codex-rs/cli/src/desktop_app/mac.rs index 0a36baa62f..12abe08aa8 100644 --- a/codex-rs/cli/src/desktop_app/mac.rs +++ b/codex-rs/cli/src/desktop_app/mac.rs @@ -6,6 +6,7 @@ use tempfile::Builder; use tokio::process::Command; const CODEX_BUNDLE_IDENTIFIER: &str = "com.openai.codex"; +const OPENAI_APPLE_TEAM_IDENTIFIER: &str = "2DC432GLL2"; const CODEX_DMG_URL_ARM64: &str = "https://persistent.oaistatic.com/codex-app-prod/Codex.dmg"; const CODEX_DMG_URL_X64: &str = "https://persistent.oaistatic.com/codex-app-prod/Codex-latest-x64.dmg"; @@ -99,6 +100,7 @@ fn is_codex_app_bundle(app_path: &Path) -> bool { } async fn open_codex_app(app_path: &Path, workspace: &Path) -> anyhow::Result<()> { + verify_codex_app_bundle(app_path).await?; eprintln!( "Opening workspace {workspace}...", workspace = workspace.display() @@ -123,6 +125,29 @@ async fn open_codex_app(app_path: &Path, workspace: &Path) -> anyhow::Result<()> ); } +async fn verify_codex_app_bundle(app_path: &Path) -> anyhow::Result<()> { + let requirement = format!( + "identifier \"{CODEX_BUNDLE_IDENTIFIER}\" and anchor apple generic and certificate leaf[subject.OU] = \"{OPENAI_APPLE_TEAM_IDENTIFIER}\"" + ); + let output = Command::new("/usr/bin/codesign") + .args(["--verify", "--deep", "--strict"]) + .arg(format!("-R={requirement}")) + .arg(app_path) + .output() + .await + .context("failed to verify Desktop app signature")?; + + if output.status.success() { + return Ok(()); + } + + anyhow::bail!( + "Desktop app at {} failed OpenAI signature verification (team {OPENAI_APPLE_TEAM_IDENTIFIER}, bundle {CODEX_BUNDLE_IDENTIFIER}): {}", + app_path.display(), + String::from_utf8_lossy(&output.stderr).trim() + ); +} + fn codex_new_thread_url(workspace: &Path) -> String { let workspace = workspace.as_os_str().to_string_lossy(); let mut serializer = url::form_urlencoded::Serializer::new(String::new()); @@ -151,6 +176,9 @@ async fn download_and_install_codex_to_user_applications(dmg_url: &str) -> anyho let result = async { let app_in_volume = find_codex_app_in_mount(&mount_point) .context("failed to locate Codex.app in mounted dmg")?; + verify_codex_app_bundle(&app_in_volume) + .await + .context("refusing to install an unverified Desktop app")?; install_codex_app_bundle(&app_in_volume).await } .await; @@ -325,7 +353,9 @@ fn parse_hdiutil_attach_mount_point(output: &str) -> Option { mod tests { use super::codex_new_thread_url; use super::find_existing_codex_app_path; + use super::open_codex_app; use super::parse_hdiutil_attach_mount_point; + use super::verify_codex_app_bundle; use pretty_assertions::assert_eq; use std::fs; use std::path::Path; @@ -367,6 +397,80 @@ mod tests { ); } + #[tokio::test] + async fn rejects_unsigned_app_with_codex_bundle_identifier() { + let temp_dir = tempfile::tempdir().expect("create temp dir"); + let app_path = temp_dir.path().join("Codex.app"); + write_app_bundle(&app_path, "com.openai.codex"); + + let err = verify_codex_app_bundle(&app_path) + .await + .expect_err("unsigned app should not satisfy the OpenAI signing requirement"); + + assert!( + err.to_string() + .contains("failed OpenAI signature verification"), + "unexpected verification error: {err}" + ); + } + + #[tokio::test] + async fn rejects_valid_signature_without_openai_signing_identity() { + let temp_dir = tempfile::tempdir().expect("create temp dir"); + let app_path = temp_dir.path().join("Codex.app"); + let contents_path = app_path.join("Contents"); + let executable_path = contents_path.join("MacOS/Codex"); + fs::create_dir_all(executable_path.parent().expect("executable parent")) + .expect("create executable directory"); + fs::copy("/usr/bin/true", &executable_path).expect("copy executable into app bundle"); + fs::write( + contents_path.join("Info.plist"), + r#"CFBundleIdentifiercom.openai.codexCFBundleExecutableCodex"#, + ) + .expect("write Info.plist"); + + let sign_status = std::process::Command::new("/usr/bin/codesign") + .args(["--force", "--sign", "-"]) + .arg(&app_path) + .status() + .expect("sign app bundle"); + assert!(sign_status.success(), "failed to ad-hoc sign app bundle"); + + let verify_status = std::process::Command::new("/usr/bin/codesign") + .args(["--verify", "--deep", "--strict"]) + .arg(&app_path) + .status() + .expect("verify app bundle signature"); + assert!(verify_status.success(), "ad-hoc signature should be valid"); + + let err = verify_codex_app_bundle(&app_path) + .await + .expect_err("valid signature without the OpenAI team should not be trusted"); + + assert!( + err.to_string() + .contains("failed OpenAI signature verification"), + "unexpected verification error: {err}" + ); + } + + #[tokio::test] + async fn refuses_to_launch_unsigned_existing_codex_app() { + let temp_dir = tempfile::tempdir().expect("create temp dir"); + let app_path = temp_dir.path().join("Codex.app"); + write_app_bundle(&app_path, "com.openai.codex"); + + let err = open_codex_app(&app_path, temp_dir.path()) + .await + .expect_err("unsigned existing app should not be launched"); + + assert!( + err.to_string() + .contains("failed OpenAI signature verification"), + "unexpected launch error: {err}" + ); + } + #[test] fn parses_mount_point_from_tab_separated_hdiutil_output() { let output = "/dev/disk2s1\tApple_HFS\tCodex\t/Volumes/Codex\n";