Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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[@]}"

Expand All @@ -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
Expand Down
19 changes: 17 additions & 2 deletions cmake/PackagingMac.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions docs/macos-code-signing.md
Original file line number Diff line number Diff line change
@@ -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=<identity>`, 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.
Loading