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:
parent
928fe1482e
commit
93d7f96a63
5 changed files with 180 additions and 197 deletions
|
|
@ -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)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,43 +5,42 @@
|
|||
/*
|
||||
Fingerprintd:Actions — what a finger means, beyond "it was you".
|
||||
|
||||
The trustlet reports WHICH finger matched, and until now the daemon only used
|
||||
that to answer yes. This module holds the table that gives each finger a
|
||||
meaning: run a command as root, tell the user's session, or answer no-match
|
||||
while doing one of those anyway -- which is the duress case, where the phone
|
||||
should look like it simply did not recognise the finger.
|
||||
The trustlet reports WHICH finger matched, and the daemon used to spend that
|
||||
only on answering yes. Two things can now hang off it.
|
||||
|
||||
Two rules shape the whole design.
|
||||
The first needs no configuration at all: every matched finger is announced on
|
||||
the bus as net.catcrafts.Fingerprintd1.FingerMatched(finger, uid), always. That
|
||||
is how a finger launches an application — an agent in the user's own session
|
||||
hears it and decides what it means, from the user's own configuration. Root has
|
||||
no session bus, no display and no business starting someone's applications, so
|
||||
the daemon deliberately does not try; and since the signal is unconditional,
|
||||
there is nothing to declare here to receive it.
|
||||
|
||||
ROOT DOES NOT LAUNCH APPS. The daemon runs as root with no session bus, no
|
||||
Wayland display and no user environment, so it cannot meaningfully start a
|
||||
user's application, and trying would either fail or run the user's software as
|
||||
root. So a session action is not a command here at all: the daemon emits a
|
||||
signal naming the finger, and an agent in the user's own session decides what
|
||||
that means from the user's own configuration. The only commands in this file
|
||||
are ones root is supposed to run.
|
||||
What is left for this file is the two things that DO need the daemon:
|
||||
|
||||
WHICH MAKES THIS FILE A ROOT-EXECUTION SURFACE. Anything that can write it
|
||||
gets root at the next press of a finger. The parser therefore refuses a file
|
||||
that is not owned by root or that anyone else can write, and refuses it
|
||||
WHOLESALE rather than skipping the offending line -- a half-applied security
|
||||
policy is worse than none. Ownership is checked by the shell, which has the
|
||||
stat; this module states the rule and holds the verdict.
|
||||
<finger> [no-unlock] [absolute command...]
|
||||
|
||||
Format, one rule per line, four fields:
|
||||
finger an fprintd finger name, e.g. right-index-finger
|
||||
no-unlock this finger never unlocks. The client is told the finger did
|
||||
not match, whatever really happened. The duress case.
|
||||
command run by root when this finger matches.
|
||||
|
||||
<finger> <where> <verdict> <command...>
|
||||
At least one of the two must be present; a line with neither says nothing the
|
||||
signal above does not already say, and is more likely a mistake than an
|
||||
intention. '#' comments and blank lines are ignored, and a finger with no line
|
||||
behaves exactly as it always did.
|
||||
|
||||
finger an fprintd finger name, e.g. right-index-finger
|
||||
where system -- root runs the command
|
||||
session -- the user's agent is told; no command is run here
|
||||
verdict match -- the client is told the finger matched (normal)
|
||||
no-match -- the client is told it did not, whatever really
|
||||
happened. The duress case.
|
||||
command required for system, and must be absent for session
|
||||
THIS FILE IS A ROOT-EXECUTION SURFACE. Anything that can write it gets root at
|
||||
the next press of a finger. The parser refuses a file that is not owned by root
|
||||
or that anyone else can write, and refuses it WHOLESALE rather than skipping
|
||||
the offending line -- a half-applied security policy is worse than none.
|
||||
Ownership is checked by the shell, which has the stat; this module states the
|
||||
rule and holds the verdict.
|
||||
|
||||
'#' comments and blank lines are ignored. A finger with no rule behaves
|
||||
exactly as before, which is what makes the feature absent until configured.
|
||||
WHAT no-unlock IS NOT. It is a panic button, not deniability. The rejection it
|
||||
fabricates is far faster than a real one, this file names the finger in plain
|
||||
text, and the finger still shows as enrolled. It reliably runs your script; it
|
||||
does not reliably hide that it did.
|
||||
*/
|
||||
|
||||
export module Fingerprintd:Actions;
|
||||
|
|
@ -50,14 +49,13 @@ import :Store;
|
|||
|
||||
export namespace fingerprintd::actions {
|
||||
|
||||
enum class Where { System, Session };
|
||||
enum class Verdict { Match, NoMatch };
|
||||
|
||||
struct Rule {
|
||||
store::Finger finger{};
|
||||
Where where = Where::System;
|
||||
Verdict verdict = Verdict::Match;
|
||||
std::string command; // empty for Session
|
||||
// False for a duress finger: it matches, and the client is told it did
|
||||
// not. Named for what the administrator wants rather than for the
|
||||
// fprintd status it produces.
|
||||
bool unlocks = true;
|
||||
std::string command; // empty = announce only
|
||||
};
|
||||
|
||||
// Why a file was rejected. Rejection is total: no rule from a file that
|
||||
|
|
@ -66,12 +64,9 @@ export namespace fingerprintd::actions {
|
|||
None,
|
||||
NotWritableOnlyByRoot, // the shell's stat says someone else can write it
|
||||
UnknownFinger,
|
||||
UnknownWhere,
|
||||
UnknownVerdict,
|
||||
MissingCommand, // system without a command
|
||||
UnexpectedCommand, // session with one
|
||||
DuplicateFinger, // two rules for one finger: ambiguous, not merged
|
||||
RelativeCommand, // a command that is not an absolute path
|
||||
NothingToDo, // neither no-unlock nor a command
|
||||
DuplicateFinger, // two rules for one finger: ambiguous, not merged
|
||||
};
|
||||
|
||||
inline constexpr std::string_view Describe(Error e) {
|
||||
|
|
@ -79,12 +74,9 @@ export namespace fingerprintd::actions {
|
|||
case Error::None: return "ok";
|
||||
case Error::NotWritableOnlyByRoot: return "the file must be owned by root and writable by no one else";
|
||||
case Error::UnknownFinger: return "not an fprintd finger name";
|
||||
case Error::UnknownWhere: return "expected 'system' or 'session'";
|
||||
case Error::UnknownVerdict: return "expected 'match' or 'no-match'";
|
||||
case Error::MissingCommand: return "a system rule needs a command";
|
||||
case Error::UnexpectedCommand: return "a session rule runs no command here; the user's agent decides";
|
||||
case Error::DuplicateFinger: return "two rules for the same finger";
|
||||
case Error::RelativeCommand: return "the command must be an absolute path";
|
||||
case Error::NothingToDo: return "expected 'no-unlock', a command, or both";
|
||||
case Error::DuplicateFinger: return "two rules for the same finger";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
|
@ -96,21 +88,12 @@ export namespace fingerprintd::actions {
|
|||
bool Ok() const { return error == Error::None; }
|
||||
};
|
||||
|
||||
// Split on runs of spaces and tabs, keeping the tail intact from `upto`
|
||||
// fields onward so a command may contain spaces.
|
||||
inline std::vector<std::string_view> Fields(std::string_view line, std::size_t upto) {
|
||||
std::vector<std::string_view> out;
|
||||
std::size_t i = 0;
|
||||
while (i < line.size()) {
|
||||
while (i < line.size() && (line[i] == ' ' || line[i] == '\t')) i++;
|
||||
if (i >= line.size()) break;
|
||||
if (out.size() == upto) { out.push_back(line.substr(i)); break; }
|
||||
std::size_t j = i;
|
||||
while (j < line.size() && line[j] != ' ' && line[j] != '\t') j++;
|
||||
out.push_back(line.substr(i, j - i));
|
||||
i = j;
|
||||
}
|
||||
return out;
|
||||
inline constexpr std::string_view NoUnlockKeyword = "no-unlock";
|
||||
|
||||
inline std::string_view TrimBlanks(std::string_view v) {
|
||||
while (!v.empty() && (v.front() == ' ' || v.front() == '\t')) v.remove_prefix(1);
|
||||
while (!v.empty() && (v.back() == ' ' || v.back() == '\t')) v.remove_suffix(1);
|
||||
return v;
|
||||
}
|
||||
|
||||
// `rootOnlyWritable` is the shell's answer about the file's mode and
|
||||
|
|
@ -136,42 +119,36 @@ export namespace fingerprintd::actions {
|
|||
lineNo++;
|
||||
std::string_view line(part.begin(), part.end());
|
||||
if (!line.empty() && line.back() == '\r') line.remove_suffix(1);
|
||||
// Trim leading blanks so a comment may be indented.
|
||||
std::size_t s = line.find_first_not_of(" \t");
|
||||
if (s == std::string_view::npos) continue;
|
||||
line.remove_prefix(s);
|
||||
if (line.front() == '#') continue;
|
||||
line = TrimBlanks(line);
|
||||
if (line.empty() || line.front() == '#') continue;
|
||||
|
||||
auto f = Fields(line, 3);
|
||||
if (f.size() < 3) return reject(Error::UnknownWhere, lineNo);
|
||||
std::size_t sp = line.find_first_of(" \t");
|
||||
std::string_view name = line.substr(0, sp);
|
||||
std::string_view rest = sp == std::string_view::npos
|
||||
? std::string_view{} : TrimBlanks(line.substr(sp));
|
||||
|
||||
Rule r;
|
||||
auto fin = store::FingerFromName(std::string(f[0]));
|
||||
auto fin = store::FingerFromName(std::string(name));
|
||||
if (!fin) return reject(Error::UnknownFinger, lineNo);
|
||||
r.finger = *fin;
|
||||
|
||||
if (f[1] == "system") r.where = Where::System;
|
||||
else if (f[1] == "session") r.where = Where::Session;
|
||||
else return reject(Error::UnknownWhere, lineNo);
|
||||
|
||||
if (f[2] == "match") r.verdict = Verdict::Match;
|
||||
else if (f[2] == "no-match") r.verdict = Verdict::NoMatch;
|
||||
else return reject(Error::UnknownVerdict, lineNo);
|
||||
|
||||
if (f.size() > 3) {
|
||||
std::string_view cmd = f[3];
|
||||
while (!cmd.empty() && (cmd.back() == ' ' || cmd.back() == '\t')) cmd.remove_suffix(1);
|
||||
r.command = std::string(cmd);
|
||||
// The keyword is optional and, when present, leads.
|
||||
if (rest == NoUnlockKeyword) {
|
||||
r.unlocks = false;
|
||||
rest = {};
|
||||
} else if (rest.starts_with(NoUnlockKeyword)
|
||||
&& (rest[NoUnlockKeyword.size()] == ' '
|
||||
|| rest[NoUnlockKeyword.size()] == '\t')) {
|
||||
r.unlocks = false;
|
||||
rest = TrimBlanks(rest.substr(NoUnlockKeyword.size()));
|
||||
}
|
||||
r.command = std::string(rest);
|
||||
|
||||
if (r.where == Where::System && r.command.empty())
|
||||
return reject(Error::MissingCommand, lineNo);
|
||||
if (r.where == Where::Session && !r.command.empty())
|
||||
return reject(Error::UnexpectedCommand, lineNo);
|
||||
if (r.unlocks && r.command.empty()) return reject(Error::NothingToDo, lineNo);
|
||||
// An absolute path only. Resolving a bare name through PATH would
|
||||
// make what root executes depend on an environment this daemon
|
||||
// does not control.
|
||||
if (r.where == Where::System && !r.command.starts_with('/'))
|
||||
if (!r.command.empty() && !r.command.starts_with('/'))
|
||||
return reject(Error::RelativeCommand, lineNo);
|
||||
for (const Rule& e : p.rules) {
|
||||
if (e.finger == r.finger) return reject(Error::DuplicateFinger, lineNo);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
# Alpine, so an APKBUILD that compiled from source could not be built by
|
||||
# anyone but us either.
|
||||
pkgname=fingerprintd
|
||||
pkgver=0.2.0
|
||||
pkgver=0.2.1
|
||||
pkgrel=0
|
||||
pkgdesc="Fingerprint daemon for the Fairphone 6 (FocalTech FT9391 behind QTEE)"
|
||||
url="https://forgejo.catcrafts.net/Catcrafts/fingerprintd"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,25 @@
|
|||
# fingerprintd — per-finger actions.
|
||||
#
|
||||
# Install as /etc/fingerprintd/actions.conf. With no such file, a finger does
|
||||
# exactly what it always did: it unlocks, and nothing else happens.
|
||||
# exactly what it always did: it unlocks.
|
||||
#
|
||||
# THIS FILE IS A ROOT SHELL. Every `system` line is a command root runs when
|
||||
# that finger touches the sensor, so anything able to write this file owns the
|
||||
# machine at the next press. fingerprintd refuses the whole file — not just the
|
||||
# offending line — unless root owns it and no one else can write it:
|
||||
# YOU PROBABLY DO NOT NEED THIS FILE. Every matched finger is already
|
||||
# announced on the system bus, unconditionally and with nothing configured:
|
||||
#
|
||||
# net.catcrafts.Fingerprintd1.FingerMatched(finger, uid)
|
||||
# on /net/reactivated/Fprint/Device/0
|
||||
#
|
||||
# That is how a finger launches an application. An agent in your own session
|
||||
# hears the signal and decides what the finger means, from your own
|
||||
# configuration, running as you with your bus and your display. The daemon is
|
||||
# root and deliberately does not try to do that for you.
|
||||
#
|
||||
# This file is for the two things that do need the daemon.
|
||||
#
|
||||
# THIS FILE IS A ROOT SHELL. Every command here is run by root when that
|
||||
# finger touches the sensor, so anything able to write it owns the machine at
|
||||
# the next press. fingerprintd refuses the whole file — not just the offending
|
||||
# line — unless root owns it and no one else can write it:
|
||||
#
|
||||
# sudo install -Dm644 -o root -g root actions.conf.example \
|
||||
# /etc/fingerprintd/actions.conf
|
||||
|
|
@ -14,48 +27,43 @@
|
|||
# It is read once, at startup. Editing it means restarting the unit, which is
|
||||
# also when you get to see the parse errors.
|
||||
#
|
||||
# Format, four fields:
|
||||
# Format:
|
||||
#
|
||||
# <finger> <where> <verdict> <command...>
|
||||
# <finger> [no-unlock] [absolute command...]
|
||||
#
|
||||
# finger an fprintd finger name: left-thumb, left-index-finger,
|
||||
# left-middle-finger, left-ring-finger, left-little-finger, and
|
||||
# the right-* equivalents.
|
||||
# finger an fprintd finger name: left-thumb, left-index-finger,
|
||||
# left-middle-finger, left-ring-finger, left-little-finger, and
|
||||
# the right-* equivalents.
|
||||
#
|
||||
# where system root runs the command below.
|
||||
# session no command here. The daemon emits
|
||||
# net.catcrafts.Fingerprintd1.FingerMatched(finger, uid)
|
||||
# and an agent in your session decides what it means.
|
||||
# This is how you launch an application: root has no
|
||||
# session bus and no display, and running your software
|
||||
# as root to get one would be a poor trade.
|
||||
# no-unlock this finger never unlocks. The client is told it did not match,
|
||||
# whatever really happened.
|
||||
#
|
||||
# verdict match the client is told the finger matched. Normal.
|
||||
# no-match the client is told it did NOT, whatever really
|
||||
# happened, while the action runs anyway.
|
||||
# command an ABSOLUTE path, passed to /bin/sh -c with a fixed environment
|
||||
# plus FINGERPRINTD_FINGER. Double-forked, so it may outlive the
|
||||
# daemon and can never delay an unlock.
|
||||
#
|
||||
# command an ABSOLUTE path, required for system, forbidden for session.
|
||||
# It is passed to /bin/sh -c with a fixed environment plus
|
||||
# FINGERPRINTD_FINGER. It is double-forked, so it may outlive the
|
||||
# daemon and will never delay an unlock.
|
||||
#
|
||||
# A finger with no line here is untouched.
|
||||
# At least one of the two is required. A finger listed on its own says nothing
|
||||
# the signal above does not already say.
|
||||
|
||||
# --- Launching things in your session -----------------------------------
|
||||
# The daemon only announces the finger; your agent maps it to an app.
|
||||
#right-ring-finger session match
|
||||
# --- A finger that also does something, as root -----------------------------
|
||||
#right-ring-finger /usr/local/bin/toggle-something
|
||||
|
||||
# --- A duress finger ------------------------------------------------------
|
||||
# The phone reports that it did not recognise this finger, and runs the
|
||||
# script anyway. Think carefully before making that script destructive:
|
||||
# --- A finger that does not unlock ------------------------------------------
|
||||
#left-thumb no-unlock
|
||||
|
||||
# --- A duress finger: rejected, and the script runs anyway ------------------
|
||||
#
|
||||
# Think carefully before making that script destructive:
|
||||
#
|
||||
# * a false accept that opens a camera is a shrug; one that wipes is not,
|
||||
# * and anyone who can compel one unlock can usually compel a second, so
|
||||
# this is a panic button, not protection for data at rest. Only
|
||||
# encryption is that, and by unlock time your session is already
|
||||
# decrypted in RAM.
|
||||
# * and anyone who can compel one unlock can usually compel a second.
|
||||
#
|
||||
#left-little-finger system no-match /etc/fingerprintd/panic.sh
|
||||
|
||||
# --- Something harmless to try it with ------------------------------------
|
||||
#left-thumb system match /usr/bin/logger -t fingerprintd "thumb"
|
||||
# AND KNOW WHAT THIS IS NOT. It is a panic button, not deniability. The
|
||||
# rejection it fabricates is far faster than a real one — a finger the sensor
|
||||
# genuinely does not know takes about three seconds to be refused, this takes
|
||||
# milliseconds — the daemon's journal records that the finger really matched,
|
||||
# this file names it in plain text, and the finger still shows as enrolled in
|
||||
# fprintd-list. It reliably runs your script. It does not reliably hide that
|
||||
# it did.
|
||||
#
|
||||
#left-little-finger no-unlock /etc/fingerprintd/panic.sh
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ tests here are mostly about REFUSAL. The load-bearing properties:
|
|||
* a file anyone but root can write is rejected before a single rule is read,
|
||||
* a malformed rule rejects the WHOLE file rather than being skipped -- a
|
||||
half-applied policy is the dangerous outcome, not the safe one,
|
||||
* a system command must be an absolute path, because resolving a bare name
|
||||
through PATH would make what root runs depend on an environment this
|
||||
daemon does not control,
|
||||
* a command must be an absolute path, because resolving a bare name through
|
||||
PATH would make what root runs depend on an environment this daemon does
|
||||
not control,
|
||||
* and no rule at all means no behaviour change, which is what keeps the
|
||||
feature absent until someone configures it.
|
||||
*/
|
||||
|
|
@ -37,7 +37,7 @@ namespace {
|
|||
int main() {
|
||||
// ---- the file's own permissions are checked before its contents
|
||||
{
|
||||
Parsed p = P("right-index-finger system match /bin/true", /*rootOnly*/ false);
|
||||
Parsed p = P("right-index-finger /bin/true", /*rootOnly*/ false);
|
||||
Check(!p.Ok(), "a file others can write is refused");
|
||||
Check(p.error == Error::NotWritableOnlyByRoot, "and refused for that reason");
|
||||
Check(p.line == 0, "the file is the fault, not a line");
|
||||
|
|
@ -52,59 +52,56 @@ int main() {
|
|||
Check(c.Ok() && c.rules.empty(), "comments and blank lines are ignored");
|
||||
}
|
||||
|
||||
// ---- a well-formed table
|
||||
// ---- the three shapes a line can take
|
||||
{
|
||||
Parsed p = P("# finger where verdict command\n"
|
||||
"right-index-finger session match\n"
|
||||
"left-little-finger system no-match /etc/fingerprintd/panic.sh\n");
|
||||
Parsed p = P("# finger what\n"
|
||||
"right-ring-finger /usr/bin/logger -t fp ring\n"
|
||||
"left-little-finger no-unlock /etc/fingerprintd/panic.sh\n"
|
||||
"left-thumb no-unlock\n");
|
||||
Check(p.Ok(), "a valid file parses");
|
||||
Check(p.rules.size() == 2, "both rules");
|
||||
Check(p.rules.size() == 3, "all three rules");
|
||||
|
||||
const Rule* idx = Find(p.rules, Finger::RightIndex);
|
||||
Check(idx != nullptr, "the index finger has a rule");
|
||||
Check(idx && idx->where == Where::Session, "session");
|
||||
Check(idx && idx->verdict == Verdict::Match, "and it still unlocks");
|
||||
Check(idx && idx->command.empty(), "a session rule carries no command");
|
||||
const Rule* ring = Find(p.rules, Finger::RightRing);
|
||||
Check(ring && ring->unlocks, "a command-only finger still unlocks");
|
||||
Check(ring && ring->command == "/usr/bin/logger -t fp ring",
|
||||
"and the command arrives whole, spaces and all");
|
||||
|
||||
const Rule* pin = Find(p.rules, Finger::LeftLittle);
|
||||
Check(pin != nullptr, "the duress finger has a rule");
|
||||
Check(pin && pin->where == Where::System, "root runs it");
|
||||
Check(pin && pin->verdict == Verdict::NoMatch,
|
||||
"and the client is told it did NOT match -- the whole point of duress");
|
||||
Check(pin && pin->command == "/etc/fingerprintd/panic.sh", "the command");
|
||||
const Rule* duress = Find(p.rules, Finger::LeftLittle);
|
||||
Check(duress && !duress->unlocks, "no-unlock is recorded");
|
||||
Check(duress && duress->command == "/etc/fingerprintd/panic.sh",
|
||||
"alongside its command -- the duress case needs both");
|
||||
|
||||
Check(Find(p.rules, Finger::RightThumb) == nullptr,
|
||||
const Rule* thumb = Find(p.rules, Finger::LeftThumb);
|
||||
Check(thumb && !thumb->unlocks, "no-unlock alone is a complete rule");
|
||||
Check(thumb && thumb->command.empty(), "with no command");
|
||||
|
||||
Check(Find(p.rules, Finger::RightIndex) == nullptr,
|
||||
"a finger with no rule has no rule");
|
||||
}
|
||||
|
||||
// ---- a command may contain spaces; the tail is not re-split
|
||||
// ---- a command that merely STARTS like the keyword is a command
|
||||
{
|
||||
Parsed p = P("right-ring-finger system match /usr/bin/env FOO=1 /usr/local/bin/x -v\n");
|
||||
Check(p.Ok(), "a command with arguments parses");
|
||||
const Rule* r = Find(p.rules, Finger::RightRing);
|
||||
Check(r && r->command == "/usr/bin/env FOO=1 /usr/local/bin/x -v",
|
||||
"and arrives whole");
|
||||
Parsed p = P("left-index-finger /usr/local/bin/no-unlock-helper\n");
|
||||
Check(p.Ok(), "parses");
|
||||
const Rule* r = Find(p.rules, Finger::LeftIndex);
|
||||
Check(r && r->unlocks, "the finger still unlocks");
|
||||
Check(r && r->command == "/usr/local/bin/no-unlock-helper",
|
||||
"and the path was not mistaken for the keyword");
|
||||
}
|
||||
|
||||
// ---- every rejection rejects the whole file
|
||||
{
|
||||
struct Case { std::string_view text; Error want; std::string_view why; };
|
||||
const Case cases[] = {
|
||||
{ "not-a-finger system match /bin/true\n", Error::UnknownFinger,
|
||||
{ "not-a-finger /bin/true\n", Error::UnknownFinger,
|
||||
"an unknown finger name" },
|
||||
{ "right-index-finger elsewhere match /bin/true\n", Error::UnknownWhere,
|
||||
"an unknown 'where'" },
|
||||
{ "right-index-finger system maybe /bin/true\n", Error::UnknownVerdict,
|
||||
"an unknown verdict" },
|
||||
{ "right-index-finger system match\n", Error::MissingCommand,
|
||||
"a system rule with no command" },
|
||||
{ "right-index-finger session match /bin/true\n", Error::UnexpectedCommand,
|
||||
"a session rule with a command" },
|
||||
{ "right-index-finger system match reboot\n", Error::RelativeCommand,
|
||||
{ "right-index-finger\n", Error::NothingToDo,
|
||||
"a finger on its own, which the signal already covers" },
|
||||
{ "right-index-finger reboot\n", Error::RelativeCommand,
|
||||
"a command that is not an absolute path" },
|
||||
{ "right-index-finger system match\n", Error::MissingCommand,
|
||||
"a truncated line" },
|
||||
{ "right-index-finger session match\nright-index-finger system match /bin/true\n",
|
||||
{ "right-index-finger no-unlock reboot\n", Error::RelativeCommand,
|
||||
"a relative command after the keyword" },
|
||||
{ "left-thumb no-unlock\nleft-thumb /bin/true\n",
|
||||
Error::DuplicateFinger, "two rules for one finger" },
|
||||
};
|
||||
for (const Case& c : cases) {
|
||||
|
|
@ -118,8 +115,8 @@ int main() {
|
|||
|
||||
// ---- a valid rule before a bad one is discarded with it
|
||||
{
|
||||
Parsed p = P("right-index-finger session match\n"
|
||||
"left-thumb system match reboot\n");
|
||||
Parsed p = P("right-index-finger no-unlock\n"
|
||||
"left-thumb reboot\n");
|
||||
Check(!p.Ok(), "the file fails");
|
||||
Check(p.line == 2, "on the offending line");
|
||||
Check(p.rules.empty(),
|
||||
|
|
@ -130,9 +127,8 @@ int main() {
|
|||
// ---- every Error has a description; a switch that forgets one shows up here
|
||||
{
|
||||
const Error all[] = { Error::None, Error::NotWritableOnlyByRoot, Error::UnknownFinger,
|
||||
Error::UnknownWhere, Error::UnknownVerdict, Error::MissingCommand,
|
||||
Error::UnexpectedCommand, Error::DuplicateFinger,
|
||||
Error::RelativeCommand };
|
||||
Error::RelativeCommand, Error::NothingToDo,
|
||||
Error::DuplicateFinger };
|
||||
for (Error e : all)
|
||||
Check(Describe(e) != "unknown", "every error describes itself");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue