diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 08b3b91..d4916c3 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -47,9 +47,21 @@ not a prerequisite automatically run by the build. Do not run it as a routine build repair. kururu uses its own application identity and does not inherit another application's permission grants. -Official signing, notarization and release configuration remain pending in -the [roadmap](../ROADMAP.md). An existing local signing certificate or a -successful development build is not evidence that distribution is configured. +The release workflow uses the protected `release-signing` environment. Configure +`SIGNING_CERT_P12` (base64 PKCS#12), `SIGNING_CERT_PASSWORD`, and the variables +`APPLE_SIGNING_IDENTITY` (certificate SHA-1) and `APPLE_TEAM_ID`. The build selects +the project certificate by fingerprint because other projects can share its name. + +For notarization, provide `APPLE_ID` and `APPLE_APP_SPECIFIC_PASSWORD`, or the +existing `NOTARY_API_KEY_P8`, `NOTARY_KEY_ID` and `NOTARY_ISSUER_ID` secrets. Locally, +`NOTARY_KEYCHAIN_PROFILE` selects credentials saved by `notarytool store-credentials`. +A release fails if signing or notarization is unavailable. The app accepts updates +from PathGao/kururu only after validating its Apple team and bundle identity. +The release publisher requires immutable releases in the repository settings. +Run **Actions → Release → Run workflow** on `main`, entering the version tag +matching `Resources/Info.plist` and `CHANGELOG.md`. Approve the signing environment +when prompted. GitHub builds, signs, notarizes, creates the tag and publishes the +release without using the maintainer's local keychain. ## Project layout @@ -193,7 +205,7 @@ and the release workflow uses the matching section for GitHub release notes. ## Privacy -kururu stores preferences and feature data locally in its own application domain. It has no account or hosted feedback service. This page describes the current fork, including the disabled release capabilities. +kururu stores preferences and feature data locally in its own application domain. It has no account or hosted feedback service. This page describes the current source build. ### The short version @@ -220,7 +232,7 @@ When a feature needs a macOS permission such as Accessibility, Screen Recording kururu opens only a few kinds of connection, and each one belongs to a visible feature. -1. **kururu self-updates are disabled in this build.** No release check, download or installation runs without this product's own update configuration. +1. **kururu updates.** Release builds check PathGao/kururu on GitHub when update checking is enabled. Installation validates the disk image and application against kururu's Apple team and bundle identity. Developer builds cannot self-update. 2. **The internet speed test, only when you ask.** The optional speed test in the Network section reaches Cloudflare's public speed endpoints at `speed.cloudflare.com` to measure latency and your download and upload throughput. This happens only when you start a test yourself, and never on its own. @@ -273,8 +285,8 @@ permission appears enabled but a feature cannot use it, see Closed-lid keep-awake uses `pmset disablesleep`, which requires administrator rights. The optional password-free setup installs a narrowly scoped `sudoers` rule for that command. It is separate from macOS privacy permissions. -Privileged fan control is unavailable pending kururu's own release signing -configuration. +The privileged fan helper authenticates the configured Apple team and kururu's +application identity in both directions. ## Troubleshooting @@ -284,12 +296,9 @@ The permission and uninstall commands below all point at kururu's bundle identif ### The app will not open -The current kururu release is locally signed and has not been notarized by -Apple. macOS may block the first launch. If you trust the downloaded copy, -open **System Settings → Privacy & Security**, find the blocked-app notice -and choose **Open Anyway**, then confirm. See -[Apple's Gatekeeper guide](https://support.apple.com/en-us/102445). - +Official releases use Apple Developer ID signing and notarization. Download the +current DMG from this repository's release page. If macOS rejects it, verify that +the file came from that page and report the exact system message. kururu lives in the menu bar, so once it starts, look for its icon up there rather than in the Dock. ### A feature does nothing, or a permission will not stick diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e391fa9..fd74446 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,11 +1,19 @@ name: Release -# Only an owner-created version tag can reach the protected signing job. The -# preflight runs without release credentials and proves that the tag names a -# verified commit already present on main before the environment can be approved. +# Owner-triggered releases must name a verified commit on main before the +# protected signing environment can be approved. on: push: tags: ["v[0-9]*"] + workflow_dispatch: + inputs: + tag: + description: Version tag matching Resources/Info.plist (for example v0.1.2) + required: true + type: string + +env: + RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }} permissions: {} @@ -17,11 +25,11 @@ jobs: preflight: name: Verify trusted release ref if: >- - github.repository == 'vorssaint/vorssaint-utils' && - github.event_name == 'push' && - github.ref_type == 'tag' && - github.actor == 'vorssaint' && - github.triggering_actor == 'vorssaint' + github.repository == 'PathGao/kururu' && + ((github.event_name == 'push' && github.ref_type == 'tag') || + (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')) && + github.actor == 'PathGao' && + github.triggering_actor == 'PathGao' runs-on: macos-26 timeout-minutes: 30 permissions: @@ -37,8 +45,8 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - if [[ ! "$GITHUB_REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-(beta|rc|alpha)\.[0-9]+)?$ ]]; then - echo "Unsupported release tag: $GITHUB_REF_NAME" >&2 + if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-(beta|rc|alpha)\.[0-9]+)?$ ]]; then + echo "Unsupported release tag: $RELEASE_TAG" >&2 exit 1 fi @@ -55,10 +63,10 @@ jobs: exit 1 fi - VERSION="${GITHUB_REF_NAME#v}" + VERSION="${RELEASE_TAG#v}" APP_VERSION="$(/usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' Resources/Info.plist)" if [[ "$APP_VERSION" != "$VERSION" ]]; then - echo "Tag $GITHUB_REF_NAME does not match app version $APP_VERSION" >&2 + echo "Tag $RELEASE_TAG does not match app version $APP_VERSION" >&2 exit 1 fi if ! grep -Eq "^## \\[$VERSION\\] - [0-9]{4}-[0-9]{2}-[0-9]{2}$" CHANGELOG.md; then @@ -74,14 +82,17 @@ jobs: needs: preflight if: >- needs.preflight.result == 'success' && - github.repository == 'vorssaint/vorssaint-utils' && - github.event_name == 'push' && - github.ref_type == 'tag' && - github.actor == 'vorssaint' && - github.triggering_actor == 'vorssaint' + github.repository == 'PathGao/kururu' && + ((github.event_name == 'push' && github.ref_type == 'tag') || + (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')) && + github.actor == 'PathGao' && + github.triggering_actor == 'PathGao' runs-on: macos-26 timeout-minutes: 30 environment: release-signing + env: + APPLE_SIGNING_IDENTITY: ${{ vars.APPLE_SIGNING_IDENTITY }} + REQUIRE_SIGNING: "1" outputs: commit_sha: ${{ steps.source.outputs.commit_sha }} prerelease: ${{ steps.notes.outputs.prerelease }} @@ -100,10 +111,12 @@ jobs: run: | set -euo pipefail COMMIT_SHA="$(git rev-parse 'HEAD^{commit}')" - REMOTE_COMMIT="$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_REF_NAME" --jq '.sha')" - if [[ "$REMOTE_COMMIT" != "$COMMIT_SHA" ]]; then - echo "Release tag moved after preflight" >&2 - exit 1 + if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then + REMOTE_COMMIT="$(gh api "repos/$GITHUB_REPOSITORY/commits/$RELEASE_TAG" --jq '.sha')" + if [[ "$REMOTE_COMMIT" != "$COMMIT_SHA" ]]; then + echo "Release tag moved after preflight" >&2 + exit 1 + fi fi RELATION="$(gh api "repos/$GITHUB_REPOSITORY/compare/$COMMIT_SHA...main" --jq '.status + ":" + .merge_base_commit.sha')" if [[ "$RELATION" != "identical:$COMMIT_SHA" && "$RELATION" != "ahead:$COMMIT_SHA" ]]; then @@ -117,26 +130,31 @@ jobs: REQUIRE_SIGNING: "1" SIGNING_CERT_P12: ${{ secrets.SIGNING_CERT_P12 }} SIGNING_CERT_PASSWORD: ${{ secrets.SIGNING_CERT_PASSWORD }} - run: ./Tools/ci-setup-signing.sh + run: | + test -n "$APPLE_SIGNING_IDENTITY" + ./Tools/ci-setup-signing.sh - name: Build run: ./build.sh - name: Verify release signature run: | - REQUIREMENT='identifier "com.vorssaint.utils" and anchor apple generic and certificate leaf[subject.OU] = "3D485NHW29"' - codesign --verify --deep --strict -R="$REQUIREMENT" build/stage/Vorssaint.app + REQUIREMENT='identifier "com.pathgao.kururu" and anchor apple generic and certificate leaf[subject.OU] = "GN56VLVTJ6"' + codesign --verify --deep --strict -R="$REQUIREMENT" build/stage/kururu.app - name: Selftest - run: ./build/Vorssaint --selftest + run: ./build/kururu --selftest - name: Notarize app env: REQUIRE_NOTARIZATION: "1" + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} NOTARY_API_KEY_P8: ${{ secrets.NOTARY_API_KEY_P8 }} NOTARY_KEY_ID: ${{ secrets.NOTARY_KEY_ID }} NOTARY_ISSUER_ID: ${{ secrets.NOTARY_ISSUER_ID }} - run: ./Tools/notarize.sh build/stage/Vorssaint.app + run: ./Tools/notarize.sh build/stage/kururu.app - name: Package DMG run: ./Tools/make-dmg.sh @@ -145,13 +163,16 @@ jobs: run: | DMG=$(ls dist/*.dmg | head -1) codesign --force --timestamp \ - --sign 'Developer ID Application: Pedro Gomes (3D485NHW29)' "$DMG" - REQUIREMENT='anchor apple generic and certificate leaf[subject.OU] = "3D485NHW29"' + --sign "$APPLE_SIGNING_IDENTITY" "$DMG" + REQUIREMENT='anchor apple generic and certificate leaf[subject.OU] = "GN56VLVTJ6"' codesign --verify --strict -R="$REQUIREMENT" "$DMG" - name: Notarize DMG env: REQUIRE_NOTARIZATION: "1" + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} NOTARY_API_KEY_P8: ${{ secrets.NOTARY_API_KEY_P8 }} NOTARY_KEY_ID: ${{ secrets.NOTARY_KEY_ID }} NOTARY_ISSUER_ID: ${{ secrets.NOTARY_ISSUER_ID }} @@ -160,7 +181,7 @@ jobs: - name: Verify notarization run: | DMG=$(ls dist/*.dmg | head -1) - DMG_REQUIREMENT='anchor apple generic and certificate leaf[subject.OU] = "3D485NHW29"' + DMG_REQUIREMENT='anchor apple generic and certificate leaf[subject.OU] = "GN56VLVTJ6"' codesign --verify --strict -R="$DMG_REQUIREMENT" "$DMG" xcrun stapler validate "$DMG" MP=$(mktemp -d) @@ -170,16 +191,16 @@ jobs: } trap cleanup EXIT hdiutil attach -nobrowse -readonly -mountpoint "$MP" "$DMG" - spctl -a -t exec -vv "$MP/Vorssaint.app" 2>&1 | tee /tmp/spctl.txt + spctl -a -t exec -vv "$MP/kururu.app" 2>&1 | tee /tmp/spctl.txt grep -q "source=Notarized Developer ID" /tmp/spctl.txt - REQUIREMENT='identifier "com.vorssaint.utils" and anchor apple generic and certificate leaf[subject.OU] = "3D485NHW29"' - codesign --verify --deep --strict -R="$REQUIREMENT" "$MP/Vorssaint.app" - xcrun stapler validate "$MP/Vorssaint.app" + REQUIREMENT='identifier "com.pathgao.kururu" and anchor apple generic and certificate leaf[subject.OU] = "GN56VLVTJ6"' + codesign --verify --deep --strict -R="$REQUIREMENT" "$MP/kururu.app" + xcrun stapler validate "$MP/kururu.app" - name: Release notes from CHANGELOG id: notes run: | - VERSION="${GITHUB_REF_NAME#v}" + VERSION="${RELEASE_TAG#v}" NOTES="$(awk -v hdr="## [$VERSION]" 'index($0,hdr)==1{g=1;next} /^## \[/&&g{exit} g{print}' CHANGELOG.md)" if [[ -z "${NOTES//[[:space:]]/}" ]]; then echo "CHANGELOG.md entry for $VERSION is empty" >&2 @@ -189,7 +210,7 @@ jobs: { printf '%s\n' "$NOTES" echo "" - echo "Signed with an Apple Developer ID and notarized by Apple, so it downloads and opens normally. Requires macOS 14 or later. Open the .dmg below and drag Vorssaint to Applications." + echo "Signed with an Apple Developer ID and notarized by Apple, so it downloads and opens normally. Requires macOS 14 or later. Open the .dmg below and drag kururu to Applications." } > "$NOTES_FILE" { echo "file=$NOTES_FILE" @@ -223,7 +244,7 @@ jobs: - name: Upload verified release artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: release-${{ github.ref_name }} + name: release-${{ env.RELEASE_TAG }} path: ${{ runner.temp }}/release-bundle if-no-files-found: error retention-days: 1 @@ -234,11 +255,11 @@ jobs: needs: release if: >- needs.release.result == 'success' && - github.repository == 'vorssaint/vorssaint-utils' && - github.event_name == 'push' && - github.ref_type == 'tag' && - github.actor == 'vorssaint' && - github.triggering_actor == 'vorssaint' + github.repository == 'PathGao/kururu' && + ((github.event_name == 'push' && github.ref_type == 'tag') || + (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')) && + github.actor == 'PathGao' && + github.triggering_actor == 'PathGao' runs-on: macos-26 timeout-minutes: 10 permissions: @@ -252,7 +273,7 @@ jobs: - name: Download verified release artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: release-${{ github.ref_name }} + name: release-${{ env.RELEASE_TAG }} path: release-bundle - name: Verify release artifact @@ -269,9 +290,17 @@ jobs: PRERELEASE: ${{ needs.release.outputs.prerelease }} run: | set -euo pipefail - REMOTE_COMMIT="$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_REF_NAME" --jq '.sha')" + if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then + # GITHUB_TOKEN does not trigger another tag workflow. The tag is only + # created after the signed and notarized artifacts have passed verification. + if ! gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" >/dev/null 2>&1; then + gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \ + -f "ref=refs/tags/$RELEASE_TAG" -f "sha=$COMMIT_SHA" >/dev/null + fi + fi + REMOTE_COMMIT="$(gh api "repos/$GITHUB_REPOSITORY/commits/$RELEASE_TAG" --jq '.sha')" if [[ "$REMOTE_COMMIT" != "$COMMIT_SHA" ]]; then - echo "Release tag moved after signing" >&2 + echo "Release tag does not match the verified source" >&2 exit 1 fi FILES=(release-bundle/*.dmg) @@ -284,7 +313,7 @@ jobs: local release_id asset_count file name digest record local expected_metadata metadata body_base64 expected_body_base64 - release_id="$(gh release view "$GITHUB_REF_NAME" --json databaseId --jq '.databaseId')" + release_id="$(gh release view "$RELEASE_TAG" --json databaseId --jq '.databaseId')" asset_count="$(gh api "repos/$GITHUB_REPOSITORY/releases/$release_id/assets" --jq 'length')" if [[ "$asset_count" -ne "${#FILES[@]}" ]]; then echo "Release contains unexpected assets" >&2 @@ -302,10 +331,10 @@ jobs: fi done - expected_metadata="$GITHUB_REF_NAME"$'\t'"$expected_draft"$'\t'"$PRERELEASE"$'\t'"github-actions[bot]" - metadata="$(gh release view "$GITHUB_REF_NAME" --json name,isDraft,isPrerelease,author \ + expected_metadata="$RELEASE_TAG"$'\t'"$expected_draft"$'\t'"$PRERELEASE"$'\t'"github-actions[bot]" + metadata="$(gh release view "$RELEASE_TAG" --json name,isDraft,isPrerelease,author \ --jq '[.name, .isDraft, .isPrerelease, .author.login] | @tsv')" - body_base64="$(gh release view "$GITHUB_REF_NAME" --json body --jq '.body | @base64')" + body_base64="$(gh release view "$RELEASE_TAG" --json body --jq '.body | @base64')" expected_body_base64="$(base64 < "$NOTES_FILE" | tr -d '\n')" if [[ "$metadata" != "$expected_metadata" || "$body_base64" != "$expected_body_base64" ]]; then echo "Release metadata verification failed" >&2 @@ -313,10 +342,10 @@ jobs: fi } - if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then - IS_DRAFT="$(gh release view "$GITHUB_REF_NAME" --json isDraft --jq '.isDraft')" - IS_IMMUTABLE="$(gh release view "$GITHUB_REF_NAME" --json isImmutable --jq '.isImmutable')" - AUTHOR="$(gh release view "$GITHUB_REF_NAME" --json author --jq '.author.login')" + if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + IS_DRAFT="$(gh release view "$RELEASE_TAG" --json isDraft --jq '.isDraft')" + IS_IMMUTABLE="$(gh release view "$RELEASE_TAG" --json isImmutable --jq '.isImmutable')" + AUTHOR="$(gh release view "$RELEASE_TAG" --json author --jq '.author.login')" if [[ "$IS_DRAFT" == "false" ]]; then if [[ "$IS_IMMUTABLE" != "true" || "$AUTHOR" != "github-actions[bot]" ]]; then echo "Refusing to reuse an untrusted or mutable published release" >&2 @@ -330,13 +359,13 @@ jobs: echo "Refusing to reuse an untrusted or mutable published release" >&2 exit 1 fi - gh release upload "$GITHUB_REF_NAME" "${FILES[@]}" --clobber + gh release upload "$RELEASE_TAG" "${FILES[@]}" --clobber else CREATE_ARGS=( - "$GITHUB_REF_NAME" + "$RELEASE_TAG" "${FILES[@]}" --verify-tag - --title "$GITHUB_REF_NAME" + --title "$RELEASE_TAG" --notes-file "$NOTES_FILE" --draft ) @@ -347,9 +376,9 @@ jobs: fi EDIT_ARGS=( - "$GITHUB_REF_NAME" + "$RELEASE_TAG" --draft - --title "$GITHUB_REF_NAME" + --title "$RELEASE_TAG" --notes-file "$NOTES_FILE" ) if [[ "$PRERELEASE" == "true" ]]; then @@ -361,13 +390,13 @@ jobs: verify_release true if [[ "$PRERELEASE" == "true" ]]; then - gh release edit "$GITHUB_REF_NAME" --draft=false --prerelease --latest=false + gh release edit "$RELEASE_TAG" --draft=false --prerelease --latest=false else - gh release edit "$GITHUB_REF_NAME" --draft=false --prerelease=false --latest + gh release edit "$RELEASE_TAG" --draft=false --prerelease=false --latest fi IS_IMMUTABLE=false for _ in {1..10}; do - IS_IMMUTABLE="$(gh release view "$GITHUB_REF_NAME" --json isImmutable --jq '.isImmutable')" + IS_IMMUTABLE="$(gh release view "$RELEASE_TAG" --json isImmutable --jq '.isImmutable')" [[ "$IS_IMMUTABLE" == "true" ]] && break sleep 1 done diff --git a/CHANGELOG.md b/CHANGELOG.md index 65eb496..9d6cfea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,12 @@ newest release first. The app and release workflow read these version headings. - Removed capabilities or compatibility. --> +## [0.1.2] - 2026-09-16 + +### Changed +- Sign releases with kururu's Apple Developer ID and notarize them through GitHub Actions. +- Enable in-app updates from PathGao/kururu with Apple team and application identity verification. + ## [0.1.1] - 2026-09-15 ### Added diff --git a/README.md b/README.md index 71bcf29..ab8da86 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,12 @@ Forked from [Vorssaint](https://github.com/vorssaint/vorssaint-utils), kururu is **[Download kururu for Apple Silicon](https://github.com/PathGao/kururu/releases/latest)** -Requires **macOS 14 or later** on an **Apple Silicon Mac**. The current release is [0.1.1](https://github.com/PathGao/kururu/releases/tag/v0.1.1). +Requires **macOS 14 or later** on an **Apple Silicon Mac**. The current release is [0.1.2](https://github.com/PathGao/kururu/releases/tag/v0.1.2). 1. Download the DMG from the release page. 2. Open it and drag kururu to Applications. 3. Launch kururu, choose your features, and grant the permissions they need. -> **Early release:** the downloadable app is locally signed and has not been notarized by Apple, so macOS may block first launch. Automatic updates are currently unavailable; download new versions from the release page. ## Features diff --git a/README.zh-Hans.md b/README.zh-Hans.md index 1ab0c15..a7e5a28 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -33,13 +33,12 @@ kururu 分叉自 [Vorssaint](https://github.com/vorssaint/vorssaint-utils),目 **[下载适用于 Apple Silicon 的 kururu](https://github.com/PathGao/kururu/releases/latest)** -需要搭载 **Apple Silicon 芯片的 Mac**,运行 **macOS 14 或更新版本**。当前发布版本为 [0.1.1](https://github.com/PathGao/kururu/releases/tag/v0.1.1)。 +需要搭载 **Apple Silicon 芯片的 Mac**,运行 **macOS 14 或更新版本**。当前发布版本为 [0.1.2](https://github.com/PathGao/kururu/releases/tag/v0.1.2)。 1. 从发布页下载 DMG。 2. 打开 DMG,将 kururu 拖入“应用程序”。 3. 启动 kururu,选择需要的功能,并授予相应权限。 -> **早期版本:** 下载的应用使用本地签名,尚未经过 Apple 公证,因此 macOS 可能阻止首次启动。目前不支持自动更新,请从发布页下载新版本。 ## 功能 diff --git a/Resources/Info.plist b/Resources/Info.plist index b9d30df..11959f0 100644 --- a/Resources/Info.plist +++ b/Resources/Info.plist @@ -39,13 +39,13 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.1.1 + 0.1.2 CFBundleSupportedPlatforms MacOSX CFBundleVersion - 2 + 3 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/Sources/Vorssaint/Core/ProductIdentity.swift b/Sources/Vorssaint/Core/ProductIdentity.swift index b2cbc39..b71414c 100644 --- a/Sources/Vorssaint/Core/ProductIdentity.swift +++ b/Sources/Vorssaint/Core/ProductIdentity.swift @@ -10,9 +10,9 @@ enum ProductIdentity { static let developmentBundleID = "com.pathgao.kururu.dev" static let repositoryURL = URL(string: "https://github.com/PathGao/kururu")! /// Changelog entry describing this product build; numbered entries retain upstream history. - static let currentReleaseNotesVersion = "0.1.1" - static let allowsSelfUpdates = false - static let signingTeamID: String? = nil + static let currentReleaseNotesVersion = "0.1.2" + static let allowsSelfUpdates = true + static let signingTeamID: String? = "GN56VLVTJ6" static func bundleID(development: Bool) -> String { development ? developmentBundleID : releaseBundleID diff --git a/Tests/BuildCapabilityPolicyTests.swift b/Tests/BuildCapabilityPolicyTests.swift index 6638212..1e3a521 100644 --- a/Tests/BuildCapabilityPolicyTests.swift +++ b/Tests/BuildCapabilityPolicyTests.swift @@ -6,20 +6,20 @@ import Security enum BuildCapabilityPolicyTests { static func run(_ expect: (Bool, String) -> Void) { let installer = UpdateInstallerSupport.installerScript() - expect(installer.contains("DMG_VERIFY_REQ='never'"), "generated installer refuses disk images without product signing identity") - expect(installer.contains("VERIFY_REQ='never'"), "generated installer refuses application bundles without product signing identity") + expect(installer.contains("DMG_VERIFY_REQ='anchor apple generic and certificate leaf[subject.OU] = \"GN56VLVTJ6\"'"), "generated installer requires our Apple team for disk images") + expect(installer.contains("identifier \"com.pathgao.kururu\""), "generated installer requires the kururu bundle identity") expect(!installer.contains("3D485NHW29") && !installer.contains("com.vorssaint.utils"), "generated installer never trusts upstream identity") expect(ProductIdentity.bundleID(development: false) == "com.pathgao.kururu", "release identity belongs to kururu") expect(ProductIdentity.bundleID(development: true) == "com.pathgao.kururu.dev", "development has independent identity") - expect(!ProductIdentity.allowsSelfUpdates && ProductIdentity.signingTeamID == nil, "current identity does not claim configured updater or signing team") + expect(ProductIdentity.allowsSelfUpdates && ProductIdentity.signingTeamID == "GN56VLVTJ6", "release update validation uses our configured Apple team") #if VORSSAINT_DEVELOPMENT expect(FanControlIdentifiers.appBundleID == ProductIdentity.developmentBundleID, "development helper matches development app") #else expect(FanControlIdentifiers.appBundleID == ProductIdentity.releaseBundleID, "release helper matches release app") #endif expect(FanControlIdentifiers.helperID == FanControlIdentifiers.appBundleID + ".fan-control", "helper service uses product-owned namespace") - expect(!FanControlIdentifiers.isConfigured, "actual fan helper stays disabled without product team") - expect(FanControlIdentifiers.appCodeRequirement == "never" && FanControlIdentifiers.helperCodeRequirement == "never", "both actual helper authentication directions fail closed") + expect(FanControlIdentifiers.isConfigured, "helper uses configured product team") + expect(FanControlIdentifiers.appCodeRequirement.contains("GN56VLVTJ6") && FanControlIdentifiers.helperCodeRequirement.contains("GN56VLVTJ6"), "both helper authentication directions require our Apple team") expect(!BuildCapabilityPolicy.allowsPrivilegedHelper(teamID: nil), "missing team disables helper") expect(!BuildCapabilityPolicy.allowsPrivilegedHelper(teamID: ""), "empty team disables helper") expect(!BuildCapabilityPolicy.allowsPrivilegedHelper(teamID: "bad\" or true"), "invalid team cannot inject requirement") diff --git a/Tools/notarize.sh b/Tools/notarize.sh index 88220db..a85385e 100755 --- a/Tools/notarize.sh +++ b/Tools/notarize.sh @@ -11,6 +11,8 @@ # NOTARY_KEY_ID the key's ID # NOTARY_ISSUER_ID the issuer UUID # +# Alternatively use NOTARY_KEYCHAIN_PROFILE locally, or APPLE_ID, +# APPLE_APP_SPECIFIC_PASSWORD and APPLE_TEAM_ID. # When the credentials are absent it skips quietly (exit 0), so a plain build # without notarization still succeeds. set -euo pipefail @@ -23,12 +25,17 @@ if [[ -z "$TARGET" ]]; then exit 1 fi -if [[ -z "${NOTARY_API_KEY_P8:-}" || -z "${NOTARY_KEY_ID:-}" || -z "${NOTARY_ISSUER_ID:-}" ]]; then +AUTH=() +if [[ -n "${NOTARY_KEYCHAIN_PROFILE:-}" ]]; then + AUTH=(--keychain-profile "$NOTARY_KEYCHAIN_PROFILE") +elif [[ -n "${APPLE_ID:-}" && -n "${APPLE_APP_SPECIFIC_PASSWORD:-}" && -n "${APPLE_TEAM_ID:-}" ]]; then + AUTH=(--apple-id "$APPLE_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --team-id "$APPLE_TEAM_ID") +elif [[ -z "${NOTARY_API_KEY_P8:-}" || -z "${NOTARY_KEY_ID:-}" || -z "${NOTARY_ISSUER_ID:-}" ]]; then if [[ "${REQUIRE_NOTARIZATION:-0}" == "1" ]]; then echo "Release notarization credentials are incomplete." >&2 exit 1 fi - echo "No notarization credentials in the environment — skipping ($TARGET)." + echo "No notarization credentials — skipping ($TARGET)." exit 0 fi if [[ ! -e "$TARGET" ]]; then @@ -39,8 +46,11 @@ fi WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT -P8="$WORK/AuthKey.p8" -printf '%s' "$NOTARY_API_KEY_P8" | base64 --decode > "$P8" +if (( ${#AUTH[@]} == 0 )); then + P8="$WORK/AuthKey.p8" + printf '%s' "$NOTARY_API_KEY_P8" | base64 --decode > "$P8" + AUTH=(--key "$P8" --key-id "$NOTARY_KEY_ID" --issuer "$NOTARY_ISSUER_ID") +fi # notarytool needs a zip/dmg/pkg. A .app is zipped first; a .dmg is submitted # as-is. Stapling always targets the original artifact. @@ -58,8 +68,7 @@ esac echo "▸ Submitting $(basename "$TARGET") to the notary service (can take a few minutes)…" xcrun notarytool submit "$SUBMIT" \ - --key "$P8" --key-id "$NOTARY_KEY_ID" --issuer "$NOTARY_ISSUER_ID" \ - --wait + "${AUTH[@]}" --wait # Staple the ticket so it is recognized even offline. Fails (and fails the build) # if notarization did not actually succeed, so a bad result never ships. diff --git a/build.sh b/build.sh index c7b7872..e6d3792 100755 --- a/build.sh +++ b/build.sh @@ -67,12 +67,16 @@ TARGET="arm64-apple-macosx14.0" ENTITLEMENTS="Resources/Vorssaint.entitlements" LEGACY_IDENTITY="Vorssaint Utils Signing" +# Select by fingerprint: the three project certificates share the same Apple name. +PROJECT_SIGNING_IDENTITY="${APPLE_SIGNING_IDENTITY:-E045AE3830A8EF200546AB97B6E020C1B40384CA}" developer_id_identity() { security find-identity -v -p codesigning 2>/dev/null \ - | grep 'Developer ID Application' \ - | head -1 \ - | sed -E 's/.*"(.*)".*/\1/' || true + | awk -v identity="$PROJECT_SIGNING_IDENTITY" '$2 == identity && /Developer ID Application/ { print $2; exit }' } +if [[ "${REQUIRE_SIGNING:-0}" == "1" && -z "$(developer_id_identity)" ]]; then + echo "The configured kururu Developer ID certificate is unavailable." >&2 + exit 1 +fi # A find-identity listing also names certificates codesign then rejects (an # expired one fails the build with errSecInternalComponent), and -v excludes