mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-08-24 02:57:43 +02:00
Sign and notarize macOS builds
jpackage cannot produce a distributable macOS bundle on its own. It ad-hoc signs the embedded runtime and then re-runs codesign on the same files without --force, which codesign rejects; and "--type dmg --app-image" re-signs the app it is handed, replacing a Developer ID signature with an ad-hoc one. So the build now creates an unsigned app-image, signs it from the inside out, and wraps it with hdiutil. Apple's notary service also unpacks JARs and checks the native libraries inside them, which sqlite-jdbc ships for both architectures. Those are signed before the bundle is sealed, since rewriting a JAR afterwards would invalidate the seal. A preflight check verifies Apple's two criteria locally, so a missed binary costs seconds rather than a round trip to the notary service. Two long-standing defects surfaced while testing and are fixed here: the bundle identifier defaulted to the main class's package name (kst4contest.view instead of de.x08.KST4Contest), and every release reported version 1.0 in Finder because --app-version was never passed. Neither affects existing users: the app keeps its settings in ~/.praktiKST, independent of the bundle ID. Both workflows call the same script the local Mac uses, so the two cannot drift apart. Signing needs a keychain that can answer a UI prompt, which a runner cannot, so ci-import-cert.sh creates a throwaway keychain whose password is generated per job and discarded with it. Notarization goes through an App Store Connect API key and needs no keychain at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WYcmHra3YndA1ahkHeNdJ2
This commit is contained in:
@@ -7,6 +7,7 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- "src/**"
|
- "src/**"
|
||||||
- "packaging/icons/**"
|
- "packaging/icons/**"
|
||||||
|
- "packaging/macos/**"
|
||||||
- "pom.xml"
|
- "pom.xml"
|
||||||
- "mvnw"
|
- "mvnw"
|
||||||
- "mvnw.cmd"
|
- "mvnw.cmd"
|
||||||
@@ -634,34 +635,38 @@ jobs:
|
|||||||
- name: Ensure mvnw is executable
|
- name: Ensure mvnw is executable
|
||||||
run: chmod +x mvnw
|
run: chmod +x mvnw
|
||||||
|
|
||||||
- name: Build JAR and copy runtime dependencies
|
- name: Import signing certificate
|
||||||
run: |
|
|
||||||
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
|
||||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
|
||||||
|
|
||||||
- name: Build macOS DMG with jpackage
|
|
||||||
run: |
|
|
||||||
mkdir -p dist
|
|
||||||
ADD_MODULES="$(java packaging/AddModules.java)"
|
|
||||||
jpackage \
|
|
||||||
--type dmg \
|
|
||||||
--name KST4Contest \
|
|
||||||
--icon packaging/icons/kst4contest.icns \
|
|
||||||
--input target/dist-libs \
|
|
||||||
--main-jar app.jar \
|
|
||||||
--main-class kst4contest.view.Kst4ContestApplication \
|
|
||||||
--module-path target/dist-libs \
|
|
||||||
--add-modules "$ADD_MODULES" \
|
|
||||||
--dest dist
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MACOSX_DEPLOYMENT_TARGET: "13.0"
|
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
|
||||||
|
MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
|
||||||
|
run: ./packaging/macos/ci-import-cert.sh
|
||||||
|
|
||||||
|
# Builds the jar, signs the app bundle and every native library inside it,
|
||||||
|
# wraps it into a DMG and has Apple notarize the result. Same script the
|
||||||
|
# local Mac uses, so the two cannot drift apart.
|
||||||
|
- name: Build signed and notarized DMG
|
||||||
|
env:
|
||||||
|
MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
|
||||||
|
NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
|
||||||
|
NOTARY_ISSUER: ${{ secrets.MACOS_NOTARY_ISSUER }}
|
||||||
|
run: |
|
||||||
|
printf '%s' "$MACOS_NOTARY_KEY" | base64 --decode > "$RUNNER_TEMP/notary.p8"
|
||||||
|
export NOTARY_KEY="$RUNNER_TEMP/notary.p8"
|
||||||
|
./packaging/macos/build-signed-dmg.sh
|
||||||
|
|
||||||
|
- name: Remove signing credentials
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
rm -f "$RUNNER_TEMP/notary.p8"
|
||||||
|
if [ -n "${SIGNING_KEYCHAIN:-}" ]; then
|
||||||
|
security delete-keychain "$SIGNING_KEYCHAIN" || true
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Rename DMG artifact
|
- name: Rename DMG artifact
|
||||||
run: |
|
run: |
|
||||||
DMG=$(ls dist/*.dmg | head -n 1)
|
DMG=$(ls dist/*.dmg | head -n 1)
|
||||||
if [ -z "$DMG" ]; then
|
if [ -z "$DMG" ]; then
|
||||||
echo "No DMG produced by jpackage" && exit 1
|
echo "No DMG produced by the build" && exit 1
|
||||||
fi
|
fi
|
||||||
mv "$DMG" "dist/${ASSET_BASENAME}-macos-${ARCH}.dmg"
|
mv "$DMG" "dist/${ASSET_BASENAME}-macos-${ARCH}.dmg"
|
||||||
|
|
||||||
|
|||||||
@@ -522,35 +522,39 @@ jobs:
|
|||||||
- name: Ensure mvnw is executable
|
- name: Ensure mvnw is executable
|
||||||
run: chmod +x mvnw
|
run: chmod +x mvnw
|
||||||
|
|
||||||
- name: Build JAR and copy runtime dependencies
|
- name: Import signing certificate
|
||||||
run: |
|
|
||||||
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
|
||||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
|
||||||
|
|
||||||
- name: Build macOS DMG with jpackage
|
|
||||||
run: |
|
|
||||||
mkdir -p dist
|
|
||||||
ADD_MODULES="$(java packaging/AddModules.java)"
|
|
||||||
jpackage \
|
|
||||||
--type dmg \
|
|
||||||
--name KST4Contest \
|
|
||||||
--icon packaging/icons/kst4contest.icns \
|
|
||||||
--input target/dist-libs \
|
|
||||||
--main-jar app.jar \
|
|
||||||
--main-class kst4contest.view.Kst4ContestApplication \
|
|
||||||
--module-path target/dist-libs \
|
|
||||||
--add-modules "$ADD_MODULES" \
|
|
||||||
--dest dist
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MACOSX_DEPLOYMENT_TARGET: "13.0"
|
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
|
||||||
|
MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
|
||||||
|
run: ./packaging/macos/ci-import-cert.sh
|
||||||
|
|
||||||
|
# Builds the jar, signs the app bundle and every native library inside it,
|
||||||
|
# wraps it into a DMG and has Apple notarize the result. Same script the
|
||||||
|
# local Mac uses, so the two cannot drift apart.
|
||||||
|
- name: Build signed and notarized DMG
|
||||||
|
env:
|
||||||
|
MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
|
||||||
|
NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
|
||||||
|
NOTARY_ISSUER: ${{ secrets.MACOS_NOTARY_ISSUER }}
|
||||||
|
run: |
|
||||||
|
printf '%s' "$MACOS_NOTARY_KEY" | base64 --decode > "$RUNNER_TEMP/notary.p8"
|
||||||
|
export NOTARY_KEY="$RUNNER_TEMP/notary.p8"
|
||||||
|
./packaging/macos/build-signed-dmg.sh
|
||||||
|
|
||||||
|
- name: Remove signing credentials
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
rm -f "$RUNNER_TEMP/notary.p8"
|
||||||
|
if [ -n "${SIGNING_KEYCHAIN:-}" ]; then
|
||||||
|
security delete-keychain "$SIGNING_KEYCHAIN" || true
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Rename DMG artifact
|
- name: Rename DMG artifact
|
||||||
run: |
|
run: |
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
DMG=$(ls dist/*.dmg | head -n 1)
|
DMG=$(ls dist/*.dmg | head -n 1)
|
||||||
if [ -z "$DMG" ]; then
|
if [ -z "$DMG" ]; then
|
||||||
echo "No DMG produced by jpackage" && exit 1
|
echo "No DMG produced by the build" && exit 1
|
||||||
fi
|
fi
|
||||||
mv "$DMG" "dist/KST4Contest-${{ github.ref_name }}-macos-${ARCH}.dmg"
|
mv "$DMG" "dist/KST4Contest-${{ github.ref_name }}-macos-${ARCH}.dmg"
|
||||||
|
|
||||||
|
|||||||
@@ -44,3 +44,6 @@ website/_site/
|
|||||||
|
|
||||||
# Local secrets for act testing
|
# Local secrets for act testing
|
||||||
.secrets
|
.secrets
|
||||||
|
|
||||||
|
# Apple notarization private keys - never commit these
|
||||||
|
*.p8
|
||||||
|
|||||||
Executable
+265
@@ -0,0 +1,265 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Local signed (and optionally notarized) macOS build.
|
||||||
|
#
|
||||||
|
# jpackage cannot sign the app itself: it ad-hoc signs the embedded runtime and
|
||||||
|
# then re-runs codesign on the same files without --force, which codesign
|
||||||
|
# rejects with "is already signed". So this builds an unsigned app-image, signs
|
||||||
|
# it from the inside out ourselves, and only then wraps it into a DMG.
|
||||||
|
#
|
||||||
|
# Required:
|
||||||
|
# SIGNING_IDENTITY The name part of the Developer ID Application certificate,
|
||||||
|
# without the "Developer ID Application: " prefix. Example:
|
||||||
|
# SIGNING_IDENTITY="Philipp Wagner (ABCDE12345)"
|
||||||
|
# List available ones with:
|
||||||
|
# security find-identity -v -p codesigning
|
||||||
|
#
|
||||||
|
# Optional:
|
||||||
|
# Notarization, either as three separate values (what CI uses)...
|
||||||
|
# NOTARY_KEY Path to the App Store Connect .p8 private key
|
||||||
|
# NOTARY_KEY_ID The key's ID, also part of the .p8 filename
|
||||||
|
# NOTARY_ISSUER The issuer UUID, shown above the key list in the portal
|
||||||
|
# ...or as a keychain profile previously created with
|
||||||
|
# NOTARY_PROFILE xcrun notarytool store-credentials <name>
|
||||||
|
#
|
||||||
|
# With neither, the build is signed but not notarized -- enough to test
|
||||||
|
# locally, not enough to distribute.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
REPO_ROOT="$PWD"
|
||||||
|
|
||||||
|
BUNDLE_ID="de.x08.KST4Contest"
|
||||||
|
ENTITLEMENTS="packaging/macos/kst4contest.entitlements"
|
||||||
|
|
||||||
|
if [ -z "${SIGNING_IDENTITY:-}" ]; then
|
||||||
|
echo "SIGNING_IDENTITY is not set. Available signing identities:" >&2
|
||||||
|
security find-identity -v -p codesigning >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
FULL_IDENTITY="Developer ID Application: $SIGNING_IDENTITY"
|
||||||
|
|
||||||
|
# notarytool takes either an API key triple or a stored keychain profile. The
|
||||||
|
# triple needs no keychain at all, which is why CI uses it.
|
||||||
|
NOTARY_ARGS=()
|
||||||
|
if [ -n "${NOTARY_KEY:-}" ] && [ -n "${NOTARY_KEY_ID:-}" ] && [ -n "${NOTARY_ISSUER:-}" ]; then
|
||||||
|
NOTARY_ARGS=(--key "$NOTARY_KEY" --key-id "$NOTARY_KEY_ID" --issuer "$NOTARY_ISSUER")
|
||||||
|
elif [ -n "${NOTARY_PROFILE:-}" ]; then
|
||||||
|
NOTARY_ARGS=(--keychain-profile "$NOTARY_PROFILE")
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Building JAR and collecting runtime dependencies"
|
||||||
|
chmod +x mvnw
|
||||||
|
./mvnw -B -DskipTests package \
|
||||||
|
dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||||
|
JAR="$(ls -t target/praktiKST-*.jar | head -n 1)"
|
||||||
|
cp "$JAR" target/dist-libs/app.jar
|
||||||
|
|
||||||
|
# jpackage only accepts a numeric major[.minor[.patch]] as the macOS bundle
|
||||||
|
# version, so a Maven qualifier like "-nightly" has to be trimmed off.
|
||||||
|
POM_VERSION="${JAR##*/praktiKST-}"
|
||||||
|
POM_VERSION="${POM_VERSION%.jar}"
|
||||||
|
APP_VERSION="$(printf '%s' "$POM_VERSION" | sed -e 's/[^0-9.].*$//' -e 's/\.*$//')"
|
||||||
|
[ -n "$APP_VERSION" ] || { echo "Could not derive app version from $JAR" >&2; exit 1; }
|
||||||
|
echo "==> Version: $POM_VERSION -> bundle version $APP_VERSION"
|
||||||
|
|
||||||
|
echo "==> Step 1/4: jpackage app-image (unsigned)"
|
||||||
|
rm -rf dist
|
||||||
|
mkdir -p dist
|
||||||
|
ADD_MODULES="$(java packaging/AddModules.java)"
|
||||||
|
|
||||||
|
MACOSX_DEPLOYMENT_TARGET="13.0" jpackage \
|
||||||
|
--type app-image \
|
||||||
|
--name KST4Contest \
|
||||||
|
--app-version "$APP_VERSION" \
|
||||||
|
--icon packaging/icons/kst4contest.icns \
|
||||||
|
--input target/dist-libs \
|
||||||
|
--main-jar app.jar \
|
||||||
|
--main-class kst4contest.view.Kst4ContestApplication \
|
||||||
|
--module-path target/dist-libs \
|
||||||
|
--add-modules "$ADD_MODULES" \
|
||||||
|
--mac-package-identifier "$BUNDLE_ID" \
|
||||||
|
--mac-package-name KST4Contest \
|
||||||
|
--dest dist/appimage
|
||||||
|
|
||||||
|
APP="dist/appimage/KST4Contest.app"
|
||||||
|
[ -d "$APP" ] || { echo "jpackage produced no app image" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "==> Step 2/4: signing bundle contents (this takes a few minutes)"
|
||||||
|
|
||||||
|
# Apple's notary service unpacks JARs and checks the native libraries inside
|
||||||
|
# them. sqlite-jdbc ships libsqlitejdbc.dylib for both architectures that way,
|
||||||
|
# and an unsigned binary in there fails the whole submission. So sign those
|
||||||
|
# first: the app bundle's seal covers Contents/app, and rewriting a JAR
|
||||||
|
# afterwards would invalidate it.
|
||||||
|
echo " scanning jars for native libraries"
|
||||||
|
find "$APP/Contents/app" -name '*.jar' -type f | while read -r JARPATH; do
|
||||||
|
# Only unpack jars that can plausibly hold a native library. Note the
|
||||||
|
# plain grep: "grep -q" exits at the first match, which hands unzip a
|
||||||
|
# SIGPIPE, and under "set -o pipefail" that failure becomes the pipeline's
|
||||||
|
# status -- inverting this very test.
|
||||||
|
if ! unzip -l "$JARPATH" | grep -E '\.(dylib|jnilib|so)$' >/dev/null; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
JARABS="$(cd "$(dirname "$JARPATH")" && pwd)/$(basename "$JARPATH")"
|
||||||
|
JARTMP="$(mktemp -d)"
|
||||||
|
unzip -q "$JARABS" -d "$JARTMP"
|
||||||
|
|
||||||
|
NATIVES="$(mktemp)"
|
||||||
|
( cd "$JARTMP" && find . -type f \( -name '*.dylib' -o -name '*.jnilib' -o -name '*.so' \) \
|
||||||
|
| while read -r n; do
|
||||||
|
if [ "$(file --mime-type -b "$n")" = "application/x-mach-binary" ]; then
|
||||||
|
printf '%s\n' "${n#./}"
|
||||||
|
fi
|
||||||
|
done ) > "$NATIVES"
|
||||||
|
|
||||||
|
if [ -s "$NATIVES" ]; then
|
||||||
|
echo " $(basename "$JARPATH"): $(wc -l < "$NATIVES" | tr -d ' ') native lib(s)"
|
||||||
|
( cd "$JARTMP" && xargs -I {} codesign --force --timestamp --options runtime \
|
||||||
|
--sign "$FULL_IDENTITY" {} < "$NATIVES" )
|
||||||
|
# Update in place rather than repacking, so the rest of the jar --
|
||||||
|
# manifest, module descriptor, entry order -- stays byte for byte.
|
||||||
|
( cd "$JARTMP" && xargs jar --update --file "$JARABS" < "$NATIVES" )
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$JARTMP" "$NATIVES"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Every Mach-O file has to carry its own signature before the enclosing bundle
|
||||||
|
# can be sealed, so collect them first. jpackage leaves them ad-hoc signed,
|
||||||
|
# hence --force on every call.
|
||||||
|
# file(1) pads its output into columns when given several arguments at once,
|
||||||
|
# so ask it one file at a time with -b and get an unambiguous answer.
|
||||||
|
MACHO_LIST="$(mktemp)"
|
||||||
|
find "$APP" -type f -print0 | while IFS= read -r -d '' f; do
|
||||||
|
case "$(file --mime-type -b "$f")" in
|
||||||
|
application/x-mach-binary) printf '%s\n' "$f" ;;
|
||||||
|
esac
|
||||||
|
done > "$MACHO_LIST"
|
||||||
|
|
||||||
|
COUNT="$(wc -l < "$MACHO_LIST" | tr -d ' ')"
|
||||||
|
echo " $COUNT Mach-O files to sign"
|
||||||
|
|
||||||
|
# codesign contacts Apple's timestamp server on every call, so run a handful in
|
||||||
|
# parallel or this takes far longer than it needs to.
|
||||||
|
xargs -P 8 -I {} codesign --force --timestamp --options runtime \
|
||||||
|
--sign "$FULL_IDENTITY" {} < "$MACHO_LIST"
|
||||||
|
rm -f "$MACHO_LIST"
|
||||||
|
|
||||||
|
# The embedded JDK is a bundle in its own right and must be sealed before the
|
||||||
|
# app that contains it.
|
||||||
|
echo " sealing embedded runtime"
|
||||||
|
codesign --force --timestamp --options runtime \
|
||||||
|
--sign "$FULL_IDENTITY" "$APP/Contents/runtime"
|
||||||
|
|
||||||
|
# Entitlements go on the outermost bundle: the hardened runtime derives the
|
||||||
|
# process's entitlements from the main executable's signature.
|
||||||
|
echo " sealing app bundle"
|
||||||
|
codesign --force --timestamp --options runtime \
|
||||||
|
--entitlements "$ENTITLEMENTS" \
|
||||||
|
--sign "$FULL_IDENTITY" "$APP"
|
||||||
|
|
||||||
|
# Apple rejects the whole submission over a single unsigned native library, and
|
||||||
|
# a round trip to the notary service costs minutes. Check its two criteria --
|
||||||
|
# a Developer ID authority and a secure timestamp -- locally first.
|
||||||
|
echo " preflight: verifying native libraries inside jars"
|
||||||
|
PREFLIGHT_ERRORS="$(mktemp)"
|
||||||
|
find "$APP/Contents/app" -name '*.jar' -type f | while read -r JARPATH; do
|
||||||
|
if ! unzip -l "$JARPATH" | grep -E '\.(dylib|jnilib|so)$' >/dev/null; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
CHECKTMP="$(mktemp -d)"
|
||||||
|
unzip -q "$JARPATH" -d "$CHECKTMP"
|
||||||
|
find "$CHECKTMP" -type f \( -name '*.dylib' -o -name '*.jnilib' -o -name '*.so' \) \
|
||||||
|
| while read -r NATIVE; do
|
||||||
|
[ "$(file --mime-type -b "$NATIVE")" = "application/x-mach-binary" ] || continue
|
||||||
|
INFO="$(codesign -dv --verbose=2 "$NATIVE" 2>&1 || true)"
|
||||||
|
LABEL="$(basename "$JARPATH")/${NATIVE#"$CHECKTMP"/}"
|
||||||
|
printf '%s' "$INFO" | grep -q "Authority=Developer ID Application" \
|
||||||
|
|| echo "$LABEL: not signed with a Developer ID certificate" >> "$PREFLIGHT_ERRORS"
|
||||||
|
printf '%s' "$INFO" | grep -q "Timestamp=" \
|
||||||
|
|| echo "$LABEL: signature has no secure timestamp" >> "$PREFLIGHT_ERRORS"
|
||||||
|
done
|
||||||
|
rm -rf "$CHECKTMP"
|
||||||
|
done
|
||||||
|
if [ -s "$PREFLIGHT_ERRORS" ]; then
|
||||||
|
echo "ERROR: these would fail notarization:" >&2
|
||||||
|
sed 's/^/ /' "$PREFLIGHT_ERRORS" >&2
|
||||||
|
rm -f "$PREFLIGHT_ERRORS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
rm -f "$PREFLIGHT_ERRORS"
|
||||||
|
echo " preflight ok"
|
||||||
|
|
||||||
|
echo "==> Step 3/4: building the dmg"
|
||||||
|
|
||||||
|
# Not with jpackage: "jpackage --type dmg --app-image" re-signs the app it is
|
||||||
|
# handed, replacing our Developer ID signature with an ad-hoc one and dropping
|
||||||
|
# the hardened runtime flag. hdiutil copies the bundle verbatim instead.
|
||||||
|
DMG="dist/KST4Contest-${APP_VERSION}.dmg"
|
||||||
|
STAGE="$(mktemp -d)"
|
||||||
|
# ditto rather than cp -R: it preserves the extended attributes the code
|
||||||
|
# signature depends on.
|
||||||
|
ditto "$APP" "$STAGE/KST4Contest.app"
|
||||||
|
ln -s /Applications "$STAGE/Applications"
|
||||||
|
|
||||||
|
hdiutil create -volname "KST4Contest" -srcfolder "$STAGE" \
|
||||||
|
-ov -format UDZO -quiet "$DMG"
|
||||||
|
rm -rf "$STAGE"
|
||||||
|
[ -f "$DMG" ] || { echo "hdiutil produced no DMG" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Signing the DMG itself is not what Gatekeeper judges -- that is the .app
|
||||||
|
# inside -- but Apple expects the container to be signed too.
|
||||||
|
codesign --force --timestamp --sign "$FULL_IDENTITY" "$DMG"
|
||||||
|
echo "==> Built $DMG"
|
||||||
|
|
||||||
|
echo "==> Step 4/4: verification"
|
||||||
|
|
||||||
|
if [ ${#NOTARY_ARGS[@]} -gt 0 ]; then
|
||||||
|
echo " submitting for notarization (waits for Apple's verdict)"
|
||||||
|
# Without a timeout a stalled submission would hang a CI job forever.
|
||||||
|
xcrun notarytool submit "$DMG" "${NOTARY_ARGS[@]}" --wait --timeout 30m
|
||||||
|
echo " stapling ticket"
|
||||||
|
xcrun stapler staple "$DMG"
|
||||||
|
else
|
||||||
|
echo " no notarization credentials set, skipping notarization"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Everything below inspects the app as it actually ships, mounted from the DMG,
|
||||||
|
# rather than the staging copy on disk.
|
||||||
|
MOUNT_POINT="$(mktemp -d)"
|
||||||
|
hdiutil attach "$DMG" -nobrowse -quiet -mountpoint "$MOUNT_POINT"
|
||||||
|
trap 'hdiutil detach "$MOUNT_POINT" -quiet 2>/dev/null || hdiutil detach "$MOUNT_POINT" -force -quiet 2>/dev/null || true' EXIT
|
||||||
|
SHIPPED_APP="$MOUNT_POINT/KST4Contest.app"
|
||||||
|
|
||||||
|
echo "--- codesign --verify on the app inside the DMG ---"
|
||||||
|
codesign --verify --deep --strict --verbose=2 "$SHIPPED_APP"
|
||||||
|
|
||||||
|
echo "--- app identity ---"
|
||||||
|
codesign -dv --verbose=2 "$SHIPPED_APP" 2>&1 | grep -iE "identifier|authority|teamidentifier|flags"
|
||||||
|
|
||||||
|
# An ad-hoc signature here means something along the way re-signed the bundle.
|
||||||
|
if codesign -dv "$SHIPPED_APP" 2>&1 | grep -q "adhoc"; then
|
||||||
|
echo "ERROR: the app inside the DMG is ad-hoc signed, not Developer ID signed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "--- entitlements as signed ---"
|
||||||
|
codesign -d --entitlements - --xml "$SHIPPED_APP" 2>/dev/null | plutil -convert xml1 -o - - | grep -E "key|true|false"
|
||||||
|
|
||||||
|
echo "--- dmg identity ---"
|
||||||
|
codesign -dv --verbose=2 "$DMG" 2>&1 | grep -iE "authority|teamidentifier" | head -2
|
||||||
|
|
||||||
|
echo "--- spctl assessment ---"
|
||||||
|
# Without notarization this reports "rejected"; that is expected.
|
||||||
|
spctl --assess --type execute --verbose=4 "$SHIPPED_APP" || true
|
||||||
|
|
||||||
|
if [ ${#NOTARY_ARGS[@]} -gt 0 ]; then
|
||||||
|
echo "--- stapler validate ---"
|
||||||
|
xcrun stapler validate "$DMG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Done: $REPO_ROOT/$DMG"
|
||||||
Executable
+64
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Import the Developer ID certificate into a throwaway keychain on a CI runner.
|
||||||
|
#
|
||||||
|
# A runner cannot answer the keychain's authorization dialog, so the login
|
||||||
|
# keychain is unusable there. This creates a dedicated keychain instead, whose
|
||||||
|
# password is generated here and needed nowhere else -- it is discarded with the
|
||||||
|
# keychain at the end of the job.
|
||||||
|
#
|
||||||
|
# Reads from the environment:
|
||||||
|
# MACOS_CERT_P12 base64 of the exported .p12
|
||||||
|
# MACOS_CERT_PASSWORD the password that .p12 was exported with
|
||||||
|
#
|
||||||
|
# Exports to $GITHUB_ENV:
|
||||||
|
# SIGNING_IDENTITY for packaging/macos/build-signed-dmg.sh
|
||||||
|
# SIGNING_KEYCHAIN so the cleanup step knows what to delete
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${MACOS_CERT_P12:?MACOS_CERT_P12 is not set}"
|
||||||
|
: "${MACOS_CERT_PASSWORD:?MACOS_CERT_PASSWORD is not set}"
|
||||||
|
: "${RUNNER_TEMP:?RUNNER_TEMP is not set}"
|
||||||
|
: "${GITHUB_ENV:?GITHUB_ENV is not set}"
|
||||||
|
|
||||||
|
KEYCHAIN="$RUNNER_TEMP/kst4contest-signing.keychain-db"
|
||||||
|
KEYCHAIN_PASSWORD="$(uuidgen)"
|
||||||
|
CERT="$RUNNER_TEMP/cert.p12"
|
||||||
|
|
||||||
|
printf '%s' "$MACOS_CERT_P12" | base64 --decode > "$CERT"
|
||||||
|
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
|
||||||
|
# Keychains re-lock after five minutes by default, which would strand a build
|
||||||
|
# halfway through signing.
|
||||||
|
security set-keychain-settings -lut 21600 "$KEYCHAIN"
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
|
||||||
|
|
||||||
|
security import "$CERT" -k "$KEYCHAIN" -P "$MACOS_CERT_PASSWORD" \
|
||||||
|
-T /usr/bin/codesign -T /usr/bin/security
|
||||||
|
rm -f "$CERT"
|
||||||
|
|
||||||
|
# Lets codesign reach the private key without the UI prompt a runner has no way
|
||||||
|
# of answering.
|
||||||
|
security set-key-partition-list -S apple-tool:,apple:,codesign: \
|
||||||
|
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" >/dev/null
|
||||||
|
|
||||||
|
# codesign searches the keychain list, so the new keychain has to be on it --
|
||||||
|
# added to whatever the runner already had, not in place of it.
|
||||||
|
EXISTING_KEYCHAINS="$(security list-keychains -d user | sed -e 's/^[[:space:]]*"//' -e 's/"$//')"
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
security list-keychains -d user -s "$KEYCHAIN" $EXISTING_KEYCHAINS
|
||||||
|
|
||||||
|
IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN" \
|
||||||
|
| sed -n 's/.*"Developer ID Application: \(.*\)".*/\1/p' | head -n 1)"
|
||||||
|
|
||||||
|
if [ -z "$IDENTITY" ]; then
|
||||||
|
echo "No 'Developer ID Application' identity found in the imported certificate." >&2
|
||||||
|
echo "What the keychain does contain:" >&2
|
||||||
|
security find-identity -v -p codesigning "$KEYCHAIN" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Imported identity: Developer ID Application: $IDENTITY"
|
||||||
|
echo "SIGNING_IDENTITY=$IDENTITY" >> "$GITHUB_ENV"
|
||||||
|
echo "SIGNING_KEYCHAIN=$KEYCHAIN" >> "$GITHUB_ENV"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<!-- The JVM compiles bytecode to machine code at runtime and executes it
|
||||||
|
from memory it allocated itself. Under the hardened runtime all three
|
||||||
|
of these are required or the app is killed on launch. -->
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<!-- jpackage bundles JavaFX native libraries that are signed with our own
|
||||||
|
identity rather than Apple's, and the JVM dlopen()s them at runtime. -->
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<!-- KST4Contest talks to the ON4KST chat servers. -->
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Reference in New Issue
Block a user