diff --git a/.github/workflows/macos-signing-check.yml b/.github/workflows/macos-signing-check.yml new file mode 100644 index 0000000..0878cfb --- /dev/null +++ b/.github/workflows/macos-signing-check.yml @@ -0,0 +1,206 @@ +name: macOS signing check + +# On-demand check of the macOS signing secrets, without a release. It signs and notarizes a tiny +# throwaway executable and installer package with the stored Developer ID certificates and App +# Store Connect key, then verifies the result the way release.yml does. Nothing is published or +# uploaded. +# +# The job is bound to the `release-signing` environment, so it needs the same approval as a release +# and is the only way to read the secrets. If a secret is absent the run fails and lists the missing +# names: an explicit check never passes by skipping. +# +# The steps from the import to the verification mirror the `sign-macos` job of release.yml; keep +# the two in step. Like that job, this one checks nothing out and runs no repository code. +on: + workflow_dispatch: + +permissions: {} + +concurrency: + group: macos-signing-check + cancel-in-progress: false + +jobs: + check: + name: check the macOS signing secrets + runs-on: macos-latest + timeout-minutes: 60 + environment: release-signing + permissions: + contents: read # the job's default; nothing is checked out + steps: + - name: Check the signing inputs are present + env: + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }} + MACOS_INSTALLER_CERTIFICATE: ${{ secrets.MACOS_INSTALLER_CERTIFICATE }} + MACOS_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_PASSWORD }} + MACOS_INSTALLER_IDENTITY: ${{ secrets.MACOS_INSTALLER_IDENTITY }} + APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }} + APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} + APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} + run: | + set -euo pipefail + missing="" + for name in MACOS_CERTIFICATE MACOS_CERTIFICATE_PASSWORD MACOS_SIGNING_IDENTITY \ + MACOS_INSTALLER_CERTIFICATE MACOS_INSTALLER_CERTIFICATE_PASSWORD \ + MACOS_INSTALLER_IDENTITY APPLE_NOTARIZATION_KEY APPLE_NOTARIZATION_KEY_ID \ + APPLE_NOTARIZATION_ISSUER_ID; do + if [ -z "${!name}" ]; then + echo "::error::The secret $name is missing from the release-signing environment." + missing="$missing $name" + fi + done + if [ -n "$missing" ]; then + { + printf '### macOS signing check\n\n' + printf "Failed: these secrets are missing from the \`release-signing\` environment:\n\n" + for name in $missing; do printf -- "- \`%s\`\n" "$name"; done + printf '\nStore them as described in docs/release-signing.md, then run this check again.\n' + } >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi + + - name: Build a universal test executable + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/check" + printf 'int main(void) { return 0; }\n' > "$RUNNER_TEMP/check/main.c" + cc -arch x86_64 -arch arm64 -o "$RUNNER_TEMP/check/signing-check" "$RUNNER_TEMP/check/main.c" + lipo -info "$RUNNER_TEMP/check/signing-check" + + - name: Import the Developer ID certificates into a temporary keychain + env: + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + MACOS_INSTALLER_CERTIFICATE: ${{ secrets.MACOS_INSTALLER_CERTIFICATE }} + MACOS_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_PASSWORD }} + run: | + set -euo pipefail + keychain="$RUNNER_TEMP/signing.keychain-db" + password="$(openssl rand -hex 24)" + echo "::add-mask::$password" + security create-keychain -p "$password" "$keychain" + security set-keychain-settings -lut 1800 "$keychain" + security unlock-keychain -p "$password" "$keychain" + printf '%s' "$MACOS_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/application.p12" + printf '%s' "$MACOS_INSTALLER_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/installer.p12" + security import "$RUNNER_TEMP/application.p12" -k "$keychain" \ + -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + security import "$RUNNER_TEMP/installer.p12" -k "$keychain" \ + -P "$MACOS_INSTALLER_CERTIFICATE_PASSWORD" -T /usr/bin/productsign + rm -f "$RUNNER_TEMP/application.p12" "$RUNNER_TEMP/installer.p12" + security list-keychains -d user -s "$keychain" login.keychain + security set-key-partition-list -S apple-tool:,apple: -s -k "$password" "$keychain" > /dev/null + + - name: Sign the executable (hardened runtime, secure timestamp) + env: + MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }} + run: | + set -euo pipefail + bin="$RUNNER_TEMP/check/signing-check" + codesign --force --timestamp --options runtime \ + --identifier com.hodeitek.hodeishield.signing-check \ + --keychain "$RUNNER_TEMP/signing.keychain-db" \ + --sign "$MACOS_SIGNING_IDENTITY" "$bin" + codesign --verify --strict --verbose=2 "$bin" + # The signature must come from a Developer ID Application certificate of a team, with the + # hardened runtime and a secure timestamp. codesign prints no secrets. + details="$(codesign --display -vv "$bin" 2>&1)" + echo "$details" + team="$(sed -n 's/^TeamIdentifier=//p' <<< "$details")" + for expected in '^Authority=Developer ID Application: ' '^CodeDirectory .*flags=.*runtime' '^Timestamp='; do + if ! grep -q -- "$expected" <<< "$details"; then + echo "::error::The signature lacks the expected '${expected#^}'." + exit 1 + fi + done + if [ -z "$team" ] || [ "$team" = "not set" ]; then + echo "::error::The signature carries no TeamIdentifier." + exit 1 + fi + + - name: Build and sign a test installer package + env: + MACOS_INSTALLER_IDENTITY: ${{ secrets.MACOS_INSTALLER_IDENTITY }} + run: | + set -euo pipefail + root="$RUNNER_TEMP/pkgroot" + mkdir -p "$root/usr/local/bin" + cp -p "$RUNNER_TEMP/check/signing-check" "$root/usr/local/bin/" + pkgbuild --root "$root" --install-location / \ + --identifier com.hodeitek.hodeishield.signing-check --version 0.0.0 \ + "$RUNNER_TEMP/unsigned.pkg" + productsign --timestamp --keychain "$RUNNER_TEMP/signing.keychain-db" \ + --sign "$MACOS_INSTALLER_IDENTITY" \ + "$RUNNER_TEMP/unsigned.pkg" "$RUNNER_TEMP/signing-check.pkg" + pkgutil --check-signature "$RUNNER_TEMP/signing-check.pkg" + + - name: Remove the temporary keychain + if: always() + run: | + security delete-keychain "$RUNNER_TEMP/signing.keychain-db" 2>/dev/null || true + rm -f "$RUNNER_TEMP/application.p12" "$RUNNER_TEMP/installer.p12" + + - name: Notarize the executable and the package, staple the package + env: + APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }} + APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} + APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} + run: | + set -euo pipefail + key="$RUNNER_TEMP/notary.p8" + trap 'rm -f "$key"' EXIT + printf '%s' "$APPLE_NOTARIZATION_KEY" | base64 --decode > "$key" + notarize() { + xcrun notarytool submit "$1" --key "$key" --key-id "$APPLE_NOTARIZATION_KEY_ID" \ + --issuer "$APPLE_NOTARIZATION_ISSUER_ID" --wait --timeout 30m + } + ditto -c -k "$RUNNER_TEMP/check/signing-check" "$RUNNER_TEMP/notarize.zip" + notarize "$RUNNER_TEMP/notarize.zip" + pkg="$RUNNER_TEMP/signing-check.pkg" + notarize "$pkg" + xcrun stapler staple "$pkg" + xcrun stapler validate "$pkg" + spctl --assess --type install --verbose=2 "$pkg" + + - name: Verify the signed and notarized files + run: | + set -euo pipefail + bin="$RUNNER_TEMP/check/signing-check" + pkg="$RUNNER_TEMP/signing-check.pkg" + team="$(codesign --display -vv "$bin" 2>&1 | sed -n 's/^TeamIdentifier=//p')" + if [ -z "$team" ] || [ "$team" = "not set" ]; then + echo "::error::The signature carries no TeamIdentifier." + exit 1 + fi + accepted=false + for _ in 1 2 3 4 5 6; do + out="$(spctl --assess --type execute --verbose=2 "$bin" 2>&1 || true)" + if grep -qx 'source=Notarized Developer ID' <<< "$out"; then accepted=true; break; fi + sleep 20 + done + echo "$out" + if [ "$accepted" != true ]; then + echo "::error::Gatekeeper does not report the executable as 'Notarized Developer ID'." + exit 1 + fi + sig="$(pkgutil --check-signature "$pkg")" + echo "$sig" + if ! grep -q 'Developer ID Installer: ' <<< "$sig"; then + echo "::error::The package is not signed with a Developer ID Installer certificate." + exit 1 + fi + xcrun stapler validate "$pkg" + spctl --assess --type install --verbose=2 "$pkg" + { + printf '### macOS signing check\n\n' + printf 'Passed: the stored secrets sign and notarize with the Developer ID of team %s.\n\n' "$team" + printf -- '- All nine secrets are present.\n' + printf -- "- \`codesign --verify --strict\`: passed; Developer ID Application authority, hardened runtime and secure timestamp present.\n" + printf -- "- \`spctl --assess --type execute\`: \`source=Notarized Developer ID\`.\n" + printf -- "- \`pkgutil --check-signature\`: signed with a Developer ID Installer certificate.\n" + printf -- "- \`xcrun stapler validate\` and \`spctl --assess --type install\` on the package: passed.\n" + printf '\nOnly a throwaway test executable was signed; no release file was touched or uploaded.\n' + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d740002..28a1251 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -255,6 +255,9 @@ jobs: # - a .pkg that installs /usr/local/bin/hodeishield, signed with Developer ID Installer, # notarized and stapled, so it also passes Gatekeeper offline and can be deployed by MDM. # + # The same import, sign, notarize and verify chain runs without a release in + # macos-signing-check.yml; keep the two in step. + # # Environment secrets (release-signing): MACOS_CERTIFICATE and MACOS_CERTIFICATE_PASSWORD # (Developer ID Application, base64 .p12), MACOS_SIGNING_IDENTITY, MACOS_INSTALLER_CERTIFICATE, # MACOS_INSTALLER_CERTIFICATE_PASSWORD, MACOS_INSTALLER_IDENTITY (Developer ID Installer), @@ -305,6 +308,22 @@ jobs: chmod 0755 "stage/${BIN_NAME}" find stage/man -type f -exec chmod 0644 {} \; + # A skip must never read as a pass: say so in the run summary and as an annotation. + - name: Record that macOS signing was skipped + if: env.SIGN != 'true' + env: + EVENT_NAME: ${{ github.event_name }} + run: | + set -euo pipefail + if [ "$EVENT_NAME" = push ]; then + why="MACOS_SIGNING_ENABLED is not true" + else + why="this is a manual dry run, which never signs" + fi + msg="macOS signing: skipped — ${why}; the binary is not Developer ID signed or notarized." + echo "::notice title=macOS signing skipped::${msg}" + printf '### macOS signing\n\n%s\n' "$msg" >> "$GITHUB_STEP_SUMMARY" + - name: Check the signing inputs are present if: env.SIGN == 'true' env: @@ -367,7 +386,21 @@ jobs: --keychain "$RUNNER_TEMP/signing.keychain-db" \ --sign "$MACOS_SIGNING_IDENTITY" "stage/${BIN_NAME}" codesign --verify --strict --verbose=2 "stage/${BIN_NAME}" - codesign --display --verbose=2 "stage/${BIN_NAME}" + # The signature must come from a Developer ID Application certificate of a team, with the + # hardened runtime and a secure timestamp. codesign prints no secrets. + details="$(codesign --display -vv "stage/${BIN_NAME}" 2>&1)" + echo "$details" + team="$(sed -n 's/^TeamIdentifier=//p' <<< "$details")" + for expected in '^Authority=Developer ID Application: ' '^CodeDirectory .*flags=.*runtime' '^Timestamp='; do + if ! grep -q -- "$expected" <<< "$details"; then + echo "::error::The signature lacks the expected '${expected#^}'." + exit 1 + fi + done + if [ -z "$team" ] || [ "$team" = "not set" ]; then + echo "::error::The signature carries no TeamIdentifier." + exit 1 + fi - name: Build and sign the installer package if: env.SIGN == 'true' @@ -418,6 +451,48 @@ jobs: xcrun stapler validate "$pkg" spctl --assess --type install --verbose=2 "$pkg" + # What Gatekeeper and the system tools say about the finished files, and the run summary. The + # binary cannot be stapled, so Gatekeeper looks its ticket up online; that can lag a little + # behind the notarization, hence the retries. + - name: Verify the signed and notarized files + if: env.SIGN == 'true' + run: | + set -euo pipefail + bin="stage/${BIN_NAME}" + pkg="dist/hodeishield-${VERSION}-${TARGET}.pkg" + team="$(codesign --display -vv "$bin" 2>&1 | sed -n 's/^TeamIdentifier=//p')" + if [ -z "$team" ] || [ "$team" = "not set" ]; then + echo "::error::The signature carries no TeamIdentifier." + exit 1 + fi + accepted=false + for _ in 1 2 3 4 5 6; do + out="$(spctl --assess --type execute --verbose=2 "$bin" 2>&1 || true)" + if grep -qx 'source=Notarized Developer ID' <<< "$out"; then accepted=true; break; fi + sleep 20 + done + echo "$out" + if [ "$accepted" != true ]; then + echo "::error::Gatekeeper does not report the binary as 'Notarized Developer ID'." + exit 1 + fi + sig="$(pkgutil --check-signature "$pkg")" + echo "$sig" + if ! grep -q 'Developer ID Installer: ' <<< "$sig"; then + echo "::error::The package is not signed with a Developer ID Installer certificate." + exit 1 + fi + xcrun stapler validate "$pkg" + spctl --assess --type install --verbose=2 "$pkg" + { + printf '### macOS signing\n\n' + printf 'macOS signing: signed and notarized with the Developer ID of team %s.\n\n' "$team" + printf -- "- \`codesign --verify --strict\`: passed; Developer ID Application authority, hardened runtime and secure timestamp present.\n" + printf -- "- \`spctl --assess --type execute\` on the binary: \`source=Notarized Developer ID\`.\n" + printf -- "- \`pkgutil --check-signature\` on the package: signed with a Developer ID Installer certificate.\n" + printf -- "- \`xcrun stapler validate\` and \`spctl --assess --type install\` on the package: passed.\n" + } >> "$GITHUB_STEP_SUMMARY" + - name: Package archive run: | set -euo pipefail diff --git a/CHANGELOG.md b/CHANGELOG.md index 76ddb06..353fead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,12 @@ commands, `--json` output and exit codes. Each release's notes start with its se `SECURITY.md`; `SECURITY.md` points to it. A weekly check validates the file and opens an issue when it is invalid or has less than 60 days left before its expiry date. ([#125](https://github.com/Hodeitek/hodeishield-cli/issues/125)) +- The release workflow can sign and notarize the macOS binaries with a Developer ID: the universal + binary is signed with the hardened runtime and notarized, and a `.pkg` installer is signed, + notarized and stapled. A new on-demand workflow, "macOS signing check", tests the stored signing + secrets without a release. Until the signing identity is stored, releases say in their run summary + that macOS signing was skipped. + ([#50](https://github.com/Hodeitek/hodeishield-cli/issues/50)) ### Changed diff --git a/docs/release-signing.md b/docs/release-signing.md index dafd098..10790ef 100644 --- a/docs/release-signing.md +++ b/docs/release-signing.md @@ -87,12 +87,58 @@ The signatures of every signed release so far (0.2.0 to 0.4.4) carry: | Subject | `CN=Hodeitek S.L., O=Hodeitek S.L., L=Aretxabaleta, S=Gipuzkoa, C=ES` | | Certificate profile EKU | `1.3.6.1.4.1.311.97.449523565.535854065.272767902.439799148` | -### macOS - -macOS signing is not active ([#50](https://github.com/Hodeitek/hodeishield-cli/issues/50)): -`MACOS_SIGNING_ENABLED` is not set, and no Apple certificate or notarization key is stored. The -workflow's macOS steps would use Developer ID certificates and an App Store Connect key kept as -`release-signing` environment secrets. This document must cover them before they are used. +### Developer ID (macOS) + +The `sign-macos` job of `release.yml` signs the universal `hodeishield` binary with an Apple +Developer ID Application certificate, with the hardened runtime and a secure timestamp. It builds +`hodeishield--universal-apple-darwin.pkg`, which installs `/usr/local/bin/hodeishield`, and +signs it with a Developer ID Installer certificate. Both the binary and the package are notarized +by Apple, and the package is stapled. A bare executable cannot carry a stapled ticket, so Gatekeeper +checks the binary's notarization online on first run. See +[#50](https://github.com/Hodeitek/hodeishield-cli/issues/50). + +- **Identities.** A Developer ID Application certificate for the binary and a Developer ID Installer + certificate for the package, both issued to Hodeitek S.L. by Apple's Developer ID certification + authority. The signature carries the Apple Developer Team ID of the account. +- **Notarization.** `notarytool` authenticates with an App Store Connect API key (a `.p8` private + key, its key ID and the issuer ID). +- **Where they are kept.** Only as secrets of the `release-signing` environment; none is in the + repository or in a variable. The job imports the certificates into a temporary keychain that is + deleted at the end of the job, and writes the API key to a temporary file that is deleted too. + + | Secret | Content | + |---|---| + | `MACOS_CERTIFICATE`, `MACOS_CERTIFICATE_PASSWORD` | Developer ID Application certificate and key (`.p12`, base64) and its password | + | `MACOS_SIGNING_IDENTITY` | Name of that certificate, as `codesign` takes it | + | `MACOS_INSTALLER_CERTIFICATE`, `MACOS_INSTALLER_CERTIFICATE_PASSWORD` | Developer ID Installer certificate and key (`.p12`, base64) and its password | + | `MACOS_INSTALLER_IDENTITY` | Name of that certificate, as `productsign` takes it | + | `APPLE_NOTARIZATION_KEY` | App Store Connect API private key (`.p8`, base64) | + | `APPLE_NOTARIZATION_KEY_ID`, `APPLE_NOTARIZATION_ISSUER_ID` | Key ID and issuer ID of that key | + +- **Switch.** The repository variable `MACOS_SIGNING_ENABLED` turns the signing on, and only for a + run started by a pushed `v*` tag. The environment's secrets are visible only to a job bound to + the environment, so the workflow cannot tell in advance whether they exist: when the variable is + `true` and a secret is missing, the job fails and names it. When the variable is not `true`, the + release still builds and publishes, the macOS binary is not signed or notarized, and the run + summary says so. +- **Checking the secrets without a release.** The **macOS signing check** workflow + (`macos-signing-check.yml`, started by hand) is bound to the same environment, so it needs the + same approval. It signs and notarizes a throwaway test executable and package with the stored + secrets, verifies them like a release does, and uploads nothing. It fails, listing the names, if a + secret is missing. + +To enable it, a maintainer with the right to change the environment: + +1. stores the nine secrets in the `release-signing` environment; +2. lets the **macOS signing check** run: the environment accepts deployments from `v*` tags only, so + a repository administrator adds `main`, where the workflow runs, to its allowed branches for the + check and removes it afterwards; +3. runs **macOS signing check** and reads its summary: it must pass; +4. sets the repository variable `MACOS_SIGNING_ENABLED` to `true`. + +The first release signed this way is the first one whose run summary says +"macOS signing: signed and notarized". Releases before it are not signed with a Developer ID. To +check a download, see [verifying-releases.md](verifying-releases.md#5-platform-signatures-windows-macos). ### Package repository key @@ -111,11 +157,16 @@ separately in [#114](https://github.com/Hodeitek/hodeishield-cli/issues/114). runs after the Windows signing jobs, which wait for an approval, but needs no approval of its own: a `release.yml` changed at a tag could sign without one. - **The `release-signing` environment.** Every job that uses it waits for a required reviewer, and - only a `v*` tag can deploy to it. It holds the Azure identifiers and the signing account names. + only a `v*` tag can deploy to it. It holds the Azure identifiers and the signing account names, + and the Apple certificates and notarization key. - **The `packaging` environment.** The same protections. It gates the package managers and the container image. - **Repository settings.** Repository administrators manage the repository variables, the environments with their reviewers, secrets and variables, and the ruleset. +- **Apple Developer account.** The Developer ID certificates are issued, and revoked, in the Apple + Developer account of the organisation, and the notarization key in App Store Connect. Only the + account's Account Holder can create or revoke Developer ID certificates. Storing or replacing the + secrets needs administrator rights on the repository. - **Azure.** The signing account, its identity validations and certificate profiles are managed in the Azure subscription of the account. One account holds the Owner role on that subscription. Revoking a certificate needs that role and is done in the Azure portal. The app registration has @@ -231,10 +282,33 @@ the profile that the release workflow did not sign. document and in [verifying-releases.md](verifying-releases.md#6-releases-after-a-signing-change). Windows users check the new signature as described in -[verifying-releases.md](verifying-releases.md#5-platform-signatures-windows-macos-in-a-later-release): +[verifying-releases.md](verifying-releases.md#5-platform-signatures-windows-macos): `Status` is `Valid`, the subject names Hodeitek S.L., a timestamp is present, and the profile EKU is the new one given in the advisory. +## If the macOS Developer ID signing is compromised + +This applies when a Developer ID certificate or its private key, the notarization key or the +`release-signing` environment is compromised, or when a file is found signed with the certificate +that the release workflow did not sign. + +1. **Stop signing.** Set the repository variable `MACOS_SIGNING_ENABLED` to `false`, reject any + pending deployment to `release-signing` and cancel running Release workflow runs. A release + built meanwhile has an unsigned macOS binary and no `.pkg`: do not publish one before step 3. +2. **Revoke.** In the Apple Developer account, revoke the affected Developer ID certificate or + certificates. In App Store Connect, revoke the notarization API key. Apple stops trusting new + signatures made with a revoked certificate. Binaries notarized earlier stay valid unless Apple + revokes their notarization tickets, which the maintainers can ask Apple to do for files known to + be malicious. +3. **Replace.** Create new certificates and a new API key, store them as the secrets listed under + [Developer ID (macOS)](#developer-id-macos), run the **macOS signing check** workflow, and set + `MACOS_SIGNING_ENABLED` back to `true`. Then publish a new patch release of each + [supported version](../SECURITY.md#supported-versions) through the normal release process: a + published file cannot be signed again in place. +4. **Tell users** as in the Windows section above (security advisory, notice in the README, release + notes), naming the affected versions and the Team ID, and update this document and + [verifying-releases.md](verifying-releases.md#6-releases-after-a-signing-change). + ## If the release workflow identity is compromised This applies when a malicious change to `release.yml` reaches a tag, when a token or account that can diff --git a/docs/verifying-releases.md b/docs/verifying-releases.md index 6dd9122..eb13afb 100644 --- a/docs/verifying-releases.md +++ b/docs/verifying-releases.md @@ -115,7 +115,7 @@ This confirms the archive was built by the `release.yml` GitHub Actions workflow, from the `Hodeitek/hodeishield-cli` source repository, at the tag you expect — not from a modified build or a malicious fork. -## 5. Platform signatures (Windows; macOS in a later release) +## 5. Platform signatures (Windows, macOS) From 0.2.0, `hodeishield.exe` also carries an Authenticode signature, which Windows checks by itself. It does not replace the steps above: it says the file comes from Hodeitek, not which @@ -142,9 +142,33 @@ starts with `1.3.6.1.4.1.311.97.`. To list the EKU values: From 0.2.0 the profile EKU is `1.3.6.1.4.1.311.97.449523565.535854065.272767902.439799148` ([release-signing.md](release-signing.md#authenticode-windows) lists it with the subject). -The macOS binary is not yet signed with a Developer ID or notarized; that, and a signed and -notarized `.pkg` installer, come in a later release -([#50](https://github.com/Hodeitek/hodeishield-cli/issues/50)). +### macOS + +This applies from the first release signed with a Developer ID; earlier macOS binaries are not +signed or notarized, and the release's run summary says whether a release's macOS files are. For a +release that is signed, the binary carries a Developer ID Application signature and is notarized by +Apple, and the `.pkg` installer carries a Developer ID Installer signature, is notarized and has its +ticket stapled. + +```sh +# The binary: signed by Hodeitek's Developer ID, with the hardened runtime and a timestamp. +codesign -dvv ./hodeishield 2>&1 | grep -E 'Authority|TeamIdentifier|Timestamp|flags' +codesign --verify --strict --verbose=2 ./hodeishield + +# Gatekeeper's verdict. Expect "accepted" and "source=Notarized Developer ID". A bare +# executable cannot be stapled, so this check needs a network connection. +spctl -a -vv -t exec ./hodeishield + +# The installer package. +pkgutil --check-signature hodeishield--universal-apple-darwin.pkg +xcrun stapler validate hodeishield--universal-apple-darwin.pkg +``` + +`Authority` must list `Developer ID Application: Hodeitek S.L.`, and `TeamIdentifier` must be the +Team ID that [release-signing.md](release-signing.md#developer-id-macos) and the release notes give. +The package must show a `Developer ID Installer: Hodeitek S.L.` certificate. As with Windows, this +says the file comes from Hodeitek; the Sigstore and SLSA checks above say which workflow run built +it. ## 6. Releases after a signing change