#!/usr/bin/env bash
#
# Create a local, self-signed code-signing identity for OpenLatch dev builds.
#
# Why you want this
# -----------------
# Cargo's linker ad-hoc-signs every binary it produces, and an ad-hoc signature
# has no stable identity — its designated requirement is the CDHash, which
# changes on every compile. macOS keys keychain ACLs to that identity, so each
# rebuild makes the OS treat `openlatch` as a brand-new program and re-prompt:
#
#   "openlatch wants to use your confidential information stored in
#    'openlatch' in your keychain."
#
# Clicking "Always Allow" cannot help, because the thing it remembers is
# invalidated by the next `cargo build`. Signing dev builds with one stable
# self-signed certificate makes the ACL stick permanently.
#
# This is a *local development* identity. It is not trusted by anyone else's
# machine and is not a substitute for the Developer ID certificate used for
# released binaries.
#
# Usage
# -----
#   ci/setup-dev-codesign-macos.sh            # create if missing, then print
#                                             # the export line to use
#   ci/setup-dev-codesign-macos.sh --remove   # delete the identity again
#
# After creating it:
#   export OPENLATCH_CODESIGN_IDENTITY="OpenLatch Dev Signing"
#   cargo build --release
#   ci/codesign-macos.sh target/release/openlatch target/release/openlatch-hook
#
# You will be asked for your login password once, when the private key is
# imported and again when its partition list is set — that is macOS confirming
# you want a new signing key, not the recurring dialog this removes.

set -euo pipefail

CN="OpenLatch Dev Signing"
KEYCHAIN="${HOME}/Library/Keychains/login.keychain-db"

if [ "$(uname -s)" != "Darwin" ]; then
  echo "setup-dev-codesign: not macOS, nothing to do."
  exit 0
fi

# Usable for signing: present AND trusted. `-v` filters out an imported but
# untrusted cert, which reports CSSMERR_TP_NOT_TRUSTED.
identity_valid() {
  security find-identity -v -p codesigning 2>/dev/null | grep -qF "$CN"
}

# Present in the keychain at all, trusted or not.
cert_present() {
  security find-certificate -c "$CN" "$KEYCHAIN" >/dev/null 2>&1
}

# Trusting a self-signed cert raises a GUI authorization dialog. It is split
# out so an interrupted or cancelled run can be resumed without minting a
# second certificate — the failure mode that "create if find-identity -v is
# empty" would otherwise produce every retry.
trust_cert() {
  local pem
  pem="$(mktemp)"
  security find-certificate -c "$CN" -p "$KEYCHAIN" >"$pem"
  local rc=0
  security add-trusted-cert -r trustRoot -p codeSign -k "$KEYCHAIN" "$pem" || rc=$?
  rm -f "$pem"
  return "$rc"
}

if [ "${1:-}" = "--remove" ]; then
  echo "Removing '$CN' from $KEYCHAIN ..."
  # Certificate and private key are separate keychain items.
  while security find-certificate -c "$CN" "$KEYCHAIN" >/dev/null 2>&1; do
    security delete-certificate -c "$CN" -t "$KEYCHAIN"
  done
  echo "Removed. Unset OPENLATCH_CODESIGN_IDENTITY in your shell profile too."
  exit 0
fi

if identity_valid; then
  echo "Code-signing identity '$CN' already present and trusted."
elif cert_present; then
  # Resume path: a previous run imported the key but its trust dialog was
  # cancelled or timed out.
  echo "Identity '$CN' exists but is not trusted yet — completing setup."
  echo "Approve the 'Certificate Trust Settings' dialog (login password)."
  if ! trust_cert || ! identity_valid; then
    echo "ERROR: could not trust '$CN'." >&2
    echo "       Re-run this script and approve the dialog, or start over with:" >&2
    echo "         $0 --remove && $0" >&2
    exit 1
  fi
  echo "Trusted."
else
  echo "Creating self-signed code-signing identity '$CN' ..."

  WORK="$(mktemp -d)"
  trap 'rm -rf "$WORK"' EXIT

  # basicConstraints + keyUsage + extendedKeyUsage=codeSigning are all required
  # for `security find-identity -p codesigning` to consider this usable.
  openssl req -x509 \
    -newkey rsa:2048 \
    -nodes \
    -keyout "$WORK/key.pem" \
    -out "$WORK/cert.pem" \
    -days 3650 \
    -subj "/CN=${CN}" \
    -addext "basicConstraints=critical,CA:false" \
    -addext "keyUsage=critical,digitalSignature" \
    -addext "extendedKeyUsage=critical,codeSigning" \
    2>/dev/null

  # The transport password is deliberately non-empty: macOS Security.framework
  # rejects an empty-password PKCS#12 produced by the system LibreSSL with
  # "MAC verification failed". The bundle exists only inside $WORK and is
  # deleted on exit, so the value carries no secrecy requirement.
  P12_PASS="openlatch-dev-transport"

  openssl pkcs12 -export \
    -inkey "$WORK/key.pem" \
    -in "$WORK/cert.pem" \
    -out "$WORK/identity.p12" \
    -name "$CN" \
    -passout "pass:${P12_PASS}"

  # -T /usr/bin/codesign puts codesign on the private key's ACL so signing does
  # not prompt on every invocation.
  security import "$WORK/identity.p12" \
    -k "$KEYCHAIN" \
    -P "$P12_PASS" \
    -T /usr/bin/codesign

  # Self-signed certs are not trusted by default, so codesign would refuse the
  # identity with CSSMERR_TP_NOT_TRUSTED. trustRoot in the *user* domain needs
  # no sudo, but does raise a GUI authorization dialog.
  echo "Approve the 'Certificate Trust Settings' dialog (login password)."
  if ! trust_cert; then
    echo "ERROR: trust settings were not applied (dialog cancelled?)." >&2
    echo "       The key is imported — just re-run this script to finish:" >&2
    echo "         $0" >&2
    exit 1
  fi

  # Best effort: pre-authorizing codesign on the key's partition list avoids
  # even the one-time "allow access to this key" dialog. It needs the login
  # keychain password, which we deliberately do not ask for or store — if it
  # fails, the only cost is clicking "Always Allow" once the first time you
  # sign. That click sticks, unlike the ones this whole exercise removes.
  if ! security set-key-partition-list \
    -S apple-tool:,apple:,codesign: \
    -s "$KEYCHAIN" >/dev/null 2>&1; then
    echo "  (partition list not pre-authorized — expect one 'Always Allow'"
    echo "   dialog the first time you sign; it will not repeat)"
  fi

  if ! identity_valid; then
    echo "ERROR: identity was created but is not usable for code signing." >&2
    echo "       Check: security find-identity -v -p codesigning" >&2
    exit 1
  fi

  echo "Created."
fi

cat <<EOF

Identity ready. Add this to your shell profile so every dev build is signed:

    export OPENLATCH_CODESIGN_IDENTITY="$CN"

Then, after each build:

    ci/codesign-macos.sh target/release/openlatch target/release/openlatch-hook

The first keychain dialog after switching to signed builds still appears once —
click "Always Allow" and it will not come back, because the ACL now records a
certificate that survives rebuilds instead of a per-compile hash.
EOF
