diff --git a/.github/macos-release-signing.md b/.github/macos-release-signing.md new file mode 100644 index 00000000..fdd465c0 --- /dev/null +++ b/.github/macos-release-signing.md @@ -0,0 +1,87 @@ +# macOS stable permission identity prototype + +Entropy currently ships GitHub artifact files without Apple Developer Program membership. Developer ID signing and notarization are therefore unavailable. + +This prototype uses one project-owned self-signed code-signing certificate across releases. Its only goal is a stable, certificate-anchored designated requirement so macOS privacy controls can recognize changed binaries as the same app. + +## Limits + +- Apple explicitly advises against shipping self-signed apps. +- Gatekeeper does not trust this certificate. Users still see an unidentified-developer warning and may need Privacy & Security > Open Anyway. +- Apple notarization remains unavailable. +- TCC persistence must be verified manually on supported macOS versions before this workflow ships. +- Losing or rotating the private key changes app identity and requires users to grant Accessibility and Input Monitoring again. +- A compromised private key lets an attacker sign code that matches Entropy's permission identity. Keep it restricted to release maintainers. + +Do not replace the certificate anchor with a bundle-ID-only requirement. Any binary can copy a bundle ID; Accessibility grants make that unsafe. + +Before storing the signing secrets, create a repository ruleset for `v0.*` tags that restricts tag creation, updates, and deletion to trusted release maintainers and blocks force updates. The release workflow executes code from the tagged commit before using the project signing key, so branch protection alone is not sufficient. + +## Create release identity + +Create this identity once on a secure Mac using Keychain Access > Certificate Assistant > Create a Certificate: + +1. Name: `Entropy Open Source Release Signing`. +2. Identity Type: Self Signed Root. +3. Certificate Type: Code Signing. +4. Enable Let me override defaults and choose a long validity period. +5. Export certificate and private key together as an encrypted `.p12`. + +The common name must remain exact because release workflow uses it to select identity. Store encrypted `.p12` and password in maintainer-controlled offline backup. Never commit private material. + +Configure two GitHub Actions repository secrets and one repository variable: + +| Secret | Value | +| --- | --- | +| `MACOS_CERTIFICATE_P12_BASE64` | `base64 -i entropy-release-signing.p12` output | +| `MACOS_CERTIFICATE_PASSWORD` | `.p12` export password | + +| Variable | Value | +| --- | --- | +| `MACOS_SIGNING_CERTIFICATE_SHA1` | Approved 40-character SHA-1 fingerprint of the signing certificate, without colons | + +Derive the public fingerprint from the exported identity, verify it against the certificate shown in Keychain Access, and store the result as the repository variable: + +```bash +openssl pkcs12 -in entropy-release-signing.p12 -nokeys | + openssl x509 -noout -fingerprint -sha1 | + sed 's/^.*=//; s/://g' +``` + +Release workflow builds before loading signing credentials, imports identity into a temporary keychain restricted to `/usr/bin/codesign`, verifies its certificate hash against the approved repository variable, embeds an explicit requirement containing that certificate plus `com.ergohaven.entropy`, signs app, and rejects ad-hoc or mismatched output. DMG is not notarized. Shipped requirement does not contain `anchor trusted`; users do not install or trust this certificate. + +## Automated proof + +Run: + +```bash +scripts/test_macos_stable_signing.sh +scripts/test_macos_stable_identity_e2e.sh +``` + +End-to-end test creates temporary self-signed identity and two different binaries. Test passes only when code hashes differ while designated requirements match. Temporary keychain and keys are deleted on exit. Run it on macOS 26 before manual TCC testing; macOS 15 runners do not expose generic OpenSSL-generated self-signed certificates as code-signing identities. + +## Required manual TCC test + +1. Produce two Entropy app builds with different binaries and same release identity. +2. Install first build as `/Applications/Entropy.app`. +3. Remove old Entropy entries from Accessibility and Input Monitoring. Add current app, grant both permissions, and verify Universal Symbols. +4. Replace app with second build without changing path. +5. Launch second build. Verify Universal Symbols still work without removing or re-adding permission entries. +6. Repeat on Apple Silicon and Intel machines. + +Inspect both builds: + +```bash +codesign -dv --verbose=4 Entropy.app +codesign -d -r- Entropy.app +spctl --assess --type execute --verbose=4 Entropy.app +``` + +Expected designated requirement: + +```text +designated => certificate root = H"" and identifier "com.ergohaven.entropy" +``` + +`spctl` rejection is expected for this prototype. Any `cdhash`-only requirement, changed certificate hash, or repeated TCC grant invalidates prototype. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cb9c189e..56965ff7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,8 +72,16 @@ jobs: if: matrix.kind != 'macos' run: cargo build --release + - name: Build macOS binary + if: matrix.kind == 'macos' + env: + MACOSX_DEPLOYMENT_TARGET: '10.15' + run: cargo build --release --target "${{ matrix.target }}" + - name: Build macOS app bundle if: matrix.kind == 'macos' + env: + SKIP_CARGO_BUILD: '1' run: TARGET="${{ matrix.target }}" scripts/build_macos_app.sh - name: Test @@ -92,6 +100,10 @@ jobs: cargo test --all-targets app::vial_hid_task::tests::macro_write_uses_the_serialized_background_transport -- --exact cargo test --all-targets app::app_lifecycle::tests::layer_completion_starts_settings_queued_behind_it -- --exact + - name: Test macOS stable signing validation + if: matrix.kind == 'macos' + run: scripts/test_macos_stable_signing.sh + - name: Upload artifact uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d4a9135b..116380b8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,6 +63,84 @@ jobs: if: matrix.kind == 'windows' run: cargo build --release + - name: Build macOS binary + if: matrix.kind == 'macos' + shell: bash + env: + MACOSX_DEPLOYMENT_TARGET: '10.15' + run: cargo build --release --target "${{ matrix.target }}" + + - name: Import macOS self-signed release identity + if: matrix.kind == 'macos' + shell: bash + env: + CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + EXPECTED_MACOS_SIGNING_CERTIFICATE_SHA1: ${{ vars.MACOS_SIGNING_CERTIFICATE_SHA1 }} + run: | + umask 077 + if [[ -z "$CERTIFICATE_P12_BASE64" ]] || + [[ -z "$MACOS_CERTIFICATE_PASSWORD" ]]; then + echo "macOS stable signing secrets are not configured" >&2 + exit 1 + fi + + expected_certificate_sha1="$( + printf '%s' "$EXPECTED_MACOS_SIGNING_CERTIFICATE_SHA1" | + tr -d '[:space:]:' | + tr '[:lower:]' '[:upper:]' + )" + if [[ ! "$expected_certificate_sha1" =~ ^[[:xdigit:]]{40}$ ]]; then + echo "MACOS_SIGNING_CERTIFICATE_SHA1 must be a 40-character SHA-1 fingerprint" >&2 + exit 1 + fi + + certificate_path="$RUNNER_TEMP/entropy-release-signing.p12" + certificate_pem_path="$RUNNER_TEMP/entropy-release-signing.pem" + keychain_path="$RUNNER_TEMP/entropy-signing.keychain-db" + keychain_password="$(openssl rand -base64 32)" + + printf '%s' "$CERTIFICATE_P12_BASE64" | + base64 -D > "$certificate_path" + 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 "$certificate_path" \ + -P "$MACOS_CERTIFICATE_PASSWORD" \ + -T /usr/bin/codesign \ + -f pkcs12 \ + -k "$keychain_path" + security set-key-partition-list \ + -S apple-tool:,apple:,codesign: \ + -s \ + -k "$keychain_password" \ + "$keychain_path" + security list-keychains -d user -s "$keychain_path" + + openssl pkcs12 \ + -in "$certificate_path" \ + -nokeys \ + -passin "pass:$MACOS_CERTIFICATE_PASSWORD" \ + -out "$certificate_pem_path" + certificate_sha1="$( + openssl x509 \ + -in "$certificate_pem_path" \ + -noout \ + -fingerprint \ + -sha1 | + sed 's/^.*=//; s/://g' | + tr '[:lower:]' '[:upper:]' + )" + if [[ ! "$certificate_sha1" =~ ^[[:xdigit:]]{40}$ ]]; then + echo "Could not derive macOS signing certificate SHA-1" >&2 + exit 1 + fi + if [[ "$certificate_sha1" != "$expected_certificate_sha1" ]]; then + echo "Configured macOS signing certificate does not match the approved fingerprint" >&2 + exit 1 + fi + echo "MACOS_SIGNING_CERTIFICATE_SHA1=$certificate_sha1" >> "$GITHUB_ENV" + - name: Package Linux artifact if: matrix.kind == 'linux' shell: bash @@ -81,11 +159,37 @@ jobs: - name: Build macOS app bundle and DMG if: matrix.kind == 'macos' shell: bash + env: + CODESIGN_IDENTITY: Entropy Open Source Release Signing + CODESIGN_KEYCHAIN: ${{ runner.temp }}/entropy-signing.keychain-db + REQUIRE_STABLE_SIGNING: '1' + SKIP_CARGO_BUILD: '1' run: | TARGET="${{ matrix.target }}" scripts/build_macos_app.sh + + - name: Package macOS release artifact + if: matrix.kind == 'macos' + shell: bash + run: | + dmg_paths=(dist/macos/*.dmg) + if [[ ${#dmg_paths[@]} -ne 1 || ! -f "${dmg_paths[0]}" ]]; then + echo "Expected exactly one macOS DMG" >&2 + exit 1 + fi + mkdir -p dist/release asset="entropy-${GITHUB_REF_NAME}-macos-${{ matrix.arch }}.dmg" - cp dist/macos/*.dmg "dist/release/$asset" + cp "${dmg_paths[0]}" "dist/release/$asset" + + - name: Remove macOS signing credentials + if: matrix.kind == 'macos' && always() + shell: bash + run: | + keychain_path="$RUNNER_TEMP/entropy-signing.keychain-db" + security delete-keychain "$keychain_path" || true + rm -f \ + "$RUNNER_TEMP/entropy-release-signing.p12" \ + "$RUNNER_TEMP/entropy-release-signing.pem" - name: Upload artifact uses: actions/upload-artifact@v4 @@ -121,7 +225,8 @@ jobs: printf '\n## Downloads\n\n' >> RELEASE_NOTES.md printf '%s\n' '- Linux: download the `.AppImage`, make it executable, and run it.' >> RELEASE_NOTES.md printf '%s\n' '- Windows: download and run the portable `.exe`.' >> RELEASE_NOTES.md - printf '%s\n' '- macOS: download the unsigned `.dmg` for your Mac (`arm64` for Apple Silicon, `x86_64` for Intel), drag Entropy to Applications, then remove quarantine if macOS blocks the app.' >> RELEASE_NOTES.md + printf '%s\n' '- macOS: download the project-signed, non-notarized `.dmg` for your Mac (`arm64` for Apple Silicon, `x86_64` for Intel), then drag Entropy to Applications. macOS may require Privacy & Security > Open Anyway.' >> RELEASE_NOTES.md + printf '%s\n' '- When upgrading from an earlier unsigned Entropy release, remove old Entropy entries from Accessibility and Input Monitoring, then grant both permissions once for the project-signed app. Later builds signed with the same project key are intended to preserve this identity.' >> RELEASE_NOTES.md - name: Create GitHub Release uses: softprops/action-gh-release@v2 diff --git a/scripts/build_macos_app.sh b/scripts/build_macos_app.sh index c1fe984c..7b9a8b88 100755 --- a/scripts/build_macos_app.sh +++ b/scripts/build_macos_app.sh @@ -5,9 +5,15 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" APP_NAME="${APP_NAME:-Entropy}" BUNDLE_ID="${BUNDLE_ID:-com.ergohaven.entropy}" CODESIGN_IDENTITY="${CODESIGN_IDENTITY:--}" +CODESIGN_KEYCHAIN="${CODESIGN_KEYCHAIN:-}" +MACOS_SIGNING_CERTIFICATE_SHA1="${MACOS_SIGNING_CERTIFICATE_SHA1:-}" +REQUIRE_STABLE_SIGNING="${REQUIRE_STABLE_SIGNING:-0}" +SKIP_CARGO_BUILD="${SKIP_CARGO_BUILD:-0}" MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-10.15}" export MACOSX_DEPLOYMENT_TARGET +source "$ROOT/scripts/macos_stable_signing.sh" + VERSION="$( awk -F '"' '/^version = / { print $2; exit }' "$ROOT/Cargo.toml" )" @@ -81,17 +87,40 @@ sign_app_bundle() { fi if ! command -v codesign >/dev/null 2>&1; then + if [[ "$REQUIRE_STABLE_SIGNING" == "1" ]]; then + echo "codesign not found; cannot build a signed release" >&2 + return 1 + fi echo "codesign not found; skipped app bundle signing" return fi local codesign_args=(--force --sign "$CODESIGN_IDENTITY") + if [[ -n "$CODESIGN_KEYCHAIN" ]]; then + codesign_args+=(--keychain "$CODESIGN_KEYCHAIN") + fi if [[ "$CODESIGN_IDENTITY" == "-" ]]; then codesign_args+=(--timestamp=none) + else + codesign_args+=( + --identifier "$BUNDLE_ID" + --requirements "$(macos_stable_designated_requirement \ + "$MACOS_SIGNING_CERTIFICATE_SHA1" \ + "$BUNDLE_ID")" + --options runtime + --timestamp=none + ) fi codesign "${codesign_args[@]}" "$APP_PATH" - codesign --verify --strict "$APP_PATH" + if [[ "$REQUIRE_STABLE_SIGNING" == "1" ]]; then + macos_verify_stable_signature \ + "$APP_PATH" \ + "$MACOS_SIGNING_CERTIFICATE_SHA1" \ + "$BUNDLE_ID" + else + codesign --verify --strict "$APP_PATH" + fi } create_dmg_with_retries() { @@ -125,8 +154,28 @@ create_dmg_with_retries() { return "$status" } +macos_validate_stable_signing_configuration \ + "$CODESIGN_IDENTITY" \ + "$REQUIRE_STABLE_SIGNING" \ + "$MACOS_SIGNING_CERTIFICATE_SHA1" \ + "$BUNDLE_ID" + cd "$ROOT" -cargo build "${BUILD_ARGS[@]}" +case "$SKIP_CARGO_BUILD" in +0) + cargo build "${BUILD_ARGS[@]}" + ;; +1) + if [[ ! -x "$BIN" ]]; then + echo "SKIP_CARGO_BUILD=1 requires a prebuilt executable at $BIN" >&2 + exit 1 + fi + ;; +*) + echo "SKIP_CARGO_BUILD must be 0 or 1" >&2 + exit 1 + ;; +esac validate_binary_arch rm -rf "$APP_PATH" "$ZIP_PATH" "$DMG_PATH" @@ -188,6 +237,9 @@ fi if command -v hdiutil >/dev/null 2>&1; then create_dmg_with_retries +elif [[ "$REQUIRE_STABLE_SIGNING" == "1" ]]; then + echo "hdiutil not found; cannot build a macOS release DMG" >&2 + exit 1 else echo "hdiutil not found; skipped DMG build" fi diff --git a/scripts/macos_stable_signing.sh b/scripts/macos_stable_signing.sh new file mode 100755 index 00000000..e38d2412 --- /dev/null +++ b/scripts/macos_stable_signing.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +macos_stable_designated_requirement() { + local certificate_sha1="${1:-}" + local bundle_id="${2:-}" + local normalized_sha1 + + normalized_sha1="$(printf '%s' "$certificate_sha1" | tr '[:upper:]' '[:lower:]')" + + printf '=designated => anchor = H"%s" and identifier "%s"\n' \ + "$normalized_sha1" \ + "$bundle_id" +} + +macos_validate_stable_signing_configuration() { + local identity="${1:-}" + local required="${2:-0}" + local certificate_sha1="${3:-}" + local bundle_id="${4:-}" + + if [[ "$required" != "0" && "$required" != "1" ]]; then + echo "REQUIRE_STABLE_SIGNING must be 0 or 1" >&2 + return 1 + fi + + if [[ "$required" == "1" && ( -z "$identity" || "$identity" == "-" ) ]]; then + echo "Stable signing is required; refusing to build an ad-hoc release" >&2 + return 1 + fi + + if [[ -n "$identity" && "$identity" != "-" ]]; then + if [[ ! "$certificate_sha1" =~ ^[[:xdigit:]]{40}$ ]]; then + echo "MACOS_SIGNING_CERTIFICATE_SHA1 must contain 40 hexadecimal characters" >&2 + return 1 + fi + if [[ ! "$bundle_id" =~ ^[[:alnum:].-]+$ ]]; then + echo "BUNDLE_ID contains unsupported characters" >&2 + return 1 + fi + fi +} + +macos_validate_stable_signature_output() { + local details="$1" + local requirement="$2" + local certificate_sha1="$3" + local bundle_id="$4" + local expected_requirement + + certificate_sha1="$(printf '%s' "$certificate_sha1" | tr '[:upper:]' '[:lower:]')" + expected_requirement="designated => certificate root = H\"$certificate_sha1\" and identifier \"$bundle_id\"" + + if [[ "$details" == *"Signature=adhoc"* ]]; then + echo "Release app is ad-hoc signed" >&2 + return 1 + fi + if [[ "$details" != *"Authority="* ]]; then + echo "Release app has no certificate authority" >&2 + return 1 + fi + if [[ "$requirement" == *"cdhash "* ]]; then + echo "Release app designated requirement is tied to one binary hash" >&2 + return 1 + fi + if [[ "$requirement" != *"$expected_requirement"* ]]; then + echo "Release app designated requirement does not match expected certificate and bundle ID" >&2 + return 1 + fi +} + +macos_verify_stable_signature() { + local app_path="$1" + local certificate_sha1="$2" + local bundle_id="$3" + local details + local requirement + + codesign --verify --deep --strict --verbose=2 "$app_path" || return 1 + details="$(codesign -dv --verbose=4 "$app_path" 2>&1)" || return 1 + requirement="$(codesign -d -r- "$app_path" 2>&1)" || return 1 + macos_validate_stable_signature_output \ + "$details" \ + "$requirement" \ + "$certificate_sha1" \ + "$bundle_id" || return 1 + echo "Validated stable certificate-anchored signature for $app_path" +} diff --git a/scripts/test_macos_stable_identity_e2e.sh b/scripts/test_macos_stable_identity_e2e.sh new file mode 100755 index 00000000..8bf6065f --- /dev/null +++ b/scripts/test_macos_stable_identity_e2e.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "Skipped macOS stable identity end-to-end test" + exit 0 +fi + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "$ROOT/scripts/macos_stable_signing.sh" + +TMP_DIR="$(mktemp -d /tmp/entropy-stable-signing.XXXXXX)" +KEYCHAIN_PATH="$TMP_DIR/test.keychain-db" +KEYCHAIN_PASSWORD="entropy-test-keychain" +P12_PASSWORD="entropy-test-p12" +IDENTITY="Entropy Open Source Release Signing" +BUNDLE_ID="com.ergohaven.entropy" +ORIGINAL_KEYCHAINS=() + +while IFS= read -r keychain; do + keychain="${keychain//\"/}" + ORIGINAL_KEYCHAINS+=("$keychain") +done < <(security list-keychains -d user) + +cleanup() { + security list-keychains -d user -s "${ORIGINAL_KEYCHAINS[@]}" >/dev/null 2>&1 || true + security delete-keychain "$KEYCHAIN_PATH" >/dev/null 2>&1 || true + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +openssl req -new -newkey rsa:2048 -x509 -sha256 -nodes \ + -days 30 \ + -subj "/CN=$IDENTITY/O=Entropy Open Source" \ + -addext "basicConstraints=critical,CA:TRUE" \ + -addext "keyUsage=critical,digitalSignature,keyCertSign" \ + -addext "extendedKeyUsage=codeSigning" \ + -keyout "$TMP_DIR/key.pem" \ + -out "$TMP_DIR/cert.pem" >/dev/null 2>&1 +openssl pkcs12 -export \ + -inkey "$TMP_DIR/key.pem" \ + -in "$TMP_DIR/cert.pem" \ + -name "$IDENTITY" \ + -passout "pass:$P12_PASSWORD" \ + -out "$TMP_DIR/identity.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 "$TMP_DIR/identity.p12" \ + -P "$P12_PASSWORD" \ + -T /usr/bin/codesign \ + -f pkcs12 \ + -k "$KEYCHAIN_PATH" >/dev/null +security set-key-partition-list \ + -S apple-tool:,apple:,codesign: \ + -s \ + -k "$KEYCHAIN_PASSWORD" \ + "$KEYCHAIN_PATH" >/dev/null +security list-keychains -d user -s "$KEYCHAIN_PATH" "${ORIGINAL_KEYCHAINS[@]}" + +openssl pkcs12 \ + -in "$TMP_DIR/identity.p12" \ + -nokeys \ + -passin "pass:$P12_PASSWORD" \ + -out "$TMP_DIR/imported-cert.pem" + +printf '%s\n' 'int main(void) { return 1; }' | + clang -x c - -o "$TMP_DIR/entropy-v1" +printf '%s\n' 'int main(void) { return 2; }' | + clang -x c - -o "$TMP_DIR/entropy-v2" + +CERTIFICATE_SHA1="$( + openssl x509 -in "$TMP_DIR/imported-cert.pem" -noout -fingerprint -sha1 | + sed 's/^.*=//; s/://g' +)" +REQUIREMENT="$(macos_stable_designated_requirement "$CERTIFICATE_SHA1" "$BUNDLE_ID")" + +sign_test_binary() { + local binary="$1" + + codesign --force \ + --sign "$IDENTITY" \ + --keychain "$KEYCHAIN_PATH" \ + --identifier "$BUNDLE_ID" \ + --requirements "$REQUIREMENT" \ + --options runtime \ + --timestamp=none \ + "$binary" +} + +sign_test_binary "$TMP_DIR/entropy-v1" +macos_verify_stable_signature \ + "$TMP_DIR/entropy-v1" \ + "$CERTIFICATE_SHA1" \ + "$BUNDLE_ID" + +for binary in "$TMP_DIR/entropy-v1" "$TMP_DIR/entropy-v2"; do + if [[ "$binary" == "$TMP_DIR/entropy-v1" ]]; then + continue + fi + sign_test_binary "$binary" + macos_verify_stable_signature "$binary" "$CERTIFICATE_SHA1" "$BUNDLE_ID" +done + +requirement_v1="$(codesign -d -r- "$TMP_DIR/entropy-v1" 2>&1 | sed -n '/^designated =>/p')" +requirement_v2="$(codesign -d -r- "$TMP_DIR/entropy-v2" 2>&1 | sed -n '/^designated =>/p')" +cdhash_v1="$(codesign -dvvv "$TMP_DIR/entropy-v1" 2>&1 | awk -F= '/^CDHash=/{print $2}')" +cdhash_v2="$(codesign -dvvv "$TMP_DIR/entropy-v2" 2>&1 | awk -F= '/^CDHash=/{print $2}')" + +if [[ "$requirement_v1" != "$requirement_v2" ]]; then + echo "Different builds produced different designated requirements" >&2 + exit 1 +fi +if [[ -z "$cdhash_v1" || -z "$cdhash_v2" || "$cdhash_v1" == "$cdhash_v2" ]]; then + echo "Test binaries did not produce distinct code hashes" >&2 + exit 1 +fi + +echo "$requirement_v1" +echo "macOS stable identity end-to-end test passed" diff --git a/scripts/test_macos_stable_signing.sh b/scripts/test_macos_stable_signing.sh new file mode 100755 index 00000000..a7d1f863 --- /dev/null +++ b/scripts/test_macos_stable_signing.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "$ROOT/scripts/macos_stable_signing.sh" + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +expect_success() { + local description="$1" + shift + "$@" || fail "$description" +} + +expect_failure() { + local description="$1" + shift + if "$@"; then + fail "$description" + fi +} + +adhoc_details=$'Signature=adhoc\nTeamIdentifier=not set' +adhoc_requirement='designated => cdhash H"d8b56c9110cf6f078cd45f0c4d19876a3ff9b288"' +certificate_sha1='0123456789abcdef0123456789abcdef01234567' +bundle_id='com.ergohaven.entropy' +self_signed_details=$'Authority=Entropy Open Source Release Signing\nTeamIdentifier=not set' +self_signed_requirement="designated => certificate root = H\"$certificate_sha1\" and identifier \"$bundle_id\"" +generated_requirement="$(macos_stable_designated_requirement "$certificate_sha1" "$bundle_id")" +expected_generated_requirement="=designated => anchor = H\"$certificate_sha1\" and identifier \"$bundle_id\"" + +if [[ "$generated_requirement" != "$expected_generated_requirement" ]]; then + fail "stable designated requirement generator returned an unexpected requirement" +fi + +expect_failure \ + "required stable signing accepted an ad-hoc identity" \ + macos_validate_stable_signing_configuration "-" "1" "" "$bundle_id" +expect_success \ + "local ad-hoc packaging should remain available" \ + macos_validate_stable_signing_configuration "-" "0" "" "$bundle_id" +expect_failure \ + "stable signing accepted a missing certificate hash" \ + macos_validate_stable_signing_configuration \ + "Entropy Open Source Release Signing" \ + "1" \ + "" \ + "$bundle_id" +expect_failure \ + "stable signing accepted an invalid certificate hash" \ + macos_validate_stable_signing_configuration \ + "Entropy Open Source Release Signing" \ + "1" \ + "invalid" \ + "$bundle_id" +expect_success \ + "valid stable signing configuration failed" \ + macos_validate_stable_signing_configuration \ + "Entropy Open Source Release Signing" \ + "1" \ + "$certificate_sha1" \ + "$bundle_id" +expect_failure \ + "ad-hoc signature passed stable validation" \ + macos_validate_stable_signature_output \ + "$adhoc_details" \ + "$adhoc_requirement" \ + "$certificate_sha1" \ + "$bundle_id" +expect_failure \ + "hash-only designated requirement passed stable validation" \ + macos_validate_stable_signature_output \ + "$self_signed_details" \ + "$adhoc_requirement" \ + "$certificate_sha1" \ + "$bundle_id" +expect_failure \ + "wrong certificate passed stable validation" \ + macos_validate_stable_signature_output \ + "$self_signed_details" \ + "$self_signed_requirement" \ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \ + "$bundle_id" +expect_success \ + "self-signed certificate requirement failed stable validation" \ + macos_validate_stable_signature_output \ + "$self_signed_details" \ + "$self_signed_requirement" \ + "$certificate_sha1" \ + "$bundle_id" + +mock_codesign_details="$adhoc_details" +mock_codesign_requirement="$adhoc_requirement" +codesign() { + case "$1" in + --verify) return 0 ;; + -dv) printf '%s\n' "$mock_codesign_details" >&2 ;; + -d) printf '%s\n' "$mock_codesign_requirement" >&2 ;; + *) fail "unexpected mock codesign arguments: $*" ;; + esac +} + +expect_failure \ + "app verification swallowed ad-hoc signature rejection" \ + macos_verify_stable_signature \ + "/tmp/Entropy.app" \ + "$certificate_sha1" \ + "$bundle_id" +mock_codesign_details="$self_signed_details" +mock_codesign_requirement="$self_signed_requirement" +expect_success \ + "app verification rejected a stable self-signed signature" \ + macos_verify_stable_signature \ + "/tmp/Entropy.app" \ + "$certificate_sha1" \ + "$bundle_id" + +echo "macOS stable signing unit tests passed"