From c0e1b782532d8a7ce902d043315fcbd7f94de4b8 Mon Sep 17 00:00:00 2001 From: riley-oai Date: Fri, 18 Sep 2026 02:53:21 +0000 Subject: [PATCH] Preserve the provisioned macOS CLI's code-signing identity (#46495) ## Why Existing login-keychain access rules identify the CLI as `codex`. Packaging it in an app bundle must preserve that code-signing identifier independently of the bundle identifier and provisioned App ID. ## What changed - Sign the provisioned CLI with the identifier `codex`, retaining `com.openai.codex.cli` as its bundle identifier. - Require the expected signing identifier and team during signature verification, and reject unexpected bundle identifiers, executable names, or package types. - Document the identity distinction and keychain compatibility limits. ## Testing Extend signing-driver tests to check the signing identifier, verification requirement, bundle metadata, and provisioned entitlements, and to reject altered bundle identity fields. These tests use generated credentials and stubbed native tools; they do not verify runtime keychain access or credential recovery. GitOrigin-RevId: ab00072e48189551adb0e70008210fee6d36241d --- .github/scripts/macos-signing/README.md | 40 +++++++++++ .../provisioned_macos_cli_package.py | 10 +++ .../macos-signing/sign_macos_cli_package.py | 21 ++++-- .../scripts/macos-signing/sign_macos_code.sh | 3 +- .../test_sign_macos_cli_package.py | 70 ++++++++++++++++++- 5 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 .github/scripts/macos-signing/README.md diff --git a/.github/scripts/macos-signing/README.md b/.github/scripts/macos-signing/README.md new file mode 100644 index 0000000000..b7ce5914b0 --- /dev/null +++ b/.github/scripts/macos-signing/README.md @@ -0,0 +1,40 @@ +# Provisioned CLI signing identity + +The provisioned CLI has two deliberately separate identifiers: + +| Purpose | Identifier | +| ------------------------------------- | ------------------------------- | +| Code-signing identifier | `codex` | +| Bundle identifier | `com.openai.codex.cli` | +| Provisioned App ID and keychain group | `.com.openai.codex.cli` | + +The code-signing identifier preserves access to existing login-keychain items +whose access rules require the original CLI identity. Giving the CLI an app +bundle must not change that identifier. The bundle identifier, App ID, +entitlements, profile, and approved certificate remain independently validated. +The native-verification keys use the provisioned keychain group; they do not +replace existing login-keychain credential access rules. + +Changing the new binary's designated requirement to accept both identifiers +would not make it satisfy an existing item's requirement for `codex`. Items +created by a binary signed as `com.openai.codex.cli` are a separate compatibility +case: restoring `codex` does not silently authorize access to those items. + +The signing backend must honor an explicit `--binary-identifier` when signing +an app bundle. The wrapper signs the bundle once; rcodesign seals its metadata +and resources while retaining `codex` as the main executable's identity. +Release tool artifact URIs and SHA-256 digests are pinned in the signing +environment configuration. + +## Release verification + +When `CODEX_PROVISIONED_MACOS_CANDIDATE` is enabled, tag releases automatically +publish the provisioned archives and versioned `codex-provisioned` manifest after +both architecture packages pass the existing signature, entitlement, profile, +architecture, stapling, Gatekeeper, and package-smoke checks. An enabled +provisioned build that fails verification blocks release publication. + +The signing-driver tests use generated credentials and stubbed native tools. +They cover the signing command contract and failure handling. Actual keychain +access and credential recovery across identity changes require runtime testing; +these unit tests do not establish that behavior. diff --git a/.github/scripts/macos-signing/provisioned_macos_cli_package.py b/.github/scripts/macos-signing/provisioned_macos_cli_package.py index 449dd15499..b85fee19fd 100644 --- a/.github/scripts/macos-signing/provisioned_macos_cli_package.py +++ b/.github/scripts/macos-signing/provisioned_macos_cli_package.py @@ -13,6 +13,9 @@ from datetime import datetime, timezone from pathlib import Path BUNDLE_ID = "com.openai.codex.cli" +# Existing login-keychain ACLs identify the CLI by its code-signing identifier. +# Keep this stable independently of the bundle and provisioned App ID. +CODE_SIGNING_ID = "codex" APP = Path("CodexCLI.app") EXECUTABLE = APP / "Contents/MacOS/codex" SIGNING = Path(__file__).resolve().parent @@ -156,6 +159,13 @@ def prepare(package, reports, configuration: ProfileConfiguration): def verify(package, reports, expected_target, configuration: ProfileConfiguration): """Check profile and certificate pins; the signing driver verifies code.""" load_profile(configuration) + info = plistlib.loads((package / APP / "Contents/Info.plist").read_bytes()) + if ( + info.get("CFBundleIdentifier") != BUNDLE_ID + or info.get("CFBundleExecutable") != EXECUTABLE.name + or info.get("CFBundlePackageType") != "APPL" + ): + raise ValueError("Unexpected provisioned CLI bundle identity or executable") if ( package / APP / "Contents/embedded.provisionprofile" ).read_bytes() != configuration.profile.read_bytes(): diff --git a/.github/scripts/macos-signing/sign_macos_cli_package.py b/.github/scripts/macos-signing/sign_macos_cli_package.py index 6d555945a7..ac214160a1 100644 --- a/.github/scripts/macos-signing/sign_macos_cli_package.py +++ b/.github/scripts/macos-signing/sign_macos_cli_package.py @@ -41,7 +41,7 @@ def main(configuration: bundle.ProfileConfiguration | None = None): if provisioned and relative == "bin/codex": binary = package / bundle.EXECUTABLE target = package / bundle.APP - identifier = bundle.BUNDLE_ID + identifier = bundle.CODE_SIGNING_ID entitlements = reports / "codex-provisioned-entitlements.plist" if args.operation == "sign": command = [ @@ -95,6 +95,18 @@ def main(configuration: bundle.ProfileConfiguration | None = None): ["lipo", str(binary), "-verify_arch", architectures[target_triple]], check=True, ) + # Preserve the CLI's existing login-keychain identity while checking + # its bundle/App ID and provisioning independently in bundle.verify. + requirement = ( + "=anchor apple generic" + " and certificate 1[field.1.2.840.113635.100.6.2.6] exists" + " and certificate leaf[field.1.2.840.113635.100.6.1.13] exists" + ) + if provisioned and relative == "bin/codex": + requirement += ( + f' and identifier "{bundle.CODE_SIGNING_ID}"' + f' and certificate leaf[subject.OU] = "{configuration.team_id}"' + ) subprocess.run( [ "codesign", @@ -102,12 +114,7 @@ def main(configuration: bundle.ProfileConfiguration | None = None): "--strict", "--verbose=2", "--test-requirement", - # Require Apple's Developer ID Application certificate chain. - ( - "=anchor apple generic" - " and certificate 1[field.1.2.840.113635.100.6.2.6] exists" - " and certificate leaf[field.1.2.840.113635.100.6.1.13] exists" - ), + requirement, str(target), ], check=True, diff --git a/.github/scripts/macos-signing/sign_macos_code.sh b/.github/scripts/macos-signing/sign_macos_code.sh index 9f86741410..1f386afeb1 100755 --- a/.github/scripts/macos-signing/sign_macos_code.sh +++ b/.github/scripts/macos-signing/sign_macos_code.sh @@ -227,8 +227,7 @@ sign_with_rcodesign() { rcodesign_args+=(--binary-identifier "$identifier") fi - rcodesign_args+=("$target") - rcodesign "${rcodesign_args[@]}" + rcodesign "${rcodesign_args[@]}" "$target" } case "${OAI_CODESIGN_BACKEND:-codesign}" in diff --git a/.github/scripts/macos-signing/test_sign_macos_cli_package.py b/.github/scripts/macos-signing/test_sign_macos_cli_package.py index 38efc12098..347458a006 100644 --- a/.github/scripts/macos-signing/test_sign_macos_cli_package.py +++ b/.github/scripts/macos-signing/test_sign_macos_cli_package.py @@ -1,8 +1,9 @@ -"""Exercise package signing and native verification with generated credentials.""" +"""Exercise signing orchestration with generated credentials and stubbed native tools.""" import itertools import json import os +import plistlib import shutil import ssl import subprocess @@ -172,6 +173,9 @@ class MacosSigningTests(ProvisioningTestCase): ) ], ) + self.assertEqual( + signing[0][signing[0].index("--identifier") + 1], "codex" + ) self.assertEqual( signing[0][signing[0].index("--entitlements") + 1], str( @@ -208,7 +212,49 @@ class MacosSigningTests(ProvisioningTestCase): ], ) if provisioned: + verification = [ + call + for call in calls + if call[0] == "codesign" and "--test-requirement" in call + ] + cli_verification = verification[0] + self.assertEqual(cli_verification[-1], str(package / bundle.APP)) + requirement = cli_verification[ + cli_verification.index("--test-requirement") + 1 + ] + self.assertIn(' and identifier "codex"', requirement) + self.assertIn( + ' and certificate leaf[subject.OU] = "TESTTEAM01"', requirement + ) with zipfile.ZipFile(root / "provisioned-cli.zip") as archive: + info = plistlib.loads( + archive.read("CodexCLI.app/Contents/Info.plist") + ) + self.assertEqual( + info["CFBundleIdentifier"], "com.openai.codex.cli" + ) + self.assertEqual(info["CFBundleExecutable"], "codex") + self.assertEqual( + plistlib.loads( + ( + root + / "signing-verification/codex-provisioned-entitlements.plist" + ).read_bytes() + ), + { + **plistlib.loads( + ( + Path(env["MOCK_SIGNING_SCRIPTS"]) + / "codex.entitlements.plist" + ).read_bytes() + ), + "com.apple.application-identifier": "TESTTEAM01.com.openai.codex.cli", + "com.apple.developer.team-identifier": "TESTTEAM01", + "keychain-access-groups": [ + "TESTTEAM01.com.openai.codex.cli" + ], + }, + ) self.assertEqual( archive.read( "CodexCLI.app/Contents/embedded.provisionprofile" @@ -225,6 +271,28 @@ class MacosSigningTests(ProvisioningTestCase): ) ) + def test_provisioned_verification_rejects_changed_bundle_identity(self): + for field, value in ( + ("CFBundleIdentifier", "codex"), + ("CFBundleExecutable", "another-codex"), + ("CFBundlePackageType", "BNDL"), + ): + with self.subTest(field=field), tempfile.TemporaryDirectory() as directory: + root = Path(directory).resolve() + package, driver, env = self.fixture(root, True) + signed = self.run_driver(driver, "sign", package, env) + self.assertEqual(signed.returncode, 0, signed.stderr) + info_path = package / bundle.APP / "Contents/Info.plist" + info = plistlib.loads(info_path.read_bytes()) + info[field] = value + info_path.write_bytes(plistlib.dumps(info)) + result = self.run_driver(driver, "verify", package, env) + self.assertNotEqual(result.returncode, 0) + self.assertIn( + "Unexpected provisioned CLI bundle identity or executable", + result.stderr, + ) + def test_signing_and_notarization_failures_stop_the_driver(self): for tool in ("sign_macos_code.sh", "notarize_with_akv.py"): with self.subTest(tool=tool), tempfile.TemporaryDirectory() as directory: