Ctrl K
ComunidadGuías…

scripts/sign-macos-release.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/bin/bash
# Signs an app bundle with the persistent release identity, or verifies a
# signed bundle against the pinned certificate. See docs/macos-signing.md.
#
# Usage: scripts/sign-macos-release.sh sign|verify /path/to/Whisper.app
#
# `sign` reads WHISPER_SIGNING_CERTIFICATE (base64 PKCS#12) and
# WHISPER_SIGNING_PASSWORD, or, when both are unset, the saved identity in
# ~/.config/whisper/signing/. It imports the identity into a temporary
# keychain that is removed on exit, success or failure.
#
# Every signed component gets a designated requirement bound to its identifier
# and the pinned certificate, so macOS keeps one identity across releases.

set -euo pipefail

root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
certificate_pem="$root/packaging/signing-certificate.pem"
entitlements="$root/packaging/entitlements.plist"
app_id="com.softmaxe.whisper"
signing_dir="$HOME/.config/whisper/signing"

die() {
  echo "[sign] $*" >&2
  exit 1
}

fingerprint() {
  local value
  value="$(openssl x509 -in "$certificate_pem" -noout -fingerprint -sha1 2>/dev/null)" ||
    die "The pinned signing certificate is missing or invalid."
  value="${value##*=}"
  value="${value//:/}"
  [[ "$value" =~ ^[0-9A-Fa-f]{40}$ ]] || die "Invalid signing certificate fingerprint."
  printf '%s' "$value" | tr 'a-f' 'A-F'
}

requirement() {
  printf 'identifier "%s" and certificate leaf = H"%s"' "$1" "$2"
}

# Lowercases certificate hashes so requirements compare as text.
normalize() {
  perl -pe 's/H"([0-9A-Fa-f]{40})"/H"\L$1"/g'
}

# Nested code first, then the app; one "identifier<TAB>path<TAB>kind" per line.
components() {
  local app="$1" bin="$1/Contents/Resources/bin" file framework_id file_kind
  [[ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$app/Contents/Info.plist")" == "$app_id" ]] ||
    die "Whisper must keep CFBundleIdentifier $app_id."
  for file in "$bin"/*; do
    if [[ -f "$file" && ! -L "$file" ]]; then
      file_kind="$(file -b "$file")" || die "Cannot inspect component $file."
      if [[ "$file_kind" == Mach-O* ]]; then
        printf '%s.%s\t%s\thelper\n' "$app_id" "$(basename "$file")" "$file"
      fi
    fi
  done
  if [[ -d "$bin/MediaRemoteAdapter.framework" ]]; then
    framework_id="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \
      "$bin/MediaRemoteAdapter.framework/Resources/Info.plist")" ||
      die "Cannot read the framework identifier."
    [[ -n "$framework_id" ]] || die "The framework identifier is empty."
    printf '%s\t%s\tframework\n' "$framework_id" "$bin/MediaRemoteAdapter.framework"
  fi
  printf '%s\t%s\tapp\n' "$app_id" "$app"
}

# Validate in the caller's control flow; process substitution hides failures.
component_list() {
  local app="$1" listing id path kind found_app=false
  listing="$(components "$app")" || die "Cannot list signed components."
  while IFS=$'\t' read -r id path kind; do
    if [[ "$id" == "$app_id" && "$path" == "$app" && "$kind" == app ]]; then
      found_app=true
    fi
  done <<<"$listing"
  [[ "$found_app" == true ]] || die "Component list does not include the app."
  printf '%s\n' "$listing"
}

verify() {
  local app="$1" fp id path kind expected actual listing
  listing="$(component_list "$app")" || die "Cannot verify an incomplete component list."
  fp="$(fingerprint)"
  codesign --verify --deep --strict "$app" || die "Signature check failed for $app."
  while IFS=$'\t' read -r id path kind; do
    expected="$(requirement "$id" "$fp")"
    codesign --verify --strict -R "=$expected" "$path" ||
      die "$(basename "$path") does not satisfy its release requirement."
    actual="$(codesign --display --requirements - "$path" 2>/dev/null | sed -n 's/^designated => //p')"
    if [[ "$(normalize <<<"$actual")" != "$(normalize <<<"$expected")" || "$actual" == *cdhash* ]]; then
      die "Unstable designated requirement for $(basename "$path")."
    fi
  done <<<"$listing"
  echo "[sign] Verified release signatures against the pinned certificate."
}

sign() {
  local app="$1" fp identity password work keychain keychain_password listing
  listing="$(component_list "$app")" || die "Cannot sign an incomplete component list."
  fp="$(fingerprint)"

  if [[ -n "${WHISPER_SIGNING_CERTIFICATE+set}" || -n "${WHISPER_SIGNING_PASSWORD+set}" ]]; then
    [[ -n "${WHISPER_SIGNING_CERTIFICATE:-}" && -n "${WHISPER_SIGNING_PASSWORD:-}" ]] ||
      die "Set both WHISPER_SIGNING_CERTIFICATE and WHISPER_SIGNING_PASSWORD."
    password="$WHISPER_SIGNING_PASSWORD"
  else
    [[ -s "$signing_dir/identity.p12" && -s "$signing_dir/password" ]] ||
      die "Signing identity is missing. Restore the Whisper signing backup or set the release signing secrets."
    password="$(<"$signing_dir/password")"
  fi

  work="$(cd "$(mktemp -d "${TMPDIR:-/tmp}/whisper-signing.XXXXXX")" && pwd -P)"
  keychain="$work/signing.keychain-db"
  keychain_password="$(openssl rand -base64 32)"
  identity="$work/identity.p12"
  # shellcheck disable=SC2064
  trap "cleanup '$work' '$keychain'" EXIT

  (
    umask 077
    if [[ -n "${WHISPER_SIGNING_CERTIFICATE:-}" ]]; then
      printf '%s' "$WHISPER_SIGNING_CERTIFICATE" | tr -d '[:space:]' | base64 -D >"$identity" 2>/dev/null ||
        die "WHISPER_SIGNING_CERTIFICATE must contain a base64-encoded PKCS#12 file."
    else
      cp "$signing_dir/identity.p12" "$identity"
    fi
  )
  [[ -s "$identity" ]] || die "WHISPER_SIGNING_CERTIFICATE must contain a base64-encoded PKCS#12 file."

  security create-keychain -p "$keychain_password" "$keychain" >/dev/null || die "Creating temporary signing keychain failed."
  # codesign --keychain narrows identity lookup but still uses the search list.
  search_list_add "$keychain"
  security unlock-keychain -p "$keychain_password" "$keychain" >/dev/null || die "Unlocking signing keychain failed."
  security set-keychain-settings -lut 3600 "$keychain" >/dev/null
  # security errors can echo arguments, which include the password.
  security import "$identity" -k "$keychain" -P "$password" -T /usr/bin/codesign >/dev/null 2>&1 ||
    die "Importing signing identity failed."
  security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain" >/dev/null 2>&1 ||
    die "Configuring signing key access failed."
  # No -v: a self-signed identity need not be trusted system-wide.
  security find-identity -p codesigning "$keychain" | grep -qi ") $fp \"" ||
    die "The imported signing identity does not match the pinned certificate or has no private key."

  local id path kind options
  while IFS=$'\t' read -r id path kind; do
    options=()
    case "$kind" in
      helper) options=(--options runtime) ;;
      app) options=(--options runtime --entitlements "$entitlements") ;;
    esac
    codesign --force --sign "$fp" --keychain "$keychain" --timestamp=none \
      --identifier "$id" --requirements "=designated => $(requirement "$id" "$fp")" \
      ${options[@]+"${options[@]}"} "$path" ||
      die "Signing $(basename "$path") failed."
  done <<<"$listing"

  verify "$app"
}

search_list() {
  security list-keychains -d user | sed -n 's/^[[:space:]]*"\(.*\)"[[:space:]]*$/\1/p'
}

search_list_add() {
  local current=()
  while IFS= read -r entry; do current+=("$entry"); done < <(search_list)
  security list-keychains -d user -s ${current[@]+"${current[@]}"} "$1"
}

cleanup() {
  local work="$1" keychain="$2" remaining=()
  if [[ -e "$keychain" ]]; then
    security delete-keychain "$keychain" >/dev/null 2>&1 || true
  fi
  while IFS= read -r entry; do
    [[ "$entry" == "$keychain" ]] || remaining+=("$entry")
  done < <(search_list)
  security list-keychains -d user -s ${remaining[@]+"${remaining[@]}"} || true
  rm -rf "$work"
}

[[ "$(uname -s)" == Darwin ]] || die "Whisper release signing requires macOS."
case "${1:-}" in
  sign | verify)
    [[ -d "${2:-}" ]] || die "Usage: $0 sign|verify /path/to/Whisper.app"
    "$1" "$(cd "$2" && pwd -P)"
    ;;
  *) die "Usage: $0 sign|verify /path/to/Whisper.app" ;;
esac