mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
## What changed - Add a tag-release workflow gated by `CODEX_PROVISIONED_MACOS_CANDIDATE` for Apple Silicon and Intel macOS candidate artifacts. - Package the CLI in `CodexCLI.app` with an embedded provisioning profile and a relocatable `bin/codex` launcher. Validate independently supplied profile, certificate, and team expectations before signing. - Sign and notarize candidates, then verify signatures, entitlements, architecture, stapling, and Gatekeeper acceptance on macOS. Retain verified artifacts after package smoke tests pass. - Filter code-mode smoke-test requests to `/v1/responses` so analytics requests are excluded from response parsing. ## Testing Add tests for profile validation, launcher relocation and symlink handling, standard and provisioned signing flows, and rejection of signing, notarization, identity, and entitlement failures. The candidate workflow runs package smoke tests, including sandboxed code mode. GitOrigin-RevId: f214f6a23dd10df62cd72a2c63c20cb0864fbc9c
178 lines
6.7 KiB
Python
178 lines
6.7 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.BUNDLE_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,
|
|
)
|
|
subprocess.run(
|
|
[
|
|
"codesign",
|
|
"--verify",
|
|
"--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"
|
|
),
|
|
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()
|