#!/bin/bash set -eo pipefail # Change to the script's directory to ensure relative paths are stable. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" cd "$SCRIPT_DIR" || exit 1 # This script handles code signing and notarization for the macOS app. # It expects the following environment variables to be set for real signing: # - MACOS_CERTIFICATE_P12_B64: The base64 encoded .p12 certificate. # - MACOS_CERTIFICATE_PASSWORD: The password for the .p12 certificate. # - MACOS_APPLE_ID: Your Apple ID email used for notarization. # - MACOS_NOTARIZATION_PASSWORD: An app-specific password for your Apple ID. # - MACOS_TEAM_ID: Your Apple Developer Team ID. # The first argument is the path to the artifact, relative to this script's location. ARTIFACT_PATH="$1" ENTITLEMENTS_PATH="entitlements.plist" if [[ -z "$ARTIFACT_PATH" ]]; then echo "Usage: $0 " exit 1 fi if [[ ! -e "$ARTIFACT_PATH" ]]; then echo "Error: Artifact not found at '$ARTIFACT_PATH'" exit 1 fi # --- AD-HOC SIGNING (if no credentials) --- if [[ -z "$MACOS_CERTIFICATE_P12_B64" ]]; then if [[ "$ARTIFACT_PATH" == *.app ]]; then echo "No signing certificate found. Performing ad-hoc signing..." find "$ARTIFACT_PATH/Contents/MacOS/" -type f -exec codesign --force --sign - {} \; codesign --force --sign - "$ARTIFACT_PATH" echo "Ad-hoc signing complete." fi exit 0 fi echo "Credentials found. Proceeding with official signing and notarization..." # --- KEYCHAIN AND CERTIFICATE SETUP --- KEYCHAIN_NAME="build.keychain" KEYCHAIN_PASSWORD="a-very-secure-password" CERTIFICATE_P12_PATH="certificate.p12" # Check if keychain already exists if [[ -f "$HOME/Library/Keychains/$KEYCHAIN_NAME-db" ]]; then echo "Keychain $KEYCHAIN_NAME already exists. Reusing it..." security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME" else echo "Creating temporary keychain: $KEYCHAIN_NAME" security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME" security default-keychain -s "$KEYCHAIN_NAME" security set-keychain-settings -t 3600 -u "$KEYCHAIN_NAME" # Decode and import the certificate echo "Decoding certificate..." echo "$MACOS_CERTIFICATE_P12_B64" | base64 --decode > "$CERTIFICATE_P12_PATH" if [[ ! -s "$CERTIFICATE_P12_PATH" ]]; then echo "Error: Certificate file is empty or invalid." exit 1 fi echo "Importing certificate into keychain..." security import "$CERTIFICATE_P12_PATH" -k "$KEYCHAIN_NAME" -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security if [[ $? -ne 0 ]]; then echo "Error: Failed to import certificate." exit 1 fi # Allow codesign access echo "Setting keychain partition list..." security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME" > /dev/null if [[ $? -ne 0 ]]; then echo "Error: Failed to set keychain partition list." exit 1 fi fi # Ensure keychain is unlocked echo "Ensuring keychain is unlocked..." security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME" # Find the signing identity echo "Searching for signing identity..." SIGNING_IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_NAME" | grep "Developer ID Application" | head -n 1 | awk -F '"' '{print $2}') if [[ -z "$SIGNING_IDENTITY" ]]; then echo "Error: No Developer ID Application signing identity found." security find-identity -v -p codesigning "$KEYCHAIN_NAME" exit 1 fi echo "Using signing identity: $SIGNING_IDENTITY" # --- MAIN LOGIC --- # Function to sign the .app bundle sign_app() { echo "Signing application bundle at: $ARTIFACT_PATH" find "$ARTIFACT_PATH" -depth \( -name "*.dylib" -o -name "*.framework" -o -path "$ARTIFACT_PATH/Contents/MacOS/*" \) -type f | while read -r comp; do echo "Signing component: $comp" codesign --force --verbose --sign "$SIGNING_IDENTITY" --options runtime --timestamp "$comp" done echo "Signing main application bundle with entitlements..." codesign --force --verbose --sign "$SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS_PATH" --options runtime --timestamp "$ARTIFACT_PATH" echo "App signing complete." echo "Verifying app signature..." codesign --verify --deep --strict --verbose=2 "$ARTIFACT_PATH" # Notarize the app ZIP_PATH="${ARTIFACT_PATH%.*}.zip" echo "Zipping app for notarization: $ZIP_PATH" ditto -c -k --sequesterRsrc --keepParent "$ARTIFACT_PATH" "$ZIP_PATH" local notary_output_file notary_output_file=$(mktemp) echo "Notarizing app zip..." if ! xcrun notarytool submit "$ZIP_PATH" \ --apple-id "$MACOS_APPLE_ID" \ --password "$MACOS_NOTARIZATION_PASSWORD" \ --team-id "$MACOS_TEAM_ID" \ --wait > "$notary_output_file" 2>&1; then echo "Error: App notarization failed." cat "$notary_output_file" REQUEST_UUID=$(grep "id:" "$notary_output_file" | head -n 1 | awk '{print $2}') if [[ -n "$REQUEST_UUID" ]]; then echo "Fetching notarization logs for UUID: $REQUEST_UUID" xcrun notarytool log "$REQUEST_UUID" \ --apple-id "$MACOS_APPLE_ID" \ --password "$MACOS_NOTARIZATION_PASSWORD" \ --team-id "$MACOS_TEAM_ID" fi rm "$notary_output_file" rm -f "$ZIP_PATH" exit 1 fi echo "App notarization successful. Full log:" cat "$notary_output_file" rm "$notary_output_file" echo "Stapling ticket to app..." xcrun stapler staple "$ARTIFACT_PATH" echo "App stapling complete." rm -f "$ZIP_PATH" } # Function to notarize and staple the .dmg notarize_dmg() { echo "Notarizing DMG at: $ARTIFACT_PATH" # Re-verify keychain state echo "Verifying keychain state before DMG signing..." security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME" security find-identity -v -p codesigning "$KEYCHAIN_NAME" # Sign the DMG echo "Signing DMG..." codesign --force --verbose --sign "$SIGNING_IDENTITY" --options runtime --timestamp "$ARTIFACT_PATH" if [[ $? -ne 0 ]]; then echo "Error: Failed to sign DMG. Checking keychain state..." security find-identity -v -p codesigning "$KEYCHAIN_NAME" exit 1 fi echo "DMG signing complete." # Verify the signature echo "Verifying DMG signature..." codesign --verify --verbose "$ARTIFACT_PATH" # Notarize the DMG local notary_output_file notary_output_file=$(mktemp) echo "Notarizing DMG..." if ! xcrun notarytool submit "$ARTIFACT_PATH" \ --apple-id "$MACOS_APPLE_ID" \ --password "$MACOS_NOTARIZATION_PASSWORD" \ --team-id "$MACOS_TEAM_ID" \ --wait > "$notary_output_file" 2>&1; then echo "Error: Notarization failed." cat "$notary_output_file" REQUEST_UUID=$(grep "id:" "$notary_output_file" | head -n 1 | awk '{print $2}') if [[ -n "$REQUEST_UUID" ]]; then echo "Fetching notarization logs for UUID: $REQUEST_UUID" xcrun notarytool log "$REQUEST_UUID" \ --apple-id "$MACOS_APPLE_ID" \ --password "$MACOS_NOTARIZATION_PASSWORD" \ --team-id "$MACOS_TEAM_ID" fi rm "$notary_output_file" exit 1 fi echo "Notarization successful. Full log:" cat "$notary_output_file" rm "$notary_output_file" echo "Stapling ticket to DMG..." xcrun stapler staple "$ARTIFACT_PATH" echo "Stapling complete." } # --- EXECUTION --- if [[ "$ARTIFACT_PATH" == *.app ]]; then sign_app elif [[ "$ARTIFACT_PATH" == *.dmg ]]; then notarize_dmg else echo "Unsupported artifact type: $ARTIFACT_PATH" exit 1 fi