diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1c28a2f..bfba8c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -117,17 +117,23 @@ jobs: if: steps.ver.outputs.skip != 'true' run: swift test - - name: Build universal .app + - name: Build signed + notarized DMG (app with embedded extension) if: steps.ver.outputs.skip != 'true' env: VERSION: ${{ steps.ver.outputs.value }} - run: ./scripts/build-app.sh - - - name: Build DMG - if: steps.ver.outputs.skip != 'true' - env: - VERSION: ${{ steps.ver.outputs.value }} - run: ./scripts/make-dmg.sh + # Signing/notarization secrets (see docs/SIGNING-SECRETS.md). When unset, the script + # falls back to ad-hoc and skips notarization — the build still succeeds, but the + # released app's extension will not register on other Macs. + MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }} + MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + NOTARY_APPLE_ID: ${{ secrets.NOTARY_APPLE_ID }} + NOTARY_TEAM_ID: ${{ secrets.NOTARY_TEAM_ID }} + NOTARY_PASSWORD: ${{ secrets.NOTARY_PASSWORD }} + run: | + sudo xcode-select -s "$(ls -d /Applications/Xcode*.app | sort -V | tail -1)" + brew list xcodegen >/dev/null 2>&1 || brew install xcodegen + ./scripts/package-signed.sh - name: Build plugin package (.appex / .radioplugin) if: steps.ver.outputs.skip != 'true' diff --git a/docs/SIGNING-SECRETS.md b/docs/SIGNING-SECRETS.md new file mode 100644 index 0000000..9f4e82f --- /dev/null +++ b/docs/SIGNING-SECRETS.md @@ -0,0 +1,44 @@ +# Release signing secrets (LP-700-App) + +The release workflow embeds the LP-700 ExtensionKit extension into `LP-700-App.app`, signs it +with Developer ID, and notarizes the DMG — so an installed app registers its extension for the +Amateur Radio Suite to host (see the suite's `docs/EXTENSIONKIT.md`). + +This only happens when these **GitHub Actions secrets** are set on this repo +(*Settings → Secrets and variables → Actions*). Without them, the release still builds but is +**ad-hoc signed and not notarized** — fine to smoke-test, but its extension won't register on +another Mac. + +## Secrets + +| Secret | What it is | +|--------|------------| +| `MACOS_CERT_P12_BASE64` | LP-700's **Developer ID Application** cert+key exported as a `.p12`, base64-encoded | +| `MACOS_CERT_PASSWORD` | the password set when exporting that `.p12` | +| `KEYCHAIN_PASSWORD` | any value — password for the throwaway CI keychain | +| `NOTARY_APPLE_ID` | Apple ID email used for notarization | +| `NOTARY_TEAM_ID` | the team id (`Y6FT52BKDA`) | +| `NOTARY_PASSWORD` | an **app-specific password** for that Apple ID ([appleid.apple.com](https://appleid.apple.com) → Sign-In & Security → App-Specific Passwords) | + +> This repo uses its **own** Developer ID cert (one per app, all under the same team), so revoking +> it won't affect the other apps. In the fresh CI keychain only this cert is present, so signing +> selects it unambiguously by name. + +## Exporting the `.p12` (one time, on the Mac that has the cert) + +Keychain Access → **login** keychain → **My Certificates** → find LP-700's +`Developer ID Application` (its private key is named e.g. `ARS LP-700`) → right-click → +**Export…** → `.p12`, set a password (that's `MACOS_CERT_PASSWORD`). Then: + +```sh +base64 -i LP-700.p12 | pbcopy # paste as MACOS_CERT_P12_BASE64 +``` + +(Tip: the LP-700 cert's SHA-1 is `A59B8647CA9706C6E8CDBB461C9801CF715132C4` if you need to pick it +among several in Keychain Access.) + +## Notarization alternative (App Store Connect API key) + +Instead of Apple ID + app-specific password you may prefer an ASC API key (no 2FA/expiry issues). +If you want that, say so and the workflow can switch to `--key/--key-id/--issuer` with +`NOTARY_API_KEY` / `NOTARY_API_KEY_ID` / `NOTARY_API_ISSUER` secrets. diff --git a/scripts/package-signed.sh b/scripts/package-signed.sh new file mode 100755 index 0000000..f2a9ec8 --- /dev/null +++ b/scripts/package-signed.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Build LP-700-App.app with the LP700 ExtensionKit extension EMBEDDED and Developer-ID +# signed, then build the DMG and (when notary creds are present) notarize + staple it. +# +# This is what makes an installed app register its extension for the Amateur Radio Suite to +# host (see the suite's docs/EXTENSIONKIT.md). Used by the release workflow; also runnable +# locally. +# +# Signing is GATED on secrets — with none set it falls back to ad-hoc so the build still +# succeeds (but an ad-hoc app's extension will NOT register on another Mac). +# +# VERSION=0.1.8 ./scripts/package-signed.sh +# +# Env — signing (from CI secrets): +# MACOS_CERT_P12_BASE64 base64 of the LP-700 Developer ID Application .p12 (cert + key) +# MACOS_CERT_PASSWORD the .p12 export password +# KEYCHAIN_PASSWORD password for the temp keychain (any value) +# Env — notarization (optional; needs the signing cert too): +# NOTARY_APPLE_ID Apple ID email +# NOTARY_TEAM_ID team id (Y6FT52BKDA) +# NOTARY_PASSWORD app-specific password for that Apple ID +set -euo pipefail +cd "$(dirname "$0")/.." + +VERSION="${VERSION:-0.0.0-dev}" +APP="dist/LP-700-App.app" +APPEX_NAME="LP700Extension.appex" +ENTITLEMENTS="Xcode/Extension/LP700.entitlements" +PROJECT="Xcode/LP700Plugin.xcodeproj" +SCHEME="LP700Extension" +TMP="${RUNNER_TEMP:-$(mktemp -d)}" +# SwiftPM's bare-repo cache trips a common global git setting under xcodebuild. +export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.bareRepository GIT_CONFIG_VALUE_0=all + +echo "==> Building standalone app (v$VERSION)" +VERSION="$VERSION" ./scripts/build-app.sh + +echo "==> Building the extension (.appex)" +( cd Xcode && xcodegen generate >/dev/null ) +DERIVED="$(mktemp -d)" +xcodebuild -project "$PROJECT" -scheme "$SCHEME" -configuration Release \ + -destination 'platform=macOS' -derivedDataPath "$DERIVED" CODE_SIGNING_ALLOWED=NO build >/dev/null +APPEX="$(find "$DERIVED/Build/Products" -name "$APPEX_NAME" | head -1)" + +echo "==> Embedding $APPEX_NAME under Contents/Extensions/" +mkdir -p "$APP/Contents/Extensions" +rm -rf "$APP/Contents/Extensions/$APPEX_NAME" +cp -R "$APPEX" "$APP/Contents/Extensions/" +rm -rf "$DERIVED" + +# --- import the signing cert into a throwaway keychain (CI) ----------------------------- +IDENTITY="" +if [ -n "${MACOS_CERT_P12_BASE64:-}" ]; then + echo "==> Importing Developer ID certificate into a temporary keychain" + KC="$TMP/ars-signing.keychain-db" + KCPW="${KEYCHAIN_PASSWORD:-ars-ci-temp}" + security create-keychain -p "$KCPW" "$KC" + security set-keychain-settings -lut 21600 "$KC" + security unlock-keychain -p "$KCPW" "$KC" + echo "$MACOS_CERT_P12_BASE64" | base64 --decode > "$TMP/cert.p12" + security import "$TMP/cert.p12" -k "$KC" -P "${MACOS_CERT_PASSWORD:-}" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple: -s -k "$KCPW" "$KC" >/dev/null + # Make the temp keychain searchable (keep the existing ones too). + security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"') + rm -f "$TMP/cert.p12" + # In the fresh keychain there is exactly one Developer ID identity, so name-selection is unambiguous. + IDENTITY="$(security find-identity -v -p codesigning "$KC" | sed -n 's/.*"\(Developer ID Application: .*\)"/\1/p' | head -1)" +fi + +# --- sign inside-out (extension first, then the app) ------------------------------------ +if [ -n "$IDENTITY" ]; then + echo "==> Signing with: $IDENTITY" + codesign --force -s "$IDENTITY" -o runtime --timestamp \ + --entitlements "$ENTITLEMENTS" "$APP/Contents/Extensions/$APPEX_NAME" + codesign --force -s "$IDENTITY" -o runtime --timestamp "$APP" +else + echo "==> WARNING: no MACOS_CERT_P12_BASE64 — ad-hoc signing (extension will NOT register on other Macs)" + codesign --force -s - --deep "$APP" +fi +codesign --verify --strict --verbose=2 "$APP" + +echo "==> Building DMG" +VERSION="$VERSION" ./scripts/make-dmg.sh +DMG="dist/LP-700-App-${VERSION}.dmg" + +# --- notarize + staple the DMG ---------------------------------------------------------- +if [ -n "$IDENTITY" ] && [ -n "${NOTARY_APPLE_ID:-}" ] && [ -n "${NOTARY_PASSWORD:-}" ]; then + echo "==> Notarizing $DMG (a few minutes)…" + xcrun notarytool submit "$DMG" \ + --apple-id "$NOTARY_APPLE_ID" --team-id "${NOTARY_TEAM_ID:-}" --password "$NOTARY_PASSWORD" --wait + xcrun stapler staple "$DMG" + echo "==> Notarized + stapled." +else + echo "==> Skipping notarization (no NOTARY_* secrets) — DMG signed but not notarized." +fi +echo "==> Done: $DMG"