Let the password prompt say why
panama-sudo is pkexec with a stated reason: the reason travels to the shell over the existing polkit IPC target, and the prompt renders it labeled "Stated reason (unverified)" beside polkitd's real action message -- beside, never instead of, because any process can claim any reason and the action text is the trust anchor. Reasons are single-shot and expire in ten seconds, so a stale one cannot dress up an unrelated prompt; without a reason, a running shell, or qs the wrapper is exactly pkexec. Built for agents, so the person typing their password learns what for. Verified live end to end -- reason shown, consumed once, expired when stale, cleared on dismissal -- and pinned by the polkit reason contract.
This commit is contained in:
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# The stated reason on the authentication prompt.
|
||||
#
|
||||
# panama-sudo lets a caller say WHY it is about to trigger a password prompt.
|
||||
# The reason is untrusted text from an unprivileged process, so the properties
|
||||
# worth pinning are the ones that keep it honest:
|
||||
#
|
||||
# 1. The prompt renders the reason BESIDE polkitd's real action message,
|
||||
# labeled as unverified -- never in place of it. Any process can claim
|
||||
# "Updating your system" while requesting something else; the action text
|
||||
# is the trust anchor and must survive.
|
||||
# 2. Single-shot and short-lived: a reason attaches to the next request
|
||||
# only, is consumed whether or not it was fresh, and expires rather than
|
||||
# dressing up an unrelated prompt minutes later.
|
||||
# 3. panama-sudo degrades to plain pkexec: no --reason, no qs, or a dead
|
||||
# shell must all still run the command.
|
||||
#
|
||||
# The wrapper is exercised for real against stub qs and pkexec; the QML side
|
||||
# is pinned statically, the way the polkit-agent contract pins its rules.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
wrapper="$repo_dir/bin/panama-sudo"
|
||||
service="$repo_dir/config/dot/quickshell/services/Polkit.qml"
|
||||
prompt="$repo_dir/config/dot/quickshell/modules/polkit/PolkitPrompt.qml"
|
||||
shell_qml="$repo_dir/config/dot/quickshell/shell.qml"
|
||||
|
||||
findings=()
|
||||
note() { findings+=("$1"); }
|
||||
|
||||
[[ -x "$wrapper" ]] || { printf 'polkit reason contract: %s is not executable\n' "$wrapper" >&2; exit 1; }
|
||||
|
||||
# ── The wrapper, for real ────────────────────────────────────────────────────
|
||||
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
calls="$work/calls"
|
||||
stub_dir="$work/bin"
|
||||
mkdir -p "$stub_dir"
|
||||
|
||||
for command in qs pkexec; do
|
||||
cat >"$stub_dir/$command" <<STUB
|
||||
#!/usr/bin/env bash
|
||||
printf '%s %s\n' "$command" "\$*" >>"$calls"
|
||||
STUB
|
||||
chmod +x "$stub_dir/$command"
|
||||
done
|
||||
|
||||
run_wrapper() { PATH="$stub_dir:$PATH" "$wrapper" "$@" >/dev/null 2>&1; }
|
||||
|
||||
# A reason reaches the shell first, then the command runs unchanged.
|
||||
: >"$calls"
|
||||
run_wrapper --reason "Test reason" -- some-command --with args \
|
||||
|| note 'the wrapper failed with a reason and a command'
|
||||
grep -q 'qs ipc call polkit reason Test reason' "$calls" \
|
||||
|| note 'the reason never reaches the shell over IPC'
|
||||
grep -q 'pkexec some-command --with args' "$calls" \
|
||||
|| note 'the command does not reach pkexec unchanged'
|
||||
[[ "$(head -1 "$calls")" == qs* ]] \
|
||||
|| note 'the reason is sent after pkexec instead of before the prompt can appear'
|
||||
|
||||
# No reason means no IPC chatter, and still pkexec.
|
||||
: >"$calls"
|
||||
run_wrapper -- some-command || note 'the wrapper failed without a reason'
|
||||
grep -q 'qs' "$calls" && note 'the wrapper calls qs even when no reason was given'
|
||||
grep -q 'pkexec some-command' "$calls" || note 'a reasonless call does not reach pkexec'
|
||||
|
||||
# A dead shell must not cost the command: qs failing is stepped over.
|
||||
cat >"$stub_dir/qs" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
exit 1
|
||||
STUB
|
||||
chmod +x "$stub_dir/qs"
|
||||
: >"$calls"
|
||||
run_wrapper --reason "Doomed" -- some-command \
|
||||
|| note 'a failing qs stops the command instead of degrading to plain pkexec'
|
||||
grep -q 'pkexec some-command' "$calls" \
|
||||
|| note 'the command is lost when the shell is not answering'
|
||||
|
||||
# No command is a usage error, not a bare pkexec prompt for nothing.
|
||||
run_wrapper --reason "Aimless" -- && note 'the wrapper accepts a reason with no command'
|
||||
|
||||
# ── The QML side, statically ─────────────────────────────────────────────────
|
||||
|
||||
# The IPC door exists and feeds the service.
|
||||
rg -Fq 'target: "polkit"' "$shell_qml" \
|
||||
|| note 'shell.qml has no polkit IPC target'
|
||||
rg -Fq 'Polkit.stateReason(text)' "$shell_qml" \
|
||||
|| note 'the polkit IPC target does not feed Polkit.stateReason'
|
||||
|
||||
# Single-shot, bounded, and cleared: consumed on adopt even when stale, aged
|
||||
# against a ten-second window, and wiped with the rest of the request state.
|
||||
rg -Fq 'root.pendingReason = null' "$service" \
|
||||
|| note 'a stated reason is not consumed when a request arrives'
|
||||
rg -Fq 'pending.at <= 10000' "$service" \
|
||||
|| note 'a stated reason never expires, so it can dress up a later prompt'
|
||||
rg -Fq 'root.statedReason = ""' "$service" \
|
||||
|| note 'the stated reason survives dismissal'
|
||||
|
||||
# The prompt shows the real message AND the labeled reason -- both, in that
|
||||
# trust order.
|
||||
rg -Fq 'text: Polkit.message' "$prompt" \
|
||||
|| note "polkitd's own action message is no longer rendered"
|
||||
rg -Fq 'text: Polkit.statedReason' "$prompt" \
|
||||
|| note 'the stated reason is never rendered'
|
||||
rg -Fq 'Stated reason (unverified)' "$prompt" \
|
||||
|| note 'the stated reason is not labeled as an unverified claim'
|
||||
|
||||
if (( ${#findings[@]} > 0 )); then
|
||||
printf 'polkit reason contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||
printf ' - %s\n' "${findings[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'polkit reason contract: PASS\n'
|
||||
Reference in New Issue
Block a user