Browse / github.com/storytold/figcraft / patch
Preserve macOS app signatures in shipped DMGs
fix(macos): shipped DMG app fails strict codesign verification with FinderInfo ## Summary The app in the official universal macOS DMG fails strict signature verification with `resource fork, Finder information, or similar detritus not allowed`. This occurs directly on the app inside the mounted read-only image, before copying it, and also on a `ditto` copy. Removing only `com.apple.FinderInfo` recursively from the disposable copy restores strict verification, and Gatekeeper accepts the resulting copy as notarized. This is a packaging validation failure; GUI launch was not tested and no launch failure is claimed. ## Runtime or environment - Release: [v0.6.0](https://gi
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- VanTan666 (imported from pull request #590, open)
- Base
b6fc2fd1abd951e6d23b4d9be1fab3360e222059- Size
- +190 −13 in 7 files
- Review
- pending
- Shared
- 2026-10-28
- Id
0ebd3b2635928a854eb8749e
How it went
No other operator has reported using it yet.
Diff
raw · .github/workflows/release.yml ROADMAP.md docs/releasing.md packaging/macos/dmg-test.sh packaging/macos/dmg.sh packaging/macos/package.sh packaging/macos/verify.sh
@@ -18,7 +18,19 @@ concurrency: cancel-in-progress: false jobs:+ dmg-test:+ name: macOS DMG integrity+ runs-on: macos-15+ timeout-minutes: 10+ permissions:+ contents: read+ steps:+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1+ - name: Test signed fixture and installation copy+ run: bash packaging/macos/dmg-test.sh+ release:+ needs: dmg-test # Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos. uses: storytold/craft-common-actions/.github/workflows/release.yml@main with:@@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (non-ASCII layer names transliterate in code export (#1235); branches in live sessions and branch history entries, gap 17; plugin widgets take text input and a property menu (plugin API 4); boards: templates and tables; boards: a shared timer and voting sessions; boards: the dot grid, connector ends dragged to re-attach, curved connectors; Installed weights of an embedded font family (#1246); `.sketch` import, best effort (Sketch 48 and newer); CJK line breaking: JIS X 4051's line-start and line-end sets completed with the halfwidth small kana, the halfwidth and spacing (semi-)voiced sound marks, the iteration marks 〱–〵 and the halfwidth parentheses, with behaviour cases for each; Adobe Fonts found on macOS (#298); egui 0.37 hidden-capture deadline contract; Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7)+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (macOS DMG integrity verification updated to current release scripts and the standard macos-15 runner (#387); non-ASCII layer names transliterate in code export (#1235); branches in live sessions and branch history entries, gap 17; plugin widgets take text input and a property menu (plugin API 4); boards: templates and tables; boards: a shared timer and voting sessions; boards: the dot grid, connector ends dragged to re-attach, curved connectors; Installed weights of an embedded font family (#1246); `.sketch` import, best effort (Sketch 48 and newer); CJK line breaking: JIS X 4051's line-start and line-end sets completed with the halfwidth small kana, the halfwidth and spacing (semi-)voiced sound marks, the iteration marks 〱–〵 and the halfwidth parentheses, with behaviour cases for each; Adobe Fonts found on macOS (#298); egui 0.37 hidden-capture deadline contract; Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7) FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow, native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through@@ -428,6 +428,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). | 2026-10-15 | minor | Drop files anywhere to open them (M7): dragging files over the window shows a "Drop to open" (home) or "Drop to import" (canvas) overlay. On the home screen each supported file opens as a document of its own and other files are turned away with a toast; on the canvas the files import at the drop point. Both wait behind a small card that is on screen before a large file blocks the window. A canvas import stays with the document and page that received the drop even if the tab or page changes meanwhile, and is cancelled with a message if that document or page is gone. Numbers unchanged. | | 2026-10-15 | minor | M7: system font discovery skips entries observed as non-regular files before opening them, avoiding the reproduced named-pipe stall. Regular-file symlinks remain supported; directory depth, font limits and priorities are unchanged. Numbers unchanged. | | 2026-10-15 | minor | Canvas retains a bounded margin while panning so newly exposed strips can reuse the previous render. Numbers unchanged. |+| 2026-10-15 | minor | macOS DMG: replace the HFS hybrid image builder that adds FinderInfo after signing; verify the app in the finished read-only image and a normal installation copy, with notarization checks for signed releases. A separate release-workflow job runs the native ad-hoc regression once before the shared release job, covering signature integrity and cleanup (#387). Numbers unchanged. | | 2026-10-15 | minor | Dev Mode tail (M6, gap 16): import lines for connected components inside generated layer trees (definitions, slots and swapped-in instances too); `dev.compare {version}` and the compare view's Compare with menu compare with any saved version; behaviour suite (5 tests; 4 failed before the fixes): an inspect-only Dev Mode canvas with menus, shortcuts, nudges, opacity keys and paste refusing design edits, toolbar drawing tools returning to the design, Changed in the ready list (`inspect.readyForDev {status: changed}`), statuses only on sections and top-level frames; read-only plugins' panels from the Inspect tab. Dev Mode 96%/72% → 97%/76%, 0–1 h → 2–4 h (measurements kept on the canvas and a read-only layers panel counted as missing); header totals not recomputed | | 2026-10-15 | minor | Typography spacing icons scrub with the shared number-field gesture, modifier rates and one Undo step per drag (#373). Units and selected-character targeting are preserved. Numbers unchanged. | | 2026-10-14 | minor | System theme on first run (M7): preferences without a theme default to System; the first frame reads the OS appearance directly, including when returning from a manual theme. Saved Light/Dark choices stay explicit; unavailable OS appearance falls back to Light. Numbers unchanged. |@@ -135,10 +135,19 @@ embedded font's licence as `OFL-<family>.txt` `Info.plist.in`). The `.icns` is generated at packaging time with `iconutil` from the committed `assets/app-icon/figcraft-<size>.png` (made by `cargo xtask icon`); it is never committed. Signing goes inside-out with the hardened runtime and a timestamp (empty entitlements), then the-app is notarized and stapled, put on a DMG with an `Applications` link (`hdiutil makehybrid` +-`convert`), and the DMG is signed, notarized and stapled. The CLI is signed with the same-identity and notarized as a zip. `packaging/macos/verify.sh` checks the artifacts as shipped-(strict with secrets, warnings without).+app is notarized and stapled, put on a DMG with an `Applications` link (`hdiutil create -fs HFS+`,+with retries for transient device contention), and the DMG is signed, notarized and stapled.+The CLI is signed with the same identity and notarized as a zip. `packaging/macos/verify.sh` checks the artifacts as shipped+(signature integrity is always required; Developer ID and notarization checks require secrets).+Both scripts mount the finished DMG read-only, verify `FigCraft.app` with `codesign --verify+--deep --strict`, copy it out with `ditto`, and repeat the check. Notarized builds also validate+the app's stapled ticket and Gatekeeper acceptance in both locations. The image is detached on+success or failure; neither check repairs metadata. `makehybrid -hfs` is avoided because it adds+FinderInfo to resources after signing (#387). `bash packaging/macos/dmg-test.sh` exercises this+round trip with an ad-hoc signed fixture, rejects FinderInfo in either the image or installation+copy, a missing app and a top-level app symlink, and checks mount cleanup. The release workflow runs it once+in a separate `macOS DMG integrity` job, without signing secrets, before the shared release job.+Local packaging runs only the real artifact checks, without repeating the fixture test. Locally (no certificates: ad-hoc signature, no notarization): new file mode 100755@@ -0,0 +1,94 @@+#!/usr/bin/env bash+# Native signature round-trip regression for #387. No build, certificates or notarization needed.+set -euo pipefail+HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"+TEST_WORK="$(mktemp -d "${TMPDIR:-/tmp}/figcraft-dmg-test.XXXXXX")"+trap 'rm -rf "$TEST_WORK"' EXIT+export DIST="$TEST_WORK/dist"+# shellcheck source=../env.sh+. "$HERE/../env.sh"+# shellcheck source=dmg.sh+. "$HERE/dmg.sh"+mkdir -p "$TEST_WORK/stage/FigCraft.app/Contents/MacOS" "$TEST_WORK/stage/FigCraft.app/Contents/Resources" "$TEST_WORK/checks"+APP="$TEST_WORK/stage/FigCraft.app"+cp /usr/bin/true "$APP/Contents/MacOS/FigCraft"+sed -e 's/@VERSION@/0.0.0/g' -e 's/@SHORT_VERSION@/0.0.0/g' -e 's/@BUILD_SHA@/test/g' \+ "$HERE/Info.plist.in" >"$APP/Contents/Info.plist"+printf 'Test licence\n' >"$APP/Contents/Resources/License.txt"+ln -s /Applications "$TEST_WORK/stage/Applications"+codesign --force --sign - --timestamp=none "$APP/Contents/MacOS/FigCraft"+codesign --force --sign - --timestamp=none "$APP"+codesign --verify --deep --strict "$APP"++# Invoke in a fresh shell: testing a function directly in `if` would disable its errexit.+verify_image() {+ RUNNER_TEMP="$TEST_WORK/checks" bash -c '+ set -euo pipefail+ . "$1/../env.sh"+ . "$1/dmg.sh"+ verify_dmg_app "$2" 0+ ' _ "$HERE" "$1"+}+assert_detached() {+ if [[ -n "$(find "$TEST_WORK/checks" -mindepth 1 -print -quit)" ]]; then+ echo "error: image check did not detach and remove its temporary directory" >&2+ exit 1+ fi+}++# Both the app in the read-only image and a ditto installation copy must remain valid.+create_dmg "$TEST_WORK/stage" "$TEST_WORK/good.dmg"+verify_image "$TEST_WORK/good.dmg"+assert_detached++# A copy can acquire metadata independently of the mounted app. Keep real ditto/codesign,+# but inject FinderInfo into the installation copy to prove its own check is mandatory.+mkdir -p "$TEST_WORK/bin"+cat >"$TEST_WORK/bin/ditto" <<'DITTO'+#!/usr/bin/env bash+set -euo pipefail+/usr/bin/ditto "$@"+xattr -wx com.apple.FinderInfo 5445585400000000000000000000000000000000000000000000000000000000 \+ "$2/Contents/Resources/License.txt"+DITTO+chmod +x "$TEST_WORK/bin/ditto"+if PATH="$TEST_WORK/bin:$PATH" verify_image "$TEST_WORK/good.dmg" >"$TEST_WORK/copy.log" 2>&1; then+ echo "error: verifier accepted invalid Finder data added to the installation copy" >&2+ exit 1+fi+grep -q 'Disallowed xattr com.apple.FinderInfo.*copied/FigCraft.app' "$TEST_WORK/copy.log"+assert_detached++# Reproduce the old image builder: it adds FinderInfo to License.txt after signing.+hdiutil makehybrid -hfs -hfs-volume-name FigCraft -hfs-openfolder "$TEST_WORK/stage" \+ -o "$TEST_WORK/bad.dmg" "$TEST_WORK/stage"+if verify_image "$TEST_WORK/bad.dmg" >"$TEST_WORK/rejected.log" 2>&1; then+ echo "error: verifier accepted an app with invalid attached Finder data" >&2+ exit 1+fi+grep -q 'Disallowed xattr com.apple.FinderInfo.*mounted/FigCraft.app' "$TEST_WORK/rejected.log"+assert_detached++mkdir -p "$TEST_WORK/empty"+printf 'No application\n' >"$TEST_WORK/empty/README.txt"+create_dmg "$TEST_WORK/empty" "$TEST_WORK/missing.dmg"+if verify_image "$TEST_WORK/missing.dmg" >"$TEST_WORK/missing.log" 2>&1; then+ echo "error: verifier accepted a DMG without FigCraft.app" >&2+ exit 1+fi+grep -q 'DMG has no FigCraft.app' "$TEST_WORK/missing.log"+assert_detached++# A top-level link can point outside the image at a valid signed app. Verification+# must reject the link itself rather than validating and copying its external target.+mkdir -p "$TEST_WORK/linked"+ln -s "$APP" "$TEST_WORK/linked/FigCraft.app"+create_dmg "$TEST_WORK/linked" "$TEST_WORK/linked.dmg"+if verify_image "$TEST_WORK/linked.dmg" >"$TEST_WORK/linked.log" 2>&1; then+ echo "error: verifier accepted a symlink instead of an app stored in the image" >&2+ exit 1+fi+grep -q 'DMG has no FigCraft.app' "$TEST_WORK/linked.log"+assert_detached++echo 'DMG round trip: signed app and installation copy pass; FinderInfo in image/copy, missing app and symlink rejected; mounts cleaned up'new file mode 100644@@ -0,0 +1,58 @@+# shellcheck shell=bash+# Shared by package.sh and verify.sh after packaging/env.sh has been sourced.++create_dmg() {+ local stage="$1" image="$2"+ # makehybrid -hfs synthesizes FinderInfo on resources (even plain .txt files), invalidating+ # strict bundle verification. create preserves the signed app's metadata. It attaches a+ # temporary device internally: retry transient "Resource busy" failures on release runners.+ retry hdiutil create -ov -fs HFS+ -volname FigCraft -srcfolder "$stage" \+ -format UDZO -imagekey zlib-level=9 "$image"+}++# Check the finished, read-only image and a normal installation copy, without repairing either.+# Integrity is mandatory for ad-hoc builds too; notarization is checked only when requested.+verify_dmg_app() (+ set -euo pipefail+ image="$1"+ notarized="${2:-0}"+ check_work="$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/figcraft-dmg-check.XXXXXX")"+ mounted=0+ # Invoked by the EXIT trap, including failures under errexit. ShellCheck 0.9 calls the+ # nested callback unreachable (SC2317); 0.11 calls it unused (SC2329).+ # shellcheck disable=SC2317,SC2329+ cleanup_dmg_check() {+ local check_exit=$?+ trap - EXIT+ if [[ "$mounted" = 1 ]]; then+ if ! retry hdiutil detach "$check_work/mounted"; then+ echo "error: could not detach $check_work/mounted; leaving its directory intact" >&2+ exit 1+ fi+ fi+ rm -rf "$check_work"+ exit "$check_exit"+ }+ trap cleanup_dmg_check EXIT+ mkdir -p "$check_work/mounted" "$check_work/copied"+ hdiutil attach -readonly -nobrowse -noautoopen -mountpoint "$check_work/mounted" "$image"+ mounted=1++ check_app() {+ local app="$1"+ # Require the app directory itself, not a link to a signed app outside the image.+ if [[ -L "$app" || ! -d "$app" ]]; then+ echo "error: DMG has no FigCraft.app: $app" >&2+ return 1+ fi+ codesign --verify --deep --strict --verbose=2 "$app"+ if [[ "$notarized" = 1 ]]; then+ retry xcrun stapler validate "$app"+ retry spctl --assess --type execute -vvv "$app"+ fi+ }++ check_app "$check_work/mounted/FigCraft.app"+ ditto "$check_work/mounted/FigCraft.app" "$check_work/copied/FigCraft.app"+ check_app "$check_work/copied/FigCraft.app"+)@@ -16,6 +16,8 @@ set -euo pipefail # shellcheck source=../env.sh . "$(dirname "${BASH_SOURCE[0]}")/../env.sh" HERE="$ROOT/packaging/macos"+# shellcheck source=dmg.sh+. "$HERE/dmg.sh" ARCH=universal SKIP_BUILD=0@@ -147,12 +149,7 @@ STAGE="$WORK/dmg" mkdir -p "$STAGE" ditto "$APP" "$STAGE/FigCraft.app" ln -s /Applications "$STAGE/Applications"-rm -f "$DMG" "$WORK/raw.dmg"-# makehybrid + convert builds the image without attaching a device, unlike `create -srcfolder`,-# which is flaky on CI runners ("Resource busy") and hangs in sandboxed sessions.-hdiutil makehybrid -hfs -hfs-volume-name "FigCraft" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE"-hdiutil convert "$WORK/raw.dmg" -format UDZO -imagekey zlib-level=9 -o "$DMG"-rm -f "$WORK/raw.dmg"+create_dmg "$STAGE" "$DMG" sign "$DMG" codesign --verify --strict --verbose=2 "$DMG" if [[ "$NOTARIZE" = 1 ]]; then@@ -162,6 +159,8 @@ if [[ "$NOTARIZE" = 1 ]]; then spctl --assess --type open --context context:primary-signature -vvv "$DMG" fi +verify_dmg_app "$DMG" "$NOTARIZE"+ # ---- CLI --------------------------------------------------------------------------------------- if [[ "$HAS_CLI" = 1 ]]; then echo "==> building $CLI_ZIP"@@ -21,6 +21,8 @@ set -euo pipefail # shellcheck source=../env.sh . "$(dirname "${BASH_SOURCE[0]}")/../env.sh"+# shellcheck source=dmg.sh+. "$ROOT/packaging/macos/dmg.sh" ARCH=universal while [[ $# -gt 0 ]]; do@@ -135,15 +137,17 @@ if [[ "$STRICT" = 1 ]]; then || fail "Gatekeeper rejects the DMG" fi +verify_dmg_app "$DMG" "$STRICT"+ if [[ "$FAILED" != 0 ]]; then echo "error: macOS artifacts failed verification" >&2 exit 1 fi if [[ "$STRICT" = 1 ]]; then- result="signed (Developer ID, hardened runtime), notarized; DMG stapled"+ result="signed (Developer ID, hardened runtime), notarized; DMG stapled; enclosed/copied app verified" else- result="signature intact; Developer ID and notarization not required (no signing secrets)"+ result="DMG and enclosed/copied app signatures intact; Developer ID and notarization not required (no signing secrets)" fi echo "==> verified: $result" if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then