Temporarily validate empty-password Developer ID certificate

This commit is contained in:
Eric Burke
2026-04-08 15:11:33 -07:00
parent f7bed6cc95
commit 48477b5b6f
5 changed files with 211 additions and 9 deletions
@@ -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"
@@ -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 }}
@@ -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)
@@ -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
+156
View File
@@ -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,
],
)