From 48477b5b6f083ec348e4cc0732a506be891f1cf8 Mon Sep 17 00:00:00 2001 From: Eric Burke Date: Wed, 8 Apr 2026 15:11:33 -0700 Subject: [PATCH] Temporarily validate empty-password Developer ID certificate --- .github/actions/macos-code-sign/action.yml | 5 - .../workflows/one-off-mac-notarization.yaml | 8 +- .../cmd_upload_mac_signing_secrets.py | 48 ++++++ tools/distribution/rotate_signing_assets.py | 3 + tools/distribution/rotation_shared.py | 156 ++++++++++++++++++ 5 files changed, 211 insertions(+), 9 deletions(-) create mode 100644 tools/distribution/cmd_upload_mac_signing_secrets.py diff --git a/.github/actions/macos-code-sign/action.yml b/.github/actions/macos-code-sign/action.yml index 200b23901f..9c14931a02 100644 --- a/.github/actions/macos-code-sign/action.yml +++ b/.github/actions/macos-code-sign/action.yml @@ -44,11 +44,6 @@ runs: exit 1 fi - if [[ -z "${APPLE_CERTIFICATE_PASSWORD:-}" ]]; then - echo "APPLE_CERTIFICATE_PASSWORD is required for macOS signing" - exit 1 - fi - cert_path="${RUNNER_TEMP}/apple_signing_certificate.p12" echo "$APPLE_CERTIFICATE" | base64 -d > "$cert_path" diff --git a/.github/workflows/one-off-mac-notarization.yaml b/.github/workflows/one-off-mac-notarization.yaml index 52827084d5..0875328d27 100644 --- a/.github/workflows/one-off-mac-notarization.yaml +++ b/.github/workflows/one-off-mac-notarization.yaml @@ -77,8 +77,8 @@ jobs: target: ${{ matrix.target }} sign-binaries: "true" sign-dmg: "false" - apple-certificate: ${{ secrets.APPLE_CERTIFICATE_P12 }} - apple-certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + apple-certificate: ${{ secrets.NEW_APPLE_CERTIFICATE_P12 }} + apple-certificate-password: ${{ secrets.NEW_APPLE_CERTIFICATE_PASSWORD }} apple-notarization-key-p8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }} apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} @@ -116,8 +116,8 @@ jobs: target: ${{ matrix.target }} sign-binaries: "false" sign-dmg: "true" - apple-certificate: ${{ secrets.APPLE_CERTIFICATE_P12 }} - apple-certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + apple-certificate: ${{ secrets.NEW_APPLE_CERTIFICATE_P12 }} + apple-certificate-password: ${{ secrets.NEW_APPLE_CERTIFICATE_PASSWORD }} apple-notarization-key-p8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }} apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} diff --git a/tools/distribution/cmd_upload_mac_signing_secrets.py b/tools/distribution/cmd_upload_mac_signing_secrets.py new file mode 100644 index 0000000000..e694724566 --- /dev/null +++ b/tools/distribution/cmd_upload_mac_signing_secrets.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import argparse + +from rotation_shared import ( + DEFAULT_GITHUB_REPO, + DEFAULT_ONE_PASSWORD_ACCOUNT, + NEW_MAC_SIGNING_CERTIFICATE_PASSWORD_SECRET_NAME, + NEW_MAC_SIGNING_CERTIFICATE_SECRET_NAME, + cmd_upload_mac_signing_secrets, +) + + +def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None: + parser = subparsers.add_parser( + "upload-mac-signing-secrets", + help="Upload a Developer ID Application p12 from 1Password to temporary GitHub Actions secrets", + ) + parser.add_argument("--github-repo", default=DEFAULT_GITHUB_REPO) + parser.add_argument( + "--one-password-item", + required=True, + help="1Password item ID or title that has one attached .p12 file", + ) + parser.add_argument( + "--one-password-vault", + required=True, + help="1Password vault containing the mac signing item", + ) + parser.add_argument( + "--one-password-account", + default=DEFAULT_ONE_PASSWORD_ACCOUNT, + help="1Password account to use when loading the item", + ) + parser.add_argument( + "--p12-file-name", + help="Attached .p12 filename to download; required only if the item has multiple .p12 files", + ) + parser.add_argument("--certificate-secret-name", default=NEW_MAC_SIGNING_CERTIFICATE_SECRET_NAME) + parser.add_argument( + "--password-secret-name", default=NEW_MAC_SIGNING_CERTIFICATE_PASSWORD_SECRET_NAME + ) + parser.add_argument( + "--certificate-password", + default="", + help="Password for the p12. Defaults to the empty password used by the temporary certificate.", + ) + parser.set_defaults(func=cmd_upload_mac_signing_secrets) diff --git a/tools/distribution/rotate_signing_assets.py b/tools/distribution/rotate_signing_assets.py index 1dfd6146db..cc207e33dd 100755 --- a/tools/distribution/rotate_signing_assets.py +++ b/tools/distribution/rotate_signing_assets.py @@ -8,6 +8,7 @@ helper: - import a manually-created App Store Connect API key, - optionally store it in 1Password, - upload the matching GitHub Actions repository secrets. + - upload an existing Developer ID Application p12 for temporary testing. """ from __future__ import annotations @@ -18,6 +19,7 @@ from typing import Sequence from cmd_import_api_key import register as register_import_api_key from cmd_upload_api_key_secrets import register as register_upload_api_key_secrets +from cmd_upload_mac_signing_secrets import register as register_upload_mac_signing_secrets from rotation_shared import DEFAULT_OUT_DIR @@ -55,6 +57,7 @@ def build_parser() -> argparse.ArgumentParser: register_import_api_key(subparsers) register_upload_api_key_secrets(subparsers) + register_upload_mac_signing_secrets(subparsers) return parser diff --git a/tools/distribution/rotation_shared.py b/tools/distribution/rotation_shared.py index f48bc91a4f..f237b7daa5 100644 --- a/tools/distribution/rotation_shared.py +++ b/tools/distribution/rotation_shared.py @@ -9,6 +9,7 @@ import os import shlex import subprocess import sys +import tempfile from datetime import datetime from pathlib import Path from typing import Any, Sequence @@ -24,6 +25,8 @@ DEFAULT_GITHUB_REPO = "openai/codex" DEFAULT_ISSUER_ID_SECRET_NAME = "APPLE_NOTARIZATION_ISSUER_ID" DEFAULT_KEY_ID_SECRET_NAME = "APPLE_NOTARIZATION_KEY_ID" DEFAULT_PRIVATE_KEY_SECRET_NAME = "APPLE_NOTARIZATION_KEY_P8" +NEW_MAC_SIGNING_CERTIFICATE_SECRET_NAME = "NEW_APPLE_CERTIFICATE_P12" +NEW_MAC_SIGNING_CERTIFICATE_PASSWORD_SECRET_NAME = "NEW_APPLE_CERTIFICATE_PASSWORD" PACIFIC_TZ = ZoneInfo("America/Los_Angeles") @@ -160,6 +163,111 @@ def load_one_password_item_fields( return _parse_one_password_fields(raw, fields) +def load_one_password_item_json( + *, + item: str, + vault: str | None, + account: str, +) -> dict[str, Any]: + argv = [ + "op", + "item", + "get", + item, + "--account", + account, + "--format", + "json", + ] + if vault: + argv.extend(["--vault", vault]) + try: + raw = subprocess.check_output(argv, text=True) + except Exception as error: + raise SystemExit(f"Failed to load 1Password item {item!r}: {error}") from error + try: + payload = json.loads(raw) + except json.JSONDecodeError as error: + raise SystemExit(f"Failed to parse 1Password item JSON for {item!r}: {error}") from error + if not isinstance(payload, dict): + raise SystemExit(f"1Password item JSON for {item!r} was not an object") + return payload + + +def _one_password_item_file_name(file_payload: object) -> str | None: + if not isinstance(file_payload, dict): + return None + for key in ("name", "fileName", "title"): + value = file_payload.get(key) + if isinstance(value, str) and value.strip(): + return Path(value).name + return None + + +def one_password_item_file_names( + *, + item: str, + vault: str | None, + account: str, +) -> list[str]: + payload = load_one_password_item_json(item=item, vault=vault, account=account) + raw_files = payload.get("files", []) + if not isinstance(raw_files, list): + return [] + return [ + file_name + for file_name in (_one_password_item_file_name(file_payload) for file_payload in raw_files) + if file_name + ] + + +def select_one_password_p12_file_name(file_names: Sequence[str]) -> str: + p12_file_names = sorted( + {file_name for file_name in file_names if file_name.lower().endswith(".p12")} + ) + if not p12_file_names: + raise SystemExit("The 1Password item does not have an attached .p12 file") + if len(p12_file_names) > 1: + raise SystemExit( + "The 1Password item has multiple .p12 files; pass --p12-file-name with one of: " + + ", ".join(p12_file_names) + ) + return p12_file_names[0] + + +def one_password_secret_reference(*, vault: str, item: str, field_or_file: str) -> str: + return f"op://{vault}/{item}/{field_or_file}" + + +def download_one_password_file( + *, + item: str, + vault: str, + account: str, + file_name: str, + output_path: Path, +) -> None: + argv = [ + "op", + "read", + "--out-file", + str(output_path), + "--file-mode", + "0600", + one_password_secret_reference(vault=vault, item=item, field_or_file=file_name), + "--account", + account, + ] + try: + subprocess.run(argv, check=True, stdout=subprocess.DEVNULL) + except subprocess.CalledProcessError as error: + raise SystemExit( + f"Failed to download 1Password file {file_name!r} from {item!r}: {error}" + ) from error + if not output_path.exists(): + raise SystemExit(f"1Password file download did not produce {output_path}") + + def _one_password_item_exists( *, item: str, @@ -398,3 +506,51 @@ def cmd_upload_api_key_secrets(args: argparse.Namespace) -> None: ], stdin_text=private_key_body, ) + + +def cmd_upload_mac_signing_secrets(args: argparse.Namespace) -> None: + runner = CommandRunner(dry_run=not args.execute, verbose=args.log_verbose) + + file_name = args.p12_file_name or select_one_password_p12_file_name( + one_password_item_file_names( + item=args.one_password_item, + vault=args.one_password_vault, + account=args.one_password_account, + ) + ) + with tempfile.TemporaryDirectory(prefix="codex-cli-mac-signing-p12-") as temp_dir: + p12_path = Path(temp_dir) / file_name + download_one_password_file( + item=args.one_password_item, + vault=args.one_password_vault, + account=args.one_password_account, + file_name=file_name, + output_path=p12_path, + ) + p12_body = normalize_base64_secret_body( + base64.b64encode(p12_path.read_bytes()).decode("ascii") + ) + + runner.run( + [ + "gh", + "secret", + "set", + "--repo", + args.github_repo, + args.certificate_secret_name, + ], + stdin_text=p12_body, + ) + runner.run( + [ + "gh", + "secret", + "set", + "--repo", + args.github_repo, + args.password_secret_name, + "--body", + args.certificate_password, + ], + )