mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +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:
@@ -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
|
from pathlib import Path
|
||||||
|
|
||||||
BUNDLE_ID = "com.openai.codex.cli"
|
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")
|
APP = Path("CodexCLI.app")
|
||||||
EXECUTABLE = APP / "Contents/MacOS/codex"
|
EXECUTABLE = APP / "Contents/MacOS/codex"
|
||||||
SIGNING = Path(__file__).resolve().parent
|
SIGNING = Path(__file__).resolve().parent
|
||||||
@@ -156,6 +159,13 @@ def prepare(package, reports, configuration: ProfileConfiguration):
|
|||||||
def verify(package, reports, expected_target, configuration: ProfileConfiguration):
|
def verify(package, reports, expected_target, configuration: ProfileConfiguration):
|
||||||
"""Check profile and certificate pins; the signing driver verifies code."""
|
"""Check profile and certificate pins; the signing driver verifies code."""
|
||||||
load_profile(configuration)
|
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 (
|
if (
|
||||||
package / APP / "Contents/embedded.provisionprofile"
|
package / APP / "Contents/embedded.provisionprofile"
|
||||||
).read_bytes() != configuration.profile.read_bytes():
|
).read_bytes() != configuration.profile.read_bytes():
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ def main(configuration: bundle.ProfileConfiguration | None = None):
|
|||||||
if provisioned and relative == "bin/codex":
|
if provisioned and relative == "bin/codex":
|
||||||
binary = package / bundle.EXECUTABLE
|
binary = package / bundle.EXECUTABLE
|
||||||
target = package / bundle.APP
|
target = package / bundle.APP
|
||||||
identifier = bundle.BUNDLE_ID
|
identifier = bundle.CODE_SIGNING_ID
|
||||||
entitlements = reports / "codex-provisioned-entitlements.plist"
|
entitlements = reports / "codex-provisioned-entitlements.plist"
|
||||||
if args.operation == "sign":
|
if args.operation == "sign":
|
||||||
command = [
|
command = [
|
||||||
@@ -95,6 +95,18 @@ def main(configuration: bundle.ProfileConfiguration | None = None):
|
|||||||
["lipo", str(binary), "-verify_arch", architectures[target_triple]],
|
["lipo", str(binary), "-verify_arch", architectures[target_triple]],
|
||||||
check=True,
|
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(
|
subprocess.run(
|
||||||
[
|
[
|
||||||
"codesign",
|
"codesign",
|
||||||
@@ -102,12 +114,7 @@ def main(configuration: bundle.ProfileConfiguration | None = None):
|
|||||||
"--strict",
|
"--strict",
|
||||||
"--verbose=2",
|
"--verbose=2",
|
||||||
"--test-requirement",
|
"--test-requirement",
|
||||||
# Require Apple's Developer ID Application certificate chain.
|
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"
|
|
||||||
),
|
|
||||||
str(target),
|
str(target),
|
||||||
],
|
],
|
||||||
check=True,
|
check=True,
|
||||||
|
|||||||
@@ -227,8 +227,7 @@ sign_with_rcodesign() {
|
|||||||
rcodesign_args+=(--binary-identifier "$identifier")
|
rcodesign_args+=(--binary-identifier "$identifier")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rcodesign_args+=("$target")
|
rcodesign "${rcodesign_args[@]}" "$target"
|
||||||
rcodesign "${rcodesign_args[@]}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "${OAI_CODESIGN_BACKEND:-codesign}" in
|
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 itertools
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import plistlib
|
||||||
import shutil
|
import shutil
|
||||||
import ssl
|
import ssl
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -172,6 +173,9 @@ class MacosSigningTests(ProvisioningTestCase):
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
signing[0][signing[0].index("--identifier") + 1], "codex"
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
signing[0][signing[0].index("--entitlements") + 1],
|
signing[0][signing[0].index("--entitlements") + 1],
|
||||||
str(
|
str(
|
||||||
@@ -208,7 +212,49 @@ class MacosSigningTests(ProvisioningTestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
if provisioned:
|
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:
|
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(
|
self.assertEqual(
|
||||||
archive.read(
|
archive.read(
|
||||||
"CodexCLI.app/Contents/embedded.provisionprofile"
|
"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):
|
def test_signing_and_notarization_failures_stop_the_driver(self):
|
||||||
for tool in ("sign_macos_code.sh", "notarize_with_akv.py"):
|
for tool in ("sign_macos_code.sh", "notarize_with_akv.py"):
|
||||||
with self.subTest(tool=tool), tempfile.TemporaryDirectory() as directory:
|
with self.subTest(tool=tool), tempfile.TemporaryDirectory() as directory:
|
||||||
|
|||||||
Reference in New Issue
Block a user