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:
riley-oai
2026-09-18 02:53:21 +00:00
committed by copyberry
parent 6d29cc6bac
commit c0e1b78253
5 changed files with 134 additions and 10 deletions

40
.github/scripts/macos-signing/README.md vendored Normal file
View 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.

View File

@@ -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():

View File

@@ -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,

View File

@@ -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

View File

@@ -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: