From b3b12b263e1b2ea026d0c13bb310d1a13aa1732d Mon Sep 17 00:00:00 2001 From: Faq Date: Fri, 25 Sep 2026 10:11:47 +0300 Subject: [PATCH] Misc - Correction to pipeline and how to ref. https://github.com/dail8859/NotepadNext/issues/1102 --- .github/workflows/build.yml | 56 ++++++++++++++++++++++++++++++++ cmake/PackagingMac.cmake | 19 +++++++++-- docs/macos-code-signing.md | 64 +++++++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 docs/macos-code-signing.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0a4babcd6..0deafaf9e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,6 +14,8 @@ env: jobs: build: + env: + MACOS_HAS_CODESIGN_SECRETS: ${{ secrets.MACOS_CERTIFICATE_P12 != '' && secrets.MACOS_CERTIFICATE_PASSWORD != '' && secrets.MACOS_CODESIGN_IDENTITY != '' }} strategy: fail-fast: false matrix: @@ -156,12 +158,35 @@ jobs: shell: bash run: echo "DISTRIBUTION=dmg" >> "$GITHUB_ENV" + - name: Import Code-Signing Certificate (mac) + if: matrix.platform == 'mac' && env.MACOS_HAS_CODESIGN_SECRETS == 'true' + env: + MACOS_CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + shell: bash + run: | + KEYCHAIN_PATH="$RUNNER_TEMP/notepadnext-signing.keychain-db" + KEYCHAIN_PASSWORD="$(uuidgen)" + echo "$MACOS_CERTIFICATE_P12" | base64 --decode -o "$RUNNER_TEMP/certificate.p12" + + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security import "$RUNNER_TEMP/certificate.p12" -P "$MACOS_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" + security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security list-keychain -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | sed 's/"//g') + + rm -f "$RUNNER_TEMP/certificate.p12" + - name: Configure shell: bash run: | CMAKE_OPTS=(-S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DAPP_DISTRIBUTION="${DISTRIBUTION}") if [ "${{ matrix.platform }}" = "mac" ]; then CMAKE_OPTS+=(-DCMAKE_OSX_ARCHITECTURES="${{ matrix.cmake_arch }}" -DCMAKE_OSX_DEPLOYMENT_TARGET="${{ matrix.deployment_target }}") + if [ "${{ env.MACOS_HAS_CODESIGN_SECRETS }}" = "true" ]; then + CMAKE_OPTS+=(-DMACOS_CODESIGN_IDENTITY="${{ secrets.MACOS_CODESIGN_IDENTITY }}") + fi fi cmake "${CMAKE_OPTS[@]}" @@ -178,6 +203,37 @@ jobs: shell: bash run: cmake --build build --target dmg --parallel + - name: Notarize and Staple DMG (mac) + if: matrix.platform == 'mac' && env.MACOS_HAS_CODESIGN_SECRETS == 'true' + env: + APPLE_ID: ${{ secrets.APPLE_NOTARIZATION_APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + shell: bash + run: | + DMG_PATH=$(ls build/NotepadNext*.dmg) + xcrun notarytool submit "$DMG_PATH" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" \ + --wait + xcrun stapler staple "$DMG_PATH" + + - name: Verify Gatekeeper Acceptance (mac) + if: matrix.platform == 'mac' && env.MACOS_HAS_CODESIGN_SECRETS == 'true' + shell: bash + run: | + DMG_PATH=$(ls build/NotepadNext*.dmg) + MOUNT_POINT=$(mktemp -d) + hdiutil attach "$DMG_PATH" -mountpoint "$MOUNT_POINT" -nobrowse -quiet + APP_PATH=$(find "$MOUNT_POINT" -maxdepth 1 -name "*.app") + + codesign --verify --deep --strict --verbose=2 "$APP_PATH" + spctl --assess --type execute --verbose=2 "$APP_PATH" + xcrun stapler validate "$DMG_PATH" + + hdiutil detach "$MOUNT_POINT" -quiet + - name: Build Linux Targets if: matrix.platform == 'linux' run: cmake --build build --target appimage --parallel diff --git a/cmake/PackagingMac.cmake b/cmake/PackagingMac.cmake index 9102113e7..51ef48056 100644 --- a/cmake/PackagingMac.cmake +++ b/cmake/PackagingMac.cmake @@ -37,10 +37,25 @@ add_custom_target(install_local find_program(MACDEPLOYQT_EXECUTABLE macdeployqt REQUIRED) +# Developer ID identity (e.g. "Developer ID Application: Name (TEAMID)") used to +# codesign the bundle with a hardened runtime and secure timestamp so it can be +# notarized. Without this, the dmg is unsigned/ad-hoc signed and Gatekeeper will +# refuse to open it once it has been downloaded (quarantined), reporting it as +# "damaged". See docs/macos-code-signing.md. +set(MACOS_CODESIGN_IDENTITY "" CACHE STRING "Codesigning identity for signing the macOS app bundle for notarization") + +set(MACDEPLOYQT_ARGS ${INSTALL_DIR}/NotepadNext.app -dmg) + +if(MACOS_CODESIGN_IDENTITY) + message(STATUS "macOS bundle will be signed for notarization with identity: ${MACOS_CODESIGN_IDENTITY}") + list(APPEND MACDEPLOYQT_ARGS "-sign-for-notarization=${MACOS_CODESIGN_IDENTITY}") +else() + message(WARNING "MACOS_CODESIGN_IDENTITY not set: dmg will not be signed for notarization and Gatekeeper will reject it as \"damaged\" once downloaded. See docs/macos-code-signing.md") +endif() + add_custom_target(dmg COMMAND ${MACDEPLOYQT_EXECUTABLE} - ${INSTALL_DIR}/NotepadNext.app - -dmg + ${MACDEPLOYQT_ARGS} COMMAND ${CMAKE_COMMAND} -E rename ${INSTALL_DIR}/NotepadNext.dmg ${CMAKE_BINARY_DIR}/NotepadNext-v${PROJECT_VERSION}.dmg diff --git a/docs/macos-code-signing.md b/docs/macos-code-signing.md new file mode 100644 index 000000000..4f21c373f --- /dev/null +++ b/docs/macos-code-signing.md @@ -0,0 +1,64 @@ +# macOS code signing and notarization + +## Why the dmg was flagged as "damaged" + +The `dmg` cmake target (`cmake/PackagingMac.cmake`) built the app bundle with +`macdeployqt` but never signed it with a Developer ID certificate, and CI never +submitted it to Apple for notarization. macOS marks any file downloaded through +a browser with the `com.apple.quarantine` extended attribute. On launch, +Gatekeeper checks a quarantined app against a notarization ticket; if the app +is unsigned or lacks one, Gatekeeper refuses to open it and reports it as +"damaged and should be moved to the Trash" — the app isn't actually corrupt, +it's just unsigned/unnotarized. + +## What CI now does + +`.github/workflows/build.yml` will sign, notarize, and staple the macOS dmg +automatically, but only when the required repository secrets are present. If +they're not set, the build behaves exactly as before (unsigned dmg, no +failure) — the workflow checks `MACOS_HAS_CODESIGN_SECRETS` before running any +of the signing/notarization steps. + +## Required repository secrets + +You need a paid Apple Developer Program membership ($99/year) to get a +Developer ID Application certificate; there is no free path to a Gatekeeper- +clean dmg for distribution outside the App Store. + +| Secret | How to get it | +| --- | --- | +| `MACOS_CERTIFICATE_P12` | In Xcode or Keychain Access, export your **Developer ID Application** certificate (with its private key) as a `.p12` file, then `base64 -i cert.p12 \| pbcopy` and paste that as the secret value. | +| `MACOS_CERTIFICATE_PASSWORD` | The password you set when exporting the `.p12`. | +| `MACOS_CODESIGN_IDENTITY` | The identity string, e.g. `Developer ID Application: Your Name (TEAMID)`. List it locally with `security find-identity -v -p codesigning`. | +| `APPLE_NOTARIZATION_APPLE_ID` | The Apple ID email tied to your Developer account. | +| `APPLE_NOTARIZATION_PASSWORD` | An **app-specific password** for that Apple ID, generated at https://appleid.apple.com/account/manage — not your normal Apple ID password. | +| `APPLE_TEAM_ID` | Your 10-character Apple Developer Team ID, visible at https://developer.apple.com/account under Membership. | + +Add these under the fork's repo Settings → Secrets and variables → Actions. + +## What happens in CI, in order + +1. **Import Code-Signing Certificate (mac)** decodes `MACOS_CERTIFICATE_P12` + into a temporary keychain scoped to the runner (`$RUNNER_TEMP`), unlocked + for the job only. +2. **Configure** passes `-DMACOS_CODESIGN_IDENTITY=...` to cmake. +3. `cmake/PackagingMac.cmake` runs `macdeployqt` with + `-sign-for-notarization=`, which codesigns the bundle with the + hardened runtime and a secure timestamp — both required for notarization. +4. **Notarize and Staple DMG (mac)** submits the dmg via + `xcrun notarytool submit --wait`, then staples the resulting ticket to the + dmg with `xcrun stapler staple` so it verifies offline. +5. **Verify Gatekeeper Acceptance (mac)** mounts the stapled dmg and runs + `codesign --verify --deep --strict`, `spctl --assess --type execute`, and + `xcrun stapler validate` against the mounted app/dmg — this is the CI + regression check that a downloaded copy will actually pass Gatekeeper; if + signing regresses, this step fails the build instead of shipping a broken + dmg silently. + +## Verifying locally after downloading a release + +```bash +spctl --assess --type execute --verbose=2 /Applications/NotepadNext.app +``` + +Should print `accepted` once a signed/notarized release is downloaded.