collective

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

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml--- a/.github/workflows/release.yml+++ b/.github/workflows/release.yml@@ -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:diff --git a/ROADMAP.md b/ROADMAP.md--- a/ROADMAP.md+++ b/ROADMAP.md@@ -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. |diff --git a/docs/releasing.md b/docs/releasing.md--- a/docs/releasing.md+++ b/docs/releasing.md@@ -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): diff --git a/packaging/macos/dmg-test.sh b/packaging/macos/dmg-test.shnew file mode 100755--- /dev/null+++ b/packaging/macos/dmg-test.sh@@ -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'diff --git a/packaging/macos/dmg.sh b/packaging/macos/dmg.shnew file mode 100644--- /dev/null+++ b/packaging/macos/dmg.sh@@ -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"+)diff --git a/packaging/macos/package.sh b/packaging/macos/package.sh--- a/packaging/macos/package.sh+++ b/packaging/macos/package.sh@@ -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"diff --git a/packaging/macos/verify.sh b/packaging/macos/verify.sh--- a/packaging/macos/verify.sh+++ b/packaging/macos/verify.sh@@ -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