mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
## 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
185 lines
7.1 KiB
Python
185 lines
7.1 KiB
Python
"""Sign and verify macOS CLI packages using the shared AKV and native tools."""
|
|
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import provisioned_macos_cli_package as bundle
|
|
|
|
|
|
def main(configuration: bundle.ProfileConfiguration | None = None):
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("operation", choices=("sign", "verify"))
|
|
parser.add_argument("package", type=Path)
|
|
args, configuration = bundle.parse_profile_args(parser, configuration)
|
|
package = args.package.resolve(strict=True)
|
|
if not package.is_dir():
|
|
parser.error("package must be an extracted CLI package directory")
|
|
repo_root = os.environ.get("CODEX_REPO_ROOT")
|
|
if not repo_root:
|
|
parser.error("CODEX_REPO_ROOT must be set")
|
|
reports = Path(repo_root) / "signing-verification"
|
|
reports.mkdir(parents=True, exist_ok=True)
|
|
provisioned = os.environ.get("PROVISIONED_MACOS", "false") == "true"
|
|
if provisioned:
|
|
if args.operation == "sign":
|
|
configuration.validate_signing_certificate(
|
|
Path(os.environ["OAI_AKV_SIGNING_CERTIFICATE_PEM"])
|
|
)
|
|
bundle.prepare(package, reports, configuration)
|
|
else:
|
|
bundle.verify(package, reports, os.environ["TARGET"], configuration)
|
|
|
|
for relative in ("bin/codex", *bundle.HELPERS):
|
|
binary = package / relative
|
|
name = binary.name
|
|
target = binary
|
|
identifier = name
|
|
entitlements = bundle.SIGNING / f"{name}.entitlements.plist"
|
|
if provisioned and relative == "bin/codex":
|
|
binary = package / bundle.EXECUTABLE
|
|
target = package / bundle.APP
|
|
identifier = bundle.CODE_SIGNING_ID
|
|
entitlements = reports / "codex-provisioned-entitlements.plist"
|
|
if args.operation == "sign":
|
|
command = [
|
|
"bash",
|
|
str(bundle.SIGNING / "sign_macos_code.sh"),
|
|
"--target",
|
|
str(target),
|
|
"--identity",
|
|
"unused",
|
|
"--deep",
|
|
"false",
|
|
"--options",
|
|
"runtime",
|
|
"--timestamp",
|
|
"true",
|
|
]
|
|
if relative.startswith("bin/"):
|
|
command.extend(
|
|
["--identifier", identifier, "--entitlements", str(entitlements)]
|
|
)
|
|
else:
|
|
command.extend(["--identifier", f"com.openai.codex.{name}"])
|
|
subprocess.run(command, check=True)
|
|
with (reports / f"{name}-signature.yaml").open("wb") as output:
|
|
subprocess.run(
|
|
["rcodesign", "print-signature-info", str(binary)],
|
|
stdout=output,
|
|
check=True,
|
|
)
|
|
if not provisioned:
|
|
subprocess.run(
|
|
[
|
|
"bash",
|
|
str(bundle.SIGNING / "notarize_macos_binary_with_akv.sh"),
|
|
"--binary",
|
|
str(binary),
|
|
"--report-dir",
|
|
str(reports / name),
|
|
],
|
|
check=True,
|
|
)
|
|
else:
|
|
target_triple = os.environ["TARGET"]
|
|
architectures = {
|
|
"aarch64-apple-darwin": "arm64",
|
|
"x86_64-apple-darwin": "x86_64",
|
|
}
|
|
if target_triple not in architectures:
|
|
raise ValueError(f"Unexpected macOS target: {target_triple}")
|
|
subprocess.run(
|
|
["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",
|
|
"--verify",
|
|
"--strict",
|
|
"--verbose=2",
|
|
"--test-requirement",
|
|
requirement,
|
|
str(target),
|
|
],
|
|
check=True,
|
|
)
|
|
signature = reports / f"{name}-signature.txt"
|
|
with signature.open("wb") as output:
|
|
subprocess.run(
|
|
["codesign", "-d", "--verbose=4", str(target)],
|
|
stderr=output,
|
|
check=True,
|
|
)
|
|
actual = reports / f"{name}-entitlements.plist"
|
|
with actual.open("wb") as output:
|
|
subprocess.run(
|
|
["codesign", "-d", "--entitlements", ":-", str(target)],
|
|
stdout=output,
|
|
check=True,
|
|
)
|
|
if relative.startswith("bin/"):
|
|
expected = reports / f"{name}-expected.plist"
|
|
subprocess.run(
|
|
[
|
|
"plutil",
|
|
"-convert",
|
|
"xml1",
|
|
"-o",
|
|
str(expected),
|
|
str(entitlements),
|
|
],
|
|
check=True,
|
|
)
|
|
subprocess.run(["plutil", "-convert", "xml1", str(actual)], check=True)
|
|
subprocess.run(["diff", "-u", str(expected), str(actual)], check=True)
|
|
elif actual.stat().st_size:
|
|
raise ValueError(
|
|
f"Bundled helper {name} must not have signing entitlements"
|
|
)
|
|
|
|
if provisioned and args.operation == "sign":
|
|
archive = Path(os.environ["RUNNER_TEMP"]) / "provisioned-cli.zip"
|
|
subprocess.run(["zip", "-q", "-r", str(archive), "."], cwd=package, check=True)
|
|
command = [
|
|
sys.executable,
|
|
str(bundle.SIGNING / "notarize_with_akv.py"),
|
|
"--file",
|
|
str(archive),
|
|
"--report-log",
|
|
str(reports / "notarization-developer-log.json"),
|
|
"--max-wait-seconds",
|
|
"1200",
|
|
]
|
|
with (
|
|
(reports / "notarization.log").open("w") as output,
|
|
subprocess.Popen(
|
|
command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
|
) as process,
|
|
):
|
|
assert process.stdout is not None
|
|
for line in process.stdout:
|
|
print(line, end="", flush=True)
|
|
output.write(line)
|
|
if returncode := process.wait():
|
|
raise subprocess.CalledProcessError(returncode, command)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|