From 7a1c336b861bd461f3a3c7c1e94c9b839b0a7f92 Mon Sep 17 00:00:00 2001
From: gotbadger
Date: Mon, 23 Feb 2026 14:29:29 +0000
Subject: [PATCH] CM-53930: fix onedir signing issues on mac
---
.github/workflows/build_executable.yml | 95 +++++++++++++++++++++++---
process_executable_file.py | 6 +-
2 files changed, 90 insertions(+), 11 deletions(-)
diff --git a/.github/workflows/build_executable.yml b/.github/workflows/build_executable.yml
index 1f1e2582..6749ca79 100644
--- a/.github/workflows/build_executable.yml
+++ b/.github/workflows/build_executable.yml
@@ -125,20 +125,34 @@ jobs:
echo "PATH_TO_CYCODE_CLI_EXECUTABLE=dist/cycode-cli/cycode-cli" >> $GITHUB_ENV
- name: Test executable
- run: time $PATH_TO_CYCODE_CLI_EXECUTABLE version
+ run: time $PATH_TO_CYCODE_CLI_EXECUTABLE status
- name: Codesign onedir binaries
if: runner.os == 'macOS' && matrix.mode == 'onedir'
env:
APPLE_CERT_NAME: ${{ secrets.APPLE_CERT_NAME }}
run: |
- # Sign all Mach-O binaries in the onedir output (excluding the main executable)
- # Main executable must be signed last after all its dependencies
- find dist/cycode-cli -type f ! -name "cycode-cli" | while read -r file; do
+ # The standalone _internal/Python fails codesign --verify --strict because it was
+ # extracted from Python.framework without Info.plist context.
+ # Fix: remove the bare copy and replace with the framework version's binary,
+ # then delete the framework directory (it's redundant).
+ if [ -d dist/cycode-cli/_internal/Python.framework ]; then
+ FRAMEWORK_PYTHON=$(find dist/cycode-cli/_internal/Python.framework/Versions -name "Python" -type f | head -1)
+ if [ -n "$FRAMEWORK_PYTHON" ]; then
+ echo "Replacing _internal/Python with framework binary"
+ rm dist/cycode-cli/_internal/Python
+ cp "$FRAMEWORK_PYTHON" dist/cycode-cli/_internal/Python
+ fi
+ rm -rf dist/cycode-cli/_internal/Python.framework
+ fi
+
+ # Sign all Mach-O binaries (excluding the main executable)
+ while IFS= read -r file; do
if file -b "$file" | grep -q "Mach-O"; then
+ echo "Signing: $file"
codesign --force --sign "$APPLE_CERT_NAME" --timestamp --options runtime "$file"
fi
- done
+ done < <(find dist/cycode-cli -type f ! -name "cycode-cli")
# Re-sign the main executable with entitlements (must be last)
codesign --force --sign "$APPLE_CERT_NAME" --timestamp --options runtime --entitlements entitlements.plist dist/cycode-cli/cycode-cli
@@ -176,15 +190,35 @@ jobs:
# we can't staple the app because it's executable
- - name: Test macOS signed executable
+ - name: Verify macOS code signatures
if: runner.os == 'macOS'
run: |
- file -b $PATH_TO_CYCODE_CLI_EXECUTABLE
- time $PATH_TO_CYCODE_CLI_EXECUTABLE version
+ FAILED=false
+ while IFS= read -r file; do
+ if file -b "$file" | grep -q "Mach-O"; then
+ if ! codesign --verify "$file" 2>&1; then
+ echo "INVALID: $file"
+ codesign -dv "$file" 2>&1 || true
+ FAILED=true
+ else
+ echo "OK: $file"
+ fi
+ fi
+ done < <(find dist/cycode-cli -type f)
+
+ if [ "$FAILED" = true ]; then
+ echo "Found binaries with invalid signatures!"
+ exit 1
+ fi
- # verify signature
codesign -dv --verbose=4 $PATH_TO_CYCODE_CLI_EXECUTABLE
+ - name: Test macOS signed executable
+ if: runner.os == 'macOS'
+ run: |
+ file -b $PATH_TO_CYCODE_CLI_EXECUTABLE
+ time $PATH_TO_CYCODE_CLI_EXECUTABLE status
+
- name: Import cert for Windows and setup envs
if: runner.os == 'Windows'
env:
@@ -222,7 +256,7 @@ jobs:
shell: cmd
run: |
:: call executable and expect correct output
- .\dist\cycode-cli.exe version
+ .\dist\cycode-cli.exe status
:: verify signature
signtool.exe verify /v /pa ".\dist\cycode-cli.exe"
@@ -236,6 +270,47 @@ jobs:
name: ${{ env.ARTIFACT_NAME }}
path: dist
+ - name: Verify macOS artifact end-to-end
+ if: runner.os == 'macOS' && matrix.mode == 'onedir'
+ uses: actions/download-artifact@v4
+ with:
+ name: ${{ env.ARTIFACT_NAME }}
+ path: /tmp/artifact-verify
+
+ - name: Verify macOS artifact signatures and run with quarantine
+ if: runner.os == 'macOS' && matrix.mode == 'onedir'
+ run: |
+ # extract the onedir zip exactly as an end user would
+ ARCHIVE=$(find /tmp/artifact-verify -name "*.zip" | head -1)
+ echo "Verifying archive: $ARCHIVE"
+ unzip "$ARCHIVE" -d /tmp/artifact-extracted
+
+ # verify all Mach-O code signatures
+ FAILED=false
+ while IFS= read -r file; do
+ if file -b "$file" | grep -q "Mach-O"; then
+ if ! codesign --verify "$file" 2>&1; then
+ echo "INVALID: $file"
+ codesign -dv "$file" 2>&1 || true
+ FAILED=true
+ else
+ echo "OK: $file"
+ fi
+ fi
+ done < <(find /tmp/artifact-extracted -type f)
+
+ if [ "$FAILED" = true ]; then
+ echo "Artifact contains binaries with invalid signatures!"
+ exit 1
+ fi
+
+ # simulate download quarantine and test execution
+ # this is the definitive test — it triggers the same dlopen checks end users experience
+ find /tmp/artifact-extracted -type f -exec xattr -w com.apple.quarantine "0081;$(printf '%x' $(date +%s));CI;$(uuidgen)" {} \;
+ EXECUTABLE=$(find /tmp/artifact-extracted -name "cycode-cli" -type f | head -1)
+ echo "Testing quarantined executable: $EXECUTABLE"
+ time "$EXECUTABLE" status
+
- name: Upload files to release
if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish }}
uses: svenstaro/upload-release-action@v2
diff --git a/process_executable_file.py b/process_executable_file.py
index 367bb18d..36d6d0d6 100755
--- a/process_executable_file.py
+++ b/process_executable_file.py
@@ -140,6 +140,10 @@ def get_cli_archive_path(output_path: Path, is_onedir: bool) -> str:
return os.path.join(output_path, get_cli_archive_filename(is_onedir))
+def archive_directory(input_path: Path, output_path: str) -> None:
+ shutil.make_archive(output_path.removesuffix(f'.{_ARCHIVE_FORMAT}'), _ARCHIVE_FORMAT, input_path)
+
+
def process_executable_file(input_path: Path, is_onedir: bool) -> str:
output_path = input_path.parent
hash_file_path = get_cli_hash_path(output_path, is_onedir)
@@ -150,7 +154,7 @@ def process_executable_file(input_path: Path, is_onedir: bool) -> str:
write_hashes_db_to_file(normalized_hashes, hash_file_path)
archived_file_path = get_cli_archive_path(output_path, is_onedir)
- shutil.make_archive(archived_file_path, _ARCHIVE_FORMAT, input_path)
+ archive_directory(input_path, f'{archived_file_path}.{_ARCHIVE_FORMAT}')
shutil.rmtree(input_path)
else:
file_hash = get_hash_of_file(input_path)