From d9cd8e328e7bb843619c36f214caa2e786180ecf Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 17 Oct 2026 05:52:26 -0400 Subject: [PATCH] fix(release): check macOS signing inputs before compilation --- .gitea/workflows/build-macos.yml | 3 +++ contrib/release/sign_macos.py | 16 ++++++++++++++-- contrib/release/tests/test_sign_macos.py | 6 ++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml index 260eebac..741e12a7 100644 --- a/.gitea/workflows/build-macos.yml +++ b/.gitea/workflows/build-macos.yml @@ -45,6 +45,9 @@ jobs: - name: Verify exact reviewed source run: python3 -B contrib/release/check_dispatch.py + - name: Check signing inputs before compiling + run: python3 -B contrib/release/sign_macos.py --check-inputs + - name: Verify Fleet-managed build dependencies run: | set -e diff --git a/contrib/release/sign_macos.py b/contrib/release/sign_macos.py index 9a651c4a..207eef3f 100644 --- a/contrib/release/sign_macos.py +++ b/contrib/release/sign_macos.py @@ -10,6 +10,7 @@ import os from pathlib import Path import secrets import subprocess +import sys import tempfile @@ -77,13 +78,18 @@ def private_file(path, content): stream.write(content) -def main(): +def signing_inputs(): credentials = {name: os.environ.get(name, "") for name in FIELDS} missing = [name for name, value in credentials.items() if not value] if missing: raise ValueError("Signing input is missing: " + ", ".join(missing)) if "Such Software" not in credentials["APPLE_SIGNING_IDENTITY"]: raise ValueError("Developer ID identity does not name the expected publisher") + return credentials + + +def main(): + credentials = signing_inputs() app = Path("build/dist/Wowlet.app").resolve() if not app.is_dir(): raise ValueError("Packaged Wowlet.app is missing") @@ -143,7 +149,13 @@ def main(): if __name__ == "__main__": try: - main() + if sys.argv[1:] == ["--check-inputs"]: + signing_inputs() + print("Required signing inputs are present") + elif sys.argv[1:]: + raise ValueError("Use no arguments to sign or --check-inputs for the cheap input guard") + else: + main() except (OSError, ValueError, RuntimeError, subprocess.TimeoutExpired) as error: # These exceptions contain logical fields, native result classes or # public file paths. Captured child output is never included. diff --git a/contrib/release/tests/test_sign_macos.py b/contrib/release/tests/test_sign_macos.py index 6d8511ce..0ece75eb 100644 --- a/contrib/release/tests/test_sign_macos.py +++ b/contrib/release/tests/test_sign_macos.py @@ -29,6 +29,12 @@ class MacSigningTests(unittest.TestCase): self.assertTrue(options["capture_output"]) self.assertNotIn("APPLE_CERTIFICATE_PASSWORD", options["env"]) + def test_missing_inputs_refuse_without_native_operations(self): + with patch.dict(signing.os.environ, {}, clear=True), patch.object(signing.subprocess, "run") as execute: + with self.assertRaises(ValueError): + signing.signing_inputs() + execute.assert_not_called() + def test_control_characters_and_unrelated_keychain_operations_refuse(self): with patch.object(signing.subprocess, "run") as execute: for value in ("line\nbreak", "nul\x00byte", "carriage\rreturn"):