mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-17 20:56:50 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0982fe212 |
No files matched your search
@@ -91,6 +91,15 @@ jobs:
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: packaging/macos/package.sh --arch universal
|
||||
# Unpacks the CLI zip and checks the binary users get: Developer ID, hardened runtime,
|
||||
# timestamp, and Gatekeeper's notarization lookup (codesign --verify, spctl). Also the DMG's
|
||||
# stapled ticket. Strict when the signing secrets are present, warnings otherwise.
|
||||
- name: Verify signatures and notarization
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: packaging/macos/verify.sh --arch universal
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-macos
|
||||
|
||||
@@ -264,6 +264,14 @@ flatpak install --user photocraft-<version>-linux-x86_64.flatpak # or -linux-a
|
||||
flatpak run ai.storyteller.photocraft
|
||||
```
|
||||
|
||||
On macOS, the command-line tool comes as `photocraft-cli-<version>-macos-universal.zip`. The binary is signed with the same Developer ID as the app and notarized by Apple. A bare binary can't carry a stapled notarization ticket the way the DMG does, so the first time you run it macOS checks the notarization online. You can confirm it yourself:
|
||||
|
||||
```sh
|
||||
ditto -x -k photocraft-cli-<version>-macos-universal.zip .
|
||||
spctl --assess --type install -vv photocraft-cli-<version>-macos-universal/photocraft-cli
|
||||
# ... accepted, source=Notarized Developer ID
|
||||
```
|
||||
|
||||
On FreeBSD 14 (x86_64), the release has a tarball laid out like `/usr/local`. Install the runtime libraries, then unpack it there:
|
||||
|
||||
```sh
|
||||
|
||||
+14
-2
@@ -77,8 +77,20 @@ Every binary reports its version, the commit and the build date: `photocraft --v
|
||||
ticket is stapled to the app. The app goes on a DMG (`hdiutil`, with an `Applications` link
|
||||
to drag onto). The DMG is signed, notarized and stapled too. The script checks the results
|
||||
with `codesign --verify --strict`, `stapler validate` and `spctl -a -vvv`.
|
||||
- **CLI:** the universal `photocraft-cli` is signed with the hardened runtime, zipped and
|
||||
notarized. A bare binary can't hold a stapled ticket, so Gatekeeper looks it up online.
|
||||
- **CLI:** the universal `photocraft-cli` is signed with the same Developer ID, the hardened
|
||||
runtime and a secure timestamp (identifier `ai.storyteller.photocraft-cli`), zipped, and the zip
|
||||
is sent to `notarytool`. Only `.app`, `.dmg` and `.pkg` can hold a stapled ticket, not a bare
|
||||
Mach-O, so Gatekeeper looks the CLI's ticket up online the first time a downloaded
|
||||
(quarantined) copy runs. Offline, that first run can be refused until the Mac is online again.
|
||||
- **Verification:** `packaging/macos/verify.sh` checks the artifacts as users download them. It
|
||||
unpacks the CLI zip and requires, for the binary inside, `codesign --verify --strict`, both
|
||||
architectures, the hardened runtime flag, a `Developer ID Application` authority from
|
||||
`APPLE_TEAM_ID`, a timestamp, and `spctl --assess --type install` reporting
|
||||
`source=Notarized Developer ID` (the same online lookup Gatekeeper does). For the DMG it runs
|
||||
`codesign --verify`, `stapler validate` and `spctl --type open`. The release workflow runs it
|
||||
as its own step after packaging. With signing secrets it fails the job on any miss; without
|
||||
them it only checks signature integrity and warns, like `package.sh`. Run it locally after a
|
||||
build: `packaging/macos/verify.sh --arch aarch64`.
|
||||
|
||||
Locally, without certificates, the script signs ad-hoc (`codesign -s -`) and skips notarization.
|
||||
That's enough to check the bundle and the DMG on your own Mac:
|
||||
|
||||
@@ -150,13 +150,16 @@ CLI_DIR="$WORK/photocraft-cli-$VERSION-macos-$ARCH"
|
||||
mkdir -p "$CLI_DIR"
|
||||
cp "$WORK/bin/photocraft-cli" "$CLI_DIR/"
|
||||
copy_docs "$CLI_DIR"
|
||||
sign --options runtime "$CLI_DIR/photocraft-cli"
|
||||
# Same Developer ID and hardened runtime as the app, with an explicit reverse-DNS identifier
|
||||
# (codesign would otherwise use the bare file name).
|
||||
sign --options runtime --identifier ai.storyteller.photocraft-cli "$CLI_DIR/photocraft-cli"
|
||||
codesign --verify --strict --verbose=2 "$CLI_DIR/photocraft-cli"
|
||||
rm -f "$CLI_ZIP"
|
||||
ditto -c -k --keepParent "$CLI_DIR" "$CLI_ZIP"
|
||||
# A bare Mach-O can't carry a stapled ticket; Gatekeeper looks the notarization up online.
|
||||
# notarytool accepts a zip of the bare binary. A bare Mach-O can't carry a stapled ticket, so
|
||||
# Gatekeeper looks the notarization up online when a quarantined copy first runs.
|
||||
if [ "$NOTARIZE" = 1 ]; then notarize "$CLI_ZIP"; fi
|
||||
|
||||
"$WORK/bin/photocraft-cli" --version
|
||||
echo "==> done"
|
||||
echo "==> done (check the artifacts as shipped with packaging/macos/verify.sh --arch $ARCH)"
|
||||
ls -lh "$DMG" "$CLI_ZIP"
|
||||
Executable
+149
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env bash
|
||||
# Check the macOS release artifacts exactly as users download them:
|
||||
#
|
||||
# $DIST/photocraft-<version>-macos-<arch>.dmg signed, notarized, ticket stapled
|
||||
# $DIST/photocraft-cli-<version>-macos-<arch>.zip unpacked here; the binary inside must be
|
||||
# Developer ID signed with the hardened runtime,
|
||||
# timestamped and notarized
|
||||
#
|
||||
# Usage: packaging/macos/verify.sh [--arch universal|aarch64|x86_64]
|
||||
#
|
||||
# With a real signing identity and notarization credentials in the environment (the same
|
||||
# MACOS_SIGN_IDENTITY / APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID that package.sh uses), every
|
||||
# check is strict and a failure exits non-zero. Without them (local ad-hoc builds, or a release run
|
||||
# whose secrets are missing) only the signature's integrity is checked and the rest is a warning,
|
||||
# matching package.sh. Nothing here reads or prints a secret's value; it only tests whether one is set.
|
||||
#
|
||||
# A bare Mach-O can't carry a stapled notarization ticket (only .app, .dmg and .pkg can), so for the
|
||||
# CLI Gatekeeper looks the ticket up online the first time a quarantined copy runs. `spctl --assess
|
||||
# --type install` does that same online lookup here, which is how this script proves the CLI was
|
||||
# notarized.
|
||||
set -euo pipefail
|
||||
# shellcheck source=../env.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
|
||||
|
||||
ARCH=universal
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--arch) ARCH="$2"; shift 2 ;;
|
||||
-h | --help) sed -n '2,20p' "$0"; exit 0 ;;
|
||||
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
case "$ARCH" in
|
||||
universal) WANT_ARCHS="x86_64 arm64" ;;
|
||||
aarch64) WANT_ARCHS="arm64" ;;
|
||||
x86_64) WANT_ARCHS="x86_64" ;;
|
||||
*) echo "unknown --arch $ARCH" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
DMG="$DIST/photocraft-$VERSION-macos-$ARCH.dmg"
|
||||
CLI_ZIP="$DIST/photocraft-cli-$VERSION-macos-$ARCH.zip"
|
||||
IDENTITY="${MACOS_SIGN_IDENTITY:--}"
|
||||
STRICT=0
|
||||
if [ "$IDENTITY" != "-" ] && [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then
|
||||
STRICT=1
|
||||
fi
|
||||
|
||||
FAILED=0
|
||||
fail() {
|
||||
if [ "$STRICT" = 1 ]; then
|
||||
echo "error: $*" >&2
|
||||
FAILED=1
|
||||
else
|
||||
warn "$* (expected without signing secrets)"
|
||||
fi
|
||||
}
|
||||
|
||||
# spctl's online ticket lookup can lag a few seconds behind notarytool's "Accepted".
|
||||
assess() {
|
||||
local out attempt tries=1
|
||||
[ "$STRICT" = 1 ] && tries=5
|
||||
for attempt in $(seq "$tries"); do
|
||||
if out="$(spctl "$@" 2>&1)"; then
|
||||
printf '%s\n' "$out"
|
||||
return 0
|
||||
fi
|
||||
[ "$attempt" = "$tries" ] || sleep 10
|
||||
done
|
||||
printf '%s\n' "$out"
|
||||
return 1
|
||||
}
|
||||
|
||||
for f in "$DMG" "$CLI_ZIP"; do
|
||||
if [ ! -f "$f" ]; then
|
||||
echo "error: missing $f" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
WORK="$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/photocraft-verify.XXXXXX")"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
# ---- CLI zip -----------------------------------------------------------------------------------
|
||||
echo "==> $(basename "$CLI_ZIP")"
|
||||
ditto -x -k "$CLI_ZIP" "$WORK/cli"
|
||||
CLI="$WORK/cli/photocraft-cli-$VERSION-macos-$ARCH/photocraft-cli"
|
||||
if [ ! -x "$CLI" ]; then
|
||||
echo "error: $(basename "$CLI_ZIP") has no executable photocraft-cli-$VERSION-macos-$ARCH/photocraft-cli" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
archs="$(lipo -archs "$CLI")"
|
||||
for a in $WANT_ARCHS; do
|
||||
case " $archs " in
|
||||
*" $a "*) ;;
|
||||
*) echo "error: photocraft-cli lacks $a (has: $archs)" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Integrity first: this must hold even for ad-hoc builds.
|
||||
codesign --verify --strict --verbose=2 "$CLI"
|
||||
details="$(codesign -d --verbose=2 "$CLI" 2>&1)"
|
||||
printf '%s\n' "$details" | grep -E '^(Identifier|Format|CodeDirectory|Authority|Timestamp|TeamIdentifier)[= ]' || true
|
||||
|
||||
printf '%s\n' "$details" | grep -Eq '^CodeDirectory .*flags=0x[0-9a-f]+\([^)]*runtime' \
|
||||
|| fail "photocraft-cli is not signed with the hardened runtime"
|
||||
printf '%s\n' "$details" | grep -q '^Authority=Developer ID Application:' \
|
||||
|| fail "photocraft-cli is not signed with a Developer ID Application identity"
|
||||
printf '%s\n' "$details" | grep -q '^Timestamp=' \
|
||||
|| fail "photocraft-cli signature has no secure timestamp"
|
||||
if [ -n "${APPLE_TEAM_ID:-}" ]; then
|
||||
printf '%s\n' "$details" | grep -qx "TeamIdentifier=$APPLE_TEAM_ID" \
|
||||
|| fail "photocraft-cli is signed by a different team"
|
||||
fi
|
||||
if gk="$(assess --assess --type install -vv "$CLI")"; then
|
||||
printf '%s\n' "$gk"
|
||||
printf '%s\n' "$gk" | grep -q 'source=Notarized Developer ID' \
|
||||
|| fail "Gatekeeper accepts photocraft-cli but not as notarized"
|
||||
else
|
||||
printf '%s\n' "$gk"
|
||||
fail "Gatekeeper rejects photocraft-cli (not notarized?)"
|
||||
fi
|
||||
"$CLI" --version
|
||||
|
||||
# ---- DMG ---------------------------------------------------------------------------------------
|
||||
echo "==> $(basename "$DMG")"
|
||||
codesign --verify --strict --verbose=2 "$DMG"
|
||||
if [ "$STRICT" = 1 ]; then
|
||||
xcrun stapler validate "$DMG" || fail "no notarization ticket stapled to the DMG"
|
||||
assess --assess --type open --context context:primary-signature -vv "$DMG" \
|
||||
|| fail "Gatekeeper rejects the DMG"
|
||||
fi
|
||||
|
||||
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"
|
||||
else
|
||||
result="signature intact; Developer ID and notarization not required (no signing secrets)"
|
||||
fi
|
||||
echo "==> verified: $result"
|
||||
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
|
||||
{
|
||||
echo "- macOS \`$(basename "$DMG")\`, \`$(basename "$CLI_ZIP")\`: $result"
|
||||
} >>"$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
Reference in new issue
Block a user