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:
Gabriel Brown
2026-08-21 18:57:26 -04:00
parent 51ceb19480
commit f42b3cfe0e
6 changed files with 221 additions and 2 deletions
@@ -94,6 +94,34 @@ PanelWindow {
wrapMode: Text.WordWrap
}
// The caller's stated reason, when panama-sudo passed one.
// Untrusted commentary from an unprivileged process, so it is
// labeled as a claim and drawn beside polkitd's message above --
// never in place of it. The real action text is the trust anchor.
Column {
width: parent.width
visible: Polkit.statedReason !== ""
spacing: 2
Text {
text: "Stated reason (unverified)"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
}
Text {
width: parent.width
text: Polkit.statedReason
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.italic: true
wrapMode: Text.WordWrap
}
}
Text {
width: parent.width
visible: Polkit.users.length > 1
+27
View File
@@ -38,12 +38,31 @@ Singleton {
// How many times a wrong password has been offered for this request.
property int attempts: 0
// A caller's stated reason for the NEXT request, and the one attached to
// the request on screen. Untrusted by design -- any process can state one
// -- so the prompt shows it clearly labeled beside polkitd's real action
// message, never in place of it. See stateReason().
property var pendingReason: null
property string statedReason: ""
readonly property bool active: root.request !== null
// Held only between pressing Enter and the helper accepting it on stdin.
property string pendingSecret: ""
// panama-sudo's side channel: state WHY the authentication request about
// to arrive is being made, so the prompt can say more than the generic
// action text. Single-shot and short-lived -- it attaches only to the next
// request, and only if that request arrives within ten seconds -- so a
// stale reason can never dress up an unrelated prompt.
function stateReason(text: string): void {
const trimmed = String(text).trim().slice(0, 200);
if (trimmed === "")
return;
root.pendingReason = { text: trimmed, at: Date.now() };
}
function begin(path: string): void {
// A second request while one is open would leave the first
// unanswerable; polkit serializes these in practice, and refusing is
@@ -69,6 +88,13 @@ Singleton {
: (root.users.length > 0 ? String(root.users[0]) : "");
root.attempts = 0;
root.failureText = "";
// Consume the stated reason whether or not it is still fresh:
// either way it must not survive to a later request.
const pending = root.pendingReason;
root.pendingReason = null;
root.statedReason = (pending !== null && Date.now() - pending.at <= 10000)
? pending.text
: "";
} catch (error) {
console.warn("Polkit: could not read the request:", error);
root.dismiss("failed");
@@ -114,6 +140,7 @@ Singleton {
root.failureText = "";
root.pendingSecret = "";
root.authenticating = false;
root.statedReason = "";
}
function responsePathFor(path: string): string {
+8 -1
View File
@@ -424,8 +424,15 @@ ShellRoot {
target: "polkit"
function begin(path: string): void { Polkit.begin(path); }
function cancel(): void { Polkit.cancel(); }
// panama-sudo's side channel: the reason a privileged command is about
// to run, shown labeled on the prompt beside polkitd's own message.
function reason(text: string): void { Polkit.stateReason(text); }
function status(): string {
return JSON.stringify({ active: Polkit.active, action: Polkit.actionId });
return JSON.stringify({
active: Polkit.active,
action: Polkit.actionId,
statedReason: Polkit.statedReason
});
}
}