Drop two fields nobody needed: session was a no-op, and no-unlock is the finger

Jorijn caught both.

`session` declared nothing. The FingerMatched signal is emitted for every
matched finger unconditionally -- it never consulted the config -- so a
`session` line was a rule the format invited you to write that did exactly
nothing. Announcing every finger is the right default anyway: a session agent
should not need a root-owned file to declare its interest in a signal it is
free to ignore. The column is gone.

Which leaves the config for the two things that really do need the daemon, and
with `session` gone the verdict column had no partner left to vary against. It
read as a property of the finger while being a property of the attempt, so it
is now written as what it is:

    <finger>  [no-unlock]  [absolute command...]

no-unlock says the finger never unlocks; a command is what root runs. At least
one is required, because a finger listed alone says nothing the signal does not
already say -- and that is a parse error rather than a silently useless line.

The example config now also states plainly what no-unlock is not. It is a panic
button, not deniability: the rejection it fabricates comes back in milliseconds
where a real one takes about three seconds, the journal records that the finger
actually matched, the file names the finger in plain text, and the finger still
shows as enrolled. Both of those weaknesses are real and neither is fixed here.
This commit is contained in:
Jorijn van der Graaf 2026-09-05 05:23:40 +02:00
commit 93d7f96a63
5 changed files with 180 additions and 197 deletions

View file

@ -68,7 +68,7 @@ namespace {
// Bumping this is what publishes a package: the registry answers 409 for a
// version it already has, which a build treats as a no-op.
constexpr const char* Version = "0.2.0";
constexpr const char* Version = "0.2.1";
bool g_verbose = false;
// 500 ms was the research harness's pace, chosen so a human could read the
@ -2160,9 +2160,9 @@ void LoadActions() {
}
g_actions = std::move(p.rules);
for (const ac::Rule& r : g_actions)
std::println("action: {} -> {}{}", fingerprintd::store::NameOf(r.finger),
r.where == ac::Where::Session ? "the user's session" : r.command,
r.verdict == ac::Verdict::NoMatch ? " (reported as no-match)" : "");
std::println("action: {}{}{}", fingerprintd::store::NameOf(r.finger),
r.unlocks ? "" : " does NOT unlock",
r.command.empty() ? "" : std::format(" runs {}", r.command));
}
// Run a system action. Double-forked so the grandchild is reparented to init
@ -2440,8 +2440,8 @@ void PostEvent(std::unique_ptr<Event> ev) {
// The verdict override happens BEFORE the client is told, because
// it is the whole point of a duress rule: the phone must look like
// it did not recognise the finger. Everything else happens after.
if (rule && rule->verdict == ac::Verdict::NoMatch) {
std::println("action: {} is configured no-match; reporting a rejection",
if (rule && !rule->unlocks) {
std::println("action: {} is configured no-unlock; reporting a rejection",
store::NameOf(*matched));
status = "verify-no-match";
}
@ -2449,15 +2449,17 @@ void PostEvent(std::unique_ptr<Event> ev) {
EmitDevice("VerifyStatus", g_variant_new("(sb)", status.c_str(), ev->done ? TRUE : FALSE));
if (ev->done && ev->status == "verify-match")
std::println("verified fid {} for uid {}", ev->fid, g_claim.uid);
// Told to the session AFTER the verdict, on the same principle
// that keeps the harvest and the save off the unlock path: an
// agent that is slow, or absent, must not delay an unlock.
// UNCONDITIONAL, and that is the design: every matched finger is
// announced, so a session agent needs nothing declared in the
// root-owned config to hear about one. What a finger should DO in
// a session is the user's business, decided by the user's own
// agent from the user's own configuration -- root has no session
// bus, no display and no business launching someone's
// applications.
//
// The signal carries the finger name and nothing else. What a
// finger should DO in a session is the user's business, decided
// by the user's own agent from the user's own configuration --
// root has no session bus, no display and no business launching
// someone's applications.
// After the verdict, on the same principle that keeps the harvest
// and the save off the unlock path: an agent that is slow, or
// absent, must not delay an unlock.
if (matched && g_conn) {
g_dbus_connection_emit_signal(
g_conn, nullptr, DevicePath, ActionIface, "FingerMatched",
@ -2465,7 +2467,7 @@ void PostEvent(std::unique_ptr<Event> ev) {
static_cast<guint32>(g_claim.uid)),
nullptr);
}
if (rule && rule->where == ac::Where::System) {
if (rule && !rule->command.empty()) {
std::println("action: {} -> running {}", store::NameOf(*matched), rule->command);
RunSystemAction(rule->command, std::string(store::NameOf(*matched)));
}