diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 08a5e66cd0..764eb0622a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -413,6 +413,45 @@ jobs: echo "$description" | grep -q "flags=.*runtime" echo "$description" | grep -q "Timestamp=" + # Tauri signs the app, its sidecar and the widget it is handed, but nothing under Resources. + # The packaged keyring addons (#6161) are Mach-O code, so Apple notarization rejects the whole + # app unless each carries the Developer ID signature, the hardened runtime and a secure + # timestamp (2.73.0-preview.20260930 was refused for exactly that). Sign them in place, after + # the certificate import and before the bundler copies them. + - name: Sign the packaged keyring addons + if: runner.os == 'macOS' + env: + MACOS_SIGN_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + DRY_RUN: ${{ inputs.dry-run }} + run: | + set -euo pipefail + shopt -s nullglob + addons=(desktop/src-tauri/resources/keyring/*.darwin-*.node) + if [ "${#addons[@]}" -eq 0 ]; then + echo "::error::No macOS keyring addon was prepared for the bundle." + exit 1 + fi + if [ "${DESKTOP_SIGNING_CONFIGURED}" != "true" ]; then + if [ "${DRY_RUN}" != "true" ]; then + echo "::error::A real release must sign the packaged keyring addons." + exit 1 + fi + echo "No signing material; the keyring addons stay as prepared for this non-release build." + exit 0 + fi + for addon in "${addons[@]}"; do + codesign --force --timestamp --options runtime --sign "$MACOS_SIGN_IDENTITY" "$addon" + codesign --verify --strict "$addon" + description="$(codesign -dvvv "$addon" 2>&1)" + echo "$description" + # Here-strings, not pipes: under pipefail a matcher that exits on its first hit can + # SIGPIPE the writer and fail the assertion it was meant to pass. + grep -q "TeamIdentifier=$APPLE_TEAM_ID" <<<"$description" + grep -q "flags=.*runtime" <<<"$description" + grep -q "Timestamp=" <<<"$description" + done + - name: Prepare Windows installer version if: runner.os == 'Windows' shell: bash diff --git a/tests/ci-workflows/release-desktop-scripts.test.ts b/tests/ci-workflows/release-desktop-scripts.test.ts index db141b5e1c..570e306750 100644 --- a/tests/ci-workflows/release-desktop-scripts.test.ts +++ b/tests/ci-workflows/release-desktop-scripts.test.ts @@ -537,6 +537,23 @@ describe("widget extension signing", () => { .toContain("APPLE_CERTIFICATE"); }); + test("the packaged keyring addons are Developer ID signed before the bundler copies them", () => { + // Notarization refused 2.73.0-preview.20260930: Resources/keyring/*.node were ad-hoc or + // unsigned and had no secure timestamp, and Tauri does not sign files under Resources. + const sign = steps.find(step => step.name === "Sign the packaged keyring addons"); + expect(sign?.if).toBe("runner.os == 'macOS'"); + expect(sign?.env?.MACOS_SIGN_IDENTITY).toContain("APPLE_SIGNING_IDENTITY"); + expect(sign?.run).toContain("desktop/src-tauri/resources/keyring/*.darwin-*.node"); + expect(sign?.run).toContain('codesign --force --timestamp --options runtime --sign "$MACOS_SIGN_IDENTITY"'); + expect(sign?.run).toContain('grep -q "TeamIdentifier=$APPLE_TEAM_ID" <<<"$description"'); + expect(sign?.run).toContain('grep -q "Timestamp=" <<<"$description"'); + // A real release never falls back to unsigned addons; only a dry run may. + expect(sign?.run).toContain("A real release must sign the packaged keyring addons."); + expect(indexOfStepRunning("security create-keychain")).toBeLessThan(indexOfStep(sign!.name!)); + expect(indexOfStep("Prepare macOS sidecars")).toBeLessThan(indexOfStep(sign!.name!)); + expect(indexOfStep(sign!.name!)).toBeLessThan(indexOfStep("Build desktop bundles")); + }); + test("the certificate is importable before the widget is signed and is removed afterwards", () => { // codesign resolves an identity through the keychain search list, and Tauri does not build // its own keychain until the bundling step, which is after this one.