Skip to content

Commit 723fa3d

Browse files
committed
CM-53930: fix onedir signing issues on mac
1 parent 457022c commit 723fa3d

File tree

2 files changed

+76
-5
lines changed

2 files changed

+76
-5
lines changed

.github/workflows/build_executable.yml

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,9 @@ jobs:
136136
# Main executable must be signed last after all its dependencies
137137
find dist/cycode-cli -type f ! -name "cycode-cli" | while read -r file; do
138138
if file -b "$file" | grep -q "Mach-O"; then
139-
codesign --force --sign "$APPLE_CERT_NAME" --timestamp --options runtime "$file"
139+
# override identifier to avoid framework-style identifiers (e.g. org.python.python)
140+
# that cause --strict verification to expect a missing Info.plist
141+
codesign --force --sign "$APPLE_CERT_NAME" --identifier "com.cycode.$(basename "$file")" --timestamp --options runtime "$file"
140142
fi
141143
done
142144
@@ -176,15 +178,35 @@ jobs:
176178
177179
# we can't staple the app because it's executable
178180
181+
- name: Verify macOS code signatures
182+
if: runner.os == 'macOS'
183+
run: |
184+
# verify all Mach-O binaries in the output are properly signed
185+
FAILED=false
186+
while IFS= read -r file; do
187+
if file -b "$file" | grep -q "Mach-O"; then
188+
if ! codesign --verify --strict "$file" 2>&1; then
189+
echo "INVALID signature: $file"
190+
codesign -dv "$file" 2>&1 || true
191+
FAILED=true
192+
fi
193+
fi
194+
done < <(find dist/cycode-cli -type f)
195+
196+
if [ "$FAILED" = true ]; then
197+
echo "Found binaries with invalid signatures!"
198+
exit 1
199+
fi
200+
201+
# verify main executable signature in detail
202+
codesign -dv --verbose=4 $PATH_TO_CYCODE_CLI_EXECUTABLE
203+
179204
- name: Test macOS signed executable
180205
if: runner.os == 'macOS'
181206
run: |
182207
file -b $PATH_TO_CYCODE_CLI_EXECUTABLE
183208
time $PATH_TO_CYCODE_CLI_EXECUTABLE version
184209
185-
# verify signature
186-
codesign -dv --verbose=4 $PATH_TO_CYCODE_CLI_EXECUTABLE
187-
188210
- name: Import cert for Windows and setup envs
189211
if: runner.os == 'Windows'
190212
env:
@@ -236,6 +258,46 @@ jobs:
236258
name: ${{ env.ARTIFACT_NAME }}
237259
path: dist
238260

261+
- name: Verify macOS artifact end-to-end
262+
if: runner.os == 'macOS' && matrix.mode == 'onedir'
263+
uses: actions/download-artifact@v4
264+
with:
265+
name: ${{ env.ARTIFACT_NAME }}
266+
path: /tmp/artifact-verify
267+
268+
- name: Verify macOS artifact signatures and run with quarantine
269+
if: runner.os == 'macOS' && matrix.mode == 'onedir'
270+
run: |
271+
# extract the onedir zip exactly as an end user would
272+
ARCHIVE=$(find /tmp/artifact-verify -name "*.zip" | head -1)
273+
echo "Verifying archive: $ARCHIVE"
274+
ditto -x -k "$ARCHIVE" /tmp/artifact-extracted
275+
276+
# verify all Mach-O code signatures (strict mode)
277+
FAILED=false
278+
while IFS= read -r file; do
279+
if file -b "$file" | grep -q "Mach-O"; then
280+
if ! codesign --verify --strict "$file" 2>&1; then
281+
echo "INVALID: $file"
282+
codesign -dv "$file" 2>&1 || true
283+
FAILED=true
284+
else
285+
echo "OK: $file"
286+
fi
287+
fi
288+
done < <(find /tmp/artifact-extracted -type f)
289+
290+
if [ "$FAILED" = true ]; then
291+
echo "Artifact contains binaries with invalid signatures!"
292+
exit 1
293+
fi
294+
295+
# simulate download quarantine and test execution
296+
find /tmp/artifact-extracted -type f -exec xattr -w com.apple.quarantine "0081;$(printf '%x' $(date +%s));CI;$(uuidgen)" {} \;
297+
EXECUTABLE=$(find /tmp/artifact-extracted -name "cycode-cli" -type f | head -1)
298+
echo "Testing quarantined executable: $EXECUTABLE"
299+
time "$EXECUTABLE" version
300+
239301
- name: Upload files to release
240302
if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish }}
241303
uses: svenstaro/upload-release-action@v2

process_executable_file.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import os
1414
import platform
1515
import shutil
16+
import subprocess
1617
from pathlib import Path
1718
from string import Template
1819
from typing import Union
@@ -140,6 +141,14 @@ def get_cli_archive_path(output_path: Path, is_onedir: bool) -> str:
140141
return os.path.join(output_path, get_cli_archive_filename(is_onedir))
141142

142143

144+
def archive_directory(input_path: Path, output_path: str) -> None:
145+
if get_os_name() == 'darwin':
146+
# use ditto on macOS to preserve code signature metadata
147+
subprocess.run(['ditto', '-c', '-k', str(input_path), output_path], check=True)
148+
else:
149+
shutil.make_archive(output_path.removesuffix(f'.{_ARCHIVE_FORMAT}'), _ARCHIVE_FORMAT, input_path)
150+
151+
143152
def process_executable_file(input_path: Path, is_onedir: bool) -> str:
144153
output_path = input_path.parent
145154
hash_file_path = get_cli_hash_path(output_path, is_onedir)
@@ -150,7 +159,7 @@ def process_executable_file(input_path: Path, is_onedir: bool) -> str:
150159
write_hashes_db_to_file(normalized_hashes, hash_file_path)
151160

152161
archived_file_path = get_cli_archive_path(output_path, is_onedir)
153-
shutil.make_archive(archived_file_path, _ARCHIVE_FORMAT, input_path)
162+
archive_directory(input_path, f'{archived_file_path}.{_ARCHIVE_FORMAT}')
154163
shutil.rmtree(input_path)
155164
else:
156165
file_hash = get_hash_of_file(input_path)

0 commit comments

Comments
 (0)