mirror of
https://github.com/openai/codex.git
synced 2026-09-10 20:26:47 +00:00
Add opt-in provisioned macOS CLI release candidates (#44307)
## 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
This commit is contained in:
229
.github/scripts/macos-signing/provisioned_macos_cli_package.py
vendored
Normal file
229
.github/scripts/macos-signing/provisioned_macos_cli_package.py
vendored
Normal file
@@ -0,0 +1,229 @@
|
||||
"""Prepare and verify provisioned CLI bundles using an explicitly approved profile."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import plistlib
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
BUNDLE_ID = "com.openai.codex.cli"
|
||||
APP = Path("CodexCLI.app")
|
||||
EXECUTABLE = APP / "Contents/MacOS/codex"
|
||||
SIGNING = Path(__file__).resolve().parent
|
||||
HELPERS = ("bin/codex-code-mode-host", "codex-path/rg", "codex-resources/zsh/bin/zsh")
|
||||
LAUNCHER = """#!/bin/sh
|
||||
set -eu
|
||||
entry="$0"
|
||||
while [ -L "$entry" ]; do
|
||||
parent=$(CDPATH= cd -P -- "$(dirname -- "$entry")" && pwd)
|
||||
entry=$(readlink "$entry")
|
||||
case "$entry" in /*) ;; *) entry="$parent/$entry" ;; esac
|
||||
done
|
||||
bin_dir=$(CDPATH= cd -P -- "$(dirname -- "$entry")" && pwd)
|
||||
exec "$bin_dir/../CodexCLI.app/Contents/MacOS/codex" "$@"
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProfileConfiguration:
|
||||
"""Independent release expectations; never infer these from the signed package."""
|
||||
|
||||
profile: Path
|
||||
profile_sha256: str
|
||||
certificate_sha256: str
|
||||
team_id: str
|
||||
|
||||
def __post_init__(self):
|
||||
for name in ("profile_sha256", "certificate_sha256"):
|
||||
if not re.fullmatch(r"[0-9a-f]{64}", getattr(self, name)):
|
||||
raise ValueError(f"{name} must be a lowercase SHA-256 digest")
|
||||
if not re.fullmatch(r"[A-Z0-9]{10}", self.team_id):
|
||||
raise ValueError("team_id must be a ten-character Apple Team ID")
|
||||
|
||||
def validate_signing_certificate(self, certificate: Path):
|
||||
certificate_der = subprocess.check_output(
|
||||
["openssl", "x509", "-in", str(certificate), "-outform", "DER"]
|
||||
)
|
||||
if hashlib.sha256(certificate_der).hexdigest() != self.certificate_sha256:
|
||||
raise ValueError("AKV signing certificate is not authorized by the profile")
|
||||
|
||||
|
||||
def load_profile(configuration: ProfileConfiguration):
|
||||
# Pin the portal-approved CMS as well as checking its signature. -noverify
|
||||
# skips OpenSSL's non-Apple trust store, not CMS signature verification.
|
||||
if (
|
||||
hashlib.sha256(configuration.profile.read_bytes()).hexdigest()
|
||||
!= configuration.profile_sha256
|
||||
):
|
||||
raise ValueError("Provisioning profile changed; review and update its pin")
|
||||
profile = plistlib.loads(
|
||||
subprocess.check_output(
|
||||
[
|
||||
"openssl",
|
||||
"cms",
|
||||
"-verify",
|
||||
"-inform",
|
||||
"DER",
|
||||
"-noverify",
|
||||
"-in",
|
||||
str(configuration.profile),
|
||||
]
|
||||
)
|
||||
)
|
||||
validate_profile(profile, configuration)
|
||||
return profile
|
||||
|
||||
|
||||
def validate_profile(profile, configuration: ProfileConfiguration):
|
||||
allowed = profile["Entitlements"]
|
||||
if (
|
||||
profile["TeamIdentifier"] != [configuration.team_id]
|
||||
or profile["ApplicationIdentifierPrefix"] != [configuration.team_id]
|
||||
or allowed["com.apple.application-identifier"]
|
||||
!= f"{configuration.team_id}.{BUNDLE_ID}"
|
||||
or allowed["com.apple.developer.team-identifier"] != configuration.team_id
|
||||
or allowed["keychain-access-groups"] != [f"{configuration.team_id}.*"]
|
||||
or allowed.get("get-task-allow", False)
|
||||
or not profile.get("ProvisionsAllDevices")
|
||||
or "ProvisionedDevices" in profile
|
||||
):
|
||||
raise ValueError("Profile does not authorize the CLI Developer ID identity")
|
||||
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
if not profile["CreationDate"] <= now < profile["ExpirationDate"]:
|
||||
raise ValueError("Provisioning profile is not currently valid")
|
||||
if [
|
||||
hashlib.sha256(cert).hexdigest() for cert in profile["DeveloperCertificates"]
|
||||
] != [configuration.certificate_sha256]:
|
||||
raise ValueError("Profile does not authorize the approved signing certificate")
|
||||
|
||||
|
||||
def entitlements(configuration: ProfileConfiguration):
|
||||
base = plistlib.loads((SIGNING / "codex.entitlements.plist").read_bytes())
|
||||
return {
|
||||
**base,
|
||||
"com.apple.application-identifier": f"{configuration.team_id}.{BUNDLE_ID}",
|
||||
"com.apple.developer.team-identifier": configuration.team_id,
|
||||
"keychain-access-groups": [f"{configuration.team_id}.{BUNDLE_ID}"],
|
||||
}
|
||||
|
||||
|
||||
def prepare(package, reports, configuration: ProfileConfiguration):
|
||||
load_profile(configuration)
|
||||
metadata = json.loads((package / "codex-package.json").read_text())
|
||||
if (
|
||||
metadata["variant"] != "codex"
|
||||
or metadata["layoutVersion"] != 1
|
||||
or metadata["target"] not in ("aarch64-apple-darwin", "x86_64-apple-darwin")
|
||||
or metadata["entrypoint"] != "bin/codex"
|
||||
):
|
||||
raise ValueError("Expected a canonical macOS CLI package")
|
||||
for relative in ("bin/codex", *HELPERS):
|
||||
path = package / relative
|
||||
if path.is_symlink() or not path.is_file() or not os.access(path, os.X_OK):
|
||||
raise ValueError(f"Expected a regular executable: {relative}")
|
||||
# Refuse to overwrite a previously prepared or signed bundle.
|
||||
(package / EXECUTABLE).parent.mkdir(parents=True, exist_ok=False)
|
||||
(package / "bin/codex").rename(package / EXECUTABLE)
|
||||
(package / "bin/codex").write_text(LAUNCHER)
|
||||
(package / "bin/codex").chmod(0o755)
|
||||
contents = package / APP / "Contents"
|
||||
shutil.copyfile(configuration.profile, contents / "embedded.provisionprofile")
|
||||
(contents / "Info.plist").write_bytes(
|
||||
plistlib.dumps(
|
||||
{
|
||||
"CFBundleIdentifier": BUNDLE_ID,
|
||||
"CFBundleExecutable": "codex",
|
||||
"CFBundleName": "Codex CLI",
|
||||
"CFBundlePackageType": "APPL",
|
||||
"CFBundleVersion": "1",
|
||||
}
|
||||
)
|
||||
)
|
||||
reports.mkdir(parents=True, exist_ok=True)
|
||||
# Keep expected entitlements separate from the shared verifier's extracted
|
||||
# codex-entitlements.plist, otherwise it would overwrite its own expectation.
|
||||
(reports / "codex-provisioned-entitlements.plist").write_bytes(
|
||||
plistlib.dumps(entitlements(configuration))
|
||||
)
|
||||
|
||||
|
||||
def verify(package, reports, expected_target, configuration: ProfileConfiguration):
|
||||
"""Check profile and certificate pins; the signing driver verifies code."""
|
||||
load_profile(configuration)
|
||||
if (
|
||||
package / APP / "Contents/embedded.provisionprofile"
|
||||
).read_bytes() != configuration.profile.read_bytes():
|
||||
raise ValueError("Embedded profile differs from the reviewed profile")
|
||||
if (package / "bin/codex").read_text() != LAUNCHER:
|
||||
raise ValueError("Unexpected CLI launcher")
|
||||
metadata = json.loads((package / "codex-package.json").read_text())
|
||||
if metadata["target"] != expected_target:
|
||||
raise ValueError(
|
||||
"Package target differs from the requested verification target"
|
||||
)
|
||||
reports.mkdir(parents=True, exist_ok=True)
|
||||
(reports / "codex-provisioned-entitlements.plist").write_bytes(
|
||||
plistlib.dumps(entitlements(configuration))
|
||||
)
|
||||
# sign_macos_cli_package.py owns shared architecture, signature and entitlement
|
||||
# checks. Here, require the exact certificate authorized by the profile.
|
||||
for relative in (EXECUTABLE, *(Path(helper) for helper in HELPERS)):
|
||||
binary = package / relative
|
||||
target = package / APP if relative == EXECUTABLE else binary
|
||||
prefix = reports / f"{binary.name}-cert-"
|
||||
subprocess.run(
|
||||
["codesign", "-d", f"--extract-certificates={prefix}", str(target)],
|
||||
check=True,
|
||||
)
|
||||
if (
|
||||
hashlib.sha256(Path(f"{prefix}0").read_bytes()).hexdigest()
|
||||
!= configuration.certificate_sha256
|
||||
):
|
||||
raise ValueError(f"Unexpected signing certificate: {relative}")
|
||||
|
||||
|
||||
def parse_profile_args(parser, configuration: ProfileConfiguration | None):
|
||||
"""Require independent profile expectations unless the caller supplies them."""
|
||||
if configuration is None:
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--profile-sha256", required=True)
|
||||
parser.add_argument("--certificate-sha256", required=True)
|
||||
parser.add_argument("--team-id", required=True)
|
||||
args = parser.parse_args()
|
||||
if configuration is None:
|
||||
configuration = ProfileConfiguration(
|
||||
args.profile, args.profile_sha256, args.certificate_sha256, args.team_id
|
||||
)
|
||||
return args, configuration
|
||||
|
||||
|
||||
def main(configuration: ProfileConfiguration | None = None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("operation", choices=("prepare", "verify", "validate-profile"))
|
||||
parser.add_argument("--package", type=Path, default=Path("package"))
|
||||
parser.add_argument("--reports", type=Path, default=Path("provisioned-reports"))
|
||||
parser.add_argument("--certificate", type=Path)
|
||||
parser.add_argument(
|
||||
"--target", choices=("aarch64-apple-darwin", "x86_64-apple-darwin")
|
||||
)
|
||||
args, configuration = parse_profile_args(parser, configuration)
|
||||
if args.certificate:
|
||||
configuration.validate_signing_certificate(args.certificate)
|
||||
if args.operation == "validate-profile":
|
||||
load_profile(configuration)
|
||||
elif args.operation == "prepare":
|
||||
prepare(args.package, args.reports, configuration)
|
||||
else:
|
||||
if not args.target:
|
||||
parser.error("--target is required for verification")
|
||||
verify(args.package, args.reports, args.target, configuration)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
177
.github/scripts/macos-signing/sign_macos_cli_package.py
vendored
Normal file
177
.github/scripts/macos-signing/sign_macos_cli_package.py
vendored
Normal file
@@ -0,0 +1,177 @@
|
||||
"""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()
|
||||
217
.github/scripts/macos-signing/test_provisioned_macos_cli_package.py
vendored
Normal file
217
.github/scripts/macos-signing/test_provisioned_macos_cli_package.py
vendored
Normal file
@@ -0,0 +1,217 @@
|
||||
"""Exercise profile validation, relocation and signing without release credentials."""
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import plistlib
|
||||
import shutil
|
||||
import ssl
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
from dataclasses import replace
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import provisioned_macos_cli_package as bundle
|
||||
|
||||
|
||||
class ProvisioningTestCase(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
directory = tempfile.TemporaryDirectory()
|
||||
cls.addClassCleanup(directory.cleanup)
|
||||
root = Path(directory.name)
|
||||
key = root / "test-key.pem"
|
||||
certificate = root / "test-certificate.pem"
|
||||
subprocess.run(
|
||||
[
|
||||
"openssl",
|
||||
"req",
|
||||
"-x509",
|
||||
"-newkey",
|
||||
"rsa:2048",
|
||||
"-nodes",
|
||||
"-keyout",
|
||||
str(key),
|
||||
"-out",
|
||||
str(certificate),
|
||||
"-days",
|
||||
"1",
|
||||
"-subj",
|
||||
"/CN=CLI provisioning test/OU=TESTTEAM01",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
cert_der = ssl.PEM_cert_to_DER_cert(certificate.read_text())
|
||||
now = datetime.now(timezone.utc).replace(tzinfo=None, microsecond=0)
|
||||
cls.profile = {
|
||||
"TeamIdentifier": ["TESTTEAM01"],
|
||||
"ApplicationIdentifierPrefix": ["TESTTEAM01"],
|
||||
"Entitlements": {
|
||||
"com.apple.application-identifier": "TESTTEAM01.com.openai.codex.cli",
|
||||
"com.apple.developer.team-identifier": "TESTTEAM01",
|
||||
"keychain-access-groups": ["TESTTEAM01.*"],
|
||||
},
|
||||
"CreationDate": now - timedelta(days=1),
|
||||
"ExpirationDate": now + timedelta(days=1),
|
||||
"ProvisionsAllDevices": True,
|
||||
"DeveloperCertificates": [cert_der],
|
||||
}
|
||||
payload = root / "profile.plist"
|
||||
payload.write_bytes(plistlib.dumps(cls.profile))
|
||||
profile_path = root / "test.provisionprofile"
|
||||
subprocess.run(
|
||||
[
|
||||
"openssl",
|
||||
"cms",
|
||||
"-sign",
|
||||
"-binary",
|
||||
"-nodetach",
|
||||
"-outform",
|
||||
"DER",
|
||||
"-in",
|
||||
str(payload),
|
||||
"-signer",
|
||||
str(certificate),
|
||||
"-inkey",
|
||||
str(key),
|
||||
"-out",
|
||||
str(profile_path),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
cls.configuration = bundle.ProfileConfiguration(
|
||||
profile=profile_path,
|
||||
profile_sha256=hashlib.sha256(profile_path.read_bytes()).hexdigest(),
|
||||
certificate_sha256=hashlib.sha256(cert_der).hexdigest(),
|
||||
team_id="TESTTEAM01",
|
||||
)
|
||||
|
||||
|
||||
class ProvisionedCliTests(ProvisioningTestCase):
|
||||
def test_profile_configuration_fails_closed(self):
|
||||
self.assertEqual(bundle.load_profile(self.configuration), self.profile)
|
||||
for changes in (
|
||||
{"profile_sha256": "0" * 64},
|
||||
{"certificate_sha256": "0" * 64},
|
||||
{"team_id": "OTHERTEAM1"},
|
||||
{"profile_sha256": ""},
|
||||
{"certificate_sha256": ""},
|
||||
{"team_id": ""},
|
||||
):
|
||||
with self.subTest(changes=changes), self.assertRaises(ValueError):
|
||||
bundle.load_profile(replace(self.configuration, **changes))
|
||||
|
||||
def test_rejects_profile_with_wrong_identity_certificate_or_validity(self):
|
||||
for field, value in (
|
||||
("TeamIdentifier", ["OTHERTEAM"]),
|
||||
("DeveloperCertificates", [b"another certificate"]),
|
||||
(
|
||||
"ExpirationDate",
|
||||
datetime(2000, 1, 1, tzinfo=timezone.utc).replace(tzinfo=None),
|
||||
),
|
||||
(
|
||||
"CreationDate",
|
||||
datetime(2099, 1, 1, tzinfo=timezone.utc).replace(tzinfo=None),
|
||||
),
|
||||
("ProvisionsAllDevices", False),
|
||||
("ProvisionedDevices", ["device"]),
|
||||
):
|
||||
with self.subTest(field=field):
|
||||
profile = copy.deepcopy(self.profile)
|
||||
profile[field] = value
|
||||
with self.assertRaises(ValueError):
|
||||
bundle.validate_profile(profile, self.configuration)
|
||||
for field, value in (
|
||||
(
|
||||
"com.apple.application-identifier",
|
||||
f"{self.configuration.team_id}.another.app",
|
||||
),
|
||||
("keychain-access-groups", [f"{self.configuration.team_id}.another.app"]),
|
||||
("get-task-allow", True),
|
||||
):
|
||||
with self.subTest(field=field):
|
||||
profile = copy.deepcopy(self.profile)
|
||||
profile["Entitlements"][field] = value
|
||||
with self.assertRaises(ValueError):
|
||||
bundle.validate_profile(profile, self.configuration)
|
||||
|
||||
def test_archive_relocation_copy_and_symlink_preserve_launcher(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
package = root / "original package"
|
||||
metadata = {
|
||||
"variant": "codex",
|
||||
"layoutVersion": 1,
|
||||
"target": "aarch64-apple-darwin",
|
||||
"entrypoint": "bin/codex",
|
||||
}
|
||||
for relative in ("bin/codex", *bundle.HELPERS):
|
||||
path = package / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text('#!/bin/sh\nprintf "%s\\n" "$@"\nexit 23\n')
|
||||
path.chmod(0o755)
|
||||
(package / "codex-package.json").write_text(json.dumps(metadata))
|
||||
original_binary = (package / "bin/codex").read_bytes()
|
||||
bundle.prepare(package, root / "reports", self.configuration)
|
||||
with self.assertRaises(FileExistsError):
|
||||
bundle.prepare(package, root / "reports", self.configuration)
|
||||
with self.assertRaisesRegex(ValueError, "Package target differs"):
|
||||
bundle.verify(
|
||||
package, root / "reports", "x86_64-apple-darwin", self.configuration
|
||||
)
|
||||
self.assertEqual(
|
||||
(package / bundle.EXECUTABLE).read_bytes(), original_binary
|
||||
)
|
||||
self.assertEqual(
|
||||
json.loads((package / "codex-package.json").read_text()), metadata
|
||||
)
|
||||
|
||||
archive_path = root / "package.tar.gz"
|
||||
with tarfile.open(archive_path, "w:gz") as archive:
|
||||
archive.add(package, arcname=".")
|
||||
relocated = root / "relocated package"
|
||||
with tarfile.open(archive_path) as archive:
|
||||
archive.extractall(relocated, filter="data")
|
||||
shutil.rmtree(package)
|
||||
# npm currently dereferences symlinks when copying packages. The
|
||||
# launcher remains a script and the provisioned executable stays put.
|
||||
copied = root / "copied package"
|
||||
shutil.copytree(relocated, copied)
|
||||
link = root / "installed codex"
|
||||
link.symlink_to(Path("copied package/bin/codex"))
|
||||
absolute_link = root / "absolute codex"
|
||||
absolute_link.symlink_to(link)
|
||||
for entry in (
|
||||
relocated / "bin/codex",
|
||||
copied / "bin/codex",
|
||||
link,
|
||||
absolute_link,
|
||||
):
|
||||
with self.subTest(entry=entry):
|
||||
result = subprocess.run(
|
||||
[str(entry), "space argument", "*", "--flag"],
|
||||
cwd="/",
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(
|
||||
(result.returncode, result.stdout, result.stderr),
|
||||
(23, "space argument\n*\n--flag\n", ""),
|
||||
)
|
||||
(copied / bundle.APP / "Contents/embedded.provisionprofile").write_bytes(
|
||||
b"tampered"
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "Embedded profile differs"):
|
||||
bundle.verify(
|
||||
copied, root / "reports", "aarch64-apple-darwin", self.configuration
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
293
.github/scripts/macos-signing/test_sign_macos_cli_package.py
vendored
Normal file
293
.github/scripts/macos-signing/test_sign_macos_cli_package.py
vendored
Normal file
@@ -0,0 +1,293 @@
|
||||
"""Exercise package signing and native verification with generated credentials."""
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import provisioned_macos_cli_package as bundle
|
||||
from test_provisioned_macos_cli_package import ProvisioningTestCase
|
||||
|
||||
NATIVE_TOOL = """#!/usr/bin/env python3
|
||||
import json, os, plistlib, sys
|
||||
from pathlib import Path
|
||||
name = Path(sys.argv[0]).name
|
||||
args = sys.argv[1:]
|
||||
with open(os.environ["CALL_LOG"], "a") as log:
|
||||
log.write(json.dumps([name, *args]) + "\\n")
|
||||
if name == "codesign":
|
||||
for arg in args:
|
||||
if arg.startswith("--extract-certificates="):
|
||||
Path(arg.split("=", 1)[1] + "0").write_bytes(Path(os.environ["MOCK_CERTIFICATE"]).read_bytes())
|
||||
if "--test-requirement" in args and os.environ.get("MOCK_WRONG_SIGNING_IDENTITY"):
|
||||
print("code failed to satisfy specified code requirement(s)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if "--entitlements" in args:
|
||||
target = Path(args[-1]).name
|
||||
if target == "CodexCLI.app":
|
||||
source = Path(os.environ["CODEX_REPO_ROOT"]) / "signing-verification/codex-provisioned-entitlements.plist"
|
||||
elif target in ("codex", "codex-code-mode-host"):
|
||||
source = Path(os.environ["MOCK_SIGNING_SCRIPTS"]) / (target + ".entitlements.plist")
|
||||
else:
|
||||
sys.exit(0)
|
||||
if os.environ.get("MOCK_WRONG_ENTITLEMENTS"):
|
||||
sys.stdout.buffer.write(plistlib.dumps({"unexpected": True}))
|
||||
else:
|
||||
sys.stdout.buffer.write(source.read_bytes())
|
||||
elif name == "plutil":
|
||||
source = Path(args[-1])
|
||||
target = Path(args[args.index("-o") + 1]) if "-o" in args else source
|
||||
target.write_bytes(plistlib.dumps(plistlib.loads(source.read_bytes())))
|
||||
"""
|
||||
|
||||
|
||||
class MacosSigningTests(ProvisioningTestCase):
|
||||
def fixture(self, root, provisioned, target="aarch64-apple-darwin"):
|
||||
source = Path(bundle.__file__).parent
|
||||
common = Path(os.path.commonpath((source, bundle.SIGNING)))
|
||||
helpers = root / source.relative_to(common)
|
||||
scripts = root / bundle.SIGNING.relative_to(common)
|
||||
helpers.mkdir(parents=True, exist_ok=True)
|
||||
scripts.mkdir(parents=True, exist_ok=True)
|
||||
for name in ("provisioned_macos_cli_package.py", "sign_macos_cli_package.py"):
|
||||
shutil.copyfile(source / name, helpers / name)
|
||||
for path in bundle.SIGNING.glob("*.entitlements.plist"):
|
||||
shutil.copyfile(path, scripts / path.name)
|
||||
(root / "record.py").write_text(
|
||||
"import json, os, sys\n"
|
||||
"with open(os.environ['CALL_LOG'], 'a') as log:\n"
|
||||
" log.write(json.dumps(sys.argv[1:]) + '\\n')\n"
|
||||
"if os.environ.get('FAIL_TOOL') == sys.argv[1]:\n"
|
||||
" print('fixture stdout', flush=True)\n"
|
||||
" print('fixture stderr', file=sys.stderr, flush=True)\n"
|
||||
" sys.exit(17)\n"
|
||||
)
|
||||
for name in ("sign_macos_code.sh", "notarize_macos_binary_with_akv.sh"):
|
||||
(scripts / name).write_text(
|
||||
'python3 "$CODEX_REPO_ROOT/record.py" "$(basename "$0")" "$@"\n'
|
||||
)
|
||||
(scripts / "notarize_with_akv.py").write_text(
|
||||
"import os, runpy, sys\n"
|
||||
"sys.argv.insert(1, 'notarize_with_akv.py')\n"
|
||||
"runpy.run_path(os.environ['CODEX_REPO_ROOT'] + '/record.py')\n"
|
||||
)
|
||||
for name in ("rcodesign", "codesign", "lipo", "plutil"):
|
||||
path = root / name
|
||||
path.write_text(NATIVE_TOOL)
|
||||
path.chmod(0o755)
|
||||
certificate_der = self.profile["DeveloperCertificates"][0]
|
||||
certificate = root / "certificate.pem"
|
||||
certificate.write_text(ssl.DER_cert_to_PEM_cert(certificate_der))
|
||||
(root / "certificate.der").write_bytes(certificate_der)
|
||||
package = root / "package with spaces"
|
||||
for relative in ("bin/codex", *bundle.HELPERS):
|
||||
path = package / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("#!/bin/sh\nexit 0\n")
|
||||
path.chmod(0o755)
|
||||
(package / "codex-package.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"variant": "codex",
|
||||
"layoutVersion": 1,
|
||||
"target": target,
|
||||
"entrypoint": "bin/codex",
|
||||
}
|
||||
)
|
||||
)
|
||||
env = {
|
||||
key: value
|
||||
for key, value in os.environ.items()
|
||||
if key not in ("GITHUB_WORKSPACE", "CODEX_PROVISIONING_HELPER")
|
||||
}
|
||||
env.update(
|
||||
CODEX_REPO_ROOT=str(root),
|
||||
RUNNER_TEMP=str(root),
|
||||
TARGET=target,
|
||||
PATH=f"{root}{os.pathsep}{os.environ['PATH']}",
|
||||
CALL_LOG=str(root / "calls.jsonl"),
|
||||
PROVISIONED_MACOS=str(provisioned).lower(),
|
||||
OAI_AKV_SIGNING_CERTIFICATE_PEM=str(certificate),
|
||||
MOCK_CERTIFICATE=str(root / "certificate.der"),
|
||||
MOCK_SIGNING_SCRIPTS=str(scripts),
|
||||
)
|
||||
return package, helpers / "sign_macos_cli_package.py", env
|
||||
|
||||
def run_driver(self, driver, operation, package, env):
|
||||
return subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(driver),
|
||||
operation,
|
||||
str(package),
|
||||
"--profile",
|
||||
str(self.configuration.profile),
|
||||
"--profile-sha256",
|
||||
self.configuration.profile_sha256,
|
||||
"--certificate-sha256",
|
||||
self.configuration.certificate_sha256,
|
||||
"--team-id",
|
||||
self.configuration.team_id,
|
||||
],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_standard_and_provisioned_signing_and_verification(self):
|
||||
for provisioned, target in itertools.product(
|
||||
(False, True), ("aarch64-apple-darwin", "x86_64-apple-darwin")
|
||||
):
|
||||
with (
|
||||
self.subTest(provisioned=provisioned, target=target),
|
||||
tempfile.TemporaryDirectory() as directory,
|
||||
):
|
||||
root = Path(directory).resolve()
|
||||
package, driver, env = self.fixture(root, provisioned, target)
|
||||
result = self.run_driver(driver, "sign", package, env)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
result = self.run_driver(driver, "verify", package, env)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
calls = [
|
||||
json.loads(line)
|
||||
for line in (root / "calls.jsonl").read_text().splitlines()
|
||||
]
|
||||
signing = [call for call in calls if call[0] == "sign_macos_code.sh"]
|
||||
self.assertEqual(
|
||||
[call[call.index("--target") + 1] for call in signing],
|
||||
[
|
||||
str(package / relative)
|
||||
for relative in (
|
||||
bundle.APP if provisioned else "bin/codex",
|
||||
*bundle.HELPERS,
|
||||
)
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
signing[0][signing[0].index("--entitlements") + 1],
|
||||
str(
|
||||
root
|
||||
/ "signing-verification/codex-provisioned-entitlements.plist"
|
||||
)
|
||||
if provisioned
|
||||
else str(
|
||||
Path(env["MOCK_SIGNING_SCRIPTS"]) / "codex.entitlements.plist"
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
["--entitlements" in call for call in signing],
|
||||
[True, True, False, False],
|
||||
)
|
||||
self.assertEqual(
|
||||
[call[0] for call in calls if call[0].startswith("notarize")],
|
||||
["notarize_with_akv.py"]
|
||||
if provisioned
|
||||
else ["notarize_macos_binary_with_akv.sh"] * 4,
|
||||
)
|
||||
self.assertEqual(
|
||||
[call[1:] for call in calls if call[0] == "lipo"],
|
||||
[
|
||||
[
|
||||
str(package / relative),
|
||||
"-verify_arch",
|
||||
"arm64" if target.startswith("aarch64") else "x86_64",
|
||||
]
|
||||
for relative in (
|
||||
bundle.EXECUTABLE if provisioned else "bin/codex",
|
||||
*bundle.HELPERS,
|
||||
)
|
||||
],
|
||||
)
|
||||
if provisioned:
|
||||
with zipfile.ZipFile(root / "provisioned-cli.zip") as archive:
|
||||
self.assertEqual(
|
||||
archive.read(
|
||||
"CodexCLI.app/Contents/embedded.provisionprofile"
|
||||
),
|
||||
self.configuration.profile.read_bytes(),
|
||||
)
|
||||
self.assertEqual(
|
||||
archive.read("bin/codex").decode(), bundle.LAUNCHER
|
||||
)
|
||||
self.assertTrue(
|
||||
all(
|
||||
helper in archive.namelist()
|
||||
for helper in bundle.HELPERS
|
||||
)
|
||||
)
|
||||
|
||||
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:
|
||||
root = Path(directory).resolve()
|
||||
package, driver, env = self.fixture(root, True)
|
||||
result = self.run_driver(
|
||||
driver, "sign", package, {**env, "FAIL_TOOL": tool}
|
||||
)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
calls = [
|
||||
json.loads(line)
|
||||
for line in (root / "calls.jsonl").read_text().splitlines()
|
||||
]
|
||||
self.assertEqual(calls[-1][0], tool)
|
||||
if tool == "notarize_with_akv.py":
|
||||
self.assertEqual(
|
||||
(root / "signing-verification/notarization.log").read_text(),
|
||||
"fixture stdout\nfixture stderr\n",
|
||||
)
|
||||
self.assertIn("fixture stdout\nfixture stderr\n", result.stdout)
|
||||
else:
|
||||
self.assertFalse((root / "provisioned-cli.zip").exists())
|
||||
|
||||
def test_verification_rejects_wrong_signing_identity_and_entitlements(self):
|
||||
for provisioned, failure in itertools.product(
|
||||
(False, True), ("MOCK_WRONG_SIGNING_IDENTITY", "MOCK_WRONG_ENTITLEMENTS")
|
||||
):
|
||||
with (
|
||||
self.subTest(provisioned=provisioned, failure=failure),
|
||||
tempfile.TemporaryDirectory() as directory,
|
||||
):
|
||||
root = Path(directory).resolve()
|
||||
package, driver, env = self.fixture(root, provisioned)
|
||||
signed = self.run_driver(driver, "sign", package, env)
|
||||
self.assertEqual(signed.returncode, 0, signed.stderr)
|
||||
result = self.run_driver(
|
||||
driver, "verify", package, {**env, failure: "true"}
|
||||
)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
if failure == "MOCK_WRONG_SIGNING_IDENTITY":
|
||||
self.assertIn(
|
||||
"code failed to satisfy specified code requirement(s)",
|
||||
result.stderr,
|
||||
)
|
||||
else:
|
||||
self.assertIn("unexpected", result.stdout)
|
||||
|
||||
def test_cli_requires_independent_profile_configuration(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(Path(bundle.__file__).with_name("sign_macos_cli_package.py")),
|
||||
"verify",
|
||||
directory,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn("the following arguments are required", result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
4
.github/workflows/repo-checks.yml
vendored
4
.github/workflows/repo-checks.yml
vendored
@@ -43,8 +43,8 @@ jobs:
|
||||
- name: Test standalone installer
|
||||
run: python3 -m unittest discover -s scripts/install -p 'test_*.py'
|
||||
|
||||
- name: Test macOS notarization
|
||||
run: python3 -m unittest discover -s .github/scripts/macos-signing -p 'test_notarize_with_akv.py'
|
||||
- name: Test macOS signing and notarization
|
||||
run: python3 -m unittest discover -s .github/scripts/macos-signing -p 'test_*.py'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
|
||||
|
||||
213
.github/workflows/rust-release-provisioned-macos.yml
vendored
Normal file
213
.github/workflows/rust-release-provisioned-macos.yml
vendored
Normal file
@@ -0,0 +1,213 @@
|
||||
# Optional side artifacts only; no release, npm, installer, or DMG publication.
|
||||
# Required secrets in the codesigning environment:
|
||||
# - CODEX_CLI_PROVISIONING_PROFILE_BASE64: profile approved for public distribution.
|
||||
# - CODEX_CLI_PROVISIONING_PROFILE_SHA256: reviewed lowercase SHA-256 of that profile.
|
||||
# - CODEX_CLI_PROVISIONING_TEAM_ID: expected ten-character Apple Team ID.
|
||||
# - AKV_CODESIGN_CERTIFICATE_SHA256: required signing certificate pin.
|
||||
# Set repository variable CODEX_PROVISIONED_MACOS_CANDIDATE to true to enable
|
||||
# candidates on normal tag releases; unset it or set it to false to disable them.
|
||||
name: Provisioned macOS CLI candidate
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CODEX_REPO_ROOT: ${{ github.workspace }}
|
||||
|
||||
jobs:
|
||||
sign:
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/rust-v') && vars.CODEX_PROVISIONED_MACOS_CANDIDATE == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
environment:
|
||||
name: codesigning
|
||||
deployment: false
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: [aarch64-apple-darwin, x86_64-apple-darwin]
|
||||
env:
|
||||
TARGET: ${{ matrix.target }}
|
||||
PROVISIONED_MACOS: "true"
|
||||
PROFILE_SHA256: ${{ secrets.CODEX_CLI_PROVISIONING_PROFILE_SHA256 }}
|
||||
CERTIFICATE_SHA256: ${{ secrets.AKV_CODESIGN_CERTIFICATE_SHA256 }}
|
||||
TEAM_ID: ${{ secrets.CODEX_CLI_PROVISIONING_TEAM_ID }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Validate the independently approved public-distribution profile
|
||||
env:
|
||||
PROFILE_BASE64: ${{ secrets.CODEX_CLI_PROVISIONING_PROFILE_BASE64 }}
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import base64
|
||||
import os
|
||||
from pathlib import Path
|
||||
profile = Path(os.environ["RUNNER_TEMP"]) / "codex-cli.provisionprofile"
|
||||
profile.write_bytes(base64.b64decode("".join(os.environ["PROFILE_BASE64"].split()), validate=True))
|
||||
PY
|
||||
python3 .github/scripts/macos-signing/provisioned_macos_cli_package.py validate-profile \
|
||||
--profile "$RUNNER_TEMP/codex-cli.provisionprofile" --profile-sha256 "$PROFILE_SHA256" \
|
||||
--certificate-sha256 "$CERTIFICATE_SHA256" --team-id "$TEAM_ID"
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
path: standard
|
||||
- name: Extract the verified package from this tag-release run
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
with tarfile.open(f"standard/codex-package-{os.environ['TARGET']}.tar.gz") as archive:
|
||||
archive.extractall("package", filter="data")
|
||||
metadata = json.loads(Path("package/codex-package.json").read_text())
|
||||
if metadata["target"] != os.environ["TARGET"]:
|
||||
raise ValueError("Package target does not match this release job")
|
||||
PY
|
||||
- name: Set up the existing AKV signer
|
||||
uses: ./.github/actions/setup-akv-pkcs11-codesigning
|
||||
with:
|
||||
rcodesign-blob-uri: ${{ secrets.AKV_CODESIGN_RCODESIGN_BLOB_URI }}
|
||||
rcodesign-sha256: ${{ secrets.AKV_CODESIGN_RCODESIGN_SHA256 }}
|
||||
akv-pkcs11-library-blob-uri: ${{ secrets.AKV_CODESIGN_PKCS11_LIBRARY_BLOB_URI }}
|
||||
akv-pkcs11-library-sha256: ${{ secrets.AKV_CODESIGN_PKCS11_LIBRARY_SHA256 }}
|
||||
azure-client-id: ${{ secrets.AKV_CODESIGN_AZURE_CLIENT_ID }}
|
||||
azure-tenant-id: ${{ secrets.AKV_CODESIGN_TENANT }}
|
||||
azure-subscription-id: ${{ secrets.AKV_CODESIGN_SUBSCRIPTION }}
|
||||
key-vault-name: ${{ secrets.AKV_CODESIGN_KEY_VAULT_NAME }}
|
||||
key-name: ${{ secrets.AKV_CODESIGN_KEY_NAME }}
|
||||
key-version: ${{ secrets.AKV_CODESIGN_KEY_VERSION || '' }}
|
||||
certificate-sha256: ${{ secrets.AKV_CODESIGN_CERTIFICATE_SHA256 }}
|
||||
- name: Sign and notarize the provisioned bundle
|
||||
env:
|
||||
APPLE_NOTARIZATION_AKV_KEY_NAME: ${{ secrets.AKV_NOTARIZATION_KEY_NAME }}
|
||||
APPLE_NOTARIZATION_AKV_KEY_VERSION: ${{ secrets.AKV_NOTARIZATION_KEY_VERSION }}
|
||||
run: |
|
||||
python3 .github/scripts/macos-signing/sign_macos_cli_package.py sign package \
|
||||
--profile "$RUNNER_TEMP/codex-cli.provisionprofile" --profile-sha256 "$PROFILE_SHA256" \
|
||||
--certificate-sha256 "$CERTIFICATE_SHA256" --team-id "$TEAM_ID"
|
||||
mkdir signed
|
||||
tar -czf "signed/codex-provisioned-package-$TARGET.tar.gz" -C package \
|
||||
bin codex-resources codex-path codex-package.json CodexCLI.app
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: provisioned-macos-signed-candidate-${{ matrix.target }}
|
||||
path: signed/*.tar.gz
|
||||
if-no-files-found: error
|
||||
- name: Retain signing reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: provisioned-macos-signing-reports-${{ matrix.target }}
|
||||
path: signing-verification/
|
||||
if-no-files-found: warn
|
||||
|
||||
verify:
|
||||
needs: sign
|
||||
runs-on: macos-15-xlarge
|
||||
timeout-minutes: 30
|
||||
environment:
|
||||
name: codesigning
|
||||
deployment: false
|
||||
# Read independent profile/certificate expectations without a signing token.
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: none
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: [aarch64-apple-darwin, x86_64-apple-darwin]
|
||||
env:
|
||||
TARGET: ${{ matrix.target }}
|
||||
PROVISIONED_MACOS: "true"
|
||||
PROFILE_SHA256: ${{ secrets.CODEX_CLI_PROVISIONING_PROFILE_SHA256 }}
|
||||
CERTIFICATE_SHA256: ${{ secrets.AKV_CODESIGN_CERTIFICATE_SHA256 }}
|
||||
TEAM_ID: ${{ secrets.CODEX_CLI_PROVISIONING_TEAM_ID }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: provisioned-macos-signed-candidate-${{ matrix.target }}
|
||||
path: signed
|
||||
- name: Extract candidate and load independent verification profile
|
||||
env:
|
||||
PROFILE_BASE64: ${{ secrets.CODEX_CLI_PROVISIONING_PROFILE_BASE64 }}
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import base64
|
||||
import os
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
profile = Path(os.environ["RUNNER_TEMP"]) / "codex-cli.provisionprofile"
|
||||
profile.write_bytes(base64.b64decode("".join(os.environ["PROFILE_BASE64"].split()), validate=True))
|
||||
with tarfile.open(f"signed/codex-provisioned-package-{os.environ['TARGET']}.tar.gz") as archive:
|
||||
archive.extractall("package", filter="data")
|
||||
PY
|
||||
- name: Verify signatures, profile, architecture, stapling and Gatekeeper
|
||||
run: |
|
||||
profile_args=(--profile "$RUNNER_TEMP/codex-cli.provisionprofile"
|
||||
--profile-sha256 "$PROFILE_SHA256" --certificate-sha256 "$CERTIFICATE_SHA256"
|
||||
--team-id "$TEAM_ID")
|
||||
python3 .github/scripts/macos-signing/sign_macos_cli_package.py verify package "${profile_args[@]}"
|
||||
xcrun stapler staple package/CodexCLI.app
|
||||
xcrun stapler validate package/CodexCLI.app
|
||||
spctl --assess --type execute --verbose=4 package/CodexCLI.app
|
||||
python3 .github/scripts/macos-signing/sign_macos_cli_package.py verify package "${profile_args[@]}"
|
||||
mkdir verified
|
||||
tar -czf "verified/codex-provisioned-package-$TARGET.tar.gz" -C package \
|
||||
bin codex-resources codex-path codex-package.json CodexCLI.app
|
||||
(cd verified && shasum -a 256 "codex-provisioned-package-$TARGET.tar.gz" > SHA256SUMS)
|
||||
- uses: ./.github/actions/setup-ci
|
||||
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
version: "0.11.3"
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: ${{ matrix.target }}-app-server
|
||||
path: app-server
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: ${{ matrix.target }}-symbols
|
||||
path: symbols
|
||||
- name: Test the exact verified package, including sandboxed code mode
|
||||
working-directory: scripts/codex_package/smoke_tests
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}/sdk/python/src:${{ github.workspace }}/sdk/python/tests
|
||||
run: |
|
||||
# This repackaging does not change debug code. The normal release's
|
||||
# primary symbols omit app-server symbols, so skip symbol-only tests.
|
||||
uv run --frozen pytest -v --compression gzip --package-target "$TARGET" \
|
||||
--cli-archive "$CODEX_REPO_ROOT/verified/codex-provisioned-package-$TARGET.tar.gz" \
|
||||
--app-server-archive "$CODEX_REPO_ROOT/app-server/codex-app-server-package-$TARGET.tar.gz" \
|
||||
--symbols-archive "$CODEX_REPO_ROOT/symbols/codex-symbols-$TARGET.tar.gz" \
|
||||
test_codex_package.py -k 'not debug_symbols'
|
||||
- name: Retain verified candidate only after package tests pass
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: provisioned-macos-verified-candidate-${{ matrix.target }}
|
||||
path: verified/*
|
||||
if-no-files-found: error
|
||||
- name: Retain verification reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: provisioned-macos-verification-reports-${{ matrix.target }}
|
||||
path: signing-verification/
|
||||
if-no-files-found: warn
|
||||
10
.github/workflows/rust-release.yml
vendored
10
.github/workflows/rust-release.yml
vendored
@@ -1394,6 +1394,16 @@ jobs:
|
||||
path: codex-rs/dist/${{ matrix.target }}/*
|
||||
if-no-files-found: error
|
||||
|
||||
# Separate opt-in candidates never enter the normal release download patterns.
|
||||
provisioned-macos-candidate:
|
||||
needs: finalize-macos
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/rust-v') && vars.CODEX_PROVISIONED_MACOS_CANDIDATE == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
uses: ./.github/workflows/rust-release-provisioned-macos.yml
|
||||
secrets: inherit
|
||||
|
||||
build-windows:
|
||||
needs: tag-check
|
||||
uses: ./.github/workflows/rust-release-windows.yml
|
||||
|
||||
Reference in New Issue
Block a user