mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
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
This commit is contained in:
40
.github/scripts/macos-signing/README.md
vendored
Normal file
40
.github/scripts/macos-signing/README.md
vendored
Normal file
@@ -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 | `<TeamID>.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.
|
||||
@@ -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():
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user