Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,26 @@ jobs:
- name: Test
run: scripts/coverage.sh

- name: Add the Developer ID certificate to a temporary keychain
env:
DEVELOPER_ID_CERTIFICATE: ${{ secrets.DEVELOPER_ID_CERTIFICATE }}
DEVELOPER_ID_CERTIFICATE_PASSWORD: ${{ secrets.DEVELOPER_ID_CERTIFICATE_PASSWORD }}
run: scripts/add-signing-certificate.sh

- name: Build the universal app
run: UNIVERSAL=1 scripts/build-app.sh

- name: Make the disk image
run: scripts/make-dmg.sh

- name: Notarize the disk image
if: env.RELEASING == 'true'
env:
NOTARY_APPLE_ID: ${{ secrets.NOTARY_APPLE_ID }}
NOTARY_TEAM_ID: ${{ secrets.NOTARY_TEAM_ID }}
NOTARY_PASSWORD: ${{ secrets.NOTARY_PASSWORD }}
run: scripts/notarize.sh "build/Mapsmith-$VERSION.dmg"

- name: Keep the disk image of a trial build
if: env.RELEASING != 'true'
uses: actions/upload-artifact@v7
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ macOS 15 or later.
Download the disk image from the [releases page](https://github.com/scooper4711/mapsmith/releases), open it
and drag Mapsmith to Applications.

The app is not notarized by Apple, so macOS blocks it the first time:
Releases are signed with the Developer ID and notarized by Apple, so macOS opens them like any other app. Older
releases aren't, and macOS blocks them the first time:

1. Open Mapsmith once. macOS says it cannot be opened; click Done.
2. Open System Settings › Privacy & Security and scroll down to the message about Mapsmith.
Expand Down Expand Up @@ -108,8 +109,9 @@ make dmg # packages the app into a disk image
make icon # redraws Resources/AppIcon.icns
```

Building needs Xcode 16 or later (Swift 6 toolchain). The app is ad-hoc signed. Open `Package.swift` in Xcode
to run and debug it.
Building needs Xcode 16 or later (Swift 6 toolchain). `scripts/sign-app.sh` signs the app with the Developer ID when
the keychain holds a Developer ID Application certificate, and ad hoc otherwise. Open `Package.swift` in Xcode to
run and debug it.

Tests that read real maps use PDFs in `TestData/`, which is not committed because the maps are copyrighted.
Copy or link your own Flip-Mat PDFs there to run them; they are skipped when the folder is empty.
Expand Down
13 changes: 12 additions & 1 deletion docs/specs/app-sandbox/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,22 @@
opened from Finder, the Dock, Open Recent and by Scrollkeeper come through
Launch Services, which grants access to them; Open Recent keeps that access
across launches. No network access: Mapsmith makes no requests.
- `scripts/build-app.sh` signs ad hoc with the entitlements and copies
- `scripts/build-app.sh` signs with the entitlements and copies
`Resources/container-migration.plist` into the bundle's Resources. On the
first sandboxed launch macOS moves the preferences
(`io.github.scooper4711.Mapsmith.plist`, window layouts only) into
`~/Library/Containers/io.github.scooper4711.Mapsmith`.
- **Signing.** `scripts/sign-app.sh` signs the app with the first Developer ID
Application certificate in the keychain, with the hardened runtime and a
secure timestamp, or ad hoc when there is none (`SIGNING_IDENTITY` overrides
it). On GitHub, `scripts/add-signing-certificate.sh` puts the certificate from
the `DEVELOPER_ID_CERTIFICATE` and `DEVELOPER_ID_CERTIFICATE_PASSWORD` secrets
into a temporary keychain; without it a release fails and a trial build is
signed ad hoc. For a release, `scripts/notarize.sh` signs the disk image, has
Apple's notary service check it (the `NOTARY_APPLE_ID`, `NOTARY_TEAM_ID` and
`NOTARY_PASSWORD` secrets, or a notarytool keychain profile on a Mac) and
staples the ticket to it. The team ID is kept only in the secrets and the
keychain.
- `DroppedImageStore` writes dropped images under `temporaryDirectory`, which
is inside the container.
- SwiftUI doesn't restore document windows in the sandbox: it would need a
Expand Down
32 changes: 32 additions & 0 deletions scripts/add-signing-certificate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/bin/sh
# On GitHub, puts the Developer ID certificate into a temporary keychain that codesign searches, so that
# scripts/sign-app.sh signs with it. Without the certificate a trial build is signed ad hoc and a release fails.
#
# DEVELOPER_ID_CERTIFICATE the certificate with its private key, as a base64-encoded .p12 file
# DEVELOPER_ID_CERTIFICATE_PASSWORD the .p12 file's password
# RELEASING "true" when building a release
set -eu

if [ -z "${DEVELOPER_ID_CERTIFICATE:-}" ]; then
if [ "${RELEASING:-false}" = "true" ]; then
echo "::error::Signing failed: the DEVELOPER_ID_CERTIFICATE secret is missing."
exit 1
fi
echo "::warning::The DEVELOPER_ID_CERTIFICATE secret is missing, so this trial build is signed ad hoc."
exit 0
fi

KEYCHAIN="$RUNNER_TEMP/signing.keychain-db"
KEYCHAIN_PASSWORD="$(uuidgen)"
CERTIFICATE="$RUNNER_TEMP/developer-id.p12"

printf '%s' "$DEVELOPER_ID_CERTIFICATE" | base64 --decode > "$CERTIFICATE"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security import "$CERTIFICATE" -k "$KEYCHAIN" -P "$DEVELOPER_ID_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
rm "$CERTIFICATE"
# Lets codesign use the private key without a prompt no one is there to answer.
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" >/dev/null
# shellcheck disable=SC2046 # The existing keychains are one quoted path per line, without spaces.
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
5 changes: 2 additions & 3 deletions scripts/build-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ cp Resources/AppIcon.icns "$APP/Contents/Resources/AppIcon.icns"
cp Resources/container-migration.plist "$APP/Contents/Resources/container-migration.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" -c "Set :CFBundleVersion $VERSION" \
"$APP/Contents/Info.plist"
# Ad-hoc signature: enough to run locally; Gatekeeper still asks on other Macs. The entitlements turn on the
# App Sandbox.
codesign --force --sign - --entitlements Resources/Mapsmith.entitlements "$APP"
# The entitlements turn on the App Sandbox.
scripts/sign-app.sh "$APP" Resources/Mapsmith.entitlements
echo "Built $APP ($VERSION)"

if [[ "${1:-}" == "--install" ]]; then
Expand Down
36 changes: 36 additions & 0 deletions scripts/notarize.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/bin/sh
# Signs a disk image with the Developer ID, has Apple notarize it, and staples the ticket to it, so Gatekeeper
# opens the app inside on any Mac without warning. The app must already be signed with the Developer ID.
#
# Usage: scripts/notarize.sh <disk image>
# NOTARY_PROFILE the notarytool keychain profile to use (default tableside-notary), made once with
# xcrun notarytool store-credentials tableside-notary --apple-id <email> --team-id <team ID>
# NOTARY_APPLE_ID, NOTARY_TEAM_ID, NOTARY_PASSWORD used instead when NOTARY_APPLE_ID is set, as on GitHub
set -eu

DMG="$1"
IDENTITY="${SIGNING_IDENTITY:-$(security find-identity -v -p codesigning \
| awk '/"Developer ID Application:/ { print $2; exit }')}"

[ -f "$DMG" ] || { echo "Notarizing failed: $DMG does not exist." >&2; exit 1; }
[ -n "$IDENTITY" ] || {
echo "Notarizing failed: there is no Developer ID Application certificate in the keychain." >&2
exit 1
}

codesign --force --sign "$IDENTITY" --timestamp "$DMG"

if [ -n "${NOTARY_APPLE_ID:-}" ]; then
set -- --apple-id "$NOTARY_APPLE_ID" --team-id "$NOTARY_TEAM_ID" --password "$NOTARY_PASSWORD"
else
set -- --keychain-profile "${NOTARY_PROFILE:-tableside-notary}"
fi
RESULT="$(xcrun notarytool submit "$DMG" "$@" --wait --output-format json)"
STATUS="$(printf '%s' "$RESULT" | plutil -extract status raw -o - -)"
if [ "$STATUS" != "Accepted" ]; then
xcrun notarytool log "$(printf '%s' "$RESULT" | plutil -extract id raw -o - -)" "$@" >&2 || true
echo "Notarizing failed: Apple answered \"$STATUS\" for $DMG." >&2
exit 1
fi
xcrun stapler staple "$DMG"
echo "Notarized $DMG"
24 changes: 24 additions & 0 deletions scripts/sign-app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/sh
# Signs an app bundle with the Developer ID and the hardened runtime when the keychain holds a Developer ID
# Application certificate, as notarizing requires. Without one it signs ad hoc, which runs on this Mac only.
#
# Usage: scripts/sign-app.sh <app> [<entitlements>]
# SIGNING_IDENTITY the identity to sign with instead of the first Developer ID found; "-" signs ad hoc
set -eu

APP="$1"
ENTITLEMENTS="${2:-}"
IDENTITY="${SIGNING_IDENTITY:-$(security find-identity -v -p codesigning \
| awk '/"Developer ID Application:/ { print $2; exit }')}"
IDENTITY="${IDENTITY:--}"

set -- --force --sign "$IDENTITY"
if [ -n "$ENTITLEMENTS" ]; then
set -- "$@" --entitlements "$ENTITLEMENTS"
fi
if [ "$IDENTITY" = "-" ]; then
echo "Signing ad hoc: no Developer ID Application certificate was found or chosen."
else
set -- "$@" --options runtime --timestamp
fi
codesign "$@" "$APP"
Loading