diff --git a/.github/actions/import-macos-cert/action.yml b/.github/actions/import-macos-cert/action.yml new file mode 100644 index 0000000000..e9ecb02761 --- /dev/null +++ b/.github/actions/import-macos-cert/action.yml @@ -0,0 +1,52 @@ +name: Import macOS Developer ID certificate +description: > + Import a Developer ID Application certificate (base64-encoded .p12) into a + throwaway keychain so codesign/jpackage can find it during the job. Soft: + when no certificate is supplied it is a no-op and reports signing=false, so + callers build UNSIGNED artifacts exactly as before. + +inputs: + certificate-p12-base64: + description: Base64 of the Developer ID Application .p12 (cert + private key) + required: true + certificate-password: + description: Password used when the .p12 was exported + required: true + +outputs: + signing: + description: "'true' if a certificate was imported, else 'false'" + value: ${{ steps.import.outputs.signing }} + +runs: + using: composite + steps: + - id: import + shell: bash + env: + CERT_P12: ${{ inputs.certificate-p12-base64 }} + CERT_PASSWORD: ${{ inputs.certificate-password }} + run: | + set -euo pipefail + if [[ -z "${CERT_P12:-}" ]]; then + echo "::notice::No macOS signing certificate configured — artifacts will be UNSIGNED." + echo "signing=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + KEYCHAIN="$RUNNER_TEMP/amethyst-signing.keychain-db" + KEYCHAIN_PWD="$(openssl rand -base64 24)" + CERT_PATH="$RUNNER_TEMP/developer_id.p12" + security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN" + security set-keychain-settings -lut 21600 "$KEYCHAIN" + security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN" + echo "$CERT_P12" | base64 --decode > "$CERT_PATH" + security import "$CERT_PATH" -P "$CERT_PASSWORD" \ + -k "$KEYCHAIN" -T /usr/bin/codesign -T /usr/bin/productsign + # Let codesign use the private key without an interactive UI prompt. + security set-key-partition-list -S apple-tool:,apple:,codesign: \ + -s -k "$KEYCHAIN_PWD" "$KEYCHAIN" >/dev/null + # Prepend our keychain to the user search list so codesign sees it. + security list-keychains -d user -s "$KEYCHAIN" \ + $(security list-keychains -d user | sed -e 's/[\"[:space:]]//g') + rm -f "$CERT_PATH" + echo "signing=true" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index f5a1f0d97d..7223786d79 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -102,40 +102,17 @@ jobs: chmod +x desktopApp/packaging/appimage/appimagetool-x86_64.AppImage # macOS only: import the Developer ID Application cert into a throwaway - # keychain so jpackage's codesign pass can find it. Soft-gated — if the + # keychain so jpackage's codesign pass can find it. Soft — if the # MAC_CERTIFICATE_P12 secret isn't set (forks, or before Apple creds are # provisioned) the DMG is built UNSIGNED, exactly as before. notarytool # runs as part of the gradle task when the identity env is exported below. - name: Import Apple Developer ID certificate (macOS leg, if configured) if: matrix.family == 'macos' id: mac_keychain - env: - CERT_P12: ${{ secrets.MAC_CERTIFICATE_P12 }} - CERT_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }} - run: | - set -euo pipefail - if [[ -z "${CERT_P12:-}" ]]; then - echo "::notice::MAC_CERTIFICATE_P12 not set — building UNSIGNED DMG." - echo "signing=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - KEYCHAIN="$RUNNER_TEMP/amethyst-signing.keychain-db" - KEYCHAIN_PWD="$(openssl rand -base64 24)" - CERT_PATH="$RUNNER_TEMP/developer_id.p12" - security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN" - security set-keychain-settings -lut 21600 "$KEYCHAIN" - security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN" - echo "$CERT_P12" | base64 --decode > "$CERT_PATH" - security import "$CERT_PATH" -P "$CERT_PASSWORD" \ - -k "$KEYCHAIN" -T /usr/bin/codesign -T /usr/bin/productsign - # Let codesign use the private key without an interactive UI prompt. - security set-key-partition-list -S apple-tool:,apple:,codesign: \ - -s -k "$KEYCHAIN_PWD" "$KEYCHAIN" >/dev/null - # Prepend our keychain to the user search list so codesign sees it. - security list-keychains -d user -s "$KEYCHAIN" \ - $(security list-keychains -d user | sed -e 's/[\"[:space:]]//g') - rm -f "$CERT_PATH" - echo "signing=true" >> "$GITHUB_OUTPUT" + uses: ./.github/actions/import-macos-cert + with: + certificate-p12-base64: ${{ secrets.MAC_CERTIFICATE_P12 }} + certificate-password: ${{ secrets.MAC_CERTIFICATE_PASSWORD }} - name: Build desktop artifacts uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 @@ -256,7 +233,7 @@ jobs: - { os: macos-14, arch: arm64, family: macos, tasks: "amyImage" } - { os: ubuntu-latest, arch: x64, family: linux, tasks: "amyImage jpackageDeb jpackageRpm" } runs-on: ${{ matrix.os }} - timeout-minutes: 30 + timeout-minutes: 45 # macOS leg also codesigns + notarizes the jlink image defaults: run: shell: bash @@ -305,6 +282,52 @@ jobs: timeout_minutes: 15 command: ./gradlew --no-daemon :cli:${{ matrix.tasks }} + # macOS only: import the Developer ID cert (no-op without the secret) so + # the next step can codesign the jlink image. The jvm bundle for + # Homebrew-core is NOT signed here — Homebrew strips quarantine itself. + - name: Import Apple Developer ID certificate (macOS leg, if configured) + if: matrix.family == 'macos' + id: mac_keychain + uses: ./.github/actions/import-macos-cert + with: + certificate-p12-base64: ${{ secrets.MAC_CERTIFICATE_P12 }} + certificate-password: ${{ secrets.MAC_CERTIFICATE_PASSWORD }} + + # Codesign + notarize the macOS jlink image (amy--macos-arm64.tar.gz) + # for users who download it directly. A loose tarball can't be stapled + # (stapler only does .app/.dmg/.pkg), so Gatekeeper verifies notarization + # online on first run. Runs before "Collect" so the tarred image is signed. + - name: Sign + notarize amy image (macOS leg, if configured) + if: matrix.family == 'macos' && steps.mac_keychain.outputs.signing == 'true' + env: + SIGN_IDENTITY: ${{ secrets.MAC_SIGN_IDENTITY }} + NOTARY_APPLE_ID: ${{ secrets.MAC_NOTARY_APPLE_ID }} + NOTARY_PASSWORD: ${{ secrets.MAC_NOTARY_PASSWORD }} + NOTARY_TEAM_ID: ${{ secrets.MAC_NOTARY_TEAM_ID }} + run: | + set -euo pipefail + IMG="cli/build/amy-image/amy" + ENTITLEMENTS="cli/packaging/macos/amy.entitlements" + # Sign every Mach-O binary in the bundled JRE. Each is signed + # independently (no enclosing .app seals them), so order is irrelevant. + # Executables get the hardened-runtime entitlements; dylibs don't. + while IFS= read -r f; do + case "$(file -b "$f")" in + *Mach-O*executable*) + codesign --force --options runtime --timestamp \ + --entitlements "$ENTITLEMENTS" --sign "$SIGN_IDENTITY" "$f" ;; + *Mach-O*) + codesign --force --options runtime --timestamp \ + --sign "$SIGN_IDENTITY" "$f" ;; + esac + done < <(find "$IMG" -type f) + codesign --verify --strict --verbose=2 "$IMG/runtime/bin/java" + # Notarize: zip the signed image, submit, wait for Apple's verdict. + ditto -c -k --keepParent "$IMG" "$RUNNER_TEMP/amy-notarize.zip" + xcrun notarytool submit "$RUNNER_TEMP/amy-notarize.zip" \ + --apple-id "$NOTARY_APPLE_ID" --password "$NOTARY_PASSWORD" \ + --team-id "$NOTARY_TEAM_ID" --wait + # jpackage pins libicu to the build host's version (libicu74 on # ubuntu-24.04). Rewrite the .deb so it installs across Debian/Ubuntu. - name: Relax libicu dependency in .deb diff --git a/BUILDING.md b/BUILDING.md index 53bc5e3e3c..aefce7849b 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -250,7 +250,7 @@ provided automatically; everything else you set yourself.) | `SONATYPE_PASSWORD` | Maven Central user token password | Same | | `SIGNING_PRIVATE_KEY` | **GPG/PGP** private key, ASCII-armored | Signs the Maven artifacts (Central requires it) | | `SIGNING_PASSWORD` | Passphrase for that GPG key | Same | -| `MAC_CERTIFICATE_P12` | Base64 of your **Apple Developer ID Application** cert (`.p12`, includes the private key) | Signs the macOS desktop **DMG** | +| `MAC_CERTIFICATE_P12` | Base64 of your **Apple Developer ID Application** cert (`.p12`, includes the private key) | Signs the macOS desktop **DMG** and the macOS **amy** jlink tarball | | `MAC_CERTIFICATE_PASSWORD` | Password set when exporting the `.p12` | Imports the cert into the CI keychain | | `MAC_SIGN_IDENTITY` | Full identity string, e.g. `Developer ID Application: Your Name (TEAMID)` | The `codesign` identity to sign with | | `MAC_NOTARY_APPLE_ID` | Apple ID email of the notarization account | Apple notarization (`notarytool`) | @@ -266,10 +266,20 @@ Note the **three distinct signing identities** people often conflate: `MAC_SIGN_IDENTITY` + `MAC_NOTARY_*` is the **Apple Developer ID** for the macOS desktop DMG. They are unrelated — each comes from a different authority. -The macOS desktop signing secrets are **optional**: if `MAC_CERTIFICATE_P12` is -unset the release workflow still builds the DMG, just **unsigned** (the previous -behavior). Provision all six to switch signing + notarization on. Obtaining them -requires Apple Developer Program membership ($99/yr). +The macOS signing secrets are **optional**: if `MAC_CERTIFICATE_P12` is unset +the release workflow still builds the DMG **and** the macOS `amy` tarball, just +**unsigned** (the previous behavior). Provision all six to switch signing + +notarization on for both. Obtaining them requires Apple Developer Program +membership ($99/yr). The same one certificate signs both artifacts. + +The macOS `amy` tarball is the jlink image (bundled JRE), so signing it means +codesigning every Mach-O binary in that runtime with hardened-runtime +entitlements (`cli/packaging/macos/amy.entitlements` — needed so the JVM can +load the secp256k1 native library it extracts at runtime). A loose `.tar.gz` +cannot be **stapled** (Apple's `stapler` only handles `.app`/`.dmg`/`.pkg`), so +Gatekeeper verifies notarization **online** on first run — fine for a CLI. +Note the Homebrew-core jvm bundle (`amy--jvm.tar.gz`) is **not** signed: +Homebrew removes the quarantine attribute on its own downloads. Generating the values: diff --git a/cli/packaging/macos/amy.entitlements b/cli/packaging/macos/amy.entitlements new file mode 100644 index 0000000000..08352a00a9 --- /dev/null +++ b/cli/packaging/macos/amy.entitlements @@ -0,0 +1,26 @@ + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + com.apple.security.cs.allow-dyld-environment-variables + + +